Commit Graph

1079 Commits

Author SHA1 Message Date
71e9e8dda6 feat(api): lift SSO branch device-flow handlers to /openapi/v1 (Phase D.15-16)
The four EE-only SSO handlers (sso_initiate, sso_complete,
approval_context, approve_external) move from controllers/oauth_device_sso.py
to controllers/openapi/oauth_device/. Each is registered on openapi_bp
via @bp.route at the canonical path:

  /openapi/v1/oauth/device/sso-initiate
  /openapi/v1/oauth/device/sso-complete
  /openapi/v1/oauth/device/approval-context
  /openapi/v1/oauth/device/approve-external

sso-complete moves under /oauth/device/ from its previous orphan path
/v1/device/sso-complete; the IdP-side ACS callback URL hardcoded in
sso_initiate now points to the canonical path. Operators must
re-register the ACS callback with each IdP before Phase F deletes the
legacy alias.

oauth_device_sso.py shrinks to a thin re-mount file: same legacy bp
with attach_anti_framing applied, four bp.add_url_rule() calls binding
the legacy paths to the imported view functions. Same handler runs
for both mounts — no duplicated logic.

attach_anti_framing(openapi_bp) added in controllers/openapi/__init__.py
so X-Frame-Options + frame-ancestors CSP cover the canonical paths too.

Plan: docs/superpowers/plans/2026-04-26-openapi-migration.md (in difyctl repo).
2026-04-27 00:00:24 -07:00
772f450b29 feat(api): lift device-flow approve/deny to /openapi/v1 (Phase D.13-14)
DeviceApproveApi + DeviceDenyApi (cookie-authed) move to
controllers/openapi/oauth_device/{approve,deny}.py. Decorator stack
preserved verbatim: setup_required → login_required →
account_initialization_required → bearer_feature_required →
rate_limit. Audit event names ('oauth.device_flow_approved' /
'oauth.device_flow_denied') unchanged so the OTel exporter
registration keeps routing them.

The legacy /console/api/oauth/device/{approve,deny} mounts are
re-registered on console_ns from the bottom of the new files via a
local-import _register_legacy_console_mount() helper. The local
import breaks an import cycle that would otherwise form: openapi
imports console.wraps for setup_required, console.__init__.py imports
console.auth.oauth_device, which would re-import the openapi class
mid-load. Deferring console_ns past the class definition resolves it.

console/auth/oauth_device.py becomes a 9-line placeholder (the
existing console.__init__.py `from .auth import (..., oauth_device,
...)` keeps loading until Phase F prunes the import).

Plan: docs/superpowers/plans/2026-04-26-openapi-migration.md (in difyctl repo).
2026-04-26 23:57:28 -07:00
390f1f74db feat(api): add /openapi/v1/account/sessions endpoints (Phase C.11-12)
GET /openapi/v1/account/sessions lists the bearer's active OAuth
tokens (filtered to revoked_at IS NULL, expires_at > NOW(), token_hash
IS NOT NULL — no phantom devices). DELETE
/openapi/v1/account/sessions/<id> revokes a specific session with a
subject-match guard that returns 404 (not 403) on cross-subject so
token IDs don't leak across subjects.

Subject scoping abstracted into _subject_match(ctx): account subjects
filter by account_id; external_sso subjects filter by (email, issuer)
AND account_id IS NULL — preventing an SSO bearer from touching a
same-email account row from a federated tenant.

_revoke_token_by_id helper extracted so /sessions/self and
/sessions/<id> share the same UPDATE-where-revoked_at-IS-NULL
idempotent revoke + Redis cache invalidation.

No /v1/ equivalents — these are new endpoints (spec §Sessions list shape).

Plan: docs/superpowers/plans/2026-04-26-openapi-migration.md (in difyctl repo).
2026-04-26 23:51:55 -07:00
b7bd9c19ed feat(api): lift identity + self-revoke to /openapi/v1/account (Phase C.9-10)
GET /v1/me moves to GET /openapi/v1/account. DELETE
/v1/oauth/authorizations/self moves to DELETE
/openapi/v1/account/sessions/self. Both classes (AccountApi,
AccountSessionsSelfApi) are now in controllers/openapi/account.py and
re-registered on service_api_ns at the legacy paths.

service_api/oauth.py is now nothing but legacy re-mount declarations
(20 lines). All in-place handler logic has moved to openapi/. Phase F
will delete the file and the legacy mounts together.

Helper functions (_load_memberships, _pick_default_workspace,
_workspace_payload, _account_payload) move with the AccountApi class.

