Add auth hook settings and apiKey query support

master
Cristi Vîjdea 2018-10-14 17:38:39 +03:00
parent 5d8c936956
commit 060fe1881a
7 changed files with 206 additions and 31 deletions

View File

@ -222,6 +222,31 @@ set to ``None`` to remove the badge.
**Default**: :python:`'http://online.swagger.io/validator/'` |br|
*Maps to parameter*: ``validatorUrl``
PERSIST_AUTH
------------
Persist swagger-ui authorization data to local storage. |br|
**WARNING:** this may be a security risk as the data is stored unencrypted
**Default**: :python:`'False` |br|
*Maps to parameter*: -
REFETCH_SCHEMA_WITH_AUTH
------------------------
Re-fetch the OpenAPI document with the new credentials after authorization is performed through swagger-ui.
**Default**: :python:`'False` |br|
*Maps to parameter*: -
REFETCH_SCHEMA_ON_LOGOUT
------------------------
Re-fetch the OpenAPI document without credentials after authorization is removed through swagger-ui.
**Default**: :python:`'False` |br|
*Maps to parameter*: -
OPERATIONS_SORTER
-----------------

View File

@ -42,6 +42,9 @@ SWAGGER_DEFAULTS = {
'LOGOUT_URL': getattr(settings, 'LOGOUT_URL', None),
'SPEC_URL': None,
'VALIDATOR_URL': '',
'PERSIST_AUTH': False,
'REFETCH_SCHEMA_WITH_AUTH': False,
'REFETCH_SCHEMA_ON_LOGOUT': False,
'OPERATIONS_SORTER': None,
'TAGS_SORTER': None,

View File

@ -134,6 +134,9 @@ class SwaggerUIRenderer(_UIRenderer):
'oauth2RedirectUrl': swagger_settings.OAUTH2_REDIRECT_URL,
'supportedSubmitMethods': swagger_settings.SUPPORTED_SUBMIT_METHODS,
'displayOperationId': swagger_settings.DISPLAY_OPERATION_ID,
'persistAuth': swagger_settings.PERSIST_AUTH,
'refetchWithAuth': swagger_settings.REFETCH_SCHEMA_WITH_AUTH,
'refetchOnLogout': swagger_settings.REFETCH_SCHEMA_ON_LOGOUT,
}
data = filter_none(data)

View File

