"""Resource-token authentication and live resource authorization for OpenAPI. Machine credentials never inherit their creator's account or workspace role. Re-read bindings on every request so revocation takes effect without cache lag. """ import uuid from typing import override from flask import request from sqlalchemy import select from sqlalchemy.orm import Session from werkzeug.exceptions import Forbidden, Unauthorized from controllers.openapi.auth.context import Context from controllers.openapi.auth.requirements import Rank, Requirement from controllers.openapi.auth.subjects import Subject from core.db.session_factory import session_factory from libs.datetime_utils import naive_utc_now from libs.oauth_bearer import AuthContext, TokenType, sha256_hex from libs.rate_limit import enforce_bearer_rate_limit from models.account import Tenant, TenantStatus from models.enums import AppStatus from models.model import App from models.resource_access_token import ( ResourceAccessToken, ResourceAccessTokenRelation, ResourceAccessTokenResourceType, ) def authenticate_resource_token(token: str) -> AuthContext: token_hash = sha256_hex(token) enforce_bearer_rate_limit(token_hash) with session_factory.create_session() as session: row = session.scalar(select(ResourceAccessToken).where(ResourceAccessToken.token == token)) if row is None: raise Unauthorized("invalid_bearer") token_id = uuid.UUID(str(row.id)) return AuthContext( subject_email=None, subject_issuer=None, account_id=None, client_id=None, token_id=token_id, token_type=TokenType.RESOURCE_ACCESS, expires_at=None, ) class CheckResourceAccess(Requirement): # CheckSubject rejects unsupported routes first, then bindings are loaded # before any app or workspace requirements can read their context. rank = Rank.FIRST @override def run(self, subject: Subject, ctx: Context, session: Session) -> None: row = session.scalar(select(ResourceAccessToken).where(ResourceAccessToken.id == str(subject.token_id))) if row is None: raise Unauthorized("invalid_bearer") tenant = session.get(Tenant, row.tenant_id) if tenant is None or tenant.status != TenantStatus.NORMAL: raise Forbidden("workspace unavailable") workspace_id = ctx.view_args.get("workspace_id") or request.args.get("workspace_id") if workspace_id and workspace_id != str(tenant.id): raise Forbidden("resource_not_authorized") ctx._workspace = tenant ctx.resource_app_ids = frozenset( session.scalars( select(App.id) .join(ResourceAccessTokenRelation, ResourceAccessTokenRelation.app_id == App.id) .where( ResourceAccessTokenRelation.token_id == row.id, ResourceAccessTokenRelation.resource_type == ResourceAccessTokenResourceType.APP, App.tenant_id == tenant.id, ) ) ) app_id = ctx.view_args.get("app_id") if app_id: try: app_id = str(uuid.UUID(app_id)) except ValueError: raise Forbidden("resource_not_authorized") if app_id not in ctx.resource_app_ids: raise Forbidden("resource_not_authorized") app = session.scalar(select(App).where(App.id == app_id, App.tenant_id == tenant.id)) if app is None or app.status != AppStatus.NORMAL or not app.enable_api: raise Forbidden("resource_not_authorized") ctx._app = app row.last_used_at = naive_utc_now()