mirror of https://github.com/hashicorp/consul
ui: Model Layer for SSO Support (#7771)
* ui: Adds model layer required for SSO 1. oidc-provider ember-data triplet plus repo, plus addition of torii addon 2. Make blocking queries support a Cache-Control: no-cache header 3. Tweaks to the token model layer in preparation for SSO work * Fix up meta related Cache-Control tests * Add tests adapter tests for URL shapes * Reset Cache-Control to the original value, return something from logoutpull/7344/head
parent
ed2444c0b5
commit
6d7a95f82d
|
@ -124,6 +124,10 @@ export default Adapter.extend({
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e;
|
error = e;
|
||||||
}
|
}
|
||||||
|
// TODO: This comes originates from ember-data
|
||||||
|
// This can be confusing if you need to use this with Promise.reject
|
||||||
|
// Consider changing this to return the error and then
|
||||||
|
// throw from the call site instead
|
||||||
throw error;
|
throw error;
|
||||||
},
|
},
|
||||||
query: function(store, type, query) {
|
query: function(store, type, query) {
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
import Adapter from './application';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
import { env } from 'consul-ui/env';
|
||||||
|
import nonEmptySet from 'consul-ui/utils/non-empty-set';
|
||||||
|
|
||||||
|
let Namespace;
|
||||||
|
if (env('CONSUL_NSPACES_ENABLED')) {
|
||||||
|
Namespace = nonEmptySet('Namespace');
|
||||||
|
} else {
|
||||||
|
Namespace = () => ({});
|
||||||
|
}
|
||||||
|
export default Adapter.extend({
|
||||||
|
env: service('env'),
|
||||||
|
requestForQuery: function(request, { dc, ns, index }) {
|
||||||
|
return request`
|
||||||
|
GET /v1/internal/ui/oidc-auth-methods?${{ dc }}
|
||||||
|
|
||||||
|
${{
|
||||||
|
index,
|
||||||
|
...this.formatNspace(ns),
|
||||||
|
}}
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
requestForQueryRecord: function(request, { dc, ns, id }) {
|
||||||
|
if (typeof id === 'undefined') {
|
||||||
|
throw new Error('You must specify an id');
|
||||||
|
}
|
||||||
|
return request`
|
||||||
|
POST /v1/acl/oidc/auth-url?${{ dc }}
|
||||||
|
Cache-Control: no-store
|
||||||
|
|
||||||
|
${{
|
||||||
|
...Namespace(ns),
|
||||||
|
AuthMethod: id,
|
||||||
|
RedirectURI: `${this.env.var('CONSUL_BASE_UI_URL')}/torii/redirect.html`,
|
||||||
|
}}
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
requestForAuthorize: function(request, { dc, ns, id, code, state }) {
|
||||||
|
if (typeof id === 'undefined') {
|
||||||
|
throw new Error('You must specify an id');
|
||||||
|
}
|
||||||
|
if (typeof code === 'undefined') {
|
||||||
|
throw new Error('You must specify an code');
|
||||||
|
}
|
||||||
|
if (typeof state === 'undefined') {
|
||||||
|
throw new Error('You must specify an state');
|
||||||
|
}
|
||||||
|
return request`
|
||||||
|
POST /v1/acl/oidc/callback?${{ dc }}
|
||||||
|
Cache-Control: no-store
|
||||||
|
|
||||||
|
${{
|
||||||
|
...Namespace(ns),
|
||||||
|
AuthMethod: id,
|
||||||
|
Code: code,
|
||||||
|
State: state,
|
||||||
|
}}
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
requestForLogout: function(request, { id }) {
|
||||||
|
if (typeof id === 'undefined') {
|
||||||
|
throw new Error('You must specify an id');
|
||||||
|
}
|
||||||
|
return request`
|
||||||
|
POST /v1/acl/logout
|
||||||
|
Cache-Control: no-store
|
||||||
|
X-Consul-Token: ${id}
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
authorize: function(store, type, id, snapshot) {
|
||||||
|
return this.request(
|
||||||
|
function(adapter, request, serialized, unserialized) {
|
||||||
|
return adapter.requestForAuthorize(request, serialized, unserialized);
|
||||||
|
},
|
||||||
|
function(serializer, respond, serialized, unserialized) {
|
||||||
|
return serializer.respondForAuthorize(respond, serialized, unserialized);
|
||||||
|
},
|
||||||
|
snapshot,
|
||||||
|
type.modelName
|
||||||
|
);
|
||||||
|
},
|
||||||
|
logout: function(store, type, id, snapshot) {
|
||||||
|
return this.request(
|
||||||
|
function(adapter, request, serialized, unserialized) {
|
||||||
|
return adapter.requestForLogout(request, serialized, unserialized);
|
||||||
|
},
|
||||||
|
function(serializer, respond, serialized, unserialized) {
|
||||||
|
// its ok to return nothing here for the moment at least
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
snapshot,
|
||||||
|
type.modelName
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -104,6 +104,7 @@ export default Adapter.extend({
|
||||||
return request`
|
return request`
|
||||||
GET /v1/acl/token/self?${{ dc }}
|
GET /v1/acl/token/self?${{ dc }}
|
||||||
X-Consul-Token: ${secret}
|
X-Consul-Token: ${secret}
|
||||||
|
Cache-Control: no-store
|
||||||
|
|
||||||
${{ index }}
|
${{ index }}
|
||||||
`;
|
`;
|
||||||
|
@ -132,7 +133,7 @@ export default Adapter.extend({
|
||||||
return adapter.requestForSelf(request, serialized, data);
|
return adapter.requestForSelf(request, serialized, data);
|
||||||
},
|
},
|
||||||
function(serializer, respond, serialized, data) {
|
function(serializer, respond, serialized, data) {
|
||||||
return serializer.respondForQueryRecord(respond, serialized, data);
|
return serializer.respondForSelf(respond, serialized, data);
|
||||||
},
|
},
|
||||||
unserialized,
|
unserialized,
|
||||||
type.modelName
|
type.modelName
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import Oauth2CodeProvider from 'torii/providers/oauth2-code';
|
||||||
|
const NAME = 'oidc-with-url';
|
||||||
|
const Provider = Oauth2CodeProvider.extend({
|
||||||
|
name: NAME,
|
||||||
|
buildUrl: function() {
|
||||||
|
return this.baseUrl;
|
||||||
|
},
|
||||||
|
open: function(options) {
|
||||||
|
const name = this.get('name'),
|
||||||
|
url = this.buildUrl(),
|
||||||
|
responseParams = ['state', 'code'],
|
||||||
|
responseType = 'code';
|
||||||
|
return this.get('popup')
|
||||||
|
.open(url, responseParams, options)
|
||||||
|
.then(function(authData) {
|
||||||
|
// the same as the parent class but with an authorizationState added
|
||||||
|
return {
|
||||||
|
authorizationState: authData.state,
|
||||||
|
authorizationCode: decodeURIComponent(authData[responseType]),
|
||||||
|
provider: name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
close: function() {
|
||||||
|
const popup = this.get('popup.remote') || {};
|
||||||
|
if (typeof popup.close === 'function') {
|
||||||
|
return popup.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export function initialize(application) {
|
||||||
|
application.register(`torii-provider:${NAME}`, Provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
initialize,
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
import Model from 'ember-data/model';
|
||||||
|
import attr from 'ember-data/attr';
|
||||||
|
|
||||||
|
export const PRIMARY_KEY = 'uid';
|
||||||
|
export const SLUG_KEY = 'Name';
|
||||||
|
export default Model.extend({
|
||||||
|
[PRIMARY_KEY]: attr('string'),
|
||||||
|
[SLUG_KEY]: attr('string'),
|
||||||
|
meta: attr(),
|
||||||
|
Datacenter: attr('string'),
|
||||||
|
DisplayName: attr('string'),
|
||||||
|
Kind: attr('string'),
|
||||||
|
Namespace: attr('string'),
|
||||||
|
AuthURL: attr('string'),
|
||||||
|
});
|
|
@ -20,6 +20,7 @@ export default Model.extend({
|
||||||
Description: attr('string', {
|
Description: attr('string', {
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
}),
|
}),
|
||||||
|
meta: attr(),
|
||||||
Datacenter: attr('string'),
|
Datacenter: attr('string'),
|
||||||
Namespace: attr('string'),
|
Namespace: attr('string'),
|
||||||
Local: attr('boolean'),
|
Local: attr('boolean'),
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
HEADERS_DATACENTER as HTTP_HEADERS_DATACENTER,
|
HEADERS_DATACENTER as HTTP_HEADERS_DATACENTER,
|
||||||
HEADERS_NAMESPACE as HTTP_HEADERS_NAMESPACE,
|
HEADERS_NAMESPACE as HTTP_HEADERS_NAMESPACE,
|
||||||
} from 'consul-ui/utils/http/consul';
|
} from 'consul-ui/utils/http/consul';
|
||||||
|
import { CACHE_CONTROL as HTTP_HEADERS_CACHE_CONTROL } from 'consul-ui/utils/http/headers';
|
||||||
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
|
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
|
||||||
import { NSPACE_KEY } from 'consul-ui/models/nspace';
|
import { NSPACE_KEY } from 'consul-ui/models/nspace';
|
||||||
import createFingerprinter from 'consul-ui/utils/create-fingerprinter';
|
import createFingerprinter from 'consul-ui/utils/create-fingerprinter';
|
||||||
|
@ -101,7 +102,7 @@ export default Serializer.extend({
|
||||||
// this could get confusing if you tried to override
|
// this could get confusing if you tried to override
|
||||||
// say `normalizeQueryResponse`
|
// say `normalizeQueryResponse`
|
||||||
// TODO: consider creating a method for each one of the `normalize...Response` family
|
// TODO: consider creating a method for each one of the `normalize...Response` family
|
||||||
normalizeResponse: function(store, primaryModelClass, payload, id, requestType) {
|
normalizeResponse: function(store, modelClass, payload, id, requestType) {
|
||||||
// Pick the meta/headers back off the payload and cleanup
|
// Pick the meta/headers back off the payload and cleanup
|
||||||
// before we go through serializing
|
// before we go through serializing
|
||||||
const headers = payload[HTTP_HEADERS_SYMBOL] || {};
|
const headers = payload[HTTP_HEADERS_SYMBOL] || {};
|
||||||
|
@ -114,34 +115,39 @@ export default Serializer.extend({
|
||||||
// (which was the reason for the Symbol-like property earlier)
|
// (which was the reason for the Symbol-like property earlier)
|
||||||
// use a method modelled on ember-data methods so we have the opportunity to
|
// use a method modelled on ember-data methods so we have the opportunity to
|
||||||
// do this on a per-model level
|
// do this on a per-model level
|
||||||
const meta = this.normalizeMeta(
|
const meta = this.normalizeMeta(store, modelClass, headers, normalizedPayload, id, requestType);
|
||||||
store,
|
if (requestType !== 'query') {
|
||||||
primaryModelClass,
|
|
||||||
headers,
|
|
||||||
normalizedPayload,
|
|
||||||
id,
|
|
||||||
requestType
|
|
||||||
);
|
|
||||||
if (requestType === 'queryRecord') {
|
|
||||||
normalizedPayload.meta = meta;
|
normalizedPayload.meta = meta;
|
||||||
}
|
}
|
||||||
return this._super(
|
const res = this._super(
|
||||||
store,
|
store,
|
||||||
primaryModelClass,
|
modelClass,
|
||||||
{
|
{
|
||||||
meta: meta,
|
meta: meta,
|
||||||
[primaryModelClass.modelName]: normalizedPayload,
|
[modelClass.modelName]: normalizedPayload,
|
||||||
},
|
},
|
||||||
id,
|
id,
|
||||||
requestType
|
requestType
|
||||||
);
|
);
|
||||||
|
// If the result of the super normalizeResponse is undefined
|
||||||
|
// its because the JSONSerializer (which REST inherits from)
|
||||||
|
// doesn't recognise the requestType, in this case its likely to be an 'action'
|
||||||
|
// request rather than a specific 'load me some data' one.
|
||||||
|
// Therefore its ok to bypass the store here for the moment
|
||||||
|
// we currently use this for self, but it also would affect any custom
|
||||||
|
// methods that use a serializer in our custom service/store
|
||||||
|
if (typeof res === 'undefined') {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
},
|
},
|
||||||
timestamp: function() {
|
timestamp: function() {
|
||||||
return new Date().getTime();
|
return new Date().getTime();
|
||||||
},
|
},
|
||||||
normalizeMeta: function(store, primaryModelClass, headers, payload, id, requestType) {
|
normalizeMeta: function(store, modelClass, headers, payload, id, requestType) {
|
||||||
const meta = {
|
const meta = {
|
||||||
cursor: headers[HTTP_HEADERS_INDEX],
|
cacheControl: headers[HTTP_HEADERS_CACHE_CONTROL.toLowerCase()],
|
||||||
|
cursor: headers[HTTP_HEADERS_INDEX.toLowerCase()],
|
||||||
dc: headers[HTTP_HEADERS_DATACENTER.toLowerCase()],
|
dc: headers[HTTP_HEADERS_DATACENTER.toLowerCase()],
|
||||||
nspace: headers[HTTP_HEADERS_NAMESPACE.toLowerCase()],
|
nspace: headers[HTTP_HEADERS_NAMESPACE.toLowerCase()],
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import Serializer from './application';
|
||||||
|
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/oidc-provider';
|
||||||
|
|
||||||
|
export default Serializer.extend({
|
||||||
|
primaryKey: PRIMARY_KEY,
|
||||||
|
slugKey: SLUG_KEY,
|
||||||
|
respondForAuthorize: function(respond, serialized, data) {
|
||||||
|
// we avoid the parent serializer here as it tries to create a
|
||||||
|
// fingerprint for an 'action' request
|
||||||
|
// but we still need to pass the headers through
|
||||||
|
return respond((headers, body) => {
|
||||||
|
return this.attachHeaders(headers, body, data);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
respondForQueryRecord: function(respond, query) {
|
||||||
|
// add the name and nspace here so we can merge this
|
||||||
|
// TODO: Look to see if we always want the merging functionality
|
||||||
|
return this._super(
|
||||||
|
cb =>
|
||||||
|
respond((headers, body) =>
|
||||||
|
cb(headers, {
|
||||||
|
Name: query.id,
|
||||||
|
Namespace: query.ns,
|
||||||
|
...body,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
query
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -31,6 +31,9 @@ export default Serializer.extend(WithPolicies, WithRoles, {
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
respondForSelf: function(respond, query) {
|
||||||
|
return this.respondForQueryRecord(respond, query);
|
||||||
|
},
|
||||||
respondForUpdateRecord: function(respond, serialized, data) {
|
respondForUpdateRecord: function(respond, serialized, data) {
|
||||||
return this._super(
|
return this._super(
|
||||||
cb =>
|
cb =>
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
import Service, { inject as service } from '@ember/service';
|
import Service, { inject as service } from '@ember/service';
|
||||||
import { get, set } from '@ember/object';
|
import { get, set } from '@ember/object';
|
||||||
|
|
||||||
|
import { CACHE_CONTROL, CONTENT_TYPE } from 'consul-ui/utils/http/headers';
|
||||||
|
|
||||||
|
import { HEADERS_TOKEN as CONSUL_TOKEN } from 'consul-ui/utils/http/consul';
|
||||||
|
|
||||||
import { env } from 'consul-ui/env';
|
import { env } from 'consul-ui/env';
|
||||||
import getObjectPool from 'consul-ui/utils/get-object-pool';
|
import getObjectPool from 'consul-ui/utils/get-object-pool';
|
||||||
import Request from 'consul-ui/utils/http/request';
|
import Request from 'consul-ui/utils/http/request';
|
||||||
|
@ -29,7 +33,7 @@ class HTTPError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const dispose = function(request) {
|
const dispose = function(request) {
|
||||||
if (request.headers()['content-type'] === 'text/event-stream') {
|
if (request.headers()[CONTENT_TYPE.toLowerCase()] === 'text/event-stream') {
|
||||||
const xhr = request.connection();
|
const xhr = request.connection();
|
||||||
// unsent and opened get aborted
|
// unsent and opened get aborted
|
||||||
// headers and loading means wait for it
|
// headers and loading means wait for it
|
||||||
|
@ -127,30 +131,40 @@ export default Service.extend({
|
||||||
const [url, ...headerParts] = urlParts.join(' ').split('\n');
|
const [url, ...headerParts] = urlParts.join(' ').split('\n');
|
||||||
|
|
||||||
return client.settings.findBySlug('token').then(function(token) {
|
return client.settings.findBySlug('token').then(function(token) {
|
||||||
|
const requestHeaders = createHeaders(headerParts);
|
||||||
const headers = {
|
const headers = {
|
||||||
// default to application/json
|
// default to application/json
|
||||||
...{
|
...{
|
||||||
'Content-Type': 'application/json; charset=utf-8',
|
[CONTENT_TYPE]: 'application/json; charset=utf-8',
|
||||||
},
|
},
|
||||||
// add any application level headers
|
// add any application level headers
|
||||||
...{
|
...{
|
||||||
'X-Consul-Token': typeof token.SecretID === 'undefined' ? '' : token.SecretID,
|
[CONSUL_TOKEN]: typeof token.SecretID === 'undefined' ? '' : token.SecretID,
|
||||||
},
|
},
|
||||||
// but overwrite or add to those from anything in the specific request
|
// but overwrite or add to those from anything in the specific request
|
||||||
...createHeaders(headerParts),
|
...requestHeaders,
|
||||||
};
|
};
|
||||||
|
// We use cache-control in the response
|
||||||
|
// but we don't want to send it, but we artificially
|
||||||
|
// tag it onto the response below if it is set on the request
|
||||||
|
delete headers[CACHE_CONTROL];
|
||||||
|
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
const options = {
|
const options = {
|
||||||
url: url.trim(),
|
url: url.trim(),
|
||||||
method: method,
|
method: method,
|
||||||
contentType: headers['Content-Type'],
|
contentType: headers[CONTENT_TYPE],
|
||||||
// type: 'json',
|
// type: 'json',
|
||||||
complete: function(xhr, textStatus) {
|
complete: function(xhr, textStatus) {
|
||||||
client.complete(this.id);
|
client.complete(this.id);
|
||||||
},
|
},
|
||||||
success: function(response, status, xhr) {
|
success: function(response, status, xhr) {
|
||||||
const headers = createHeaders(xhr.getAllResponseHeaders().split('\n'));
|
const headers = createHeaders(xhr.getAllResponseHeaders().split('\n'));
|
||||||
|
if (typeof requestHeaders[CACHE_CONTROL] !== 'undefined') {
|
||||||
|
// if cache-control was on the request, artificially tag
|
||||||
|
// it back onto the response, also see comment above
|
||||||
|
headers[CACHE_CONTROL] = requestHeaders[CACHE_CONTROL];
|
||||||
|
}
|
||||||
const respond = function(cb) {
|
const respond = function(cb) {
|
||||||
return cb(headers, response);
|
return cb(headers, response);
|
||||||
};
|
};
|
||||||
|
@ -191,7 +205,7 @@ export default Service.extend({
|
||||||
// for write-like actions
|
// for write-like actions
|
||||||
// potentially we should change things so you _have_ to do that
|
// potentially we should change things so you _have_ to do that
|
||||||
// as doing it this way is a little magical
|
// as doing it this way is a little magical
|
||||||
if (method !== 'GET' && headers['Content-Type'].indexOf('json') !== -1) {
|
if (method !== 'GET' && headers[CONTENT_TYPE].indexOf('json') !== -1) {
|
||||||
options.data = JSON.stringify(body);
|
options.data = JSON.stringify(body);
|
||||||
} else {
|
} else {
|
||||||
// TODO: Does this need urlencoding? Assuming jQuery does this
|
// TODO: Does this need urlencoding? Assuming jQuery does this
|
||||||
|
@ -204,7 +218,7 @@ export default Service.extend({
|
||||||
// also see adapters/kv content-types in requestForCreate/UpdateRecord
|
// also see adapters/kv content-types in requestForCreate/UpdateRecord
|
||||||
// also see https://github.com/hashicorp/consul/issues/3804
|
// also see https://github.com/hashicorp/consul/issues/3804
|
||||||
options.contentType = 'application/json; charset=utf-8';
|
options.contentType = 'application/json; charset=utf-8';
|
||||||
headers['Content-Type'] = options.contentType;
|
headers[CONTENT_TYPE] = options.contentType;
|
||||||
//
|
//
|
||||||
options.beforeSend = function(xhr) {
|
options.beforeSend = function(xhr) {
|
||||||
if (headers) {
|
if (headers) {
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import RepositoryService from 'consul-ui/services/repository';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { getOwner } from '@ember/application';
|
||||||
|
import { set } from '@ember/object';
|
||||||
|
|
||||||
|
const modelName = 'oidc-provider';
|
||||||
|
const OAUTH_PROVIDER_NAME = 'oidc-with-url';
|
||||||
|
export default RepositoryService.extend({
|
||||||
|
manager: service('torii'),
|
||||||
|
init: function() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.provider = getOwner(this).lookup(`torii-provider:${OAUTH_PROVIDER_NAME}`);
|
||||||
|
},
|
||||||
|
getModelName: function() {
|
||||||
|
return modelName;
|
||||||
|
},
|
||||||
|
authorize: function(id, code, state, dc, nspace, configuration = {}) {
|
||||||
|
const query = {
|
||||||
|
id: id,
|
||||||
|
code: code,
|
||||||
|
state: state,
|
||||||
|
dc: dc,
|
||||||
|
ns: nspace,
|
||||||
|
};
|
||||||
|
return this.store.authorize(this.getModelName(), query);
|
||||||
|
},
|
||||||
|
logout: function(id, code, state, dc, nspace, configuration = {}) {
|
||||||
|
// TODO: Temporarily call this secret, as we alreayd do that with
|
||||||
|
// self in the `store` look to see whether we should just call it id like
|
||||||
|
// the rest
|
||||||
|
const query = {
|
||||||
|
id: id,
|
||||||
|
};
|
||||||
|
return this.store.logout(this.getModelName(), query);
|
||||||
|
},
|
||||||
|
close: function() {
|
||||||
|
this.manager.close(OAUTH_PROVIDER_NAME);
|
||||||
|
},
|
||||||
|
findCodeByURL: function(src) {
|
||||||
|
// TODO: Maybe move this to the provider itself
|
||||||
|
set(this.provider, 'baseUrl', src);
|
||||||
|
return this.manager.open(OAUTH_PROVIDER_NAME, {}).catch(e => {
|
||||||
|
let err;
|
||||||
|
switch (true) {
|
||||||
|
case e.message.startsWith('remote was closed'):
|
||||||
|
err = new Error('Remote was closed');
|
||||||
|
err.statusCode = 499;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
err = new Error(e.message);
|
||||||
|
err.statusCode = 500;
|
||||||
|
}
|
||||||
|
this.store.adapterFor(this.getModelName()).error(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
|
@ -21,7 +21,16 @@ export default Store.extend({
|
||||||
self: function(modelName, token) {
|
self: function(modelName, token) {
|
||||||
// TODO: no normalization, type it properly for the moment
|
// TODO: no normalization, type it properly for the moment
|
||||||
const adapter = this.adapterFor(modelName);
|
const adapter = this.adapterFor(modelName);
|
||||||
return adapter.self(this, { modelName: modelName }, token.secret, token);
|
const serializer = this.serializerFor(modelName);
|
||||||
|
const modelClass = { modelName: modelName };
|
||||||
|
// self is the only custom store method that goes through the serializer for the moment
|
||||||
|
// this means it will have its meta data set correctly
|
||||||
|
// if other methods need meta adding, then this should be carried over to
|
||||||
|
// other methods. Ideally this would have been done from the outset
|
||||||
|
// TODO: Carry this over to the other methods ^
|
||||||
|
return adapter
|
||||||
|
.self(this, modelClass, token.secret, token)
|
||||||
|
.then(payload => serializer.normalizeResponse(this, modelClass, payload, token, 'self'));
|
||||||
},
|
},
|
||||||
//
|
//
|
||||||
// TODO: This one is only for nodes, should fail nicely if you call it
|
// TODO: This one is only for nodes, should fail nicely if you call it
|
||||||
|
@ -31,10 +40,21 @@ export default Store.extend({
|
||||||
const adapter = this.adapterFor(modelName);
|
const adapter = this.adapterFor(modelName);
|
||||||
return adapter.queryLeader(this, { modelName: modelName }, null, query);
|
return adapter.queryLeader(this, { modelName: modelName }, null, query);
|
||||||
},
|
},
|
||||||
// TODO: This one is only for ACL, should fail nicely if you call it
|
// TODO: This one is only for nspaces and OIDC, should fail nicely if you call it
|
||||||
// for anything other than ACLs for good DX
|
// for anything other than nspaces/OIDC for good DX
|
||||||
authorize: function(modelName, query = {}) {
|
authorize: function(modelName, query = {}) {
|
||||||
// TODO: no normalization, type it properly for the moment
|
const adapter = this.adapterFor(modelName);
|
||||||
return this.adapterFor(modelName).authorize(this, { modelName: modelName }, null, query);
|
const serializer = this.serializerFor(modelName);
|
||||||
|
const modelClass = { modelName: modelName };
|
||||||
|
return adapter
|
||||||
|
.authorize(this, modelClass, null, query)
|
||||||
|
.then(payload =>
|
||||||
|
serializer.normalizeResponse(this, modelClass, payload, undefined, 'authorize')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
logout: function(modelName, query = {}) {
|
||||||
|
const adapter = this.adapterFor(modelName);
|
||||||
|
const modelClass = { modelName: modelName };
|
||||||
|
return adapter.logout(this, modelClass, query.id, query);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -96,10 +96,13 @@ export default function(EventSource, backoff = create5xxBackoff()) {
|
||||||
// pick off the `cursor` from the meta and add it to configuration
|
// pick off the `cursor` from the meta and add it to configuration
|
||||||
// along with cursor validation
|
// along with cursor validation
|
||||||
configuration.cursor = validateCursor(meta.cursor, configuration.cursor);
|
configuration.cursor = validateCursor(meta.cursor, configuration.cursor);
|
||||||
|
configuration.cacheControl = meta.cacheControl;
|
||||||
}
|
}
|
||||||
this.currentEvent = event;
|
if ((configuration.cacheControl || '').indexOf('no-store') === -1) {
|
||||||
this.dispatchEvent(this.currentEvent);
|
this.currentEvent = event;
|
||||||
const throttledResolve = throttle(configuration, this.currentEvent, this.previousEvent);
|
}
|
||||||
|
this.dispatchEvent(event);
|
||||||
|
const throttledResolve = throttle(configuration, event, this.previousEvent);
|
||||||
this.previousEvent = this.currentEvent;
|
this.previousEvent = this.currentEvent;
|
||||||
return throttledResolve(result);
|
return throttledResolve(result);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
// TODO: Need to make all these headers capital case
|
// TODO: Need to make all these headers capital case
|
||||||
export const HEADERS_NAMESPACE = 'X-Consul-Namespace';
|
export const HEADERS_NAMESPACE = 'X-Consul-Namespace';
|
||||||
export const HEADERS_DATACENTER = 'x-consul-datacenter';
|
export const HEADERS_DATACENTER = 'X-Consul-Datacenter';
|
||||||
export const HEADERS_INDEX = 'x-consul-index';
|
export const HEADERS_INDEX = 'X-Consul-Index';
|
||||||
export const HEADERS_DIGEST = 'x-consul-contenthash';
|
export const HEADERS_TOKEN = 'X-Consul-Token';
|
||||||
|
export const HEADERS_DIGEST = 'X-Consul-ContentHash';
|
||||||
//
|
//
|
||||||
export const HEADERS_SYMBOL = '__consul_ui_http_headers__';
|
export const HEADERS_SYMBOL = '__consul_ui_http_headers__';
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const CACHE_CONTROL = 'Cache-Control';
|
||||||
|
export const CONTENT_TYPE = 'Content-Type';
|
|
@ -123,7 +123,8 @@
|
||||||
"prettier": "^1.10.2",
|
"prettier": "^1.10.2",
|
||||||
"qunit-dom": "^1.0.0",
|
"qunit-dom": "^1.0.0",
|
||||||
"tape": "^4.13.0",
|
"tape": "^4.13.0",
|
||||||
"text-encoding": "^0.7.0"
|
"text-encoding": "^0.7.0",
|
||||||
|
"torii": "^0.10.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "10.* || >= 12"
|
"node": "10.* || >= 12"
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupTest } from 'ember-qunit';
|
||||||
|
|
||||||
|
import { env } from '../../../env';
|
||||||
|
const shouldHaveNspace = function(nspace) {
|
||||||
|
return typeof nspace !== 'undefined' && env('CONSUL_NSPACES_ENABLED');
|
||||||
|
};
|
||||||
|
module('Integration | Adapter | oidc-provider', function(hooks) {
|
||||||
|
setupTest(hooks);
|
||||||
|
const dc = 'dc-1';
|
||||||
|
const id = 'slug';
|
||||||
|
const undefinedNspace = 'default';
|
||||||
|
[undefinedNspace, 'team-1', undefined].forEach(nspace => {
|
||||||
|
test('requestForQuery returns the correct url/method', function(assert) {
|
||||||
|
const adapter = this.owner.lookup('adapter:oidc-provider');
|
||||||
|
const client = this.owner.lookup('service:client/http');
|
||||||
|
const expected = `GET /v1/internal/ui/oidc-auth-methods?dc=${dc}`;
|
||||||
|
let actual = adapter.requestForQuery(client.url, {
|
||||||
|
dc: dc,
|
||||||
|
ns: nspace,
|
||||||
|
});
|
||||||
|
actual = actual.split('\n');
|
||||||
|
assert.equal(actual.shift().trim(), expected);
|
||||||
|
actual = actual.join('\n').trim();
|
||||||
|
assert.equal(actual, `${shouldHaveNspace(nspace) ? `ns=${nspace}` : ``}`);
|
||||||
|
});
|
||||||
|
test('requestForQueryRecord returns the correct url/method', function(assert) {
|
||||||
|
const adapter = this.owner.lookup('adapter:oidc-provider');
|
||||||
|
const client = this.owner.lookup('service:client/http');
|
||||||
|
const expected = `POST /v1/acl/oidc/auth-url?dc=${dc}`;
|
||||||
|
const actual = adapter
|
||||||
|
.requestForQueryRecord(client.url, {
|
||||||
|
dc: dc,
|
||||||
|
id: id,
|
||||||
|
ns: nspace,
|
||||||
|
})
|
||||||
|
.split('\n')
|
||||||
|
.shift();
|
||||||
|
assert.equal(actual, expected);
|
||||||
|
});
|
||||||
|
test("requestForQueryRecord throws if you don't specify an id", function(assert) {
|
||||||
|
const adapter = this.owner.lookup('adapter:oidc-provider');
|
||||||
|
const client = this.owner.lookup('service:client/http');
|
||||||
|
assert.throws(function() {
|
||||||
|
adapter.requestForQueryRecord(client.url, {
|
||||||
|
dc: dc,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('requestForAuthorize returns the correct url/method', function(assert) {
|
||||||
|
const adapter = this.owner.lookup('adapter:oidc-provider');
|
||||||
|
const client = this.owner.lookup('service:client/http');
|
||||||
|
const expected = `POST /v1/acl/oidc/callback?dc=${dc}`;
|
||||||
|
const actual = adapter
|
||||||
|
.requestForAuthorize(client.url, {
|
||||||
|
dc: dc,
|
||||||
|
id: id,
|
||||||
|
code: 'code',
|
||||||
|
state: 'state',
|
||||||
|
ns: nspace,
|
||||||
|
})
|
||||||
|
.split('\n')
|
||||||
|
.shift();
|
||||||
|
assert.equal(actual, expected);
|
||||||
|
});
|
||||||
|
test('requestForLogout returns the correct url/method', function(assert) {
|
||||||
|
const adapter = this.owner.lookup('adapter:oidc-provider');
|
||||||
|
const client = this.owner.lookup('service:client/http');
|
||||||
|
const expected = `POST /v1/acl/logout`;
|
||||||
|
const actual = adapter
|
||||||
|
.requestForLogout(client.url, {
|
||||||
|
id: id,
|
||||||
|
})
|
||||||
|
.split('\n')
|
||||||
|
.shift();
|
||||||
|
assert.equal(actual, expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -28,6 +28,7 @@ test('findBySlug returns the correct data for item endpoint', function(assert) {
|
||||||
Datacenter: dc,
|
Datacenter: dc,
|
||||||
uid: `["default","${dc}","${id}"]`,
|
uid: `["default","${dc}","${id}"]`,
|
||||||
meta: {
|
meta: {
|
||||||
|
cacheControl: undefined,
|
||||||
cursor: undefined,
|
cursor: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -63,6 +63,7 @@ test('findBySlug returns the correct data for item endpoint', function(assert) {
|
||||||
Datacenter: dc,
|
Datacenter: dc,
|
||||||
uid: `["${nspace}","${dc}","${item.ID}"]`,
|
uid: `["${nspace}","${dc}","${item.ID}"]`,
|
||||||
meta: {
|
meta: {
|
||||||
|
cacheControl: undefined,
|
||||||
cursor: undefined,
|
cursor: undefined,
|
||||||
dc: dc,
|
dc: dc,
|
||||||
nspace: nspace,
|
nspace: nspace,
|
||||||
|
|
|
@ -71,6 +71,7 @@ const undefinedNspace = 'default';
|
||||||
Namespace: item.Namespace || undefinedNspace,
|
Namespace: item.Namespace || undefinedNspace,
|
||||||
uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.ID}"]`,
|
uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.ID}"]`,
|
||||||
meta: {
|
meta: {
|
||||||
|
cacheControl: undefined,
|
||||||
cursor: undefined,
|
cursor: undefined,
|
||||||
dc: dc,
|
dc: dc,
|
||||||
nspace: item.Namespace || undefinedNspace,
|
nspace: item.Namespace || undefinedNspace,
|
||||||
|
|
|
@ -118,6 +118,7 @@ const undefinedNspace = 'default';
|
||||||
service.Tags = [...new Set(payload.Nodes[0].Service.Tags)];
|
service.Tags = [...new Set(payload.Nodes[0].Service.Tags)];
|
||||||
service.Namespace = payload.Namespace;
|
service.Namespace = payload.Namespace;
|
||||||
service.meta = {
|
service.meta = {
|
||||||
|
cacheControl: undefined,
|
||||||
cursor: undefined,
|
cursor: undefined,
|
||||||
dc: dc,
|
dc: dc,
|
||||||
nspace: payload.Namespace,
|
nspace: payload.Namespace,
|
||||||
|
|
|
@ -69,6 +69,12 @@ const undefinedNspace = 'default';
|
||||||
CreateTime: new Date(item.CreateTime),
|
CreateTime: new Date(item.CreateTime),
|
||||||
Namespace: item.Namespace || undefinedNspace,
|
Namespace: item.Namespace || undefinedNspace,
|
||||||
uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.AccessorID}"]`,
|
uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.AccessorID}"]`,
|
||||||
|
meta: {
|
||||||
|
cacheControl: undefined,
|
||||||
|
cursor: undefined,
|
||||||
|
dc: dc,
|
||||||
|
nspace: item.Namespace || undefinedNspace,
|
||||||
|
},
|
||||||
Policies: createPolicies(item),
|
Policies: createPolicies(item),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupTest } from 'ember-qunit';
|
||||||
|
|
||||||
|
module('Unit | Adapter | oidc-provider', function(hooks) {
|
||||||
|
setupTest(hooks);
|
||||||
|
|
||||||
|
// Replace this with your real tests.
|
||||||
|
test('it exists', function(assert) {
|
||||||
|
let adapter = this.owner.lookup('adapter:oidc-provider');
|
||||||
|
assert.ok(adapter);
|
||||||
|
});
|
||||||
|
});
|
|
@ -12420,6 +12420,13 @@ toidentifier@1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
||||||
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
||||||
|
|
||||||
|
torii@^0.10.1:
|
||||||
|
version "0.10.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/torii/-/torii-0.10.1.tgz#caad0a81e82189fc0483b65e68ee28041ad3590f"
|
||||||
|
integrity sha512-csUz/coeSumt9FjyIXLpRj0ii7TfH3fUm3x9rdf+XXnJ0tVTKqwCRynwY0HKuNkGzACyR84hog3B9a8BQefBHA==
|
||||||
|
dependencies:
|
||||||
|
ember-cli-babel "^6.11.0"
|
||||||
|
|
||||||
tough-cookie@^2.3.3, tough-cookie@^2.4.3, tough-cookie@~2.5.0:
|
tough-cookie@^2.3.3, tough-cookie@^2.4.3, tough-cookie@~2.5.0:
|
||||||
version "2.5.0"
|
version "2.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
|
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
|
||||||
|
|
Loading…
Reference in New Issue