Source code for flux_restful.auth.base

"""
The authentication backend interface.

A backend answers two questions: "are these credentials valid?" and "who does
this bearer token belong to?" Everything else in the server (routers, the web
views, job submission) only ever sees a Principal, so backends can be swapped
by setting FLUX_AUTH_BACKEND without touching the rest of the code.
"""

import logging
import pwd
from dataclasses import dataclass
from typing import Optional

from sqlalchemy.orm import Session

from flux_restful.auth import tokens
from flux_restful.core.config import settings

logger = logging.getLogger("flux-restful")


[docs]class AuthError(Exception): """A backend is misconfigured or cannot perform the requested operation."""
[docs]@dataclass class Principal: """ An authenticated identity, independent of the backend that produced it. user_name is the login name and, in multi-user mode, the system account that jobs are submitted as. """ user_name: str is_superuser: bool = False is_active: bool = True backend: str = ""
[docs]def is_admin(username: str) -> bool: """ Users listed in FLUX_ADMIN_USERS are superusers regardless of backend. """ return username in settings.admin_users
[docs]def is_system_user(username: str) -> bool: """ Whether a name is a system account that a backend may map to. Accounts below FLUX_MIN_UID (root, daemons) are never acceptable, so a bad claim mapping or a misconfigured PAM stack cannot yield them. """ try: record = pwd.getpwnam(username) except KeyError: return False if record.pw_uid < settings.min_uid: logger.warning( "Refusing system account %s (uid %s below FLUX_MIN_UID=%s)", username, record.pw_uid, settings.min_uid, ) return False return True
[docs]class AuthBackend: """ Base class for authentication backends. Subclasses set ``name`` (the value used in FLUX_AUTH_BACKEND) and override ``authenticate`` and/or ``resolve``. Token issuing and verification default to server-signed JWTs and only need overriding when tokens come from somewhere else (for example an OIDC provider). """ name = "base" # Whether authenticate(username, password) is meaningful for this backend. # Password backends get HTTP Basic auth for the web views, the OAuth2 # password form login, and (with FLUX_SECRET_KEY set) the shared-secret # token handshake used by the Python client. supports_password = False # Whether the server issues access tokens for this backend (and therefore # needs FLUX_TOKEN_SIGNING_KEY). False when tokens come from elsewhere. issues_tokens = True
[docs] def validate(self) -> None: """ Check configuration at startup. Raise AuthError if unusable. """
[docs] def authenticate( self, db: Session, username: str, password: str ) -> Optional[Principal]: """ Validate a username and password. Return a Principal, or None. """ return None
[docs] def resolve(self, db: Session, username: str) -> Optional[Principal]: """ Resolve a username (typically a token subject) to a Principal. Return None if the user is unknown or inactive. """ return None
[docs] def issue_token(self, principal: Principal) -> str: """ Issue an access token for an authenticated principal. """ return tokens.create_access_token(principal.user_name, backend=self.name)
[docs] def verify_token(self, db: Session, token: str) -> Optional[Principal]: """ Verify a bearer token and return its Principal, or None if invalid. """ try: payload = tokens.decode_access_token(token) except tokens.TokenError: return None # A token issued under a different backend is not valid here, even # if the signing key is the same. if payload.get("backend") != self.name: return None subject = payload.get("sub") if not subject: return None return self.resolve(db, subject)