From 6d7a95f82d9abdef694487750e073f76176579bd Mon Sep 17 00:00:00 2001 From: John Cowen Date: Tue, 5 May 2020 17:29:35 +0100 Subject: [PATCH] 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 logout --- ui-v2/app/adapters/http.js | 4 + ui-v2/app/adapters/oidc-provider.js | 97 +++++++++++++++++++ ui-v2/app/adapters/token.js | 3 +- ui-v2/app/initializers/oidc-provider.js | 37 +++++++ ui-v2/app/models/oidc-provider.js | 15 +++ ui-v2/app/models/token.js | 1 + ui-v2/app/serializers/application.js | 36 ++++--- ui-v2/app/serializers/oidc-provider.js | 30 ++++++ ui-v2/app/serializers/token.js | 3 + ui-v2/app/services/client/http.js | 28 ++++-- .../app/services/repository/oidc-provider.js | 56 +++++++++++ ui-v2/app/services/store.js | 30 +++++- ui-v2/app/utils/dom/event-source/blocking.js | 9 +- ui-v2/app/utils/http/consul.js | 7 +- ui-v2/app/utils/http/headers.js | 2 + ui-v2/package.json | 3 +- .../adapters/oidc-provider-test.js | 79 +++++++++++++++ .../repository/discovery-chain-test.js | 1 + .../services/repository/node-test.js | 1 + .../services/repository/policy-test.js | 1 + .../services/repository/service-test.js | 1 + .../services/repository/token-test.js | 6 ++ .../tests/unit/adapters/oidc-provider-test.js | 12 +++ ui-v2/yarn.lock | 7 ++ 24 files changed, 434 insertions(+), 35 deletions(-) create mode 100644 ui-v2/app/adapters/oidc-provider.js create mode 100644 ui-v2/app/initializers/oidc-provider.js create mode 100644 ui-v2/app/models/oidc-provider.js create mode 100644 ui-v2/app/serializers/oidc-provider.js create mode 100644 ui-v2/app/services/repository/oidc-provider.js create mode 100644 ui-v2/app/utils/http/headers.js create mode 100644 ui-v2/tests/integration/adapters/oidc-provider-test.js create mode 100644 ui-v2/tests/unit/adapters/oidc-provider-test.js diff --git a/ui-v2/app/adapters/http.js b/ui-v2/app/adapters/http.js index 4a90fdc450..e8bd64e244 100644 --- a/ui-v2/app/adapters/http.js +++ b/ui-v2/app/adapters/http.js @@ -124,6 +124,10 @@ export default Adapter.extend({ } catch (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; }, query: function(store, type, query) { diff --git a/ui-v2/app/adapters/oidc-provider.js b/ui-v2/app/adapters/oidc-provider.js new file mode 100644 index 0000000000..7d1da6df3c --- /dev/null +++ b/ui-v2/app/adapters/oidc-provider.js @@ -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 + ); + }, +}); diff --git a/ui-v2/app/adapters/token.js b/ui-v2/app/adapters/token.js index cc1f8d29ca..afed469dbf 100644 --- a/ui-v2/app/adapters/token.js +++ b/ui-v2/app/adapters/token.js @@ -104,6 +104,7 @@ export default Adapter.extend({ return request` GET /v1/acl/token/self?${{ dc }} X-Consul-Token: ${secret} + Cache-Control: no-store ${{ index }} `; @@ -132,7 +133,7 @@ export default Adapter.extend({ return adapter.requestForSelf(request, serialized, data); }, function(serializer, respond, serialized, data) { - return serializer.respondForQueryRecord(respond, serialized, data); + return serializer.respondForSelf(respond, serialized, data); }, unserialized, type.modelName diff --git a/ui-v2/app/initializers/oidc-provider.js b/ui-v2/app/initializers/oidc-provider.js new file mode 100644 index 0000000000..b86ad2405c --- /dev/null +++ b/ui-v2/app/initializers/oidc-provider.js @@ -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, +}; diff --git a/ui-v2/app/models/oidc-provider.js b/ui-v2/app/models/oidc-provider.js new file mode 100644 index 0000000000..3c1e32a2f4 --- /dev/null +++ b/ui-v2/app/models/oidc-provider.js @@ -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'), +}); diff --git a/ui-v2/app/models/token.js b/ui-v2/app/models/token.js index dfbd7101eb..1f6a2e57e9 100644 --- a/ui-v2/app/models/token.js +++ b/ui-v2/app/models/token.js @@ -20,6 +20,7 @@ export default Model.extend({ Description: attr('string', { defaultValue: '', }), + meta: attr(), Datacenter: attr('string'), Namespace: attr('string'), Local: attr('boolean'), diff --git a/ui-v2/app/serializers/application.js b/ui-v2/app/serializers/application.js index 512904aeb5..af5f1c3d5c 100644 --- a/ui-v2/app/serializers/application.js +++ b/ui-v2/app/serializers/application.js @@ -7,6 +7,7 @@ import { HEADERS_DATACENTER as HTTP_HEADERS_DATACENTER, HEADERS_NAMESPACE as HTTP_HEADERS_NAMESPACE, } 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 { NSPACE_KEY } from 'consul-ui/models/nspace'; 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 // say `normalizeQueryResponse` // 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 // before we go through serializing const headers = payload[HTTP_HEADERS_SYMBOL] || {}; @@ -114,34 +115,39 @@ export default Serializer.extend({ // (which was the reason for the Symbol-like property earlier) // use a method modelled on ember-data methods so we have the opportunity to // do this on a per-model level - const meta = this.normalizeMeta( - store, - primaryModelClass, - headers, - normalizedPayload, - id, - requestType - ); - if (requestType === 'queryRecord') { + const meta = this.normalizeMeta(store, modelClass, headers, normalizedPayload, id, requestType); + if (requestType !== 'query') { normalizedPayload.meta = meta; } - return this._super( + const res = this._super( store, - primaryModelClass, + modelClass, { meta: meta, - [primaryModelClass.modelName]: normalizedPayload, + [modelClass.modelName]: normalizedPayload, }, id, 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() { return new Date().getTime(); }, - normalizeMeta: function(store, primaryModelClass, headers, payload, id, requestType) { + normalizeMeta: function(store, modelClass, headers, payload, id, requestType) { 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()], nspace: headers[HTTP_HEADERS_NAMESPACE.toLowerCase()], }; diff --git a/ui-v2/app/serializers/oidc-provider.js b/ui-v2/app/serializers/oidc-provider.js new file mode 100644 index 0000000000..aacd96b1e7 --- /dev/null +++ b/ui-v2/app/serializers/oidc-provider.js @@ -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 + ); + }, +}); diff --git a/ui-v2/app/serializers/token.js b/ui-v2/app/serializers/token.js index c12824efa9..b4addcf45e 100644 --- a/ui-v2/app/serializers/token.js +++ b/ui-v2/app/serializers/token.js @@ -31,6 +31,9 @@ export default Serializer.extend(WithPolicies, WithRoles, { } return data; }, + respondForSelf: function(respond, query) { + return this.respondForQueryRecord(respond, query); + }, respondForUpdateRecord: function(respond, serialized, data) { return this._super( cb => diff --git a/ui-v2/app/services/client/http.js b/ui-v2/app/services/client/http.js index a7edca107f..f1bd40ced0 100644 --- a/ui-v2/app/services/client/http.js +++ b/ui-v2/app/services/client/http.js @@ -2,6 +2,10 @@ import Service, { inject as service } from '@ember/service'; 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 getObjectPool from 'consul-ui/utils/get-object-pool'; import Request from 'consul-ui/utils/http/request'; @@ -29,7 +33,7 @@ class HTTPError extends Error { } } 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(); // unsent and opened get aborted // headers and loading means wait for it @@ -127,30 +131,40 @@ export default Service.extend({ const [url, ...headerParts] = urlParts.join(' ').split('\n'); return client.settings.findBySlug('token').then(function(token) { + const requestHeaders = createHeaders(headerParts); const headers = { // default to application/json ...{ - 'Content-Type': 'application/json; charset=utf-8', + [CONTENT_TYPE]: 'application/json; charset=utf-8', }, // 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 - ...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) { const options = { url: url.trim(), method: method, - contentType: headers['Content-Type'], + contentType: headers[CONTENT_TYPE], // type: 'json', complete: function(xhr, textStatus) { client.complete(this.id); }, success: function(response, status, xhr) { 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) { return cb(headers, response); }; @@ -191,7 +205,7 @@ export default Service.extend({ // for write-like actions // potentially we should change things so you _have_ to do that // 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); } else { // 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 https://github.com/hashicorp/consul/issues/3804 options.contentType = 'application/json; charset=utf-8'; - headers['Content-Type'] = options.contentType; + headers[CONTENT_TYPE] = options.contentType; // options.beforeSend = function(xhr) { if (headers) { diff --git a/ui-v2/app/services/repository/oidc-provider.js b/ui-v2/app/services/repository/oidc-provider.js new file mode 100644 index 0000000000..ac8db7d9c3 --- /dev/null +++ b/ui-v2/app/services/repository/oidc-provider.js @@ -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); + }); + }, +}); diff --git a/ui-v2/app/services/store.js b/ui-v2/app/services/store.js index 81773cdc32..668a525a9d 100644 --- a/ui-v2/app/services/store.js +++ b/ui-v2/app/services/store.js @@ -21,7 +21,16 @@ export default Store.extend({ self: function(modelName, token) { // TODO: no normalization, type it properly for the moment 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 @@ -31,10 +40,21 @@ export default Store.extend({ const adapter = this.adapterFor(modelName); return adapter.queryLeader(this, { modelName: modelName }, null, query); }, - // TODO: This one is only for ACL, should fail nicely if you call it - // for anything other than ACLs for good DX + // TODO: This one is only for nspaces and OIDC, should fail nicely if you call it + // for anything other than nspaces/OIDC for good DX authorize: function(modelName, query = {}) { - // TODO: no normalization, type it properly for the moment - return this.adapterFor(modelName).authorize(this, { modelName: modelName }, null, query); + const adapter = this.adapterFor(modelName); + 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); }, }); diff --git a/ui-v2/app/utils/dom/event-source/blocking.js b/ui-v2/app/utils/dom/event-source/blocking.js index 3fff08ad92..767275883a 100644 --- a/ui-v2/app/utils/dom/event-source/blocking.js +++ b/ui-v2/app/utils/dom/event-source/blocking.js @@ -96,10 +96,13 @@ export default function(EventSource, backoff = create5xxBackoff()) { // pick off the `cursor` from the meta and add it to configuration // along with cursor validation configuration.cursor = validateCursor(meta.cursor, configuration.cursor); + configuration.cacheControl = meta.cacheControl; } - this.currentEvent = event; - this.dispatchEvent(this.currentEvent); - const throttledResolve = throttle(configuration, this.currentEvent, this.previousEvent); + if ((configuration.cacheControl || '').indexOf('no-store') === -1) { + this.currentEvent = event; + } + this.dispatchEvent(event); + const throttledResolve = throttle(configuration, event, this.previousEvent); this.previousEvent = this.currentEvent; return throttledResolve(result); }); diff --git a/ui-v2/app/utils/http/consul.js b/ui-v2/app/utils/http/consul.js index 4ce38d33b5..1bcd269395 100644 --- a/ui-v2/app/utils/http/consul.js +++ b/ui-v2/app/utils/http/consul.js @@ -1,7 +1,8 @@ // TODO: Need to make all these headers capital case export const HEADERS_NAMESPACE = 'X-Consul-Namespace'; -export const HEADERS_DATACENTER = 'x-consul-datacenter'; -export const HEADERS_INDEX = 'x-consul-index'; -export const HEADERS_DIGEST = 'x-consul-contenthash'; +export const HEADERS_DATACENTER = 'X-Consul-Datacenter'; +export const HEADERS_INDEX = 'X-Consul-Index'; +export const HEADERS_TOKEN = 'X-Consul-Token'; +export const HEADERS_DIGEST = 'X-Consul-ContentHash'; // export const HEADERS_SYMBOL = '__consul_ui_http_headers__'; diff --git a/ui-v2/app/utils/http/headers.js b/ui-v2/app/utils/http/headers.js new file mode 100644 index 0000000000..51dc7f9c9a --- /dev/null +++ b/ui-v2/app/utils/http/headers.js @@ -0,0 +1,2 @@ +export const CACHE_CONTROL = 'Cache-Control'; +export const CONTENT_TYPE = 'Content-Type'; diff --git a/ui-v2/package.json b/ui-v2/package.json index b5312e18a8..22289a6c47 100644 --- a/ui-v2/package.json +++ b/ui-v2/package.json @@ -123,7 +123,8 @@ "prettier": "^1.10.2", "qunit-dom": "^1.0.0", "tape": "^4.13.0", - "text-encoding": "^0.7.0" + "text-encoding": "^0.7.0", + "torii": "^0.10.1" }, "engines": { "node": "10.* || >= 12" diff --git a/ui-v2/tests/integration/adapters/oidc-provider-test.js b/ui-v2/tests/integration/adapters/oidc-provider-test.js new file mode 100644 index 0000000000..be865e2f08 --- /dev/null +++ b/ui-v2/tests/integration/adapters/oidc-provider-test.js @@ -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); + }); + }); +}); diff --git a/ui-v2/tests/integration/services/repository/discovery-chain-test.js b/ui-v2/tests/integration/services/repository/discovery-chain-test.js index 75b45524a9..9b5ea1e89e 100644 --- a/ui-v2/tests/integration/services/repository/discovery-chain-test.js +++ b/ui-v2/tests/integration/services/repository/discovery-chain-test.js @@ -28,6 +28,7 @@ test('findBySlug returns the correct data for item endpoint', function(assert) { Datacenter: dc, uid: `["default","${dc}","${id}"]`, meta: { + cacheControl: undefined, cursor: undefined, }, }, diff --git a/ui-v2/tests/integration/services/repository/node-test.js b/ui-v2/tests/integration/services/repository/node-test.js index e2a9895eda..a6cb0f65d6 100644 --- a/ui-v2/tests/integration/services/repository/node-test.js +++ b/ui-v2/tests/integration/services/repository/node-test.js @@ -63,6 +63,7 @@ test('findBySlug returns the correct data for item endpoint', function(assert) { Datacenter: dc, uid: `["${nspace}","${dc}","${item.ID}"]`, meta: { + cacheControl: undefined, cursor: undefined, dc: dc, nspace: nspace, diff --git a/ui-v2/tests/integration/services/repository/policy-test.js b/ui-v2/tests/integration/services/repository/policy-test.js index 537cea05df..287db17397 100644 --- a/ui-v2/tests/integration/services/repository/policy-test.js +++ b/ui-v2/tests/integration/services/repository/policy-test.js @@ -71,6 +71,7 @@ const undefinedNspace = 'default'; Namespace: item.Namespace || undefinedNspace, uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.ID}"]`, meta: { + cacheControl: undefined, cursor: undefined, dc: dc, nspace: item.Namespace || undefinedNspace, diff --git a/ui-v2/tests/integration/services/repository/service-test.js b/ui-v2/tests/integration/services/repository/service-test.js index 059a2ed6b4..be2745912e 100644 --- a/ui-v2/tests/integration/services/repository/service-test.js +++ b/ui-v2/tests/integration/services/repository/service-test.js @@ -118,6 +118,7 @@ const undefinedNspace = 'default'; service.Tags = [...new Set(payload.Nodes[0].Service.Tags)]; service.Namespace = payload.Namespace; service.meta = { + cacheControl: undefined, cursor: undefined, dc: dc, nspace: payload.Namespace, diff --git a/ui-v2/tests/integration/services/repository/token-test.js b/ui-v2/tests/integration/services/repository/token-test.js index a4acc5f8ad..8d26cc39c4 100644 --- a/ui-v2/tests/integration/services/repository/token-test.js +++ b/ui-v2/tests/integration/services/repository/token-test.js @@ -69,6 +69,12 @@ const undefinedNspace = 'default'; CreateTime: new Date(item.CreateTime), Namespace: item.Namespace || undefinedNspace, uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.AccessorID}"]`, + meta: { + cacheControl: undefined, + cursor: undefined, + dc: dc, + nspace: item.Namespace || undefinedNspace, + }, Policies: createPolicies(item), }); }) diff --git a/ui-v2/tests/unit/adapters/oidc-provider-test.js b/ui-v2/tests/unit/adapters/oidc-provider-test.js new file mode 100644 index 0000000000..291f4afe96 --- /dev/null +++ b/ui-v2/tests/unit/adapters/oidc-provider-test.js @@ -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); + }); +}); diff --git a/ui-v2/yarn.lock b/ui-v2/yarn.lock index 3249937cbb..1d46f5bec4 100644 --- a/ui-v2/yarn.lock +++ b/ui-v2/yarn.lock @@ -12420,6 +12420,13 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" 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: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"