mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
92 lines
3.8 KiB
Python
92 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from functools import partial, wraps
|
|
from typing import Any
|
|
|
|
from flask import request
|
|
from werkzeug.exceptions import NotFound, Unauthorized
|
|
|
|
from configs import dify_config
|
|
from controllers.openapi.auth.context import Context
|
|
from controllers.openapi.auth.pipelines import pipeline_for_subject
|
|
from controllers.openapi.auth.requirements import assert_license_valid
|
|
from controllers.openapi.auth.resource_access import authenticate_resource_token
|
|
from controllers.openapi.auth.spec import EndpointSpec
|
|
from controllers.openapi.auth.subjects import subject_from_auth
|
|
from core.db.session_factory import session_factory
|
|
from enums import DeploymentEdition
|
|
from libs.oauth_bearer import (
|
|
InvalidBearerError,
|
|
TokenType,
|
|
assert_bearer_feature_enabled,
|
|
extract_bearer,
|
|
get_authenticator,
|
|
)
|
|
|
|
|
|
class AuthRouter:
|
|
def guard(self, spec: EndpointSpec) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
def decorator(view: Callable[..., Any]) -> Callable[..., Any]:
|
|
@wraps(view)
|
|
def decorated(*args: Any, **kwargs: Any) -> Any:
|
|
return self._execute(spec, partial(view, *args, **kwargs))
|
|
|
|
return decorated
|
|
|
|
return decorator
|
|
|
|
def _execute(self, spec: EndpointSpec, call: Callable[..., Any]) -> Any:
|
|
"""The order is the contract. An endpoint the edition does not expose
|
|
answers 404 before anything reveals whether the bearer was valid, and
|
|
an enterprise deployment's licence answers 403 before the missing-bearer
|
|
401. The licence is a fact about the deployment, not the route or the
|
|
caller, so this is the one place it is checked. The OAuth feature flag
|
|
answers 503 before OAuth authentication, but does not gate independent
|
|
resource credentials. Missing or unknown credentials still receive 503
|
|
when OAuth is disabled.
|
|
"""
|
|
if spec.edition is not None and dify_config.DEPLOYMENT_EDITION not in spec.edition:
|
|
raise NotFound()
|
|
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE:
|
|
assert_license_valid()
|
|
token = extract_bearer(request)
|
|
is_resource_token = token is not None and TokenType.for_token(token) is TokenType.RESOURCE_ACCESS
|
|
if not is_resource_token:
|
|
assert_bearer_feature_enabled()
|
|
if not token:
|
|
raise Unauthorized("bearer required")
|
|
|
|
try:
|
|
auth = authenticate_resource_token(token) if is_resource_token else get_authenticator().authenticate(token)
|
|
except InvalidBearerError:
|
|
# One answer for every rejection reason - unknown prefix, no live row,
|
|
# expired - so a caller cannot probe which one it hit. Same reasoning as
|
|
# the 404-not-403 elsewhere on this surface.
|
|
raise Unauthorized("invalid bearer")
|
|
subject = subject_from_auth(auth)
|
|
pipeline = pipeline_for_subject(subject)
|
|
|
|
# One session spans the loaders and the handler, so the objects the
|
|
# requirements load stay attached while the handler reads them. It is
|
|
# finalised here, like `with_session` does, until repositories own it.
|
|
with session_factory.create_session() as session:
|
|
ctx = Context(subject, session, dict(request.view_args or {}))
|
|
try:
|
|
result = pipeline.run(
|
|
subject=subject,
|
|
auth=auth,
|
|
spec=spec,
|
|
ctx=ctx,
|
|
session=session,
|
|
call=call,
|
|
)
|
|
except Exception:
|
|
session.rollback() # guard-ignore: no-new-controller-sqlalchemy -- the router owns the rollback
|
|
raise
|
|
session.commit()
|
|
return result
|
|
|
|
|
|
subject_router = AuthRouter()
|