mirror of
https://github.com/langgenius/dify.git
synced 2026-05-20 08:46:57 +08:00
Bearer auth surface for /openapi/v1/* run-routes:
- OAUTH_BEARER_PIPELINE (renamed from APP_PIPELINE for clarity outside this
module) composes BearerCheck → ScopeCheck → AppResolver →
WorkspaceMembershipCheck → AppAuthzCheck → CallerMount.
- BearerAuthenticator.authenticate() is the single source of identity +
rate-limit. Both pipeline (BearerCheck) and decorator (validate_bearer)
delegate to it, so per-token rate limit fires exactly once per request.
- Layer 0 (workspace membership) is CE-only; on EE the gateway owns
tenant isolation. Verdicts are cached on the AuthContext entry as
verified_tenants: dict[str, bool] (legacy "ok"/"denied" strings tolerated
by from_cache for one TTL cycle, then removed).
- check_workspace_membership(...) is the shared core; the pipeline step
and the inline require_workspace_member helper both delegate to it.
- Per-token rate limit: 60/min sliding window, RFC-7231-compliant 429
with Retry-After header + JSON body { error, retry_after_ms }. Bucket
key is sha256(token) so all replicas share state via Redis.
API hygiene:
- Scope StrEnum (FULL, APPS_READ, APPS_RUN) replaces bare string literals.
- /openapi/v1/apps/<id>/info: scope flipped from apps:run to apps:read.
- /info migrates off the pipeline to validate_bearer + require_scope +
require_workspace_member (no AppAuthzCheck/CallerMount needed for reads).
- ResolvedRow gains to_cache() / from_cache() classmethods.
- AuthContext gains token_hash + verified_tenants, dropping the per-route
re-hash and per-request Redis read on the cache hit path.
OPENAPI_RATE_LIMIT_PER_TOKEN config (default 60).
36 lines
1.0 KiB
Python
36 lines
1.0 KiB
Python
"""GET /openapi/v1/apps/<app_id>/info — port of service_api/app/app.py:AppInfoApi."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from flask import g
|
|
from flask_restx import Resource
|
|
from werkzeug.exceptions import NotFound
|
|
|
|
from controllers.openapi import openapi_ns
|
|
from controllers.openapi.apps import account_or_404, app_info_payload
|
|
from extensions.ext_database import db
|
|
from libs.oauth_bearer import (
|
|
ACCEPT_USER_ANY,
|
|
Scope,
|
|
require_scope,
|
|
require_workspace_member,
|
|
validate_bearer,
|
|
)
|
|
from models import App
|
|
|
|
|
|
@openapi_ns.route("/apps/<string:app_id>/info")
|
|
class AppInfoApi(Resource):
|
|
@validate_bearer(accept=ACCEPT_USER_ANY)
|
|
@require_scope(Scope.APPS_READ) # type: ignore[reportUntypedFunctionDecorator]
|
|
def get(self, app_id: str):
|
|
ctx = g.auth_ctx
|
|
account_or_404(ctx)
|
|
|
|
app = db.session.get(App, app_id)
|
|
if not app or app.status != "normal":
|
|
raise NotFound("app not found")
|
|
|
|
require_workspace_member(ctx, str(app.tenant_id))
|
|
return app_info_payload(app), 200
|