@ -4,11 +4,6 @@ var defaultSpecUrl = currentPath + '?format=openapi';
// load the saved authorization state from localStorage; ImmutableJS is used for consistency with swagger-ui state
var savedAuth = Immutable.fromJS({});
try {
savedAuth = Immutable.fromJS(JSON.parse(localStorage.getItem("drf-yasg-auth")) || {});
} catch (e) {
localStorage.removeItem("drf-yasg-auth");
}
// global SwaggerUI config object; can be changed directly or by hooking initSwaggerUiConfig
var swaggerUiConfig = {
@ -31,14 +26,7 @@ var swaggerUiConfig = {
headers["X-CSRFToken"] = csrftoken.value;
}
if (request.loadSpec) {
applyAuth(savedAuth, headers);
}
return request;
},
onComplete: function () {
preauthorizeAny(savedAuth, window.ui);
hookAuthActions(window.ui);
}
};
@ -100,11 +88,85 @@ function initSwaggerUi() {
* @param oauth2Settings OAUTH2_CONFIG from Django settings
*/
function initSwaggerUiConfig(swaggerSettings, oauth2Settings) {
var persistAuth = swaggerSettings.persistAuth;
var refetchWithAuth = swaggerSettings.refetchWithAuth;
var refetchOnLogout = swaggerSettings.refetchOnLogout;
delete swaggerSettings['persistAuth'];
delete swaggerSettings['refetchWithAuth'];
delete swaggerSettings['refetchOnLogout'];
for (var p in swaggerSettings) {
if (swaggerSettings.hasOwnProperty(p)) {
swaggerUiConfig[p] = swaggerSettings[p];
}
}
if (persistAuth || refetchWithAuth) {
var hookedAuth = false;
if (persistAuth) {
try {
savedAuth = Immutable.fromJS(JSON.parse(localStorage.getItem("drf-yasg-auth")) || {});
} catch (e) {
localStorage.removeItem("drf-yasg-auth");
}
}
var oldOnComplete = swaggerUiConfig.onComplete;
swaggerUiConfig.onComplete = function () {
if (persistAuth) {
preauthorizeAny(savedAuth, window.ui);
}
if (!hookedAuth) {
hookAuthActions(window.ui, persistAuth, refetchWithAuth, refetchOnLogout);
hookedAuth = true;
}
if (oldOnComplete) {
oldOnComplete();
}
};
var specRequestsInFlight = [];
var oldRequestInterceptor = swaggerUiConfig.requestInterceptor;
swaggerUiConfig.requestInterceptor = function (request) {
var headers = request.headers || {};
if (refetchWithAuth && request.loadSpec) {
var newUrl = applyAuth(savedAuth, request.url, headers) || request.url;
if (newUrl !== request.url) {
request.url = newUrl;
}
// need to manually remember requests for spec urls because
// responseInterceptor has no reference to the request...
specRequestsInFlight.push(request.url);
}
if (oldRequestInterceptor) {
request = oldRequestInterceptor(request);
}
return request;
};
var oldResponseInterceptor = swaggerUiConfig.responseInterceptor;
swaggerUiConfig.responseInterceptor = function (response) {
if (refetchWithAuth && specRequestsInFlight.indexOf(response.url) !== -1) {
// need setTimeout here because swagger-ui insists to updateUrl with the initial request url...
if (response.ok) {
setTimeout(function () {
window.ui.specActions.updateUrl(response.url);
});
}
specRequestsInFlight = specRequestsInFlight.filter(function (val) {
return val !== response.url;
});
}
if (oldResponseInterceptor) {
response = oldResponseInterceptor(response);
}
return response;
}
}
}
/**
@ -128,54 +190,126 @@ function preauthorizeAny(savedAuth, sui) {
}
}
function _usp(url, fn) {
url = url.split('?');
var usp = new URLSearchParams(url[1]);
fn(usp);
url[1] = usp.toString();
return url.join('?');
}
function addQueryParam(url, key, value) {
return _usp(url, function (usp) {
usp.set(key, value);
})
}
function removeQueryParam(url, key) {
return _usp(url, function (usp) {
usp.delete(key);
})
}
/**
* Manually apply auth headers from the given auth object.
* @param savedAuth auth object saved from authActions.authorize
* @param requestHeaders target headers
* @param {object} authScheme auth object saved from authActions.authorize
* @param {string} requestUrl the request url
* @param {object} requestHeaders target headers
* @return string new request url
*/
function applyAuth(savedAuth, requestHeaders) {
var schemeName = savedAuth.get("name"), schemeType = savedAuth.getIn(["schema", "type"]);
function applyAuth(authScheme, requestUrl, requestHeaders) {
requestHeaders = requestHeaders || {};
var schemeName = authScheme.get("name"), schemeType = authScheme.getIn(["schema", "type"]);
if (schemeType === "basic" && schemeName) {
var username = savedAuth.getIn(["value", "username"]);
var password = savedAuth.getIn(["value", "password"]);
var username = authScheme.getIn(["value", "username"]);
var password = authScheme.getIn(["value", "password"]);
if (username && password) {
requestHeaders["Authorization"] = "Basic " + btoa(username + ":" + password);
}
} else if (schemeType === "apiKey" && schemeName) {
var key = savedAuth.get("value"), _in = savedAuth.getIn(["schema", "in"]);
var paramName = savedAuth.getIn(["schema", "name"]);
if (key && paramName && _in === "header") {
requestHeaders[paramName] = key;
}
if (_in === "query") {
console.warn("WARNING: cannot apply apiKey query parameter via interceptor");
var _in = authScheme.getIn(["schema", "in"]), paramName = authScheme.getIn(["schema", "name"]);
var key = authScheme.get("value");
if (key && paramName) {
if (_in === "header") {
requestHeaders[paramName] = key;
}
if (_in === "query") {
if (requestUrl) {
requestUrl = addQueryParam(requestUrl, paramName, key);
}
else {
console.warn("WARNING: cannot apply apiKey query parameter via interceptor");
}
}
}
}
return requestUrl;
}
/**
* Remove the given authorization scheme from the url.
* @param {object} authScheme
* @param {string} requestUrl
*/
function deauthUrl(authScheme, requestUrl) {
var schemeType = authScheme.getIn(["schema", "type"]);
if (schemeType === "apiKey") {
var _in = authScheme.getIn(["schema", "in"]), paramName = authScheme.getIn(["schema", "name"]);
if (_in === "query" && requestUrl && paramName) {
requestUrl = removeQueryParam(requestUrl, paramName);
}
}
return requestUrl;
}
/**
* Hook the authorize and logout actions of SwaggerUI.
* The hooks are used to persist authorization data and trigger schema refetch.
* @param sui SwaggerUI or SwaggerUIBundle instance
* @param {boolean} persistAuth true to save auth to local storage
* @param {boolean} refetchWithAuth true to trigger schema fetch on login
* @param {boolean} refetchOnLogout true to trigger schema fetch on logout
*/
function hookAuthActions(sui) {
function hookAuthActions(sui, persistAuth, refetchWithAuth, refetchOnLogout) {
if (!persistAuth && !refetchWithAuth) {
// nothing to do
return;
}
var originalAuthorize = sui.authActions.authorize;
sui.authActions.authorize = function (authorization) {
originalAuthorize(authorization);
// authorization is map of scheme name to scheme object
// need to use ImmutableJS because schema is already an ImmutableJS object
var schemes = Immutable.fromJS(authorization);
var auth = schemes.valueSeq().first();
localStorage.setItem("drf-yasg-auth", JSON.stringify(auth.toJSON()));
savedAuth = auth;
sui.specActions.download();
savedAuth = schemes.valueSeq().first();
if (persistAuth) {
localStorage.setItem("drf-yasg-auth", JSON.stringify(savedAuth.toJSON()));
}
if (refetchWithAuth) {
var url = sui.specSelectors.url();
url = applyAuth(savedAuth, url) || url;
sui.specActions.download(url);
}
};
var originalLogout = sui.authActions.logout;
sui.authActions.logout = function (authorization) {
if (savedAuth.get("name") === authorization[0]) {
localStorage.removeItem("drf-yasg-auth");
var oldAuth = savedAuth.set("value", null);
savedAuth = Immutable.fromJS({});
if (persistAuth) {
localStorage.removeItem("drf-yasg-auth");
}
if (refetchWithAuth) {
var url = sui.specSelectors.url();
url = deauthUrl(oldAuth, url) || url;
sui.specActions.download(url);
}
}
originalLogout(authorization);
};

File diff suppressed because one or more lines are too long

View File

@ -43,6 +43,7 @@
<script src="{% static 'drf-yasg/swagger-ui-dist/swagger-ui-standalone-preset.js' %}"></script>
<script src="{% static 'drf-yasg/insQ.min.js' %}"></script>
<script src="{% static 'drf-yasg/immutable.js' %}"></script>
<script src="{% static 'drf-yasg/url-polyfill.min.js' %}"></script>
<script src="{% static 'drf-yasg/swagger-ui-init.js' %}"></script>
{% endblock %}
{% block extra_scripts %}

View File

@ -95,6 +95,9 @@ REST_FRAMEWORK = {
SWAGGER_SETTINGS = {
'LOGIN_URL': reverse_lazy('admin:login'),
'LOGOUT_URL': '/admin/logout',
'PERSIST_AUTH': True,
'REFETCH_SCHEMA_WITH_AUTH': True,
'REFETCH_SCHEMA_ON_LOGOUT': True,
'DEFAULT_INFO': 'testproj.urls.swagger_info',
@ -106,6 +109,11 @@ SWAGGER_SETTINGS = {
'type': 'apiKey',
'name': 'Authorization',
'in': 'header'
},
'Query': {
'type': 'apiKey',
'name': 'auth',
'in': 'query'
}
}
}