Files
dify/cli/docs/specs/server/gateway.md

10 KiB

title
title
server — gateway

gateway

EE gateway integration for /openapi/v1/* user-level bearers. RBAC enforcement, token resolve via inner-API, path filter, error envelope translation.

Active on EE deployments only. CE (ENTERPRISE_ENABLED=false in dify api) has no gateway — /openapi/v1/* reaches dify api directly; api middleware does Layer 0 workspace-membership in its place. EE (ENTERPRISE_ENABLED=true) routes through gateway → api; api middleware skips Layer 0.

Companion: middleware.md §Authorization (api authz pipeline, including CE-only Layer 0), tokens.md (storage + cache), security.md §Inner API trust boundary (invariants), endpoints.md §Inner APIs (HTTP surface).

Scope

Deployment Gateway RBAC Token resolve
CE absent — (api Layer 0 covers workspace membership only) api middleware direct
EE dify-enterprise gateway (Caddy + dify_rbac module) gateway interceptor gateway → enterprise svc inner-API + api middleware re-resolve

External SSO dfoe_ subjects: gateway skips RBAC. Bounded by L2 token scopes (apps:run, apps:read:permitted-external) + L1 ACL sso_verified access-mode gate.

Request flow

CLI ─Authorization: Bearer dfoa_xxxxx──→ Gateway (Caddy + dify_rbac patched)
                                          │
                                          ├─ filter chain (audit-style):
                                          │   skip /openapi/v1/oauth/device/*
                                          │
                                          ├─ POST /inner/api/auth/check-access-oauth
                                          │   header: Enterprise-Api-Secret-Key
                                          │   ←── enterprise svc
                                          │       (Go: sha256 → Redis auth:token:{hash} → dify_db read view)
                                          │   on 5xx/timeout → 503 fail-closed
                                          │
                                          ├─ POST /inner/api/rbac/check-access
                                          │   ←── enterprise svc (existing rbac branch)
                                          │   on 5xx/timeout → 503 fail-closed
                                          │
                                          ├─ on 401/403: translate {error} → {code,message,hint}
                                          │
                                          └─ forward request UNCHANGED → dify api
                                                                          │
                                                                          ├─ middleware step 3a-e
                                                                          │   sha256 → Redis auth:token:{hash} → dify_db
                                                                          │
                                                                          ├─ Layer 0 (CE only): workspace membership
                                                                          ├─ Layer 1: Resource ACL
                                                                          └─ Layer 2: Token scope → handler

The gateway does NOT inject identity headers. Api always re-resolves from the token store on the request hot path. The two resolves (gateway-side via inner-API, api-side direct) are independent — both Redis-cached against the shared auth:token:{hash} key.

Path filter

Mirrors the existing dify_audit filter pattern (pkg/gateway/audit/filter.goChain of Filters, all-must-pass). The dify_rbac module exposes a hardcoded DefaultChain with a single PrefixFilter whitelist. Whitelist miss → no RBAC, forward to api as-is.

Whitelist (RBAC enforced):

/openapi/v1/account
/openapi/v1/workspaces
/openapi/v1/apps
/openapi/v1/runs

Implicit skip (whitelist miss → no RBAC):

Path Why skipped
/openapi/v1/oauth/device/code Bearer-less — device-flow start
/openapi/v1/oauth/device/token Bearer-less — device-flow poll
/openapi/v1/oauth/device/sso-complete Browser-side SSO continuation
/openapi/v1/oauth/device/approval-context Cookie-authed self-action
/openapi/v1/oauth/device/approve Cookie-authed self-action
/openapi/v1/oauth/device/approve-external Cookie-authed self-action
/openapi/v1/oauth/device/deny Cookie-authed self-action

Whitelist lives in module source, not Caddyfile — paths are dify-specific, no operator config needed.

Caddyfile stays single-block:

handle /openapi/v1/* {
  dify_rbac <action>
  reverse_proxy http://api:5001
}

Inner API — auth check-access (OAuth)

Gateway → enterprise svc. Plain http.Handler, Enterprise-Api-Secret-Key header for caller auth, snake_case JSON, {"error": "..."} failure shape.

POST /inner/api/auth/check-access-oauth
Headers:
  Enterprise-Api-Secret-Key: <INNER_API_KEY>
  Content-Type: application/json
Body:
  { "token": "dfoa_xxxxxxxxxxxxxxxxxx" }

Success body (200):

{
  "account_id":     "<uuid>",
  "tenant_id":      "<uuid>",
  "subject_type":   "account",
  "client_id":      "difyctl",
  "scope":          ["full"],
  "expires_at":     1750000000,
  "subject_email":  "<sso-only>",
  "subject_issuer": "<sso-only>"
}

account_id, subject_email, subject_issuer are omitempty. SSO subjects emit account_id="" + populated subject_email / subject_issuer; account subjects emit account_id populated + empty SSO fields.

Resolve logic

Same as api-side middleware step 4:

  1. sha256(token) for lookup key.
  2. Redis cache: GET auth:token:{hash}. Hit → return cached struct.
  3. DB read of oauth_access_tokens on cache miss (Skip: true ent schema).
  4. Cache result on hit (SETEX auth:token:{hash} 60 <json>); cache "invalid" (10 s TTL) on miss / expired / revoked.

Hard-expire

On expires_at <= NOW(), enterprise svc inner-API performs the same hard-expire as dify api Python middleware (CAS UPDATE oauth_access_tokens SET revoked_at = NOW(), token_hash = NULL WHERE id = :id AND revoked_at IS NULL, DEL auth:token:{hash}, SETEX auth:token:{hash} 10 "invalid", audit emit oauth.token_expired, return 401 token_expired). Same flow as tokens.md §Detection + hard-expire on middleware hit. Idempotent CAS makes concurrent hits safe.

Failure responses

HTTP Body Cause
405 {"error": "method not allowed"} Non-POST request
401 {"error": "invalid inner api key"} Enterprise-Api-Secret-Key missing or mismatched
400 {"error": "invalid request body: ..."} JSON decode failed
401 {"error": "invalid_token"} Hash miss in token store
401 {"error": "token_expired"} Row expires_at past (mutation performed — see Hard-expire)
401 {"error": "token_revoked"} Row revoked_at IS NOT NULL
500 {"error": "inner api secret key not configured"} Server INNER_API_KEY env empty

Gateway side translates these to the user-facing {code, message, hint} envelope before responding to CLI — see §Error envelope.

Cache

Layer Key TTL Purpose
Api-side AuthContext cache auth:token:{hash} 60 s Hot path on every request — gateway forwards do not bypass it

Cache key auth:token:{hash} shared across dify api and enterprise svc on the same Redis instance.

Failure modes

Both inner-API calls (token resolve + RBAC check) are fail-closed: any 5xx, network error, or timeout from /inner/api/auth/check-access-oauth or RBAC_INNER_CHECK_URL → 503 to the client. Matches existing dify_rbac behavior in pkg/gateway/rbac/rbac.go (writeRBACError(w, http.StatusServiceUnavailable, ...) on checker error).

Failure Gateway response
Resolve inner-API timeout / 5xx 503 {"error": "auth resolve unavailable"}
RBAC inner-API timeout / 5xx 503 {"error": "rbac check unavailable"} (existing)
Resolve returns 401 (token invalid / expired / revoked) 401 — see §Inner API failure table
RBAC returns allowed: false 403 {"error": "<reason>"} (existing)

Stale-cache fallback deferred. Operators rely on health monitoring and redundancy rather than gateway-side fault tolerance. CLI surfaces 503 as a transient error and retries with backoff (see auth.md §HTTP error handling).

Error envelope

Gateway translates inner-API {"error": "..."} responses into the user-facing { code, message, hint } envelope (see endpoints.md) before responding to CLI clients on /openapi/v1/*. CLI error handling stays uniform regardless of which layer denied the request.

Translation lives in the dify_rbac module's ServeHTTP exit path — same place as the existing writeRBACError, but keyed on the inner-API source.

Source HTTP Inner-API error CLI code CLI message CLI hint
resolve 401 invalid_token invalid_token Bearer token not recognized. Run difyctl auth login to mint a fresh token.
resolve 401 token_expired token_expired Bearer token has expired. Run difyctl auth login to mint a fresh token.
resolve 401 token_revoked token_revoked Bearer token was revoked. The owner revoked this token. Re-authenticate.
RBAC 403 <reason> rbac_denied <reason> (echoed) Ask your workspace admin to grant your role permission for this action.

Transient 5xx not translated. auth resolve unavailable and rbac check unavailable keep the existing {"error": "..."} shape — matches dify_rbac today and lets CLI treat them as transient and retry with backoff (see auth.md §HTTP error handling).

Inner-API auth failures (invalid inner api key) are operator-facing, not user-facing — gateway logs and returns 503 auth resolve unavailable to the client. Never leaks the inner-key state.

Deployment invariants

See security.md §Inner API trust boundary for the canonical invariants table. Two load-bearing for the gateway:

  • EE deploys MUST keep /openapi/v1/* reachable only through the gateway.
  • /inner/api/* MUST NOT be exposed on public ingress in any deploy.