diff --git a/ui-v2/app/adapters/acl.js b/ui-v2/app/adapters/acl.js index 7f691be7bd57..408969a10424 100644 --- a/ui-v2/app/adapters/acl.js +++ b/ui-v2/app/adapters/acl.js @@ -1,9 +1,6 @@ import Adapter, { DATACENTER_QUERY_PARAM as API_DATACENTER_KEY } from './application'; -import { get } from '@ember/object'; -import EmberError from '@ember/error'; -import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/acl'; +import { SLUG_KEY } from 'consul-ui/models/acl'; import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc'; -import { OK as HTTP_OK, UNAUTHORIZED as HTTP_UNAUTHORIZED } from 'consul-ui/utils/http/status'; export default Adapter.extend({ requestForQuery: function(request, { dc, index }) { @@ -25,65 +22,45 @@ export default Adapter.extend({ ${{ index }} `; }, - requestForCreateRecord: function(request, data) { + requestForCreateRecord: function(request, serialized, data) { // https://www.consul.io/api/acl.html#create-acl-token return request` PUT /v1/acl/create?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} + + ${serialized} `; }, - requestForUpdateRecord: function(request, data) { - // the id is in the data, don't add it in here + requestForUpdateRecord: function(request, serialized, data) { + // the id is in the data, don't add it into the URL // https://www.consul.io/api/acl.html#update-acl-token return request` PUT /v1/acl/update?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} + + ${serialized} `; }, - requestForDeleteRecord: function(request, data) { + requestForDeleteRecord: function(request, serialized, data) { // https://www.consul.io/api/acl.html#delete-acl-token return request` PUT /v1/acl/destroy/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} `; }, - requestForCloneRecord: function(request, data) { + requestForCloneRecord: function(request, serialized, data) { // https://www.consul.io/api/acl.html#clone-acl-token return request` PUT /v1/acl/clone/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} `; }, clone: function(store, type, id, snapshot) { - const serializer = store.serializerFor(type.modelName); - const unserialized = this.snapshotToJSON(snapshot, type); - const serialized = serializer.serialize(snapshot, {}); - return get(this, 'client') - .request(request => this.requestForClone(request, unserialized), serialized) - .then(respond => serializer.respondForQueryRecord(respond, unserialized)); - }, - handleResponse: function(status, headers, payload, requestData) { - let response = payload; - const method = requestData.method; - if (status === HTTP_OK) { - const url = this.parseURL(requestData.url); - switch (true) { - case response === true: - response = this.handleBooleanResponse(url, response, PRIMARY_KEY, SLUG_KEY); - break; - case this.isQueryRecord(url): - response = this.handleSingleResponse(url, response[0], PRIMARY_KEY, SLUG_KEY); - break; - case this.isUpdateRecord(url, method): - case this.isCreateRecord(url, method): - case this.isCloneRecord(url, method): - response = this.handleSingleResponse(url, response, PRIMARY_KEY, SLUG_KEY); - break; - default: - response = this.handleBatchResponse(url, response, PRIMARY_KEY, SLUG_KEY); - } - } else if (status === HTTP_UNAUTHORIZED) { - const e = new EmberError(); - e.code = status; - e.message = payload; - throw e; - } - return this._super(status, headers, response, requestData); + return this.request( + function(adapter, request, serialized, unserialized) { + return adapter.requestForCloneRecord(request, serialized, unserialized); + }, + function(serializer, response, serialized, unserialized) { + return serializer.respondForCreateRecord(response, serialized, unserialized); + }, + snapshot, + type.modelName + ); }, }); diff --git a/ui-v2/app/adapters/application.js b/ui-v2/app/adapters/application.js index ea13bbfa9357..1e90593fe0c3 100644 --- a/ui-v2/app/adapters/application.js +++ b/ui-v2/app/adapters/application.js @@ -1,8 +1,44 @@ import Adapter from './http'; import { inject as service } from '@ember/service'; - +import { get } from '@ember/object'; export const DATACENTER_QUERY_PARAM = 'dc'; export default Adapter.extend({ repo: service('settings'), client: service('client/http'), + // TODO: kinda protected for the moment + // decide where this should go either read/write from http + // should somehow use this or vice versa + request: function(req, resp, obj, modelName) { + const client = get(this, 'client'); + const store = get(this, 'store'); + const adapter = this; + + let unserialized, serialized; + const serializer = store.serializerFor(modelName); + // workable way to decide whether this is a snapshot + // Snapshot is private so we can't do instanceof here + if (obj.constructor.name === 'Snapshot') { + unserialized = obj.attributes(); + serialized = serializer.serialize(obj, {}); + } else { + unserialized = obj; + serialized = unserialized; + } + + return client + .request(function(request) { + return req(adapter, request, serialized, unserialized); + }) + .catch(function(e) { + return adapter.error(e); + }) + .then(function(response) { + // TODO: When HTTPAdapter:responder changes, this will also need to change + return resp(serializer, response, serialized, unserialized); + }); + // TODO: Potentially add specific serializer errors here + // .catch(function(e) { + // return Promise.reject(e); + // }); + }, }); diff --git a/ui-v2/app/adapters/http.js b/ui-v2/app/adapters/http.js index c91ffa065dd5..d6d2e284451b 100644 --- a/ui-v2/app/adapters/http.js +++ b/ui-v2/app/adapters/http.js @@ -1,4 +1,5 @@ import Adapter from 'ember-data/adapter'; +import { get } from '@ember/object'; import { AbortError, TimeoutError, @@ -10,12 +11,47 @@ import { InvalidError, AdapterError, } from 'ember-data/adapters/errors'; -import { get } from '@ember/object'; +// TODO: This is a little skeleton cb function +// is to be replaced soon with something slightly more involved +const responder = function(response) { + return response; +}; +const read = function(adapter, serializer, client, type, query) { + return client + .request(function(request) { + return adapter[`requestFor${type}`](request, query); + }) + .catch(function(e) { + return adapter.error(e); + }) + .then(function(response) { + return serializer[`respondFor${type}`](responder(response), query); + }); + // TODO: Potentially add specific serializer errors here + // .catch(function(e) { + // return Promise.reject(e); + // }); +}; +const write = function(adapter, serializer, client, type, snapshot) { + const unserialized = snapshot.attributes(); + const serialized = serializer.serialize(snapshot, {}); + return client + .request(function(request) { + return adapter[`requestFor${type}`](request, serialized, unserialized); + }) + .catch(function(e) { + return adapter.error(e); + }) + .then(function(response) { + return serializer[`respondFor${type}`](responder(response), serialized, unserialized); + }); + // TODO: Potentially add specific serializer errors here + // .catch(function(e) { + // return Promise.reject(e); + // }); +}; export default Adapter.extend({ - snapshotToJSON: function(snapshot, type, options) { - return snapshot.attributes(); - }, error: function(err) { const errors = [ { @@ -62,51 +98,45 @@ export default Adapter.extend({ throw error; }, query: function(store, type, query) { - const serializer = store.serializerFor(type.modelName); - return get(this, 'client') - .request(request => this.requestForQuery(request, query)) - .catch(e => this.error(e)) - .then(respond => serializer.respondForQuery(respond, query, type)); + return read(this, store.serializerFor(type.modelName), get(this, 'client'), 'Query', query); }, queryRecord: function(store, type, query) { - const serializer = store.serializerFor(type.modelName); - return get(this, 'client') - .request(request => this.requestForQueryRecord(request, query)) - .catch(e => this.error(e)) - .then(respond => serializer.respondForQueryRecord(respond, query)); + return read( + this, + store.serializerFor(type.modelName), + get(this, 'client'), + 'QueryRecord', + query + ); }, findAll: function(store, type) { - const serializer = store.serializerFor(type.modelName); - return get(this, 'client') - .request(request => this.requestForFindAll(request)) - .catch(e => this.error(e)) - .then(respond => serializer.respondForFindAll(respond)); + return read(this, store.serializerFor(type.modelName), get(this, 'client'), 'FindAll'); }, createRecord: function(store, type, snapshot) { - const serializer = store.serializerFor(type.modelName); - const unserialized = this.snapshotToJSON(snapshot, type); - const serialized = serializer.serialize(snapshot, {}); - return get(this, 'client') - .request(request => this.requestForCreateRecord(request, unserialized), serialized) - .catch(e => this.error(e)) - .then(respond => serializer.respondForCreateRecord(respond, unserialized, type)); + return write( + this, + store.serializerFor(type.modelName), + get(this, 'client'), + 'CreateRecord', + snapshot + ); }, updateRecord: function(store, type, snapshot) { - const serializer = store.serializerFor(type.modelName); - const unserialized = this.snapshotToJSON(snapshot, type); - const serialized = serializer.serialize(snapshot, {}); - return get(this, 'client') - .request(request => this.requestForUpdateRecord(request, unserialized), serialized) - .catch(e => this.error(e)) - .then(respond => serializer.respondForUpdateRecord(respond, unserialized, type)); + return write( + this, + store.serializerFor(type.modelName), + get(this, 'client'), + 'UpdateRecord', + snapshot + ); }, deleteRecord: function(store, type, snapshot) { - const serializer = store.serializerFor(type.modelName); - const unserialized = this.snapshotToJSON(snapshot, type); - const serialized = serializer.serialize(snapshot, {}); - return get(this, 'client') - .request(request => this.requestForDeleteRecord(request, unserialized), serialized) - .catch(e => this.error(e)) - .then(respond => serializer.respondForDeleteRecord(respond, unserialized, type)); + return write( + this, + store.serializerFor(type.modelName), + get(this, 'client'), + 'DeleteRecord', + snapshot + ); }, }); diff --git a/ui-v2/app/adapters/intention.js b/ui-v2/app/adapters/intention.js index 2b30b3d2ac3e..f0dee774e805 100644 --- a/ui-v2/app/adapters/intention.js +++ b/ui-v2/app/adapters/intention.js @@ -19,18 +19,22 @@ export default Adapter.extend({ ${{ index }} `; }, - requestForCreateRecord: function(request, data) { + requestForCreateRecord: function(request, serialized, data) { // TODO: need to make sure we remove dc return request` POST /v1/connect/intentions?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} + + ${serialized} `; }, - requestForUpdateRecord: function(request, data) { + requestForUpdateRecord: function(request, serialized, data) { return request` PUT /v1/connect/intentions/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} + + ${serialized} `; }, - requestForDeleteRecord: function(request, data) { + requestForDeleteRecord: function(request, serialized, data) { return request` DELETE /v1/connect/intentions/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY], diff --git a/ui-v2/app/adapters/kv.js b/ui-v2/app/adapters/kv.js index 6b81a5dd6157..2655953e9d67 100644 --- a/ui-v2/app/adapters/kv.js +++ b/ui-v2/app/adapters/kv.js @@ -28,19 +28,25 @@ export default Adapter.extend({ ${{ index }} `; }, - requestForCreateRecord: function(request, data) { + // TODO: Should we replace text/plain here with x-www-form-encoded? + // See https://github.com/hashicorp/consul/issues/3804 + requestForCreateRecord: function(request, serialized, data) { return request` PUT /v1/kv/${keyToArray(data[SLUG_KEY])}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} - Content-Type: application/x-www-form-urlencoded + Content-Type: text/plain; charset=utf-8 + + ${serialized} `; }, - requestForUpdateRecord: function(request, data) { + requestForUpdateRecord: function(request, serialized, data) { return request` PUT /v1/kv/${keyToArray(data[SLUG_KEY])}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} - Content-Type: application/x-www-form-urlencoded + Content-Type: text/plain; charset=utf-8 + + ${serialized} `; }, - requestForDeleteRecord: function(request, data) { + requestForDeleteRecord: function(request, serialized, data) { let recurse; if (isFolder(data[SLUG_KEY])) { recurse = null; diff --git a/ui-v2/app/adapters/policy.js b/ui-v2/app/adapters/policy.js index ec2d0d244a0a..ed5772878dc5 100644 --- a/ui-v2/app/adapters/policy.js +++ b/ui-v2/app/adapters/policy.js @@ -21,17 +21,21 @@ export default Adapter.extend({ ${{ index }} `; }, - requestForCreateRecord: function(request, data) { + requestForCreateRecord: function(request, serialized, data) { return request` PUT /v1/acl/policy?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} + + ${serialized} `; }, - requestForUpdateRecord: function(request, data) { + requestForUpdateRecord: function(request, serialized, data) { return request` PUT /v1/acl/policy/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} + + ${serialized} `; }, - requestForDeleteRecord: function(request, data) { + requestForDeleteRecord: function(request, serialized, data) { return request` DELETE /v1/acl/policy/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} `; diff --git a/ui-v2/app/adapters/role.js b/ui-v2/app/adapters/role.js index 84f6d39330b7..e7089640b3a3 100644 --- a/ui-v2/app/adapters/role.js +++ b/ui-v2/app/adapters/role.js @@ -21,17 +21,21 @@ export default Adapter.extend({ ${{ index }} `; }, - requestForCreateRecord: function(request, data) { + requestForCreateRecord: function(request, serialized, data) { return request` PUT /v1/acl/role?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} + + ${serialized} `; }, - requestForUpdateRecord: function(request, data) { + requestForUpdateRecord: function(request, serialized, data) { return request` PUT /v1/acl/role/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} + + ${serialized} `; }, - requestForDeleteRecord: function(request, data) { + requestForDeleteRecord: function(request, serialized, data) { return request` DELETE /v1/acl/role/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} `; diff --git a/ui-v2/app/adapters/session.js b/ui-v2/app/adapters/session.js index f178fa877b48..c324796a1b79 100644 --- a/ui-v2/app/adapters/session.js +++ b/ui-v2/app/adapters/session.js @@ -23,7 +23,7 @@ export default Adapter.extend({ ${{ index }} `; }, - requestForDeleteRecord: function(request, data) { + requestForDeleteRecord: function(request, serialized, data) { return request` PUT /v1/session/destroy/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} `; diff --git a/ui-v2/app/adapters/token.js b/ui-v2/app/adapters/token.js index ce2a5faca869..9f51ae70b51d 100644 --- a/ui-v2/app/adapters/token.js +++ b/ui-v2/app/adapters/token.js @@ -1,6 +1,5 @@ import Adapter, { DATACENTER_QUERY_PARAM as API_DATACENTER_KEY } from './application'; import { inject as service } from '@ember/service'; -import { get } from '@ember/object'; import { SLUG_KEY } from 'consul-ui/models/token'; import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc'; @@ -24,36 +23,39 @@ export default Adapter.extend({ ${{ index }} `; }, - requestForCreateRecord: function(request, data) { + requestForCreateRecord: function(request, serialized, data) { return request` PUT /v1/acl/token?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} `; }, - requestForUpdateRecord: function(request, data) { - // TODO: Serializer - Pretty sure this can go now + requestForUpdateRecord: function(request, serialized, data) { + // TODO: here we check data['Rules'] not serialized['Rules'] + // data.Rules is not undefined, and serialized.Rules is not null + // revisit this at some point we should probably use serialized here + // If a token has Rules, use the old API if (typeof data['Rules'] !== 'undefined') { - // TODO: need to clean up vars sent - data['ID'] = data['SecretID']; - data['Name'] = data['Description']; + // https://www.consul.io/api/acl/legacy.html#update-acl-token return request` PUT /v1/acl/update?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} + + ${serialized} `; } - if (typeof data['SecretID'] !== 'undefined') { - delete data['SecretID']; - } return request` PUT /v1/acl/token/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} + + ${serialized} `; }, - requestForDeleteRecord: function(request, data) { + requestForDeleteRecord: function(request, serialized, data) { return request` DELETE /v1/acl/token/${data[SLUG_KEY]}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }} `; }, - requestForSelf: function(request, { dc, index, secret }) { - // do we need dc and index here? + requestForSelf: function(request, serialized, { dc, index, secret }) { + // TODO: Change here and elsewhere to use Authorization Bearer Token + // https://github.com/hashicorp/consul/pull/4502 return request` GET /v1/acl/token/self?${{ dc }} X-Consul-Token: ${secret} @@ -61,33 +63,48 @@ export default Adapter.extend({ ${{ index }} `; }, - requestForClone: function(request, { dc, id }) { + requestForCloneRecord: function(request, serialized, unserialized) { // this uses snapshots + const id = unserialized[SLUG_KEY]; + const dc = unserialized[DATACENTER_KEY]; if (typeof id === 'undefined') { throw new Error('You must specify an id'); } return request` - PUT /v1/acl/token/${id}/clone?${{ dc }} + PUT /v1/acl/token/${id}/clone?${{ [API_DATACENTER_KEY]: dc }} `; }, // TODO: self doesn't get passed a snapshot right now // ideally it would just for consistency - // thing is its probably not the same shape as a 'Token' - self: function(store, type, unserialized) { - const serializer = store.serializerFor(type.modelName); - // const unserialized = this.snapshotToJSON(snapshot, type); - const serialized = unserialized; //serializer.serialize(snapshot, {}); - return get(this, 'client') - .request(request => this.requestForSelf(request, unserialized), serialized) - .then(respond => serializer.respondForQueryRecord(respond, unserialized)); + // thing is its probably not the same shape as a 'Token', + // plus we can't create Snapshots as they are private, see services/store.js + self: function(store, type, id, unserialized) { + return this.request( + function(adapter, request, serialized, unserialized) { + return adapter.requestForSelf(request, serialized, unserialized); + }, + function(serializer, response, serialized, unserialized) { + return serializer.respondForQueryRecord(response, serialized, unserialized); + }, + unserialized, + type.modelName + ); }, - // TODO: Does id even need to be here now? clone: function(store, type, id, snapshot) { - const serializer = store.serializerFor(type.modelName); - const unserialized = this.snapshotToJSON(snapshot, type); - const serialized = serializer.serialize(snapshot, {}); - return get(this, 'client') - .request(request => this.requestForClone(request, unserialized), serialized) - .then(respond => serializer.respondForQueryRecord(respond, unserialized)); + return this.request( + function(adapter, request, serialized, unserialized) { + return adapter.requestForCloneRecord(request, serialized, unserialized); + }, + function(serializer, response, serialized, unserialized) { + // here we just have to pass through the dc (like when querying) + // eventually the id is created with this dc value and the id talen from the + // json response of `acls/token/*/clone` + return serializer.respondForQueryRecord(response, { + [API_DATACENTER_KEY]: unserialized[SLUG_KEY], + }); + }, + snapshot, + type.modelName + ); }, }); diff --git a/ui-v2/app/serializers/application.js b/ui-v2/app/serializers/application.js index 41bb5d63b9ca..486228251bb3 100644 --- a/ui-v2/app/serializers/application.js +++ b/ui-v2/app/serializers/application.js @@ -37,7 +37,7 @@ export default Serializer.extend({ attachHeaders(headers, this.fingerprint(this.primaryKey, this.slugKey, query.dc)(body)) ); }, - respondForCreateRecord: function(respond, data) { + respondForCreateRecord: function(respond, serialized, data) { const slugKey = this.slugKey; const primaryKey = this.primaryKey; return respond((headers, body) => { @@ -49,7 +49,7 @@ export default Serializer.extend({ return this.fingerprint(primaryKey, slugKey, data[DATACENTER_KEY])(body); }); }, - respondForUpdateRecord: function(respond, data) { + respondForUpdateRecord: function(respond, serialized, data) { const slugKey = this.slugKey; const primaryKey = this.primaryKey; return respond((headers, body) => { @@ -60,7 +60,7 @@ export default Serializer.extend({ return this.fingerprint(primaryKey, slugKey, data[DATACENTER_KEY])(body); }); }, - respondForDeleteRecord: function(respond, data) { + respondForDeleteRecord: function(respond, serialized, data) { const slugKey = this.slugKey; const primaryKey = this.primaryKey; return respond((headers, body) => { diff --git a/ui-v2/app/serializers/token.js b/ui-v2/app/serializers/token.js index ef48aa072b76..026e92f80fda 100644 --- a/ui-v2/app/serializers/token.js +++ b/ui-v2/app/serializers/token.js @@ -10,21 +10,29 @@ export default Serializer.extend(WithPolicies, WithRoles, { slugKey: SLUG_KEY, attrs: ATTRS, serialize: function(snapshot, options) { - const data = this._super(...arguments); - // TODO: Check this as it used to be only on update - // Pretty sure Rules will only ever be on update as you can't - // create legacy tokens here - if (typeof data['Rules'] !== 'undefined') { - data['ID'] = data['SecretID']; - data['Name'] = data['Description']; + let data = this._super(...arguments); + // If a token has Rules, use the old API shape + // notice we use a null check here (not an undefined check) + // as we are dealing with the serialized model not raw user data + if (data['Rules'] !== null) { + data = { + ID: data.SecretID, + Name: data.Description, + Type: data.Type, + Rules: data.Rules, + }; } // make sure we never send the SecretID - if (data && typeof data['SecretID'] !== 'undefined') { + // TODO: If we selectively format the request payload in the adapter + // we won't have to do this here + // see side note in https://github.com/hashicorp/consul/pull/6285 + // which will mean most if not all of this method can go + if (data) { delete data['SecretID']; } return data; }, - respondForUpdateRecord: function(respond, query) { + respondForUpdateRecord: function(respond, serialized, data) { return this._super( cb => respond((headers, body) => { @@ -44,7 +52,8 @@ export default Serializer.extend(WithPolicies, WithRoles, { } return cb(headers, body); }), - query + serialized, + data ); }, }); diff --git a/ui-v2/app/services/client/http.js b/ui-v2/app/services/client/http.js index dc52c4e4f62c..51d316458e71 100644 --- a/ui-v2/app/services/client/http.js +++ b/ui-v2/app/services/client/http.js @@ -30,6 +30,11 @@ const dispose = function(request) { } return request; }; +// TODO: Potentially url should check if any of the params +// passed to it are undefined (null is fine). We could then get rid of the +// multitude of checks we do throughout the adapters +// right now createURL converts undefined to '' so we need to check thats not needed +// anywhere (todo written here for visibility) const url = createURL(encodeURIComponent); export default Service.extend({ dom: service('dom'), @@ -76,9 +81,10 @@ export default Service.extend({ url: function() { return url(...arguments); }, - request: function(cb, body) { + request: function(cb) { const client = this; return cb(function(strs, ...values) { + let body = {}; const doubleBreak = strs.reduce(function(prev, item, i) { if (item.indexOf('\n\n') !== -1) { return i; @@ -87,11 +93,15 @@ export default Service.extend({ }, -1); if (doubleBreak !== -1) { body = values.splice(doubleBreak).reduce(function(prev, item) { - return { - ...prev, - ...item, - }; - }, body || {}); + if (typeof item !== 'string') { + return { + ...prev, + ...item, + }; + } else { + return item; + } + }, body); } let temp = url(strs, ...values).split(' '); const method = temp.shift(); @@ -165,15 +175,21 @@ 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') { - if (headers['Content-Type'].indexOf('json') !== -1) { - options.data = JSON.stringify(body); - } else { - // TODO: Does this need urlencoding? Assuming jQuery does this - options.data = body; - } + if (method !== 'GET' && headers['Content-Type'].indexOf('json') !== -1) { + options.data = JSON.stringify(body); + } else { + // TODO: Does this need urlencoding? Assuming jQuery does this + options.data = body; } } + // temporarily reset the headers/content-type so it works the same + // as previously, should be able to remove this once the data layer + // rewrite is over and we can assert sending via form-encoded is fine + // 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; + // options.beforeSend = function(xhr) { if (headers) { Object.keys(headers).forEach(key => xhr.setRequestHeader(key, headers[key])); diff --git a/ui-v2/app/services/store.js b/ui-v2/app/services/store.js index 081508e9a3ed..c4fa4becec01 100644 --- a/ui-v2/app/services/store.js +++ b/ui-v2/app/services/store.js @@ -21,7 +21,7 @@ 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); + return adapter.self(this, { modelName: modelName }, token.secret, token); }, queryLeader: function(modelName, query) { // TODO: no normalization, type it properly for the moment diff --git a/ui-v2/app/templates/dc/acls/tokens/edit.hbs b/ui-v2/app/templates/dc/acls/tokens/edit.hbs index 8d40c1509c16..0d35fe34dabd 100644 --- a/ui-v2/app/templates/dc/acls/tokens/edit.hbs +++ b/ui-v2/app/templates/dc/acls/tokens/edit.hbs @@ -43,7 +43,7 @@ {{/confirmation-dialog}} {{/if}} {{#if (not (token/is-legacy item))}} - + {{/if}} {{/if}} {{/block-slot}} diff --git a/ui-v2/tests/acceptance/dc/acls/tokens/clone.feature b/ui-v2/tests/acceptance/dc/acls/tokens/clone.feature new file mode 100644 index 000000000000..61d9bdf3785b --- /dev/null +++ b/ui-v2/tests/acceptance/dc/acls/tokens/clone.feature @@ -0,0 +1,29 @@ +@setupApplicationTest +Feature: dc / acls / tokens / clone: Cloning an ACL token + Background: + Given 1 datacenter model with the value "datacenter" + And 1 token model from yaml + --- + AccessorID: token + SecretID: ee52203d-989f-4f7a-ab5a-2bef004164ca + --- + Scenario: Cloning an ACL token from the listing page + When I visit the tokens page for yaml + --- + dc: datacenter + --- + And I click actions on the tokens + And I click clone on the tokens + Then a PUT request is made to "/v1/acl/token/token/clone?dc=datacenter" + Then "[data-notification]" has the "notification-clone" class + And "[data-notification]" has the "success" class + Scenario: Using an ACL token from the detail page + When I visit the token page for yaml + --- + dc: datacenter + token: token + --- + And I click clone + Then the url should be /datacenter/acls/tokens + Then "[data-notification]" has the "notification-clone" class + And "[data-notification]" has the "success" class diff --git a/ui-v2/tests/acceptance/steps/dc/acls/tokens/clone-steps.js b/ui-v2/tests/acceptance/steps/dc/acls/tokens/clone-steps.js new file mode 100644 index 000000000000..9bfbe9ac9b4f --- /dev/null +++ b/ui-v2/tests/acceptance/steps/dc/acls/tokens/clone-steps.js @@ -0,0 +1,10 @@ +import steps from '../../../steps'; + +// step definitions that are shared between features should be moved to the +// tests/acceptance/steps/steps.js file + +export default function(assert) { + return steps(assert).then('I should find a file', function() { + assert.ok(true, this.step); + }); +} diff --git a/ui-v2/tests/integration/adapters/acl-test.js b/ui-v2/tests/integration/adapters/acl-test.js index 5b921124fa02..930463de3785 100644 --- a/ui-v2/tests/integration/adapters/acl-test.js +++ b/ui-v2/tests/integration/adapters/acl-test.js @@ -37,10 +37,14 @@ module('Integration | Adapter | acl', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `PUT /v1/acl/create?dc=${dc}`; const actual = adapter - .requestForCreateRecord(client.url, { - Datacenter: dc, - ID: id, - }) + .requestForCreateRecord( + client.url, + {}, + { + Datacenter: dc, + ID: id, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); @@ -49,10 +53,14 @@ module('Integration | Adapter | acl', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `PUT /v1/acl/update?dc=${dc}`; const actual = adapter - .requestForUpdateRecord(client.url, { - Datacenter: dc, - ID: id, - }) + .requestForUpdateRecord( + client.url, + {}, + { + Datacenter: dc, + ID: id, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); @@ -61,10 +69,14 @@ module('Integration | Adapter | acl', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `PUT /v1/acl/destroy/${id}?dc=${dc}`; const actual = adapter - .requestForDeleteRecord(client.url, { - Datacenter: dc, - ID: id, - }) + .requestForDeleteRecord( + client.url, + {}, + { + Datacenter: dc, + ID: id, + } + ) .split('/n')[0]; assert.equal(actual, expected); }); @@ -73,10 +85,14 @@ module('Integration | Adapter | acl', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `PUT /v1/acl/clone/${id}?dc=${dc}`; const actual = adapter - .requestForCloneRecord(client.url, { - Datacenter: dc, - ID: id, - }) + .requestForCloneRecord( + client.url, + {}, + { + Datacenter: dc, + ID: id, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); diff --git a/ui-v2/tests/integration/adapters/intention-test.js b/ui-v2/tests/integration/adapters/intention-test.js index 5d6df796216d..172a2dd5ca80 100644 --- a/ui-v2/tests/integration/adapters/intention-test.js +++ b/ui-v2/tests/integration/adapters/intention-test.js @@ -37,10 +37,14 @@ module('Integration | Adapter | intention', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `POST /v1/connect/intentions?dc=${dc}`; const actual = adapter - .requestForCreateRecord(client.url, { - Datacenter: dc, - ID: id, - }) + .requestForCreateRecord( + client.url, + {}, + { + Datacenter: dc, + ID: id, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); @@ -49,10 +53,14 @@ module('Integration | Adapter | intention', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `PUT /v1/connect/intentions/${id}?dc=${dc}`; const actual = adapter - .requestForUpdateRecord(client.url, { - Datacenter: dc, - ID: id, - }) + .requestForUpdateRecord( + client.url, + {}, + { + Datacenter: dc, + ID: id, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); @@ -61,10 +69,14 @@ module('Integration | Adapter | intention', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `DELETE /v1/connect/intentions/${id}?dc=${dc}`; const actual = adapter - .requestForDeleteRecord(client.url, { - Datacenter: dc, - ID: id, - }) + .requestForDeleteRecord( + client.url, + {}, + { + Datacenter: dc, + ID: id, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); diff --git a/ui-v2/tests/integration/adapters/kv-test.js b/ui-v2/tests/integration/adapters/kv-test.js index 5ccdf402477e..fdcfe1c8660a 100644 --- a/ui-v2/tests/integration/adapters/kv-test.js +++ b/ui-v2/tests/integration/adapters/kv-test.js @@ -47,11 +47,15 @@ module('Integration | Adapter | kv', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `PUT /v1/kv/${id}?dc=${dc}`; const actual = adapter - .requestForCreateRecord(client.url, { - Datacenter: dc, - Key: id, - Value: '', - }) + .requestForCreateRecord( + client.url, + {}, + { + Datacenter: dc, + Key: id, + Value: '', + } + ) .split('\n')[0]; assert.equal(actual, expected); }); @@ -60,11 +64,15 @@ module('Integration | Adapter | kv', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `PUT /v1/kv/${id}?dc=${dc}`; const actual = adapter - .requestForUpdateRecord(client.url, { - Datacenter: dc, - Key: id, - Value: '', - }) + .requestForUpdateRecord( + client.url, + {}, + { + Datacenter: dc, + Key: id, + Value: '', + } + ) .split('\n')[0]; assert.equal(actual, expected); }); @@ -72,10 +80,14 @@ module('Integration | Adapter | kv', function(hooks) { const adapter = this.owner.lookup('adapter:kv'); const client = this.owner.lookup('service:client/http'); const expected = `DELETE /v1/kv/${id}?dc=${dc}`; - const actual = adapter.requestForDeleteRecord(client.url, { - Datacenter: dc, - Key: id, - }); + const actual = adapter.requestForDeleteRecord( + client.url, + {}, + { + Datacenter: dc, + Key: id, + } + ); assert.equal(actual, expected); }); test('requestForDeleteRecord returns the correct url/method for folders', function(assert) { @@ -83,10 +95,14 @@ module('Integration | Adapter | kv', function(hooks) { const client = this.owner.lookup('service:client/http'); const folder = `${id}/`; const expected = `DELETE /v1/kv/${folder}?dc=${dc}&recurse`; - const actual = adapter.requestForDeleteRecord(client.url, { - Datacenter: dc, - Key: folder, - }); + const actual = adapter.requestForDeleteRecord( + client.url, + {}, + { + Datacenter: dc, + Key: folder, + } + ); assert.equal(actual, expected); }); }); diff --git a/ui-v2/tests/integration/adapters/policy-test.js b/ui-v2/tests/integration/adapters/policy-test.js index 8b7e5f30616b..9077fc0c5eac 100644 --- a/ui-v2/tests/integration/adapters/policy-test.js +++ b/ui-v2/tests/integration/adapters/policy-test.js @@ -37,9 +37,13 @@ module('Integration | Adapter | policy', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `PUT /v1/acl/policy?dc=${dc}`; const actual = adapter - .requestForCreateRecord(client.url, { - Datacenter: dc, - }) + .requestForCreateRecord( + client.url, + {}, + { + Datacenter: dc, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); @@ -48,10 +52,14 @@ module('Integration | Adapter | policy', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `PUT /v1/acl/policy/${id}?dc=${dc}`; const actual = adapter - .requestForUpdateRecord(client.url, { - Datacenter: dc, - ID: id, - }) + .requestForUpdateRecord( + client.url, + {}, + { + Datacenter: dc, + ID: id, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); @@ -60,10 +68,14 @@ module('Integration | Adapter | policy', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `DELETE /v1/acl/policy/${id}?dc=${dc}`; const actual = adapter - .requestForDeleteRecord(client.url, { - Datacenter: dc, - ID: id, - }) + .requestForDeleteRecord( + client.url, + {}, + { + Datacenter: dc, + ID: id, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); diff --git a/ui-v2/tests/integration/adapters/role-test.js b/ui-v2/tests/integration/adapters/role-test.js index 4de929c577e5..d5a8b8bfdbe1 100644 --- a/ui-v2/tests/integration/adapters/role-test.js +++ b/ui-v2/tests/integration/adapters/role-test.js @@ -37,9 +37,13 @@ module('Integration | Adapter | role', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `PUT /v1/acl/role?dc=${dc}`; const actual = adapter - .requestForCreateRecord(client.url, { - Datacenter: dc, - }) + .requestForCreateRecord( + client.url, + {}, + { + Datacenter: dc, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); @@ -48,10 +52,14 @@ module('Integration | Adapter | role', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `PUT /v1/acl/role/${id}?dc=${dc}`; const actual = adapter - .requestForUpdateRecord(client.url, { - Datacenter: dc, - ID: id, - }) + .requestForUpdateRecord( + client.url, + {}, + { + Datacenter: dc, + ID: id, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); @@ -60,10 +68,14 @@ module('Integration | Adapter | role', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `DELETE /v1/acl/role/${id}?dc=${dc}`; const actual = adapter - .requestForDeleteRecord(client.url, { - Datacenter: dc, - ID: id, - }) + .requestForDeleteRecord( + client.url, + {}, + { + Datacenter: dc, + ID: id, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); diff --git a/ui-v2/tests/integration/adapters/session-test.js b/ui-v2/tests/integration/adapters/session-test.js index 10b71694c7d9..c8bc9614b359 100644 --- a/ui-v2/tests/integration/adapters/session-test.js +++ b/ui-v2/tests/integration/adapters/session-test.js @@ -48,10 +48,14 @@ module('Integration | Adapter | session', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `PUT /v1/session/destroy/${id}?dc=${dc}`; const actual = adapter - .requestForDeleteRecord(client.url, { - Datacenter: dc, - ID: id, - }) + .requestForDeleteRecord( + client.url, + {}, + { + Datacenter: dc, + ID: id, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); diff --git a/ui-v2/tests/integration/adapters/token-test.js b/ui-v2/tests/integration/adapters/token-test.js index c08ef0cc3220..a310e25e2750 100644 --- a/ui-v2/tests/integration/adapters/token-test.js +++ b/ui-v2/tests/integration/adapters/token-test.js @@ -57,9 +57,13 @@ module('Integration | Adapter | token', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `PUT /v1/acl/token?dc=${dc}`; const actual = adapter - .requestForCreateRecord(client.url, { - Datacenter: dc, - }) + .requestForCreateRecord( + client.url, + {}, + { + Datacenter: dc, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); @@ -68,10 +72,14 @@ module('Integration | Adapter | token', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `PUT /v1/acl/token/${id}?dc=${dc}`; const actual = adapter - .requestForUpdateRecord(client.url, { - Datacenter: dc, - AccessorID: id, - }) + .requestForUpdateRecord( + client.url, + {}, + { + Datacenter: dc, + AccessorID: id, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); @@ -80,11 +88,15 @@ module('Integration | Adapter | token', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `PUT /v1/acl/update?dc=${dc}`; const actual = adapter - .requestForUpdateRecord(client.url, { - Rules: 'key {}', - Datacenter: dc, - AccessorID: id, - }) + .requestForUpdateRecord( + client.url, + {}, + { + Rules: 'key {}', + Datacenter: dc, + AccessorID: id, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); @@ -93,11 +105,64 @@ module('Integration | Adapter | token', function(hooks) { const client = this.owner.lookup('service:client/http'); const expected = `DELETE /v1/acl/token/${id}?dc=${dc}`; const actual = adapter - .requestForDeleteRecord(client.url, { - Datacenter: dc, - AccessorID: id, - }) + .requestForDeleteRecord( + client.url, + {}, + { + Datacenter: dc, + AccessorID: id, + } + ) .split('\n')[0]; assert.equal(actual, expected); }); + test('requestForCloneRecord returns the correct url', function(assert) { + const adapter = this.owner.lookup('adapter:token'); + const client = this.owner.lookup('service:client/http'); + const expected = `PUT /v1/acl/token/${id}/clone?dc=${dc}`; + const actual = adapter + .requestForCloneRecord( + client.url, + {}, + { + Datacenter: dc, + AccessorID: id, + } + ) + .split('\n')[0]; + assert.equal(actual, expected); + }); + test('requestForSelf returns the correct url', function(assert) { + const adapter = this.owner.lookup('adapter:token'); + const client = this.owner.lookup('service:client/http'); + const expected = `GET /v1/acl/token/self?dc=${dc}`; + const actual = adapter + .requestForSelf( + client.url, + {}, + { + dc: dc, + } + ) + .split('\n')[0]; + assert.equal(actual, expected); + }); + test('requestForSelf sets a token header using a secret', function(assert) { + const adapter = this.owner.lookup('adapter:token'); + const client = this.owner.lookup('service:client/http'); + const secret = 'sssh'; + const expected = `X-Consul-Token: ${secret}`; + const actual = adapter + .requestForSelf( + client.url, + {}, + { + dc: dc, + secret: secret, + } + ) + .split('\n')[1] + .trim(); + assert.equal(actual, expected); + }); }); diff --git a/ui-v2/tests/pages/dc/acls/tokens/edit.js b/ui-v2/tests/pages/dc/acls/tokens/edit.js index 15a3e560d934..4a5c533e5fc1 100644 --- a/ui-v2/tests/pages/dc/acls/tokens/edit.js +++ b/ui-v2/tests/pages/dc/acls/tokens/edit.js @@ -14,6 +14,7 @@ export default function( ...deletable({}, 'form > div'), use: clickable('[data-test-use]'), confirmUse: clickable('button.type-delete'), + clone: clickable('[data-test-clone]'), policies: policySelector(), roles: roleSelector(), }; diff --git a/ui-v2/tests/pages/dc/acls/tokens/index.js b/ui-v2/tests/pages/dc/acls/tokens/index.js index 6cdf957d211e..a6ee639bbcdd 100644 --- a/ui-v2/tests/pages/dc/acls/tokens/index.js +++ b/ui-v2/tests/pages/dc/acls/tokens/index.js @@ -25,6 +25,7 @@ export default function( actions: clickable('label'), use: clickable('[data-test-use]'), confirmUse: clickable('button.type-delete'), + clone: clickable('[data-test-clone]'), }) ), filter: filter, diff --git a/ui-v2/tests/unit/serializers/application-test.js b/ui-v2/tests/unit/serializers/application-test.js index 51cebc93f927..88fc6d0f4b0c 100644 --- a/ui-v2/tests/unit/serializers/application-test.js +++ b/ui-v2/tests/unit/serializers/application-test.js @@ -37,7 +37,7 @@ module('Unit | Serializer | application', function(hooks) { const expected = { 'primary-key-name': 'name', }; - const actual = serializer.respondForDeleteRecord(respond, { Name: 'name', dc: 'dc-1' }); + const actual = serializer.respondForDeleteRecord(respond, {}, { Name: 'name', dc: 'dc-1' }); assert.deepEqual(actual, expected); // assert.ok(adapter.uidForURL.calledOnce); }); diff --git a/ui-v2/tests/unit/serializers/kv-test.js b/ui-v2/tests/unit/serializers/kv-test.js index a12dea80855d..b95b2da0bf6a 100644 --- a/ui-v2/tests/unit/serializers/kv-test.js +++ b/ui-v2/tests/unit/serializers/kv-test.js @@ -43,6 +43,7 @@ module('Unit | Serializer | kv', function(hooks) { const body = true; return cb(headers, body); }, + {}, { Key: uid, Datacenter: dc, @@ -72,6 +73,7 @@ module('Unit | Serializer | kv', function(hooks) { }; return cb(headers, body); }, + {}, { Key: uid, Datacenter: dc,