Plan: docs/superpowers/plans/2026-04-26-openapi-migration.md (in difyctl repo).
2026-04-26 23:50:15 -07:00
e93821af46 feat(api): lift GET /oauth/device/lookup to /openapi/v1 (Phase B.8)
Same pattern as B.6 / B.7: OAuthDeviceLookupApi moves to
controllers/openapi/oauth_device/lookup.py and is re-registered on
service_api_ns to keep /v1/oauth/device/lookup serving until Phase F.

service_api/oauth.py is now down to /me + /oauth/authorizations/self
plus three legacy mounts; remaining handlers move in Phase C.
Now-unused imports (LIMIT_LOOKUP_PUBLIC, rate_limit, reqparse, request,
DEVICE_FLOW_TTL_SECONDS, DeviceFlowRedis, DeviceFlowStatus) removed.

Plan: docs/superpowers/plans/2026-04-26-openapi-migration.md (in difyctl repo).
2026-04-26 23:44:05 -07:00
9408759954 feat(api): lift POST /oauth/device/token to /openapi/v1 (Phase B.7)
Same pattern as B.6: OAuthDeviceTokenApi moves to
controllers/openapi/oauth_device/token.py and is re-registered on
service_api_ns to keep /v1/oauth/device/token serving until Phase F.

_audit_cross_ip_if_needed helper moves with the handler. Now-unused
imports removed from service_api/oauth.py.

Plan: docs/superpowers/plans/2026-04-26-openapi-migration.md (in difyctl repo).
2026-04-26 23:42:27 -07:00
fe9412af5d feat(api): lift POST /oauth/device/code to /openapi/v1 (Phase B.6)
Canonical class OAuthDeviceCodeApi now lives in
controllers/openapi/oauth_device/code.py and is registered on
openapi_ns at /openapi/v1/oauth/device/code. service_api/oauth.py
re-registers the same class object on service_api_ns at
/v1/oauth/device/code so existing callers keep working until Phase F.

KNOWN_CLIENT_IDS literal moves to dify_config.OPENAPI_KNOWN_CLIENT_IDS
(CSV-parsed, default "difyctl") so new CLIs / SDKs can be admitted
without code changes (CLAUDE.md rule 8 — no magic strings).

_verification_uri helper moves with the handler. Single source of
truth — no duplicated logic between the two mounts.

Plan: docs/superpowers/plans/2026-04-26-openapi-migration.md (in difyctl repo).
2026-04-26 23:40:58 -07:00
4214583ae5 refactor(api): hoist bearer_feature_required to libs/oauth_bearer (Phase A.3)
The decorator was defined inline in console/auth/oauth_device.py. Phase
D will move approve/deny to controllers/openapi/oauth_device/ and the
new SSO branch under the same group needs the same gate. Hoist it to
libs/oauth_bearer.py now so the move stays a pure file rename later.

Behavior unchanged: 503 'bearer_auth_disabled' when ENABLE_OAUTH_BEARER
is off. console/auth/oauth_device.py imports it from libs and drops
the now-unused dify_config / wraps / ServiceUnavailable imports.

Plan: docs/superpowers/plans/2026-04-26-openapi-migration.md (in difyctl repo).
2026-04-26 23:26:13 -07:00
f5f224f49d feat(api): scaffold /openapi/v1 blueprint (Phase A.1)
New Flask blueprint at /openapi/v1/ that will host user-scoped
programmatic endpoints (device flow, identity, sessions, workspaces).
Ships only a smoke route GET /openapi/v1/_health for now; subsequent
phases lift handlers in from service_api, console, and the orphan
oauth_device_sso.py.

CORS is intentionally omitted here and configured in step A.5 once
the allowlist envvar lands.

Plan: docs/superpowers/plans/2026-04-26-openapi-migration.md (in difyctl repo).
2026-04-26 23:08:15 -07:00
813da349ec fix(api,web): post-review hardening for OAuth device flow
- api: account-flow stores subject_issuer="dify:account" sentinel
  instead of NULL so the rotate-in-place unique index collides as
  intended (Postgres treats NULLs as distinct in unique indices).
  mint_oauth_token validates prefix-specific issuer rules.
- api: enterprise_only inverts to an allowlist (ACTIVE / EXPIRING) so
  any future LicenseStatus value defaults to denial.
- api: consume_on_poll moved to a single Lua script (GET + status-check
  + DEL) so concurrent pollers can't both observe APPROVED.
- web: typed DeviceFlowError + central error-copy mapping; page
  surfaces rate_limited / lookup_failed view states; URL params
  scrubbed after consumption (RFC 8628 §5.4).
