UI: Catch 500 error on token endpoint and revert to legacy tokens (#4874)

In some circumstances a consul 1.4 client could be running in an
un-upgraded 1.3 or lower cluster. Currently this gives a 500 error on
the new ACL token endpoint. Here we catch this specific 500 error/message
and set the users AccessorID to null. Elsewhere in the frontend we use
this fact (AccessorID being null) to decide whether to present the
legacy or the new ACL UI to the user.

Also:
- Re-adds in most of the old style ACL acceptance tests, now that we are keeping the old style UI
- Restricts code editors to HCL only mode for all `Rules` editing (legacy/'half legacy'/new style)
- Adds a [Stop using] button to the old style ACL rows so its possible to logout.
- Updates copy and documentation links for the upgrade notices
This commit is contained in:
John Cowen 2018-11-02 14:44:36 +00:00 committed by GitHub
parent 3981c5d48c
commit f65f001675
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 335 additions and 58 deletions

View File

@ -18,6 +18,13 @@ export default Component.extend(SlotsMixin, {
$html.classList.remove(...templatize(['loading'])); $html.classList.remove(...templatize(['loading']));
} }
if (cls) { if (cls) {
// its possible for 'layout' templates to change after insert
// check for these specific layouts and clear them out
[...$html.classList].forEach(function(item, i) {
if (templatize(['edit', 'show', 'list']).indexOf(item) !== -1) {
$html.classList.remove(item);
}
});
$html.classList.add(...templatize(cls.split(' '))); $html.classList.add(...templatize(cls.split(' ')));
} }
}, },

View File

@ -1,9 +1,19 @@
import { helper } from '@ember/component/helper'; import { helper } from '@ember/component/helper';
import { get } from '@ember/object'; import { get } from '@ember/object';
const _isLegacy = function(token) {
const rules = get(token, 'Rules');
return get(token, 'Legacy') || (rules != null && rules.trim() != '');
};
export function isLegacy(params, hash) { export function isLegacy(params, hash) {
const token = params[0]; const token = params[0];
return get(token, 'Legacy') || typeof get(token, 'Rules') !== 'undefined'; // is array like (RecordManager isn't an array)
if (typeof token.length !== 'undefined') {
return token.find(function(item) {
return _isLegacy(item);
});
}
return _isLegacy(token);
} }
export default helper(isLegacy); export default helper(isLegacy);

View File

