Skip to content

Commit

Permalink
support custom swagger config, oauth config, and the swagger oauth2-r…
Browse files Browse the repository at this point in the history
…edirect html
  • Loading branch information
mmerickel committed Dec 6, 2024
1 parent 92797d9 commit 6e700b5
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 18 deletions.
48 changes: 46 additions & 2 deletions pyramid_openapi3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from urllib.parse import urlparse

import hupper
import json
import logging
import typing as t

Expand Down Expand Up @@ -147,6 +148,11 @@ def add_explorer_view(
route_name: str = "pyramid_openapi3.explorer",
template: str = "static/index.html",
ui_version: str = "5.12.0",
ui_config: t.Optional[dict[str, t.Any]] = None,
oauth_config: t.Optional[dict[str, t.Any]] = None,
oauth_redirect_route: t.Optional[str] = None,
oauth_redirect_route_name: str = "pyramid_openapi3.explorer.oauth2-redirect",
oauth_redirect_html: str = "static/oauth2-redirect.html",
permission: str = NO_PERMISSION_REQUIRED,
apiname: str = "pyramid_openapi3",
) -> None:
Expand All @@ -156,11 +162,28 @@ def add_explorer_view(
:param route_name: Route name that's being added
:param template: Dotted path to the html template that renders Swagger UI response
:param ui_version: Swagger UI version string
:param ui_config:
A dictionary conforming to the SwaggerUI API.
Any settings defined here will override those defined by default.
:param oauth_config:
If defined, then SwaggerUI.initOAuth will be invoked with the supplied config.
:param oauth_redirect_route:
URL path where the redirect will be served. By default the path is constructed
by appending a ``/oauth2-redirect`` path component to the ``route`` parameter.
:param oauth_redirect_route_name:
Route name for the redirect route.
:param oauth_redirect_html:
Dotted path to the html that renders the oauth2-redirect HTML.
:param permission: Permission for the explorer view
"""

if oauth_redirect_route is None:
oauth_redirect_route = route.rstrip("/") + "/oauth2-redirect"

def register() -> None:
resolved_template = AssetResolver().resolve(template)
asset_resolver = AssetResolver()
resolved_template = asset_resolver.resolve(template)
redirect_html = asset_resolver.resolve(oauth_redirect_html)

def explorer_view(request: Request) -> Response:
settings = config.registry.settings
Expand All @@ -171,9 +194,20 @@ def explorer_view(request: Request) -> Response:
)
with open(resolved_template.abspath()) as f:
template = Template(f.read())
merged_ui_config = {
"url": request.route_path(settings[apiname]["spec_route_name"]),
"dom_id": "#swagger-ui",
"deepLinking": True,
"validatorUrl": None,
"layout": "StandaloneLayout",
"oauth2RedirectUrl": request.route_url(oauth_redirect_route_name),
}
if ui_config:
merged_ui_config.update(ui_config)
html = template.safe_substitute(
ui_version=ui_version,
spec_url=request.route_path(settings[apiname]["spec_route_name"]),
ui_config=json.dumps(merged_ui_config),
oauth_config=json.dumps(oauth_config),
)
return Response(html)

Expand All @@ -182,6 +216,16 @@ def explorer_view(request: Request) -> Response:
route_name=route_name, permission=permission, view=explorer_view
)

def redirect_view(request: Request) -> FileResponse:
return FileResponse(redirect_html.abspath())

config.add_route(oauth_redirect_route_name, oauth_redirect_route)
config.add_view(
route_name=oauth_redirect_route_name,
permission=permission,
view=redirect_view,
)

config.action((f"{apiname}_add_explorer",), register, order=PHASE0_CONFIG)


Expand Down
23 changes: 12 additions & 11 deletions pyramid_openapi3/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,23 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/${ui_version}/swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function() {
// Build a system
const ui = SwaggerUIBundle({
url: "${spec_url}",
dom_id: '#swagger-ui',
deepLinking: true,
const uiConfig = ${ui_config};
Object.assign(uiConfig, {
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
SwaggerUIStandalonePreset,
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
SwaggerUIBundle.plugins.DownloadUrl,
],
validatorUrl: null,
layout: "StandaloneLayout"
})
window.ui = ui
};
const oauthConfig = ${oauth_config};
// Build a system
const ui = SwaggerUIBundle(uiConfig);
if (oauthConfig) {
ui.initOAuth(oauthConfig);
}
window.ui = ui;
}
</script>
</body>
Expand Down
79 changes: 79 additions & 0 deletions pyramid_openapi3/static/oauth2-redirect.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;

if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1).replace('?', '&');
} else {
qp = location.search.substring(1);
}

arr = qp.split("&");
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value);
}
) : {};

isValid = qp.state === sentState;

if ((
oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
});
}

if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}

oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}

if (document.readyState !== 'loading') {
run();
} else {
document.addEventListener('DOMContentLoaded', function () {
run();
});
}
</script>
</body>
</html>
60 changes: 55 additions & 5 deletions pyramid_openapi3/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ def test_add_explorer_view() -> None:
document.name, route="/foo.yaml", route_name="foo_api_spec"
)

config.pyramid_openapi3_add_explorer()
config.pyramid_openapi3_add_explorer(ui_config={"deepLinking": False})
request = config.registry.queryUtility(
IRouteRequest, name="pyramid_openapi3.explorer"
)
Expand All @@ -301,6 +301,56 @@ def test_add_explorer_view() -> None:
)
response = view(request=DummyRequest(config=config), context=None)
assert b"<title>Swagger UI</title>" in response.body
assert b"const oauthConfig = null;" in response.body
assert b'"deepLinking": false' in response.body
assert (
b'"oauth2RedirectUrl": "http://example.com/docs/oauth2-redirect"'
in response.body
)


def test_add_explorer_oauth_view() -> None:
"""Test registration of a view serving the Swagger UI."""
with testConfig() as config:
config.include("pyramid_openapi3")

with tempfile.NamedTemporaryFile() as document:
document.write(MINIMAL_DOCUMENT)
document.seek(0)

config.pyramid_openapi3_spec(
document.name, route="/foo.yaml", route_name="foo_api_spec"
)

config.pyramid_openapi3_add_explorer(
oauth_redirect_route="/dummy/oauth2-redirect",
oauth_config={
"usePkceWithAuthorizationCodeGrant": True,
},
)
request = config.registry.queryUtility(
IRouteRequest, name="pyramid_openapi3.explorer"
)
view = config.registry.adapters.registered(
(IViewClassifier, request, Interface), IView, name=""
)
response = view(request=DummyRequest(config=config), context=None)
assert b"<title>Swagger UI</title>" in response.body
assert b"ui.initOAuth(" in response.body
assert b"usePkceWithAuthorizationCodeGrant" in response.body
assert (
b'"oauth2RedirectUrl": "http://example.com/dummy/oauth2-redirect"'
in response.body
)

request = config.registry.queryUtility(
IRouteRequest, name="pyramid_openapi3.explorer.oauth2-redirect"
)
view = config.registry.adapters.registered(
(IViewClassifier, request, Interface), IView, name=""
)
response = view(request=DummyRequest(config=config), context=None)
assert b"<title>Swagger UI: OAuth2 Redirect</title>" in response.body


def test_add_multiple_explorer_views() -> None:
Expand Down Expand Up @@ -344,15 +394,15 @@ def test_add_multiple_explorer_views() -> None:
)
response = view(request=DummyRequest(config=config), context=None)
assert b"<title>Swagger UI</title>" in response.body
assert b'url: "/foo/openapi.yaml"' in response.body
assert b'"url": "/foo/openapi.yaml"' in response.body

request = config.registry.queryUtility(IRouteRequest, name="bar_api_explorer")
view = config.registry.adapters.registered(
(IViewClassifier, request, Interface), IView, name=""
)
response = view(request=DummyRequest(config=config), context=None)
assert b"<title>Swagger UI</title>" in response.body
assert b'url: "/bar/openapi.yaml"' in response.body
assert b'"url": "/bar/openapi.yaml"' in response.body


def test_add_multiple_explorer_views_using_directory() -> None:
Expand Down Expand Up @@ -398,15 +448,15 @@ def test_add_multiple_explorer_views_using_directory() -> None:
)
response = view(request=DummyRequest(config=config), context=None)
assert b"<title>Swagger UI</title>" in response.body
assert b'url: "/foo.yaml"' in response.body
assert b'"url": "/foo.yaml"' in response.body

request = config.registry.queryUtility(IRouteRequest, name="bar_api_explorer")
view = config.registry.adapters.registered(
(IViewClassifier, request, Interface), IView, name=""
)
response = view(request=DummyRequest(config=config), context=None)
assert b"<title>Swagger UI</title>" in response.body
assert b'url: "/bar.yaml"' in response.body
assert b'"url": "/bar.yaml"' in response.body


def test_explorer_view_missing_spec() -> None:
Expand Down

0 comments on commit 6e700b5

Please sign in to comment.