2026-04-26 23:05:07 -07:00
fe8510ad1a feat(api,web): OAuth 2.0 device flow + bearer auth (RFC 8628)
Adds a CLI-friendly authorization flow so difyctl (and future
non-browser clients) can obtain user-scoped tokens without copy-
pasting cookies or raw API keys. Two grant paths share one device
flow surface:

  1. Account branch — user signs in via the existing /signin
     methods, /device page calls console-authed approve, mints a
     dfoa_ token tied to (account_id, tenant).
  2. External-SSO branch (EE) — /v1/oauth/device/sso-initiate signs
     an SSOState envelope, hands off to Enterprise's external ACS,
     receives a signed external-subject assertion, mints a dfoe_
     token tied to (subject_email, subject_issuer).

API surface (all under /v1, EE-only endpoints 404 on CE):

  POST   /v1/oauth/device/code              — RFC 8628 start
  POST   /v1/oauth/device/token             — RFC 8628 poll
  GET    /v1/oauth/device/lookup            — pre-validate user_code
  GET    /v1/oauth/device/sso-initiate      — SSO branch entry
  GET    /v1/device/sso-complete            — SSO callback sink
  GET    /v1/oauth/device/approval-context  — /device cookie probe
  POST   /v1/oauth/device/approve-external  — SSO approve
  GET    /v1/me                             — bearer subject lookup
  DELETE /v1/oauth/authorizations/self      — self-revoke
  POST   /console/api/oauth/device/approve  — account approve
  POST   /console/api/oauth/device/deny     — account deny

Core primitives:
- libs/oauth_bearer.py: prefix-keyed TokenKindRegistry +
  BearerAuthenticator + validate_bearer decorator. Two-tier scope
  (full vs apps:run) stamped from the registry, never from the DB.
- libs/jws.py: HS256 compact JWS keyed on the shared Dify
  SECRET_KEY — same key-set verifies the SSOState envelope, the
  external-subject assertion (minted by Enterprise), and the
  approval-grant cookie.
- libs/device_flow_security.py: enterprise_only gate, approval-
  grant cookie mint/verify/consume (Path=/v1/oauth/device,
  HttpOnly, SameSite=Lax, Secure follows is_secure()), anti-
  framing headers.
- libs/rate_limit.py: typed RateLimit / RateLimitScope dispatch
  with composite-key buckets; both decorator + imperative form.
- services/oauth_device_flow.py: Redis state machine (PENDING ->
  APPROVED|DENIED with atomic consume-on-poll), token mint via
  partial unique index uq_oauth_active_per_device (rotates in
  place), env-driven TTL policy.

Storage: oauth_access_tokens table with partial unique index on
(subject_email, subject_issuer, client_id, device_label) WHERE
revoked_at IS NULL. account_id NULL distinguishes external-SSO
rows. Hard-expire is CAS UPDATE (revoked_at + nullify token_hash)
so audit events keep their token_id. Retention pruner DELETEs
revoked + zombie-expired rows past OAUTH_ACCESS_TOKEN_RETENTION_DAYS.

Frontend: /device page with code-entry, chooser (account vs SSO),
authorize-account, authorize-sso views. SSO branch detaches from
the URL user_code and reads everything from the cookie via
/approval-context. Anti-framing headers on all responses.

Wiring: ENABLE_OAUTH_BEARER feature flag; ext_oauth_bearer binds
the authenticator at startup; clean_oauth_access_tokens_task
scheduled in ext_celery.

