mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 21:28:25 +08:00
feat(api): retire legacy /v1/* and /console/api device-flow mounts (Phase F)
Web and CLI consumers now hit /openapi/v1/* directly, so the dual-mount
shims can go:
- controllers/oauth_device_sso.py (legacy /v1/oauth/device/sso-* + /v1/device/sso-complete)
- controllers/service_api/oauth.py (legacy /v1/oauth/device/*, /v1/me, /v1/oauth/authorizations/self)
- controllers/console/auth/oauth_device.py (placeholder for legacy /console/api/oauth/device/{approve,deny})
- the deferred _register_legacy_console_mount() inside openapi/oauth_device.py
Imports in controllers/console/__init__.py, controllers/service_api/__init__.py,
and extensions/ext_blueprints.py pruned. Tests rewritten to openapi-only.
This commit is contained in:
parent
eb5ef3dba5
commit
0b3b0b5ce8
@ -81,7 +81,6 @@ from .auth import (
|
||||
forgot_password,
|
||||
login,
|
||||
oauth,
|
||||
oauth_device,
|
||||
oauth_server,
|
||||
)
|
||||
|
||||
@ -190,7 +189,6 @@ __all__ = [
|
||||
"models",
|
||||
"notification",
|
||||
"oauth",
|
||||
"oauth_device",
|
||||
"oauth_server",
|
||||
"ops_trace",
|
||||
"parameter",
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
"""Placeholder. Legacy /console/api/oauth/device/{approve,deny} mounts
|
||||
are registered from the canonical openapi handlers in
|
||||
controllers/openapi/oauth_device/{approve,deny}.py. This file stays
|
||||
on disk only so controllers/console/__init__.py's
|
||||
`from .auth import (... oauth_device, ...)` keeps working until
|
||||
Phase F retires the legacy paths and prunes that import.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@ -1,50 +0,0 @@
|
||||
"""Legacy /v1/* mounts for SSO-branch device-flow endpoints. Canonical
|
||||
handlers live in controllers/openapi/oauth_device_sso.py. This file
|
||||
just re-registers them on the legacy blueprint until Phase F retires
|
||||
the legacy paths entirely.
|
||||
|
||||
Note: /v1/device/sso-complete (no /oauth/ in the path) is the existing
|
||||
ACS callback. Its canonical home is /openapi/v1/oauth/device/sso-complete.
|
||||
IdP-side ACS callback URLs need re-registration before Phase F.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
from controllers.openapi.oauth_device_sso import (
|
||||
approval_context,
|
||||
approve_external,
|
||||
sso_complete,
|
||||
sso_initiate,
|
||||
)
|
||||
from libs.device_flow_security import attach_anti_framing
|
||||
|
||||
bp = Blueprint("oauth_device_sso", __name__, url_prefix="/v1")
|
||||
attach_anti_framing(bp)
|
||||
|
||||
# Legacy /v1/* mounts — handlers live in controllers/openapi/oauth_device_sso.py.
|
||||
# Removed in Phase F.
|
||||
bp.add_url_rule(
|
||||
"/oauth/device/sso-initiate",
|
||||
endpoint="sso_initiate",
|
||||
view_func=sso_initiate,
|
||||
methods=["GET"],
|
||||
)
|
||||
bp.add_url_rule(
|
||||
"/device/sso-complete",
|
||||
endpoint="sso_complete",
|
||||
view_func=sso_complete,
|
||||
methods=["GET"],
|
||||
)
|
||||
bp.add_url_rule(
|
||||
"/oauth/device/approval-context",
|
||||
endpoint="approval_context",
|
||||
view_func=approval_context,
|
||||
methods=["GET"],
|
||||
)
|
||||
bp.add_url_rule(
|
||||
"/oauth/device/approve-external",
|
||||
endpoint="approve_external",
|
||||
view_func=approve_external,
|
||||
methods=["POST"],
|
||||
)
|
||||
@ -1,10 +1,6 @@
|
||||
"""User-scoped account endpoints. /account is the bearer-authed
|
||||
identity read; /account/sessions and /account/sessions/<id> manage
|
||||
the user's active OAuth tokens (Phase C steps 11–12).
|
||||
|
||||
The /account class is also registered on the legacy /v1/me path from
|
||||
service_api/oauth.py until Phase F retires that mount. Likewise
|
||||
/account/sessions/self is re-mounted at /v1/oauth/authorizations/self.
|
||||
the user's active OAuth tokens.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@ -10,11 +10,7 @@ sub-groups in one module:
|
||||
POST /oauth/device/approve
|
||||
POST /oauth/device/deny
|
||||
|
||||
The five Resource classes are also re-registered on legacy mounts:
|
||||
service_api_ns at /v1/oauth/device/{code,token,lookup} (from
|
||||
service_api/oauth.py) and console_ns at /console/api/oauth/device/{approve,deny}
|
||||
(from the deferred _register_legacy_console_mount() at module bottom).
|
||||
All legacy mounts retire in Phase F. SSO branch lives in oauth_device_sso.py.
|
||||
SSO branch lives in oauth_device_sso.py.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@ -369,17 +365,3 @@ def _emit_deny_audit(state) -> None:
|
||||
)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Legacy console-side mount — deferred import breaks a cycle that would
|
||||
# form between this module (imports controllers.console.wraps) and
|
||||
# controllers.console.__init__ (loads .auth.oauth_device).
|
||||
# =========================================================================
|
||||
|
||||
|
||||
def _register_legacy_console_mount() -> None:
|
||||
from controllers.console import console_ns
|
||||
console_ns.add_resource(DeviceApproveApi, "/oauth/device/approve")
|
||||
console_ns.add_resource(DeviceDenyApi, "/oauth/device/deny")
|
||||
|
||||
|
||||
_register_legacy_console_mount()
|
||||
|
||||
@ -8,8 +8,6 @@ EE-only. Browser flow:
|
||||
|
||||
Function-based (raw @bp.route) rather than Resource classes because the
|
||||
handlers do redirects + cookie kwargs that don't fit the Resource shape.
|
||||
Same handlers are also re-registered on the legacy /v1/* paths from
|
||||
controllers/oauth_device_sso.py until Phase F retires the legacy mount.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ api = ExternalApi(
|
||||
|
||||
service_api_ns = Namespace("service_api", description="Service operations", path="/")
|
||||
|
||||
from . import index, oauth
|
||||
from . import index
|
||||
from .app import (
|
||||
annotation,
|
||||
app,
|
||||
@ -54,7 +54,6 @@ __all__ = [
|
||||
"message",
|
||||
"metadata",
|
||||
"models",
|
||||
"oauth",
|
||||
"rag_pipeline_workflow",
|
||||
"segment",
|
||||
"site",
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
"""Legacy /v1/* mounts for the OAuth bearer + device-flow endpoints.
|
||||
Canonical handlers live in controllers/openapi/. This file just
|
||||
re-registers them on service_api_ns until Phase F retires the
|
||||
legacy paths entirely.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from controllers.openapi.account import AccountApi, AccountSessionsSelfApi
|
||||
from controllers.openapi.oauth_device import (
|
||||
OAuthDeviceCodeApi,
|
||||
OAuthDeviceLookupApi,
|
||||
OAuthDeviceTokenApi,
|
||||
)
|
||||
from controllers.service_api import service_api_ns
|
||||
|
||||
# Legacy /v1/* mounts — handlers live in controllers/openapi/.
|
||||
# Removed in Phase F.
|
||||
service_api_ns.add_resource(OAuthDeviceCodeApi, "/oauth/device/code")
|
||||
service_api_ns.add_resource(OAuthDeviceTokenApi, "/oauth/device/token")
|
||||
service_api_ns.add_resource(OAuthDeviceLookupApi, "/oauth/device/lookup")
|
||||
service_api_ns.add_resource(AccountApi, "/me")
|
||||
service_api_ns.add_resource(AccountSessionsSelfApi, "/oauth/authorizations/self")
|
||||
@ -109,15 +109,6 @@ def init_app(app: DifyApp):
|
||||
app.register_blueprint(inner_api_bp)
|
||||
app.register_blueprint(mcp_bp)
|
||||
|
||||
# SSO-branch device-flow routes. No CORS config — these endpoints are
|
||||
# user-interactive (same-origin browser traffic) and cookie-authed;
|
||||
# allowing cross-origin would defeat the SameSite=Lax cookie's purpose.
|
||||
# Gated on ENABLE_OAUTH_BEARER: without the bearer authenticator, tokens
|
||||
# minted here cannot be validated, so skip the blueprint entirely.
|
||||
if dify_config.ENABLE_OAUTH_BEARER:
|
||||
from controllers.oauth_device_sso import bp as oauth_device_sso_bp
|
||||
app.register_blueprint(oauth_device_sso_bp)
|
||||
|
||||
# Register trigger blueprint with CORS for webhook calls
|
||||
_apply_cors_once(
|
||||
trigger_bp,
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
"""Phase C steps 9–10: identity + self-revoke moved to /openapi/v1/account.
|
||||
Legacy /v1/me + /v1/oauth/authorizations/self stay mounted via
|
||||
re-registration in service_api/oauth.py.
|
||||
"""
|
||||
"""User-scoped identity + session endpoints under /openapi/v1/account."""
|
||||
import builtins
|
||||
|
||||
import pytest
|
||||
@ -15,17 +12,15 @@ from controllers.openapi.account import (
|
||||
AccountSessionsApi,
|
||||
AccountSessionsSelfApi,
|
||||
)
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
|
||||
if not hasattr(builtins, "MethodView"):
|
||||
builtins.MethodView = MethodView # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dual_app() -> Flask:
|
||||
def openapi_app() -> Flask:
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.register_blueprint(service_api_bp)
|
||||
app.register_blueprint(openapi_bp)
|
||||
return app
|
||||
|
||||
@ -34,71 +29,55 @@ def _rule(app: Flask, path: str):
|
||||
return next(r for r in app.url_map.iter_rules() if r.rule == path)
|
||||
|
||||
|
||||
def test_account_route_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
def test_account_route_registered(openapi_app: Flask):
|
||||
rules = {r.rule for r in openapi_app.url_map.iter_rules()}
|
||||
assert "/openapi/v1/account" in rules
|
||||
|
||||
|
||||
def test_legacy_me_route_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
assert "/v1/me" in rules
|
||||
def test_account_dispatches_to_class(openapi_app: Flask):
|
||||
rule = _rule(openapi_app, "/openapi/v1/account")
|
||||
assert openapi_app.view_functions[rule.endpoint].view_class is AccountApi
|
||||
|
||||
|
||||
def test_account_and_me_dispatch_to_same_class(dual_app: Flask):
|
||||
new = _rule(dual_app, "/openapi/v1/account")
|
||||
legacy = _rule(dual_app, "/v1/me")
|
||||
assert dual_app.view_functions[new.endpoint].view_class is AccountApi
|
||||
assert dual_app.view_functions[legacy.endpoint].view_class is AccountApi
|
||||
|
||||
|
||||
def test_account_sessions_self_route_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
def test_account_sessions_self_route_registered(openapi_app: Flask):
|
||||
rules = {r.rule for r in openapi_app.url_map.iter_rules()}
|
||||
assert "/openapi/v1/account/sessions/self" in rules
|
||||
|
||||
|
||||
def test_legacy_oauth_authorizations_self_route_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
assert "/v1/oauth/authorizations/self" in rules
|
||||
def test_sessions_self_dispatches_to_class(openapi_app: Flask):
|
||||
rule = _rule(openapi_app, "/openapi/v1/account/sessions/self")
|
||||
assert openapi_app.view_functions[rule.endpoint].view_class is AccountSessionsSelfApi
|
||||
|
||||
|
||||
def test_sessions_self_paths_dispatch_to_same_class(dual_app: Flask):
|
||||
new = _rule(dual_app, "/openapi/v1/account/sessions/self")
|
||||
legacy = _rule(dual_app, "/v1/oauth/authorizations/self")
|
||||
assert dual_app.view_functions[new.endpoint].view_class is AccountSessionsSelfApi
|
||||
assert dual_app.view_functions[legacy.endpoint].view_class is AccountSessionsSelfApi
|
||||
|
||||
|
||||
def test_account_methods(dual_app: Flask):
|
||||
rule = _rule(dual_app, "/openapi/v1/account")
|
||||
def test_account_methods(openapi_app: Flask):
|
||||
rule = _rule(openapi_app, "/openapi/v1/account")
|
||||
assert "GET" in rule.methods
|
||||
|
||||
|
||||
def test_sessions_self_methods(dual_app: Flask):
|
||||
rule = _rule(dual_app, "/openapi/v1/account/sessions/self")
|
||||
def test_sessions_self_methods(openapi_app: Flask):
|
||||
rule = _rule(openapi_app, "/openapi/v1/account/sessions/self")
|
||||
assert "DELETE" in rule.methods
|
||||
|
||||
|
||||
def test_sessions_list_route_registered(dual_app: Flask):
|
||||
"""GET /openapi/v1/account/sessions is new — no /v1/ equivalent."""
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
def test_sessions_list_route_registered(openapi_app: Flask):
|
||||
rules = {r.rule for r in openapi_app.url_map.iter_rules()}
|
||||
assert "/openapi/v1/account/sessions" in rules
|
||||
|
||||
|
||||
def test_sessions_list_dispatches_to_sessions_api(dual_app: Flask):
|
||||
rule = _rule(dual_app, "/openapi/v1/account/sessions")
|
||||
assert dual_app.view_functions[rule.endpoint].view_class is AccountSessionsApi
|
||||
def test_sessions_list_dispatches_to_sessions_api(openapi_app: Flask):
|
||||
rule = _rule(openapi_app, "/openapi/v1/account/sessions")
|
||||
assert openapi_app.view_functions[rule.endpoint].view_class is AccountSessionsApi
|
||||
assert "GET" in rule.methods
|
||||
|
||||
|
||||
def test_session_by_id_route_registered(dual_app: Flask):
|
||||
"""DELETE /openapi/v1/account/sessions/<id> is new — no /v1/ equivalent."""
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
def test_session_by_id_route_registered(openapi_app: Flask):
|
||||
rules = {r.rule for r in openapi_app.url_map.iter_rules()}
|
||||
assert "/openapi/v1/account/sessions/<string:session_id>" in rules
|
||||
|
||||
|
||||
def test_session_by_id_dispatches_to_correct_class(dual_app: Flask):
|
||||
rule = _rule(dual_app, "/openapi/v1/account/sessions/<string:session_id>")
|
||||
assert dual_app.view_functions[rule.endpoint].view_class is AccountSessionByIdApi
|
||||
def test_session_by_id_dispatches_to_correct_class(openapi_app: Flask):
|
||||
rule = _rule(openapi_app, "/openapi/v1/account/sessions/<string:session_id>")
|
||||
assert openapi_app.view_functions[rule.endpoint].view_class is AccountSessionByIdApi
|
||||
assert "DELETE" in rule.methods
|
||||
|
||||
|
||||
|
||||
@ -1,14 +1,10 @@
|
||||
"""Phase D steps 13-14: device-flow approve/deny lifted to /openapi/v1.
|
||||
Legacy /console/api/oauth/device/{approve,deny} stays mounted via
|
||||
re-registration in console/auth/oauth_device.py.
|
||||
"""
|
||||
"""Account-branch device-flow approve/deny under /openapi/v1."""
|
||||
import builtins
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from flask.views import MethodView
|
||||
|
||||
from controllers.console import bp as console_bp
|
||||
from controllers.openapi import bp as openapi_bp
|
||||
from controllers.openapi.oauth_device import DeviceApproveApi, DeviceDenyApi
|
||||
|
||||
@ -17,10 +13,9 @@ if not hasattr(builtins, "MethodView"):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dual_app() -> Flask:
|
||||
def openapi_app() -> Flask:
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.register_blueprint(console_bp)
|
||||
app.register_blueprint(openapi_bp)
|
||||
return app
|
||||
|
||||
@ -29,42 +24,28 @@ def _rule(app: Flask, path: str):
|
||||
return next(r for r in app.url_map.iter_rules() if r.rule == path)
|
||||
|
||||
|
||||
def test_openapi_approve_route_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
def test_approve_route_registered(openapi_app: Flask):
|
||||
rules = {r.rule for r in openapi_app.url_map.iter_rules()}
|
||||
assert "/openapi/v1/oauth/device/approve" in rules
|
||||
|
||||
|
||||
def test_legacy_console_approve_route_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
assert "/console/api/oauth/device/approve" in rules
|
||||
|
||||
|
||||
def test_approve_paths_dispatch_to_same_class(dual_app: Flask):
|
||||
new = _rule(dual_app, "/openapi/v1/oauth/device/approve")
|
||||
legacy = _rule(dual_app, "/console/api/oauth/device/approve")
|
||||
assert dual_app.view_functions[new.endpoint].view_class is DeviceApproveApi
|
||||
assert dual_app.view_functions[legacy.endpoint].view_class is DeviceApproveApi
|
||||
|
||||
|
||||
def test_openapi_deny_route_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
def test_deny_route_registered(openapi_app: Flask):
|
||||
rules = {r.rule for r in openapi_app.url_map.iter_rules()}
|
||||
assert "/openapi/v1/oauth/device/deny" in rules
|
||||
|
||||
|
||||
def test_legacy_console_deny_route_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
assert "/console/api/oauth/device/deny" in rules
|
||||
def test_approve_dispatches_to_class(openapi_app: Flask):
|
||||
rule = _rule(openapi_app, "/openapi/v1/oauth/device/approve")
|
||||
assert openapi_app.view_functions[rule.endpoint].view_class is DeviceApproveApi
|
||||
|
||||
|
||||
def test_deny_paths_dispatch_to_same_class(dual_app: Flask):
|
||||
new = _rule(dual_app, "/openapi/v1/oauth/device/deny")
|
||||
legacy = _rule(dual_app, "/console/api/oauth/device/deny")
|
||||
assert dual_app.view_functions[new.endpoint].view_class is DeviceDenyApi
|
||||
assert dual_app.view_functions[legacy.endpoint].view_class is DeviceDenyApi
|
||||
def test_deny_dispatches_to_class(openapi_app: Flask):
|
||||
rule = _rule(openapi_app, "/openapi/v1/oauth/device/deny")
|
||||
assert openapi_app.view_functions[rule.endpoint].view_class is DeviceDenyApi
|
||||
|
||||
|
||||
def test_approve_and_deny_methods(dual_app: Flask):
|
||||
approve = _rule(dual_app, "/openapi/v1/oauth/device/approve")
|
||||
deny = _rule(dual_app, "/openapi/v1/oauth/device/deny")
|
||||
def test_approve_and_deny_methods(openapi_app: Flask):
|
||||
approve = _rule(openapi_app, "/openapi/v1/oauth/device/approve")
|
||||
deny = _rule(openapi_app, "/openapi/v1/oauth/device/deny")
|
||||
assert "POST" in approve.methods
|
||||
assert "POST" in deny.methods
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
"""Phase B step 6: POST /openapi/v1/oauth/device/code is the canonical
|
||||
RFC 8628 device authorization endpoint. The legacy /v1/oauth/device/code
|
||||
mount stays until Phase F; both paths must dispatch to the same class.
|
||||
"""POST /openapi/v1/oauth/device/code is the canonical RFC 8628 device
|
||||
authorization endpoint.
|
||||
|
||||
Tests verify URL routing and re-registration without invoking the
|
||||
handler — invoking would require Redis, which the unit-test runtime
|
||||
does not initialise.
|
||||
Tests verify URL routing without invoking the handler — invoking would
|
||||
require Redis, which the unit-test runtime does not initialise.
|
||||
"""
|
||||
import builtins
|
||||
|
||||
@ -14,65 +12,36 @@ from flask.views import MethodView
|
||||
|
||||
from controllers.openapi import bp as openapi_bp
|
||||
from controllers.openapi.oauth_device import OAuthDeviceCodeApi
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
|
||||
if not hasattr(builtins, "MethodView"):
|
||||
builtins.MethodView = MethodView # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dual_app() -> Flask:
|
||||
"""Both blueprints registered, mirroring production layout."""
|
||||
def openapi_app() -> Flask:
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.register_blueprint(service_api_bp)
|
||||
app.register_blueprint(openapi_bp)
|
||||
return app
|
||||
|
||||
|
||||
def test_openapi_route_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
def test_openapi_route_registered(openapi_app: Flask):
|
||||
rules = {r.rule for r in openapi_app.url_map.iter_rules()}
|
||||
assert "/openapi/v1/oauth/device/code" in rules
|
||||
|
||||
|
||||
def test_legacy_v1_route_still_registered(dual_app: Flask):
|
||||
"""service_api/oauth.py re-registers the lifted class on /v1/."""
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
assert "/v1/oauth/device/code" in rules
|
||||
|
||||
|
||||
def test_both_paths_dispatch_to_same_class(dual_app: Flask):
|
||||
"""Single source of truth — no duplicated handler logic."""
|
||||
new = next(
|
||||
r for r in dual_app.url_map.iter_rules() if r.rule == "/openapi/v1/oauth/device/code"
|
||||
def test_route_dispatches_to_class(openapi_app: Flask):
|
||||
rule = next(
|
||||
r for r in openapi_app.url_map.iter_rules() if r.rule == "/openapi/v1/oauth/device/code"
|
||||
)
|
||||
legacy = next(
|
||||
r for r in dual_app.url_map.iter_rules() if r.rule == "/v1/oauth/device/code"
|
||||
assert openapi_app.view_functions[rule.endpoint].view_class is OAuthDeviceCodeApi
|
||||
|
||||
|
||||
def test_route_accepts_post(openapi_app: Flask):
|
||||
rule = next(
|
||||
r for r in openapi_app.url_map.iter_rules() if r.rule == "/openapi/v1/oauth/device/code"
|
||||
)
|
||||
|
||||
new_view = dual_app.view_functions[new.endpoint]
|
||||
legacy_view = dual_app.view_functions[legacy.endpoint]
|
||||
# Flask-RESTX wraps Resource classes in a `view_class` attribute.
|
||||
assert new_view.view_class is OAuthDeviceCodeApi
|
||||
assert legacy_view.view_class is OAuthDeviceCodeApi
|
||||
|
||||
|
||||
def test_route_accepts_post_and_options(dual_app: Flask):
|
||||
new = next(
|
||||
r for r in dual_app.url_map.iter_rules() if r.rule == "/openapi/v1/oauth/device/code"
|
||||
)
|
||||
legacy = next(
|
||||
r for r in dual_app.url_map.iter_rules() if r.rule == "/v1/oauth/device/code"
|
||||
)
|
||||
assert "POST" in new.methods
|
||||
assert "POST" in legacy.methods
|
||||
|
||||
|
||||
def test_handler_class_imports_match():
|
||||
"""service_api re-uses the openapi class, not a copy."""
|
||||
from controllers.service_api import oauth as service_api_oauth
|
||||
|
||||
assert service_api_oauth.OAuthDeviceCodeApi is OAuthDeviceCodeApi
|
||||
assert "POST" in rule.methods
|
||||
|
||||
|
||||
def test_known_client_ids_default_includes_difyctl():
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
"""Phase B step 8: GET /openapi/v1/oauth/device/lookup mounted via the
|
||||
canonical class. Legacy /v1/oauth/device/lookup re-registered. Both
|
||||
paths must dispatch to the same class.
|
||||
"""
|
||||
"""GET /openapi/v1/oauth/device/lookup is the canonical user-code lookup."""
|
||||
import builtins
|
||||
|
||||
import pytest
|
||||
@ -10,44 +7,33 @@ from flask.views import MethodView
|
||||
|
||||
from controllers.openapi import bp as openapi_bp
|
||||
from controllers.openapi.oauth_device import OAuthDeviceLookupApi
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
|
||||
if not hasattr(builtins, "MethodView"):
|
||||
builtins.MethodView = MethodView # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dual_app() -> Flask:
|
||||
def openapi_app() -> Flask:
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.register_blueprint(service_api_bp)
|
||||
app.register_blueprint(openapi_bp)
|
||||
return app
|
||||
|
||||
|
||||
def test_openapi_route_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
def test_openapi_route_registered(openapi_app: Flask):
|
||||
rules = {r.rule for r in openapi_app.url_map.iter_rules()}
|
||||
assert "/openapi/v1/oauth/device/lookup" in rules
|
||||
|
||||
|
||||
def test_legacy_v1_route_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
assert "/v1/oauth/device/lookup" in rules
|
||||
|
||||
|
||||
def test_both_paths_dispatch_to_same_class(dual_app: Flask):
|
||||
new = next(
|
||||
r for r in dual_app.url_map.iter_rules() if r.rule == "/openapi/v1/oauth/device/lookup"
|
||||
def test_route_dispatches_to_class(openapi_app: Flask):
|
||||
rule = next(
|
||||
r for r in openapi_app.url_map.iter_rules() if r.rule == "/openapi/v1/oauth/device/lookup"
|
||||
)
|
||||
legacy = next(
|
||||
r for r in dual_app.url_map.iter_rules() if r.rule == "/v1/oauth/device/lookup"
|
||||
)
|
||||
assert dual_app.view_functions[new.endpoint].view_class is OAuthDeviceLookupApi
|
||||
assert dual_app.view_functions[legacy.endpoint].view_class is OAuthDeviceLookupApi
|
||||
assert openapi_app.view_functions[rule.endpoint].view_class is OAuthDeviceLookupApi
|
||||
|
||||
|
||||
def test_route_accepts_get(dual_app: Flask):
|
||||
new = next(
|
||||
r for r in dual_app.url_map.iter_rules() if r.rule == "/openapi/v1/oauth/device/lookup"
|
||||
def test_route_accepts_get(openapi_app: Flask):
|
||||
rule = next(
|
||||
r for r in openapi_app.url_map.iter_rules() if r.rule == "/openapi/v1/oauth/device/lookup"
|
||||
)
|
||||
assert "GET" in new.methods
|
||||
assert "GET" in rule.methods
|
||||
|
||||
@ -1,14 +1,10 @@
|
||||
"""Phase D steps 15-16: SSO-branch device-flow endpoints lifted to
|
||||
/openapi/v1/oauth/device/. Legacy /v1/* mounts stay via re-registration
|
||||
in controllers/oauth_device_sso.py.
|
||||
"""
|
||||
"""SSO-branch device-flow endpoints under /openapi/v1/oauth/device/."""
|
||||
import builtins
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from flask.views import MethodView
|
||||
|
||||
from controllers.oauth_device_sso import bp as legacy_sso_bp
|
||||
from controllers.openapi import bp as openapi_bp
|
||||
from controllers.openapi.oauth_device_sso import (
|
||||
approval_context,
|
||||
@ -22,10 +18,9 @@ if not hasattr(builtins, "MethodView"):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dual_app() -> Flask:
|
||||
def openapi_app() -> Flask:
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.register_blueprint(legacy_sso_bp)
|
||||
app.register_blueprint(openapi_bp)
|
||||
return app
|
||||
|
||||
@ -34,88 +29,49 @@ def _rule(app: Flask, path: str):
|
||||
return next(r for r in app.url_map.iter_rules() if r.rule == path)
|
||||
|
||||
|
||||
# Canonical /openapi/v1/* paths
|
||||
|
||||
|
||||
def test_sso_initiate_canonical_path_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
def test_sso_initiate_registered(openapi_app: Flask):
|
||||
rules = {r.rule for r in openapi_app.url_map.iter_rules()}
|
||||
assert "/openapi/v1/oauth/device/sso-initiate" in rules
|
||||
|
||||
|
||||
def test_sso_complete_canonical_path_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
def test_sso_complete_registered(openapi_app: Flask):
|
||||
rules = {r.rule for r in openapi_app.url_map.iter_rules()}
|
||||
assert "/openapi/v1/oauth/device/sso-complete" in rules
|
||||
|
||||
|
||||
def test_approval_context_canonical_path_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
def test_approval_context_registered(openapi_app: Flask):
|
||||
rules = {r.rule for r in openapi_app.url_map.iter_rules()}
|
||||
assert "/openapi/v1/oauth/device/approval-context" in rules
|
||||
|
||||
|
||||
def test_approve_external_canonical_path_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
def test_approve_external_registered(openapi_app: Flask):
|
||||
rules = {r.rule for r in openapi_app.url_map.iter_rules()}
|
||||
assert "/openapi/v1/oauth/device/approve-external" in rules
|
||||
|
||||
|
||||
# Legacy /v1/* paths
|
||||
def test_sso_initiate_dispatches_to_function(openapi_app: Flask):
|
||||
rule = _rule(openapi_app, "/openapi/v1/oauth/device/sso-initiate")
|
||||
assert openapi_app.view_functions[rule.endpoint] is sso_initiate
|
||||
|
||||
|
||||
def test_sso_initiate_legacy_path_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
assert "/v1/oauth/device/sso-initiate" in rules
|
||||
def test_sso_complete_dispatches_to_function(openapi_app: Flask):
|
||||
rule = _rule(openapi_app, "/openapi/v1/oauth/device/sso-complete")
|
||||
assert openapi_app.view_functions[rule.endpoint] is sso_complete
|
||||
|
||||
|
||||
def test_sso_complete_legacy_path_registered(dual_app: Flask):
|
||||
"""Legacy lived under /v1/device/, not /v1/oauth/device/."""
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
assert "/v1/device/sso-complete" in rules
|
||||
def test_approval_context_dispatches_to_function(openapi_app: Flask):
|
||||
rule = _rule(openapi_app, "/openapi/v1/oauth/device/approval-context")
|
||||
assert openapi_app.view_functions[rule.endpoint] is approval_context
|
||||
|
||||
|
||||
def test_approval_context_legacy_path_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
assert "/v1/oauth/device/approval-context" in rules
|
||||
|
||||
|
||||
def test_approve_external_legacy_path_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
assert "/v1/oauth/device/approve-external" in rules
|
||||
|
||||
|
||||
# Both paths point at the same view function
|
||||
|
||||
|
||||
def test_sso_initiate_dual_mount_same_function(dual_app: Flask):
|
||||
new_rule = _rule(dual_app, "/openapi/v1/oauth/device/sso-initiate")
|
||||
legacy_rule = _rule(dual_app, "/v1/oauth/device/sso-initiate")
|
||||
assert dual_app.view_functions[new_rule.endpoint] is sso_initiate
|
||||
assert dual_app.view_functions[legacy_rule.endpoint] is sso_initiate
|
||||
|
||||
|
||||
def test_sso_complete_dual_mount_same_function(dual_app: Flask):
|
||||
new_rule = _rule(dual_app, "/openapi/v1/oauth/device/sso-complete")
|
||||
legacy_rule = _rule(dual_app, "/v1/device/sso-complete")
|
||||
assert dual_app.view_functions[new_rule.endpoint] is sso_complete
|
||||
assert dual_app.view_functions[legacy_rule.endpoint] is sso_complete
|
||||
|
||||
|
||||
def test_approval_context_dual_mount_same_function(dual_app: Flask):
|
||||
new_rule = _rule(dual_app, "/openapi/v1/oauth/device/approval-context")
|
||||
legacy_rule = _rule(dual_app, "/v1/oauth/device/approval-context")
|
||||
assert dual_app.view_functions[new_rule.endpoint] is approval_context
|
||||
assert dual_app.view_functions[legacy_rule.endpoint] is approval_context
|
||||
|
||||
|
||||
def test_approve_external_dual_mount_same_function(dual_app: Flask):
|
||||
new_rule = _rule(dual_app, "/openapi/v1/oauth/device/approve-external")
|
||||
legacy_rule = _rule(dual_app, "/v1/oauth/device/approve-external")
|
||||
assert dual_app.view_functions[new_rule.endpoint] is approve_external
|
||||
assert dual_app.view_functions[legacy_rule.endpoint] is approve_external
|
||||
def test_approve_external_dispatches_to_function(openapi_app: Flask):
|
||||
rule = _rule(openapi_app, "/openapi/v1/oauth/device/approve-external")
|
||||
assert openapi_app.view_functions[rule.endpoint] is approve_external
|
||||
|
||||
|
||||
def test_sso_complete_idp_callback_url_uses_canonical_path():
|
||||
"""sso_initiate hardcodes the IdP callback URL — must point to the
|
||||
canonical /openapi/v1/ path so IdPs are configured against the
|
||||
forward-looking ACS endpoint, not the legacy alias.
|
||||
"""sso_initiate hardcodes the IdP callback URL — must point at the
|
||||
canonical /openapi/v1/ path so IdP-side ACS configuration matches.
|
||||
"""
|
||||
from controllers.openapi import oauth_device_sso
|
||||
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
"""Phase B step 7: POST /openapi/v1/oauth/device/token mounted via the
|
||||
canonical class. Legacy /v1/oauth/device/token re-registered. Both
|
||||
paths must dispatch to the same class.
|
||||
"""
|
||||
"""POST /openapi/v1/oauth/device/token is the canonical poll endpoint."""
|
||||
import builtins
|
||||
|
||||
import pytest
|
||||
@ -10,37 +7,26 @@ from flask.views import MethodView
|
||||
|
||||
from controllers.openapi import bp as openapi_bp
|
||||
from controllers.openapi.oauth_device import OAuthDeviceTokenApi
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
|
||||
if not hasattr(builtins, "MethodView"):
|
||||
builtins.MethodView = MethodView # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dual_app() -> Flask:
|
||||
def openapi_app() -> Flask:
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.register_blueprint(service_api_bp)
|
||||
app.register_blueprint(openapi_bp)
|
||||
return app
|
||||
|
||||
|
||||
def test_openapi_route_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
def test_openapi_route_registered(openapi_app: Flask):
|
||||
rules = {r.rule for r in openapi_app.url_map.iter_rules()}
|
||||
assert "/openapi/v1/oauth/device/token" in rules
|
||||
|
||||
|
||||
def test_legacy_v1_route_registered(dual_app: Flask):
|
||||
rules = {r.rule for r in dual_app.url_map.iter_rules()}
|
||||
assert "/v1/oauth/device/token" in rules
|
||||
|
||||
|
||||
def test_both_paths_dispatch_to_same_class(dual_app: Flask):
|
||||
new = next(
|
||||
r for r in dual_app.url_map.iter_rules() if r.rule == "/openapi/v1/oauth/device/token"
|
||||
def test_route_dispatches_to_class(openapi_app: Flask):
|
||||
rule = next(
|
||||
r for r in openapi_app.url_map.iter_rules() if r.rule == "/openapi/v1/oauth/device/token"
|
||||
)
|
||||
legacy = next(
|
||||
r for r in dual_app.url_map.iter_rules() if r.rule == "/v1/oauth/device/token"
|
||||
)
|
||||
assert dual_app.view_functions[new.endpoint].view_class is OAuthDeviceTokenApi
|
||||
assert dual_app.view_functions[legacy.endpoint].view_class is OAuthDeviceTokenApi
|
||||
assert openapi_app.view_functions[rule.endpoint].view_class is OAuthDeviceTokenApi
|
||||
|
||||
Loading…
Reference in New Issue
Block a user