Files
dify/api/controllers/openapi/auth/personal_access.py
2026-11-09 17:13:22 +08:00

87 lines
3.5 KiB
Python

"""Translate personal PAT policies to the existing Account admission pipeline."""
import uuid
from typing import override
from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden, ServiceUnavailable, Unauthorized
from constants.oauth_bearer import TokenType
from constants.personal_access_token import PersonalAccessTokenPolicy
from controllers.openapi.auth.context import Context
from controllers.openapi.auth.loaders import load_app, load_workspace, route_has_app
from controllers.openapi.auth.requirements import Rank, Requirement
from controllers.openapi.auth.subjects import Subject
from extensions.ext_application_services import application_services
from libs.oauth_bearer import AuthContext, sha256_hex
from libs.rate_limit import enforce_bearer_rate_limit
from services.personal_access_token_service import (
PersonalAccessTokenAccessDeniedError,
PersonalAccessTokenInvalidError,
PersonalAccessTokenNotFoundError,
PersonalAccessTokenSigningKeyUnavailableError,
)
def authenticate_personal_token(secret: str) -> AuthContext:
enforce_bearer_rate_limit(sha256_hex(secret))
try:
record = application_services().personal_access_tokens.authenticate(secret)
except PersonalAccessTokenInvalidError:
raise Unauthorized("invalid bearer") from None
except PersonalAccessTokenSigningKeyUnavailableError:
raise ServiceUnavailable("Personal access token signing key is unavailable.") from None
return AuthContext(
token_type=TokenType.PERSONAL_ACCESS,
account_id=uuid.UUID(record.account_id),
token_id=uuid.UUID(record.summary.id),
expires_at=record.summary.expires_at,
subject_email=None,
subject_issuer=None,
client_id=None,
)
class CheckPersonalAccess(Requirement):
rank = Rank.EARLY
@override
def run(self, subject: Subject, ctx: Context, session: Session) -> None:
"""Skip non-PAT callers; authorize PATs only under an explicit route policy.
The router supplies the endpoint policy on the admission context. A missing
policy denies PAT access before this check loads resources. Successful authorization
binds the live resource grants; rejected requests never reach the handler.
"""
if subject.auth.token_type is not TokenType.PERSONAL_ACCESS:
return
policy = ctx.pat_policy
if policy is None:
raise Forbidden("personal_access_token_not_supported")
app_id = None
workspace_id = None
if policy not in {
PersonalAccessTokenPolicy.WORKSPACE_DISCOVERY,
PersonalAccessTokenPolicy.IDENTITY_DISCOVERY,
}:
if route_has_app(ctx):
app = load_app(ctx)
app_id = app.id
workspace_id = app.tenant_id
else:
workspace_id = load_workspace(ctx).id
try:
grant = application_services().personal_access_tokens.authorize(
account_id=str(subject.account_id),
token_id=str(subject.token_id),
policy=policy,
workspace_id=workspace_id,
app_id=app_id,
)
except (PersonalAccessTokenInvalidError, PersonalAccessTokenNotFoundError):
raise Unauthorized("invalid bearer") from None
except PersonalAccessTokenAccessDeniedError as error:
raise Forbidden(str(error)) from None
ctx.personal_workspace_ids = grant.workspace_ids
ctx.personal_app_ids = grant.app_ids