@ -8,13 +8,34 @@ export default Mixin.create(WithBlockingActions, {
actions: { actions: {
use: function(item) { use: function(item) {
return get(this, 'feedback').execute(() => { return get(this, 'feedback').execute(() => {
// old style legacy ACLs don't have AccessorIDs
// therefore set it to null, this way the frontend knows
// to use legacy ACLs
return get(this, 'settings') return get(this, 'settings')
.persist({ token: get(item, 'ID') }) .persist({
token: {
AccessorID: null,
SecretID: get(item, 'ID'),
},
})
.then(() => { .then(() => {
return this.transitionTo('dc.services'); return this.transitionTo('dc.services');
}); });
}, 'use'); }, 'use');
}, },
// TODO: This is also used in tokens, probably an opportunity to dry this out
logout: function(item) {
return get(this, 'feedback').execute(() => {
return get(this, 'settings')
.delete('token')
.then(() => {
// in this case we don't do the same as delete as we want to go to the new
// dc.acls.tokens page. If we get there via the dc.acls redirect/rewrite
// then we lose the flash message
return this.transitionTo('dc.acls.tokens');
});
}, 'logout');
},
clone: function(item) { clone: function(item) {
return get(this, 'feedback').execute(() => { return get(this, 'feedback').execute(() => {
return get(this, 'repo') return get(this, 'repo')

View File

@ -12,6 +12,7 @@ export default Route.extend({
this._super(...arguments); this._super(...arguments);
}, },
repo: service('repository/dc'), repo: service('repository/dc'),
settings: service('settings'),
actions: { actions: {
loading: function(transition, originRoute) { loading: function(transition, originRoute) {
let dc = null; let dc = null;
@ -58,13 +59,17 @@ export default Route.extend({
// 403 page // 403 page
// To note: Consul only gives you back a 403 if a non-existent token has been sent in the header // To note: Consul only gives you back a 403 if a non-existent token has been sent in the header
// if a token has not been sent at all, it just gives you a 200 with an empty dataset // if a token has not been sent at all, it just gives you a 200 with an empty dataset
const model = this.modelFor('dc');
if (error.status === '403') { if (error.status === '403') {
return this.transitionTo('dc.acls.tokens'); return get(this, 'settings')
.delete('token')
.then(() => {
return this.transitionTo('dc.acls.tokens', model.dc.Name);
});
} }
if (error.status === '') { if (error.status === '') {
error.message = 'Error'; error.message = 'Error';
} }
const model = this.modelFor('dc');
hash({ hash({
error: error, error: error,
dc: dc:

View File

@ -2,7 +2,6 @@ import Route from '@ember/routing/route';
import { get } from '@ember/object'; import { get } from '@ember/object';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions'; import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
export default Route.extend(WithBlockingActions, { export default Route.extend(WithBlockingActions, {
settings: service('settings'), settings: service('settings'),
feedback: service('feedback'), feedback: service('feedback'),
@ -21,8 +20,15 @@ export default Route.extend(WithBlockingActions, {
SecretID: secret, SecretID: secret,
}, },
}) })
.then(() => { .then(item => {
// a null AccessorID means we are in legacy mode
// take the user to the legacy acls
// otherwise just refresh the page
if (get(item, 'token.AccessorID') === null) {
return this.transitionTo('dc.acls');
} else {
this.refresh(); this.refresh();
}
}); });
}); });
}, 'authorize'); }, 'authorize');

View File

@ -7,6 +7,7 @@ import WithAclActions from 'consul-ui/mixins/acl/with-actions';
export default Route.extend(WithAclActions, { export default Route.extend(WithAclActions, {
repo: service('repository/acl'), repo: service('repository/acl'),
settings: service('settings'),
queryParams: { queryParams: {
s: { s: {
as: 'filter', as: 'filter',
@ -14,12 +15,24 @@ export default Route.extend(WithAclActions, {
}, },
}, },
beforeModel: function(transition) { beforeModel: function(transition) {
return this.replaceWith('dc.acls.tokens'); return get(this, 'settings')
.findBySlug('token')
.then(token => {
// If you don't have a token set or you have a
// token set with AccessorID set to not null (new ACL mode)
// then rewrite to the new acls
if (!token || get(token, 'AccessorID') !== null) {
// If you return here, you get a TransitionAborted error in the tests only
// everything works fine either way checking things manually
this.replaceWith('dc.acls.tokens');
}
});
}, },
model: function(params) { model: function(params) {
return hash({ return hash({
isLoading: false, isLoading: false,
items: get(this, 'repo').findAllByDatacenter(this.modelFor('dc').dc.Name), items: get(this, 'repo').findAllByDatacenter(this.modelFor('dc').dc.Name),
token: get(this, 'settings').findBySlug('token'),
}); });
}, },
setupController: function(controller, model) { setupController: function(controller, model) {

View File

@ -12,6 +12,19 @@ export default Route.extend(WithTokenActions, {
replace: true, replace: true,
}, },
}, },
beforeModel: function(transition) {
return get(this, 'settings')
.findBySlug('token')
.then(token => {
// If you have a token set with AccessorID set to null (legacy mode)
// then rewrite to the old acls
if (token && get(token, 'AccessorID') === null) {
// If you return here, you get a TransitionAborted error in the tests only
// everything works fine either way checking things manually
this.replaceWith('dc.acls');
}
});
},
model: function(params) { model: function(params) {
const repo = get(this, 'repo'); const repo = get(this, 'repo');
return hash({ return hash({

View File

@ -4,7 +4,9 @@ import { typeOf } from '@ember/utils';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/policy'; import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/policy';
import { Promise } from 'rsvp'; import { Promise } from 'rsvp';
import statusFactory from 'consul-ui/utils/acls-status'; import statusFactory from 'consul-ui/utils/acls-status';
const status = statusFactory(Promise); import isValidServerErrorFactory from 'consul-ui/utils/http/acl/is-valid-server-error';
const isValidServerError = isValidServerErrorFactory();
const status = statusFactory(isValidServerError, Promise);
const MODEL_NAME = 'policy'; const MODEL_NAME = 'policy';
export default Service.extend({ export default Service.extend({
getModelName: function() { getModelName: function() {

View File

@ -4,7 +4,9 @@ import { typeOf } from '@ember/utils';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/token'; import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/token';
import { Promise } from 'rsvp'; import { Promise } from 'rsvp';
import statusFactory from 'consul-ui/utils/acls-status'; import statusFactory from 'consul-ui/utils/acls-status';
const status = statusFactory(Promise); import isValidServerErrorFactory from 'consul-ui/utils/http/acl/is-valid-server-error';
const isValidServerError = isValidServerErrorFactory();
const status = statusFactory(isValidServerError, Promise);
const MODEL_NAME = 'token'; const MODEL_NAME = 'token';
export default Service.extend({ export default Service.extend({
getModelName: function() { getModelName: function() {
@ -20,9 +22,21 @@ export default Service.extend({
return status(obj); return status(obj);
}, },
self: function(secret, dc) { self: function(secret, dc) {
return get(this, 'store').self(this.getModelName(), { return get(this, 'store')
.self(this.getModelName(), {
secret: secret, secret: secret,
dc: dc, dc: dc,
})
.catch(e => {
// If we get this 500 RPC error, it means we are a legacy ACL cluster
// set AccessorID to null - which for the frontend means legacy mode
if (isValidServerError(e)) {
return {
AccessorID: null,
SecretID: secret,
};
}
return Promise.reject(e);
}); });
}, },
clone: function(item) { clone: function(item) {

View File

@ -14,7 +14,7 @@
</div> </div>
<label class="type-text"> <label class="type-text">
<span>Policy <a href="{{env 'CONSUL_DOCUMENTATION_URL'}}/guides/acl.html#rule-specification" rel="help noopener noreferrer" target="_blank">(HCL Format)</a></span> <span>Policy <a href="{{env 'CONSUL_DOCUMENTATION_URL'}}/guides/acl.html#rule-specification" rel="help noopener noreferrer" target="_blank">(HCL Format)</a></span>
{{code-editor class=(if item.error.Rules 'error') name='Rules' value=item.Rules onkeyup=(action 'change')}} {{code-editor class=(if item.error.Rules 'error') name='Rules' value=item.Rules syntax="hcl" onkeyup=(action 'change')}}
</label> </label>
{{#if create }} {{#if create }}
<label class="type-text"> <label class="type-text">

View File

@ -16,6 +16,12 @@
{{else}} {{else}}
There was an error deleting your ACL token. There was an error deleting your ACL token.
{{/if}} {{/if}}
{{ else if (eq type 'logout')}}
{{#if (eq status 'success') }}
You are now logged out.
{{else}}
There was an error logging out.
{{/if}}
{{ else if (eq type 'use')}} {{ else if (eq type 'use')}}
{{#if (eq status 'success') }} {{#if (eq status 'success') }}
Now using new ACL token. Now using new ACL token.

View File

@ -45,9 +45,16 @@
<li> <li>
<a data-test-edit href={{href-to 'dc.acls.edit' item.ID}}>Edit</a> <a data-test-edit href={{href-to 'dc.acls.edit' item.ID}}>Edit</a>
</li> </li>
{{#if (eq item.ID token.SecretID) }}
<li>
<a data-test-logout onclick={{queue (action confirm 'logout' item) (action change)}}>Stop using</a>
</li>
{{else}}
<li> <li>
<a data-test-use onclick={{queue (action confirm 'use' item) (action change)}}>Use</a> <a data-test-use onclick={{queue (action confirm 'use' item) (action change)}}>Use</a>
</li> </li>
{{/if}}
<li> <li>
<a data-test-clone onclick={{action 'sendClone' item}}>Clone</a> <a data-test-clone onclick={{action 'sendClone' item}}>Clone</a>
</li> </li>
@ -63,6 +70,8 @@
<p> <p>
{{#if (eq name 'delete')}} {{#if (eq name 'delete')}}
Are you sure you want to delete this ACL token? Are you sure you want to delete this ACL token?
{{else if (eq name 'logout')}}
Are you sure you want to stop using this ACL token? This will log you out.
{{ else if (eq name 'use')}} {{ else if (eq name 'use')}}
Are you sure you want to use this ACL token? Are you sure you want to use this ACL token?
{{/if}} {{/if}}
@ -70,6 +79,8 @@
<button type="button" class="type-delete" {{action execute}}> <button type="button" class="type-delete" {{action execute}}>
{{#if (eq name 'delete')}} {{#if (eq name 'delete')}}
Confirm Delete Confirm Delete
{{else if (eq name 'logout')}}
Confirm Logout
{{ else if (eq name 'use')}} {{ else if (eq name 'use')}}
Confirm Use Confirm Use
{{/if}} {{/if}}

View File

@ -1,4 +1,4 @@
<p class="notice policy-management"><strong>Management</strong> This global-management token is built into Consul's policy system. You can apply this special policy to Tokens for full access. This policy is not editable or removeable, but can be ignored by not applying it to any tokens. Learn more in our <a href="{{env 'CONSUL_DOCUMENTATION_URL'}}/guides/acl.html" target="_blank" rel="noopener noreferrer">documentation</a>.</p> <p class="notice policy-management"><strong>Management</strong> This global-management token is built into Consul's policy system. You can apply this special policy to tokens for full access. This policy is not editable or removeable, but can be ignored by not applying it to any tokens. Learn more in our <a href="{{env 'CONSUL_DOCUMENTATION_URL'}}/guides/acl.html#builtin-policies" target="_blank" rel="noopener noreferrer">documentation</a>.</p>
<div> <div>
<dl> <dl>
<dt>Name</dt> <dt>Name</dt>

View File

@ -15,7 +15,7 @@
{{/if}} {{/if}}
<label class="type-text"> <label class="type-text">
<span>Rules <a href="{{env 'CONSUL_DOCUMENTATION_URL'}}/guides/acl.html#rule-specification" rel="help noopener noreferrer" target="_blank">(HCL Format)</a></span> <span>Rules <a href="{{env 'CONSUL_DOCUMENTATION_URL'}}/guides/acl.html#rule-specification" rel="help noopener noreferrer" target="_blank">(HCL Format)</a></span>
{{code-editor class=(if item.error.Rules 'error') name='Rules' value=item.Rules onkeyup=(action 'change' 'Rules')}} {{code-editor class=(if item.error.Rules 'error') name='Rules' syntax='hcl' value=item.Rules onkeyup=(action 'change' 'Rules')}}
</label> </label>
{{#if create }} {{#if create }}
<label class="type-text"> <label class="type-text">

View File

@ -49,7 +49,7 @@
{{/block-slot}} {{/block-slot}}
{{#block-slot 'content'}} {{#block-slot 'content'}}
{{#if (token/is-legacy item)}} {{#if (token/is-legacy item)}}
<p class="notice info"><strong>Update.</strong> We've upgraded our ACL system by allowing you to create reusable policies which you can then apply to tokens. Don't worry, even though this token was written in the old style, it is still valid. However, we do recommend upgrading your old Tokens to the new style. Learn how in our <a href="{{env 'CONSUL_DOCUMENTATION_URL'}}/guides/acl.html" target="_blank" rel="noopener noreferrer">documentation</a>.</p> <p class="notice info"><strong>Update.</strong> We have upgraded our ACL system by allowing you to create reusable policies which you can then apply to tokens. Don't worry, even though this token was written in the old style, it is still valid. However, we do recommend upgrading your old tokens to the new style. Learn how in our <a href="{{env 'CONSUL_DOCUMENTATION_URL'}}/guide/acl-migrate-tokens.html" target="_blank" rel="noopener noreferrer">documentation</a>.</p>
{{/if}} {{/if}}
{{#if (not create) }} {{#if (not create) }}
<div> <div>

View File

@ -25,8 +25,8 @@
{{freetext-filter onchange=(action 'filter') value=filter.s placeholder="Search"}} {{freetext-filter onchange=(action 'filter') value=filter.s placeholder="Search"}}
</form> </form>
{{/if}} {{/if}}
{{#if (find-by 'legacy' true items)}} {{#if (token/is-legacy items)}}
<p class="notice info"><strong>Update.</strong> We've upgraded our ACL system by allowing you to create reusable Policies, which you can then apply to Tokens. Read more about the change and learn how to upgrade your legacy Tokens in our <a href="{{env 'CONSUL_DOCUMENTATION_URL'}}/guide/acl.html" target="_blank" rel="noopener noreferrer">documentation</a>.</p> <p data-test-notification-update class="notice info"><strong>Update.</strong> We have upgraded our ACL System to allow the creation of reusable policies that can be applied to tokens. Read more about the changes and how to upgrade legacy tokens in our <a href="{{env 'CONSUL_DOCUMENTATION_URL'}}/guide/acl-migrate-tokens.html" target="_blank" rel="noopener noreferrer">documentation</a>.</p>
{{/if}} {{/if}}
{{#if (gt filtered.length 0)}} {{#if (gt filtered.length 0)}}
{{#tabular-collection {{#tabular-collection

View File

@ -3,7 +3,8 @@
// has a valid token // has a valid token
// Right now this is very acl specific, but is likely to be // Right now this is very acl specific, but is likely to be
// made a bit more less specific // made a bit more less specific
export default function(P = Promise) {
export default function(isValidServerError, P = Promise) {
return function(obj) { return function(obj) {
const propName = Object.keys(obj)[0]; const propName = Object.keys(obj)[0];
const p = obj[propName]; const p = obj[propName];
@ -23,6 +24,12 @@ export default function(P = Promise) {
[propName]: p [propName]: p
.catch(function(e) { .catch(function(e) {
switch (e.errors[0].status) { switch (e.errors[0].status) {
case '500':
if (isValidServerError(e)) {
enable(true);
authorize(false);
}
break;
case '403': case '403':
enable(true); enable(true);
authorize(false); authorize(false);

View File

@ -0,0 +1,12 @@
// very specific error check just for one specific ACL case
// likely to be reused at a later date, so lets use the specific
// case we need right now as default
const UNKNOWN_METHOD_ERROR = "rpc error making call: rpc: can't find method ACL";
export default function(response = UNKNOWN_METHOD_ERROR) {
return function(e) {
if (e && e.errors && e.errors[0] && e.errors[0].detail) {
return e.errors[0].detail.indexOf(response) !== -1;
}
return false;
};
}

View File

@ -1,5 +1,4 @@
@setupApplicationTest @setupApplicationTest
@ignore
Feature: components / acl filter: Acl Filter Feature: components / acl filter: Acl Filter
In order to find the acl token I'm looking for easier In order to find the acl token I'm looking for easier
As a user As a user
@ -7,6 +6,7 @@ Feature: components / acl filter: Acl Filter
Scenario: Filtering [Model] Scenario: Filtering [Model]
Given 1 datacenter model with the value "dc-1" Given 1 datacenter model with the value "dc-1"
And 2 [Model] models And 2 [Model] models
And I'm using a legacy token
When I visit the [Page] page for yaml When I visit the [Page] page for yaml
--- ---
dc: dc-1 dc: dc-1

View File

@ -1,5 +1,4 @@
@setupApplicationTest @setupApplicationTest
@ignore
Feature: dc / acls / list-order Feature: dc / acls / list-order
In order to be able to find ACL tokens easier In order to be able to find ACL tokens easier
As a user As a user
@ -7,6 +6,7 @@ Feature: dc / acls / list-order
Scenario: I have 10 randomly sorted tokens Scenario: I have 10 randomly sorted tokens
Given 1 datacenter model with the value "datacenter" Given 1 datacenter model with the value "datacenter"
And I'm using a legacy token
And 10 acl model from yaml And 10 acl model from yaml
--- ---
- Name: zz - Name: zz

View File

@ -1,7 +1,7 @@
@setupApplicationTest @setupApplicationTest
Feature: dc / acls / tokens / index: ACL Token List Feature: dc / acls / tokens / index: ACL Token List
Scenario: Scenario: I see the tokens
Given 1 datacenter model with the value "dc-1" Given 1 datacenter model with the value "dc-1"
And 3 token models And 3 token models
When I visit the tokens page for yaml When I visit the tokens page for yaml
@ -9,5 +9,34 @@ Feature: dc / acls / tokens / index: ACL Token List
dc: dc-1 dc: dc-1
--- ---
Then the url should be /dc-1/acls/tokens Then the url should be /dc-1/acls/tokens
And I click actions on the tokens
Then I see 3 token models Then I see 3 token models
Scenario: I see the legacy message if I have one legacy token
Given 1 datacenter model with the value "dc-1"
And 3 token models from yaml
---
- Legacy: true
- Legacy: false
- Legacy: false
---
When I visit the tokens page for yaml
---
dc: dc-1
---
Then the url should be /dc-1/acls/tokens
And I see update
And I see 3 token models
Scenario: I don't see the legacy message if I have no legacy tokens
Given 1 datacenter model with the value "dc-1"
And 3 token models from yaml
---
- Legacy: false
- Legacy: false
- Legacy: false
---
When I visit the tokens page for yaml
---
dc: dc-1
---
Then the url should be /dc-1/acls/tokens
And I don't see update
And I see 3 token models

View File

@ -6,7 +6,7 @@ Feature: dc / acls / tokens / legacy / update: ACL Token Update
--- ---
AccessorID: key AccessorID: key
SecretID: secret SecretID: secret
Rules: '' Rules: 'key {}'
Type: client Type: client
Policies: ~ Policies: ~
--- ---

View File

@ -1,8 +1,8 @@
@setupApplicationTest @setupApplicationTest
@ignore
Feature: dc / acls / update: ACL Update Feature: dc / acls / update: ACL Update
Background: Background:
Given 1 datacenter model with the value "datacenter" Given 1 datacenter model with the value "datacenter"
And I'm using a legacy token
And 1 acl model from yaml And 1 acl model from yaml
--- ---
ID: key ID: key
@ -45,6 +45,3 @@ Feature: dc / acls / update: ACL Update
# @ignore # @ignore
# Scenario: Rules can be edited/updated # Scenario: Rules can be edited/updated
# Then ok # Then ok
# @ignore
# Scenario: The feedback dialog says success or failure
# Then ok

View File

@ -1,8 +1,8 @@
@setupApplicationTest @setupApplicationTest
@ignore
Feature: dc / acls / use: Using an ACL token Feature: dc / acls / use: Using an ACL token
Background: Background:
Given 1 datacenter model with the value "datacenter" Given 1 datacenter model with the value "datacenter"
And I'm using a legacy token
And 1 acl model from yaml And 1 acl model from yaml
--- ---
ID: token ID: token
@ -14,7 +14,7 @@ Feature: dc / acls / use: Using an ACL token
--- ---
Then I have settings like yaml Then I have settings like yaml
--- ---
token: ~ consul:token: '{"AccessorID":null,"SecretID":"id"}'
--- ---
And I click actions on the acls And I click actions on the acls
And I click use on the acls And I click use on the acls
@ -23,7 +23,7 @@ Feature: dc / acls / use: Using an ACL token
And "[data-notification]" has the "success" class And "[data-notification]" has the "success" class
Then I have settings like yaml Then I have settings like yaml
--- ---
token: token consul:token: '{"AccessorID":null,"SecretID":"token"}'
--- ---
Scenario: Using an ACL token from the detail page Scenario: Using an ACL token from the detail page
When I visit the acl page for yaml When I visit the acl page for yaml
@ -33,7 +33,7 @@ Feature: dc / acls / use: Using an ACL token
--- ---
Then I have settings like yaml Then I have settings like yaml
--- ---
token: ~ consul:token: '{"AccessorID":null,"SecretID":"id"}'
--- ---
And I click use And I click use
And I click confirmUse And I click confirmUse
@ -41,5 +41,5 @@ Feature: dc / acls / use: Using an ACL token
And "[data-notification]" has the "success" class And "[data-notification]" has the "success" class
Then I have settings like yaml Then I have settings like yaml
--- ---
token: token consul:token: '{"AccessorID":null,"SecretID":"token"}'
--- ---

View File

@ -47,7 +47,7 @@ Feature: Page Navigation
| kv | kvs | /dc-1/kv/necessitatibus-0/edit | /v1/session/info/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=dc-1 | /dc-1/kv | | kv | kvs | /dc-1/kv/necessitatibus-0/edit | /v1/session/info/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=dc-1 | /dc-1/kv |
# | acl | acls | /dc-1/acls/anonymous | /v1/acl/info/anonymous?dc=dc-1 | /dc-1/acls | # | acl | acls | /dc-1/acls/anonymous | /v1/acl/info/anonymous?dc=dc-1 | /dc-1/acls |
| intention | intentions | /dc-1/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca | /v1/internal/ui/services?dc=dc-1 | /dc-1/intentions | | intention | intentions | /dc-1/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca | /v1/internal/ui/services?dc=dc-1 | /dc-1/intentions |
| token | tokens | /dc-1/acls/tokens/7ca5cd4d-4a7e-459d-b812-bb4078cecbd4 | /v1/acl/policies?dc=dc-1 | /dc-1/acls/tokens | | token | tokens | /dc-1/acls/tokens/ee52203d-989f-4f7a-ab5a-2bef004164ca | /v1/acl/policies?dc=dc-1 | /dc-1/acls/tokens |
| policy | policies | /dc-1/acls/policies/ee52203d-989f-4f7a-ab5a-2bef004164ca | /v1/acl/tokens?policy=ee52203d-989f-4f7a-ab5a-2bef004164ca&dc=dc-1 | /dc-1/acls/policies | | policy | policies | /dc-1/acls/policies/ee52203d-989f-4f7a-ab5a-2bef004164ca | /v1/acl/tokens?policy=ee52203d-989f-4f7a-ab5a-2bef004164ca&dc=dc-1 | /dc-1/acls/policies |
# | token | tokens | /dc-1/acls/tokens/00000000-0000-0000-0000-000000000000 | /v1/acl/token/00000000-0000-0000-0000-000000000000?dc=dc-1 | /dc-1/acls/tokens | # | token | tokens | /dc-1/acls/tokens/00000000-0000-0000-0000-000000000000 | /v1/acl/token/00000000-0000-0000-0000-000000000000?dc=dc-1 | /dc-1/acls/tokens |
# | policy | policies | /dc-1/acls/policies/ee52203d-989f-4f7a-ab5a-2bef004164ca | /v1/acl/policy/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=dc-1 | /dc-1/acls/policies | # | policy | policies | /dc-1/acls/policies/ee52203d-989f-4f7a-ab5a-2bef004164ca | /v1/acl/policy/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=dc-1 | /dc-1/acls/policies |

View File

@ -1,5 +1,4 @@
@setupApplicationTest @setupApplicationTest
@ignore
Feature: token headers Feature: token headers
In order to authenticate with tokens In order to authenticate with tokens
As a user As a user
@ -15,11 +14,15 @@ Feature: token headers
--- ---
Scenario: Set the token to [Token] and then navigate to the index page Scenario: Set the token to [Token] and then navigate to the index page
Given 1 datacenter model with the value "datacenter" Given 1 datacenter model with the value "datacenter"
When I visit the settings page And the url "/v1/acl/tokens" responds with a 403 status
Then the url should be /settings When I visit the tokens page for yaml
---
dc: datacenter
---
Then the url should be /datacenter/acls/tokens
Then I fill in with yaml Then I fill in with yaml
--- ---
token: [Token] secret: [Token]
--- ---
And I submit And I submit
When I visit the index page When I visit the index page

View File

@ -54,7 +54,17 @@ export default {
policy(visitable, submitable, deletable, cancelable, clickable, attribute, collection) policy(visitable, submitable, deletable, cancelable, clickable, attribute, collection)
), ),
tokens: create( tokens: create(
tokens(visitable, deletable, creatable, clickable, attribute, collection, freetextFilter) tokens(
visitable,
submitable,
deletable,
creatable,
clickable,
attribute,
collection,
text,
freetextFilter
)
), ),
token: create( token: create(
token(visitable, submitable, deletable, cancelable, clickable, attribute, collection) token(visitable, submitable, deletable, cancelable, clickable, attribute, collection)

View File

@ -1,6 +1,18 @@
export default function(visitable, deletable, creatable, clickable, attribute, collection, filter) { export default function(
return creatable({ visitable,
submitable,
deletable,
creatable,
clickable,
attribute,
collection,
text,
filter
) {
return submitable(
creatable({
visit: visitable('/:dc/acls/tokens'), visit: visitable('/:dc/acls/tokens'),
update: text('[data-test-notification-update]'),
tokens: collection( tokens: collection(
'[data-test-tabular-row]', '[data-test-tabular-row]',
deletable({ deletable({
@ -12,5 +24,6 @@ export default function(visitable, deletable, creatable, clickable, attribute, c
}) })
), ),
filter: filter, filter: filter,
}); })
);
} }

View File

@ -77,6 +77,9 @@ export default function(assert) {
return create(number, model, data); return create(number, model, data);
} }
) )
.given(["I'm using a legacy token"], function(number, model, data) {
window.localStorage['consul:token'] = JSON.stringify({ AccessorID: null, SecretID: 'id' });
})
// TODO: Abstract this away from HTTP // TODO: Abstract this away from HTTP
.given(['the url "$url" responds with a $status status'], function(url, status) { .given(['the url "$url" responds with a $status status'], function(url, status) {
return api.server.respondWithStatus(url.split('?')[0], parseInt(status)); return api.server.respondWithStatus(url.split('?')[0], parseInt(status));

View File

@ -0,0 +1,46 @@
import { isLegacy } from 'consul-ui/helpers/token/is-legacy';
import { module, test } from 'qunit';
module('Unit | Helper | token/is-legacy');
test('it returns true if the token has a Legacy=true', function(assert) {
const actual = isLegacy([{ Legacy: true }]);
assert.ok(actual);
});
test('it returns false if the token has a Legacy=false', function(assert) {
const actual = isLegacy([{ Legacy: false }]);
assert.notOk(actual);
});
test('it returns true if the token has Rules', function(assert) {
const actual = isLegacy([{ Rules: 'some rules' }]);
assert.ok(actual);
});
test('it returns false if the token has Rules but those rules are empty', function(assert) {
const actual = isLegacy([{ Rules: '' }]);
assert.notOk(actual);
});
test('it returns false if the token has Rules but those rules are empty', function(assert) {
const actual = isLegacy([{ Rules: null }]);
assert.notOk(actual);
});
// passing arrays
test("it returns false if things don't have Legacy or Rules", function(assert) {
const actual = isLegacy([[{}, {}]]);
assert.notOk(actual);
});
test('it returns true if the token has a Legacy=true', function(assert) {
const actual = isLegacy([[{}, { Legacy: true }]]);
assert.ok(actual);
});
test('it returns false if the token has a Legacy=false', function(assert) {
const actual = isLegacy([[{}, { Legacy: false }]]);
assert.notOk(actual);
});
test('it returns true if one token has Rules', function(assert) {
const actual = isLegacy([[{}, { Rules: 'some rules' }]]);
assert.ok(actual);
});
test('it returns false if tokens have no Rules, or has Rules but those rules are empty', function(assert) {
const actual = isLegacy([[{}, { Rules: '' }]]);
assert.notOk(actual);
});

View File

@ -40,11 +40,12 @@ test('use persists the token and calls transitionTo correctly', function(assert)
}) })
); );
const item = { ID: 'id' }; const item = { ID: 'id' };
const expectedToken = { AccessorID: null, SecretID: item.ID };
this.register( this.register(
'service:settings', 'service:settings',
Service.extend({ Service.extend({
persist: function(actual) { persist: function(actual) {
assert.equal(actual.token, item.ID); assert.deepEqual(actual.token, expectedToken);
return Promise.resolve(actual); return Promise.resolve(actual);
}, },
}) })

View File

@ -2,7 +2,7 @@ import { moduleFor, test } from 'ember-qunit';
moduleFor('route:application', 'Unit | Route | application', { moduleFor('route:application', 'Unit | Route | application', {
// Specify the other units that are required for this test. // Specify the other units that are required for this test.
needs: ['service:repository/dc'], needs: ['service:repository/dc', 'service:settings'],
}); });
test('it exists', function(assert) { test('it exists', function(assert) {

View File

@ -0,0 +1,48 @@
import createIsValidServerError from 'consul-ui/utils/http/acl/is-valid-server-error';
import { module, test } from 'qunit';
module('Unit | Utility | http/acl/is valid server error');
const createEmberDataError = function(response) {
return {
errors: [
{
detail: response,
},
],
};
};
test('it returns a function', function(assert) {
const isValidServerError = createIsValidServerError();
assert.ok(typeof isValidServerError === 'function');
});
test("it returns false if there is no 'correctly' formatted error", function(assert) {
const isValidServerError = createIsValidServerError();
assert.notOk(isValidServerError());
assert.notOk(isValidServerError({}));
assert.notOk(isValidServerError({ errors: {} }));
assert.notOk(isValidServerError({ errors: [{}] }));
assert.notOk(isValidServerError({ errors: [{ notDetail: '' }] }));
});
// don't go too crazy with these, just enough for sanity check, we are essentially testing indexOf
test("it returns false if the response doesn't contain the exact error response", function(assert) {
const isValidServerError = createIsValidServerError();
[
"pc error making call: rpc: can't find method ACL",
"rpc error making call: rpc: can't find method",
"rpc rror making call: rpc: can't find method ACL",
].forEach(function(response) {
const e = createEmberDataError(response);
assert.notOk(isValidServerError(e));
});
});
test('it returns true if the response contains the exact error response', function(assert) {
const isValidServerError = createIsValidServerError();
[
"rpc error making call: rpc: can't find method ACL",
" rpc error making call: rpc: can't find method ACL",
"rpc error making call: rpc: rpc error making call: rpc: rpc error making call: rpc: can't find method ACL",
].forEach(function(response) {
const e = createEmberDataError(response);
assert.ok(isValidServerError(e));
});
});