mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
92 lines
3.7 KiB
Python
92 lines
3.7 KiB
Python
"""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()
|