Spec: docs/specs/v1.0/server/{device-flow,tokens,middleware,security}.md
2026-04-26 20:06:43 -07:00
97bb338e7d fix: prevent double /v1 in MCP server URL causing 404 authorization failure (#34596)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-20 02:42:59 +00:00
ceec00e172 fix: accept icon type in app icon updates (#35360)
Co-authored-by: Stephen Zhou <hi@hyoban.cc>
2026-04-20 02:33:49 +00:00
99
3e876e173a chore(api): adapt Graphon 0.2.2 upgrade (#35377)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-18 11:16:24 +00:00
de15e5b449 fix: scope plugin inner API end-user lookup by tenant (#35325) 2026-04-17 06:12:07 +00:00
af21dc7df8 refactor(api): migrate dataset document response schemas to BaseModel (#35298)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-17 05:02:04 +00:00
13a9359191 refactor(api): migrate console mcp-server responses to BaseModel (#35219)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
2026-04-17 04:16:14 +00:00
3193e8a712 chore: reorg imports (#35308)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-16 08:50:02 +00:00
b665eaa015 refactor(api): migrate console conversation responses to BaseModel (#35294)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
2026-04-16 05:11:21 +00:00
53a22aa41b feat: collaboration (#30781)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
2026-04-16 02:21:04 +00:00
dbceb3067e refactor(api): migrate console tag responses from marshal_with to BaseModel (#35208)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
2026-04-15 09:57:27 +00:00
e3c2116501 fix: remove enable for get (#35245)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
2026-04-15 09:18:29 +00:00
76af80e332 fix: open restore version panel raise 500 (#35240) 2026-04-15 04:43:38 +00:00
7a880ae60c fix: import DSL and copy app not work (#35239)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-15 04:43:22 +00:00
25c388d0db refactor(api): migrate console workflow-trigger responses to BaseModel (#35200)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
2026-04-14 19:52:17 +00:00
b1722c8af9 refactor(api): migrate console conversation variables response model to BaseModel (#35193)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
2026-04-14 19:51:33 +00:00
b65a5fcd97 refactor(api): migrate service api workflow responses from marshal_with to BaseModel (#35195)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
2026-04-14 19:50:59 +00:00
1c3cba281a refactor(api): migrate console message responses from marshal_with to BaseModel (#35204)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
2026-04-14 19:49:33 +00:00
800954f8ce refactor(api): migrate service conversation-variable responses to BaseModel (#35205)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
2026-04-14 19:49:21 +00:00
f66a3c49c4 refactor(api): migrate console recommended-app response to BaseModel (#35206)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
2026-04-14 19:48:43 +00:00
ef396ac84e refactor(api): migrate workspace current response from marshal_with to BaseModel (#35207)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
2026-04-14 19:48:09 +00:00
7e7b27fdec refactor: replace bare dict with dict[str, Any] in response converter… (#35212)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 19:45:04 +00:00
b1df52b8ff refactor(api): migrate console workflow app-log responses to BaseModel (#35201)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
2026-04-14 18:43:09 +00:00
e527b7c5f1 refactor(api): migrate console installed-app list response to BaseModel (#35202)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
2026-04-14 18:40:40 +00:00
e78558bc06 refactor(api): migrate dataset hit-testing response model to BaseModel (#35192)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
2026-04-14 18:12:40 +00:00
ef062fb397 refactor(api): migrate console extension endpoint from api.model to BaseModel (#35189)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
2026-04-14 17:58:33 +00:00
a2ea7ca039 refactor(api): migrate workspace account marshal_with responses to BaseModel (#35190)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
2026-04-14 17:57:34 +00:00
711fe6ba2c refactor: convert plugin permission if/elif to match/case (#30001) (#35140) 2026-04-14 07:03:53 +00:00
2f682780fa refactor: replace bare dict with dict[str, Any] in rag_pipeline and datasource_provider services (#35107) 2026-04-14 03:02:41 +00:00
f040733e28 refactor(api): type _jsonify_form_definition payload with FormDefinitionPayload TypedDict (#35094)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-13 19:21:19 +00:00
b0bf7ca486 refactor: replace bare dict with typed annotations in controllers (#35095) 2026-04-13 19:19:52 +00:00
08f3133414 fix: db session expired issue (#35049) 2026-04-13 13:06:13 +00:00
d412cddf39 refactor: replace bare dict with UtmInfo TypedDict in operation_service (#35055) 2026-04-13 13:05:47 +00:00
671c5cdd84 refactor: replace bare dict with WorkflowRunListArgs TypedDict (#35057) 2026-04-13 13:05:39 +00:00
554f060092 refactor: replace bare dict with AdvancedPromptTemplateArgs TypedDict (#35056) 2026-04-13 13:05:23 +00:00
8dd4473432 refactor(auth): standardize failed login audit logging (#35054) 2026-04-13 12:26:13 +00:00
3ead0beeb1 fix: correct typo submision to submission (#33435)
Co-authored-by: LincolnBurrows2017 <lincoln@example.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
2026-04-13 05:59:21 +00:00
5a446f8200 refactor(api): deduplicate dataset controller schemas into controller_schemas.py (#34756) 2026-04-13 05:33:20 +00:00
9121f24181 refactor(api): deduplicate TextToAudioPayload and MessageListQuery into controller_schemas.py (#34757) 2026-04-13 05:27:35 +00:00
e37aaa482d refactor: migrate apikey from marshal_with/api.model to Pydantic BaseModel (#34932)
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-13 05:18:42 +00:00