Compare commits
197
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e4ab7d881 | ||
|
|
859a3fdd83 | ||
|
|
b1568aa30f | ||
|
|
a46b44b13f | ||
|
|
59dee87141 | ||
|
|
f1a20ea33d | ||
|
|
4a5e72d77b | ||
|
|
87a6d839e7 | ||
|
|
1b449bdea4 | ||
|
|
1ba0c873e9 | ||
|
|
f8e7ec7f5f | ||
|
|
4d4affe6d7 | ||
|
|
b13befaff2 | ||
|
|
3836656042 | ||
|
|
55e3284e3f | ||
|
|
27a416001b | ||
|
|
43a41d6818 | ||
|
|
b17088803f | ||
|
|
5eb9d01b23 | ||
|
|
288a38f6ea | ||
|
|
e7724ab8a5 | ||
|
|
e7c7d2b280 | ||
|
|
80abe240b4 | ||
|
|
6960aaeffc | ||
|
|
deb8871060 | ||
|
|
ba671b4ab3 | ||
|
|
bd3a4935a4 | ||
|
|
13ce61218c | ||
|
|
fa2e1021c5 | ||
|
|
c213ee18b2 | ||
|
|
43c667729f | ||
|
|
26e6cc8a9b | ||
|
|
5d57c8dd25 | ||
|
|
f31a93085a | ||
|
|
1927ef7832 | ||
|
|
fd53f7b282 | ||
|
|
eaaeed6d1b | ||
|
|
8f8566548a | ||
|
|
40e12366e6 | ||
|
|
39ea49ccdf | ||
|
|
a69dfa251a | ||
|
|
906a336acb | ||
|
|
9c9988a305 | ||
|
|
1a901b15d2 | ||
|
|
e894dbc4e8 | ||
|
|
18e95435f5 | ||
|
|
e2154f9698 | ||
|
|
d27ddb1eee | ||
|
|
8d3aea2e23 | ||
|
|
4d47b3cf51 | ||
|
|
b741d3af16 | ||
|
|
92cf57a542 | ||
|
|
aa6d369514 | ||
|
|
1ed685d81f | ||
|
|
586a4dbe23 | ||
|
|
f858da88f2 | ||
|
|
5f342a0ee8 | ||
|
|
3468acce39 | ||
|
|
b0a93e2e2d | ||
|
|
c2b66b74eb | ||
|
|
32ec1f0f70 | ||
|
|
e539a86600 | ||
|
|
efcdd0701e | ||
|
|
9ab9800f7e | ||
|
|
4336fde21b | ||
|
|
4cb7985ac1 | ||
|
|
04726a61da | ||
|
|
53270519cb | ||
|
|
5105d80a9d | ||
|
|
53423739d3 | ||
|
|
f782d90a7e | ||
|
|
721d09899a | ||
|
|
d1303c7bca | ||
|
|
31682ffd85 | ||
|
|
b5a791d618 | ||
|
|
e110f33e40 | ||
|
|
68bf02ee25 | ||
|
|
063d643d76 | ||
|
|
b6818871fc | ||
|
|
1effa79c93 | ||
|
|
6fd075f7e8 | ||
|
|
0d8508cd4e | ||
|
|
f3bd8d0b6f | ||
|
|
8889e86cec | ||
|
|
6500d56273 | ||
|
|
80cffeb48b | ||
|
|
263c8c01e9 | ||
|
|
74531f5d85 | ||
|
|
e0eed2b4ea | ||
|
|
c9df20184a | ||
|
|
afa1deef94 | ||
|
|
5d04dca21e | ||
|
|
746f45b6da | ||
|
|
a62d36af92 | ||
|
|
a41572302a | ||
|
|
2138aa8b01 | ||
|
|
4ea53c9cc7 | ||
|
|
c4e5a4c121 | ||
|
|
e5544415b0 | ||
|
|
ce84d2405c | ||
|
|
b07c90ee53 | ||
|
|
27a28c6db2 | ||
|
|
82eed3c5fe | ||
|
|
3d13d8f412 | ||
|
|
6927008041 | ||
|
|
c978958df2 | ||
|
|
e8b2e1aa6f | ||
|
|
43528c64c2 | ||
|
|
d7f090f348 | ||
|
|
93a9efbd1b | ||
|
|
a50a0735bb | ||
|
|
838567c2d2 | ||
|
|
95e6144e44 | ||
|
|
58c9df272f | ||
|
|
3696e45275 | ||
|
|
73503f98dc | ||
|
|
15f6c408d4 | ||
|
|
0ccc6e7f78 | ||
|
|
69a10f891a | ||
|
|
18d5b60c6d | ||
|
|
f279ac44ca | ||
|
|
29f21e0dfe | ||
|
|
9e588212a4 | ||
|
|
3361aa1800 | ||
|
|
5c3b44b2f6 | ||
|
|
59d345a912 | ||
|
|
2adfdf6ef6 | ||
|
|
8aa9a51c88 | ||
|
|
60b7d0134a | ||
|
|
ae26d588a9 | ||
|
|
abc1827280 | ||
|
|
682bb14037 | ||
|
|
28ec1008a0 | ||
|
|
83ab375829 | ||
|
|
e960c34542 | ||
|
|
4d2cb86a62 | ||
|
|
22131864f8 | ||
|
|
ac8a6054b0 | ||
|
|
22d578620d | ||
|
|
82506d11c8 | ||
|
|
84818a54f5 | ||
|
|
a0b2c6883a | ||
|
|
180b74cc98 | ||
|
|
12f2042f87 | ||
|
|
f096eaf7d7 | ||
|
|
e2ff75f435 | ||
|
|
96ef767e65 | ||
|
|
f387416d6d | ||
|
|
933ade8a16 | ||
|
|
bf4d43cee2 | ||
|
|
6b3b6c4d58 | ||
|
|
4630e130d4 | ||
|
|
42ace44716 | ||
|
|
888b106780 | ||
|
|
6bd994c86b | ||
|
|
6ce09162d7 | ||
|
|
f487c4f0d9 | ||
|
|
2d0afd7b9c | ||
|
|
f406fc3861 | ||
|
|
760710ae23 | ||
|
|
0219b5c99a | ||
|
|
6056b50939 | ||
|
|
9e57f838d5 | ||
|
|
896671ceb1 | ||
|
|
ca9485053e | ||
|
|
7e6c46de2b | ||
|
|
c68c1ba0fb | ||
|
|
5f83751db2 | ||
|
|
8179ab7dec | ||
|
|
d93d48197f | ||
|
|
d010d25934 | ||
|
|
3bb9d66377 | ||
|
|
42fe8f4eca | ||
|
|
45bf901d9e | ||
|
|
a35aacdf5e | ||
|
|
feddb2efad | ||
|
|
47c664eda9 | ||
|
|
9d847348a0 | ||
|
|
9fc2b7a409 | ||
|
|
02642bc5e4 | ||
|
|
8f8e257523 | ||
|
|
e50db68abe | ||
|
|
ec23e3706b | ||
|
|
e9c297b1cf | ||
|
|
4ae5e73def | ||
|
|
78505c720f | ||
|
|
09c8680ff2 | ||
|
|
811d1dbe32 | ||
|
|
dbe7003068 | ||
|
|
05709f7ee5 | ||
|
|
3084d6cf06 | ||
|
|
ac92575b36 | ||
|
|
900f342894 | ||
|
|
2f3478d043 | ||
|
|
af8947dc62 | ||
|
|
a3c12b36e7 | ||
|
|
92640b96aa |
No files matched your search
@@ -1,5 +1,5 @@
|
||||
name: Validate Monorepo
|
||||
on: [push]
|
||||
on: pull_request
|
||||
jobs:
|
||||
|
||||
compliance:
|
||||
@@ -39,6 +39,7 @@ jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
@@ -56,6 +57,7 @@ jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
name: "Trigger Test Boundary UI release"
|
||||
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: Please use tag-like format n.n.n-someword (1.111.11-beta)
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
trigger-circleci-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Trigger Release job
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
echo "Triggering Test release using BRANCH and TEST_TAG ${{ github.event.inputs.branch }}"
|
||||
|
||||
curl -u ${{ secrets.CIRCLE_TOKEN }}: -X POST --header "Content-Type: application/json" \
|
||||
-d '{ "branch": "main", "parameters": {"TAG": "'${{ github.event.inputs.branch }}'" }}' \
|
||||
https://circleci.com/api/v2/project/github/hashicorp/boundary-ui-releases/pipeline
|
||||
@@ -0,0 +1,6 @@
|
||||
# Contributing to Boundary
|
||||
|
||||
Thank you for contributing to Boundary! Here you can find common questions around reporting issues and opening
|
||||
pull requests to our project.
|
||||
|
||||
When contributing in any way to the Boundary project (new issue, PR, etc), please be aware that our team identifies with many gender pronouns and to use non-binary pronouns when addressing our team. For more reading on our code of conduct, please see the [HashiCorp community guidelines](https://www.hashicorp.com/community-guidelines).
|
||||
@@ -3,7 +3,7 @@ import { serializeIntoHash } from '@ember-data/adapter/-private';
|
||||
/* eslint-disable-next-line ember/no-mixins */
|
||||
import AdapterBuildURLMixin from '../mixins/adapter-build-url';
|
||||
import config from 'ember-get-config';
|
||||
import { get, getWithDefault } from '@ember/object';
|
||||
import { get } from '@ember/object';
|
||||
import { InvalidError } from '@ember-data/adapter/error';
|
||||
import { dasherize } from '@ember/string';
|
||||
import { pluralize } from 'ember-inflector';
|
||||
@@ -69,7 +69,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
* @return {string}
|
||||
*/
|
||||
urlSuffix(modelName, id, snapshot = {}) {
|
||||
const method = getWithDefault(snapshot, 'adapterOptions.method', '');
|
||||
const method = get(snapshot, 'adapterOptions.method') || '';
|
||||
return method ? `:${method}` : '';
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@ import { attr } from '@ember-data/model';
|
||||
export default class GeneratedAuthMethodModel extends BaseModel {
|
||||
// =attributes
|
||||
|
||||
@attr('boolean', {
|
||||
description: 'Specifies if this is the primary auth method for its scope.',
|
||||
readOnly: true,
|
||||
})
|
||||
is_primary;
|
||||
|
||||
@attr('string', {
|
||||
description: 'The type of the resource, to help differentiate schemas',
|
||||
})
|
||||
|
||||
@@ -7,6 +7,12 @@ import { attr } from '@ember-data/model';
|
||||
export default class GeneratedScopeModel extends BaseModel {
|
||||
// =attributes
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'The ID of the primary auth method for this scope. A primary auth method\nis allowed to vivify users when new accounts are created and is the source for the users account info.',
|
||||
})
|
||||
primary_auth_method_id;
|
||||
|
||||
@attr('string', {
|
||||
description: 'The type of the resource',
|
||||
})
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
import GeneratedAuthMethodModel from '../generated/models/auth-method';
|
||||
import { equal } from '@ember/object/computed';
|
||||
|
||||
export default class AuthMethodModel extends GeneratedAuthMethodModel {}
|
||||
export default class AuthMethodModel extends GeneratedAuthMethodModel {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
@equal('is_primary', true) isPrimary;
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isPassword() {
|
||||
return this.type === 'password';
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isOIDC() {
|
||||
return this.type === 'oidc';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
export default class AuthMethodSerializer extends ApplicationSerializer {
|
||||
/**
|
||||
* Sorts the array in order of "is_primary" such that the primary method
|
||||
* appears first. The "natural" order of auth methods places primary first.
|
||||
*
|
||||
* @override
|
||||
* @return {object}
|
||||
*/
|
||||
normalizeArrayResponse() {
|
||||
const normalized = super.normalizeArrayResponse(...arguments);
|
||||
normalized.data = normalized.data.sort((a) =>
|
||||
a.attributes.is_primary ? -1 : 1
|
||||
);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets `is_primary` to `false` if it is missing in the response. The API
|
||||
* omits "zero" values per:
|
||||
* https://developers.google.com/protocol-buffers/docs/proto3#default
|
||||
*
|
||||
* TODO: generalize this so that all zero values are reified.
|
||||
* @override
|
||||
* @return {object}
|
||||
*/
|
||||
normalize() {
|
||||
const normalized = super.normalize(...arguments);
|
||||
if (!normalized.data.attributes.is_primary) {
|
||||
normalized.data.attributes.is_primary = false;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/auth-method';
|
||||
@@ -60,7 +60,17 @@ export default function () {
|
||||
this.post('/auth-methods');
|
||||
this.get('/auth-methods/:id');
|
||||
this.patch('/auth-methods/:id');
|
||||
this.del('/auth-methods/:id');
|
||||
this.del('/auth-methods/:id', ({ authMethods }, { params: { id } }) => {
|
||||
const authMethod = authMethods.find(id);
|
||||
const scope = authMethod.scope;
|
||||
authMethod.destroy();
|
||||
// If the primary auth method is deleted, the associated scope's
|
||||
// primary_auth_method_id field should be set to null.
|
||||
if (scope.primaryAuthMethodId === id) {
|
||||
scope.update({ primaryAuthMethodId: null });
|
||||
}
|
||||
return new Response(202);
|
||||
});
|
||||
|
||||
// Auth Method Accounts
|
||||
this.get(
|
||||
@@ -413,9 +423,12 @@ export default function () {
|
||||
);
|
||||
this.get('/sessions/:id', function ({ sessions }, { params: { id } }) {
|
||||
const session = sessions.find(id);
|
||||
return session.update({
|
||||
status: pickRandomStatusString(),
|
||||
});
|
||||
if (!Ember.testing) {
|
||||
session.update({
|
||||
status: pickRandomStatusString(),
|
||||
});
|
||||
}
|
||||
return session;
|
||||
});
|
||||
this.post(
|
||||
'/sessions/:idMethod',
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import factory from '../generated/factories/auth-method';
|
||||
import { trait } from 'ember-cli-mirage';
|
||||
|
||||
const types = ['password', 'oidc'];
|
||||
|
||||
export default factory.extend({
|
||||
id: (i) => `auth-method-id-${i}`,
|
||||
|
||||
// Cycle through available types
|
||||
type: (i) => types[i % types.length],
|
||||
|
||||
/**
|
||||
* Adds accounts (with associated users) to auth method.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import factory from '../generated/factories/target';
|
||||
import { trait } from 'ember-cli-mirage';
|
||||
import { random } from 'faker';
|
||||
import { random, datatype } from 'faker';
|
||||
|
||||
const randomBoolean = (chance = 0.5) => Math.random() < chance;
|
||||
const hostSetChance = 0.3;
|
||||
@@ -9,7 +9,7 @@ export default factory.extend({
|
||||
/**
|
||||
* -1 means "unlimited" and we want to generate these on occassion.
|
||||
*/
|
||||
session_connection_limit: () => random.arrayElement([-1, random.number()]),
|
||||
session_connection_limit: () => random.arrayElement([-1, datatype.number()]),
|
||||
|
||||
/**
|
||||
* Generates attributes fields by type.
|
||||
@@ -18,7 +18,7 @@ export default factory.extend({
|
||||
if (target.type === 'tcp') {
|
||||
target.update({
|
||||
attributes: {
|
||||
default_port: random.number(),
|
||||
default_port: datatype.number(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedAccountFactory
|
||||
@@ -10,6 +10,6 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
version: () => random.number(),
|
||||
version: () => datatype.number(),
|
||||
attributes: () => ({ login_name: random.words() }),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedAuthMethodModelFactory
|
||||
@@ -10,5 +10,5 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
disabled: () => datatype.boolean(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedGroupModelFactory
|
||||
@@ -10,5 +10,5 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
disabled: () => datatype.boolean(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedHostCatalogModelFactory
|
||||
@@ -10,5 +10,5 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
disabled: () => datatype.boolean(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedHostSetModelFactory
|
||||
@@ -10,6 +10,6 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
version: () => random.number(),
|
||||
size: () => random.number(),
|
||||
version: () => datatype.number(),
|
||||
size: () => datatype.number(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date, internet } from 'faker';
|
||||
import { random, date, internet, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedHostModelFactory
|
||||
@@ -10,7 +10,7 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
version: () => random.number(),
|
||||
version: () => datatype.number(),
|
||||
attributes: () => {
|
||||
return { address: internet.ipv6() };
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedProjectModelFactory
|
||||
@@ -10,5 +10,5 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
disabled: () => datatype.boolean(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedRoleModelFactory
|
||||
@@ -10,6 +10,6 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
version: () => random.number(),
|
||||
disabled: () => datatype.boolean(),
|
||||
version: () => datatype.number(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedScopeModelFactory
|
||||
@@ -9,5 +9,5 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
disabled: () => datatype.boolean(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedSessionModelFactory
|
||||
@@ -8,5 +8,5 @@ export default Factory.extend({
|
||||
type: 'tcp',
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
version: () => random.number(),
|
||||
version: () => datatype.number(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedTargetModelFactory
|
||||
@@ -7,11 +7,11 @@ import { random, date } from 'faker';
|
||||
export default Factory.extend({
|
||||
type: 'tcp',
|
||||
name: () => random.words(),
|
||||
session_max_seconds: () => random.number(),
|
||||
session_connection_limit: () => random.number(),
|
||||
session_max_seconds: () => datatype.number(),
|
||||
session_connection_limit: () => datatype.number(),
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
version: () => random.number(),
|
||||
disabled: () => datatype.boolean(),
|
||||
version: () => datatype.number(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedUserModelFactory
|
||||
@@ -10,5 +10,5 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
disabled: () => datatype.boolean(),
|
||||
});
|
||||
@@ -12,7 +12,7 @@ import { Response } from 'miragejs';
|
||||
// */
|
||||
// export default function authenticateHandler(schema, request) {
|
||||
// const payload = JSON.parse(request.requestBody);
|
||||
// if (payload.credentials.login_name === 'error') {
|
||||
// if (payload.attributes.login_name === 'error') {
|
||||
// return new Response(400);
|
||||
// } else {
|
||||
// const cookieName = config.auth.passwordCookieName;
|
||||
@@ -23,32 +23,98 @@ import { Response } from 'miragejs';
|
||||
// }
|
||||
// }
|
||||
|
||||
// Quick and dirty counter system to simulate the polling behavior
|
||||
// of the API. If the OIDC flow is pending, the route returns 100.
|
||||
// If complete it returns 200 with the token JSON.
|
||||
let oidcAttemptCounter = 0;
|
||||
const oidcRequiredAttempts = 3;
|
||||
|
||||
const commandHandlers = {
|
||||
password: {
|
||||
login: (payload, scopeAttrs) => {
|
||||
if (payload.attributes.login_name === 'error') {
|
||||
return new Response(400);
|
||||
} else {
|
||||
return new Response(
|
||||
200,
|
||||
{},
|
||||
{
|
||||
attributes: {
|
||||
scope: scopeAttrs,
|
||||
id: 'token123',
|
||||
token: 'thetokenstring',
|
||||
account_id: '1',
|
||||
user_id: 'user123',
|
||||
auth_method_id: 'authmethod123',
|
||||
created_time: '',
|
||||
updated_time: '',
|
||||
last_used_time: '',
|
||||
expiration_time: '',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* OIDC authentication is a two-step process:
|
||||
*
|
||||
* 1. `authenticate:start` kicks of authentication by requesting some details
|
||||
* from the Boundary server about the request, including the third-party
|
||||
* URL to which to redirect the user.
|
||||
* 2. `authenticate` accepts the token_id and state parameters in
|
||||
* order to retrieve a Boundary token. This endpoint may be polled until
|
||||
* the authentication flow is completed.
|
||||
*/
|
||||
oidc: {
|
||||
start: () =>
|
||||
new Response(
|
||||
200,
|
||||
{},
|
||||
{
|
||||
attributes: {
|
||||
auth_url: 'https://www.duckduckgo.com',
|
||||
token_id: 'token_1234',
|
||||
},
|
||||
}
|
||||
),
|
||||
token: (_, scopeAttrs) => {
|
||||
oidcAttemptCounter++;
|
||||
if (oidcAttemptCounter < oidcRequiredAttempts) {
|
||||
return new Response(202);
|
||||
} else {
|
||||
return new Response(
|
||||
200,
|
||||
{},
|
||||
{
|
||||
attributes: {
|
||||
scope: scopeAttrs,
|
||||
id: 'token123',
|
||||
token: 'thetokenstring',
|
||||
account_id: '1',
|
||||
user_id: 'user123',
|
||||
auth_method_id: 'authmethod123',
|
||||
created_time: '',
|
||||
updated_time: '',
|
||||
last_used_time: '',
|
||||
expiration_time: '',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function authHandler({ scopes, authMethods }, request) {
|
||||
const payload = JSON.parse(request.requestBody);
|
||||
if (payload.credentials.login_name === 'error') {
|
||||
return new Response(400);
|
||||
} else {
|
||||
const id = request.params.id_method.split(':')[0];
|
||||
const authMethod = authMethods.find(id);
|
||||
const scope = scopes.find(authMethod.scopeId);
|
||||
const scopeAttrs = this.serialize(scopes.find(scope.id));
|
||||
return new Response(
|
||||
200,
|
||||
{},
|
||||
{
|
||||
scope: scopeAttrs,
|
||||
id: 'token123',
|
||||
token: 'thetokenstring',
|
||||
account_id: '1',
|
||||
user_id: 'authenticateduser',
|
||||
auth_method_id: 'authmethod123',
|
||||
created_time: '',
|
||||
updated_time: '',
|
||||
last_used_time: '',
|
||||
expiration_time: '',
|
||||
}
|
||||
);
|
||||
}
|
||||
const [, id] = request.params.id_method.match(/(?<id>.[^:]*)/);
|
||||
const { command } = payload;
|
||||
const authMethod = authMethods.find(id);
|
||||
const scope = scopes.find(authMethod.scopeId);
|
||||
const scopeAttrs = this.serialize(scopes.find(scope.id));
|
||||
return commandHandlers[authMethod.type][command](payload, scopeAttrs);
|
||||
}
|
||||
|
||||
export function deauthHandler() {
|
||||
|
||||
@@ -20,18 +20,22 @@ export default function (server) {
|
||||
server.create('user', { id: 'authenticateduser' });
|
||||
|
||||
// Auth
|
||||
server.createList(
|
||||
const globalAuthMethods = server.createList(
|
||||
'auth-method',
|
||||
1,
|
||||
{ scope: globalScope },
|
||||
'withAccountsAndUsers'
|
||||
);
|
||||
server.createList(
|
||||
const orgAuthMethods = server.createList(
|
||||
'auth-method',
|
||||
3,
|
||||
{ scope: orgScope },
|
||||
'withAccountsAndUsers'
|
||||
);
|
||||
// Assign primary auth methods per scope
|
||||
// TODO make this generic
|
||||
globalScope.update({ primaryAuthMethodId: globalAuthMethods[0].id });
|
||||
orgScope.update({ primaryAuthMethodId: orgAuthMethods[1].id });
|
||||
|
||||
// Groups and Users
|
||||
server.createList('group', 1, { scope: globalScope }, 'withMembers');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Ember from 'ember';
|
||||
import config from 'ember-get-config';
|
||||
import { random } from 'faker';
|
||||
import { datatype } from 'faker';
|
||||
|
||||
export default function initializeMockIPC(server) {
|
||||
/**
|
||||
@@ -55,7 +55,7 @@ export default function initializeMockIPC(server) {
|
||||
});
|
||||
return {
|
||||
address: window.location.hostname,
|
||||
port: random.number(),
|
||||
port: datatype.number(),
|
||||
protocol: type,
|
||||
session_id: newSession.id,
|
||||
};
|
||||
|
||||
@@ -2,4 +2,17 @@ import ApplicationSerializer from './application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
modelName: 'auth-method',
|
||||
|
||||
_hashForModel(model) {
|
||||
const json = ApplicationSerializer.prototype._hashForModel.apply(
|
||||
this,
|
||||
arguments
|
||||
);
|
||||
// If the scope marks this auth method as primary, designate it
|
||||
// as such with a read-only boolean field (this is how the API behaves).
|
||||
const isPrimary =
|
||||
model.scope && model.scope.primaryAuthMethodId === model.id;
|
||||
json.is_primary = isPrimary;
|
||||
return json;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Serializer | auth method', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('auth-method');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('auth-method', {});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
|
||||
test('it sorts primary first in normalizeArrayResponse', function (assert) {
|
||||
assert.expect(2);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('auth-method');
|
||||
const modelClass = store.createRecord('auth-method').constructor;
|
||||
const payload = {
|
||||
items: [{ id: 1 }, { id: 2, is_primary: true }, { id: 3 }],
|
||||
};
|
||||
const normalizedArray = serializer.normalizeArrayResponse(
|
||||
store,
|
||||
modelClass,
|
||||
payload
|
||||
);
|
||||
assert.ok(payload.items[1].is_primary, 'Second payload item is primary');
|
||||
assert.deepEqual(
|
||||
normalizedArray,
|
||||
{
|
||||
included: [],
|
||||
data: [
|
||||
{
|
||||
id: '2',
|
||||
type: 'auth-method',
|
||||
attributes: { is_primary: true },
|
||||
relationships: {},
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'auth-method',
|
||||
attributes: { is_primary: false },
|
||||
relationships: {},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'auth-method',
|
||||
attributes: { is_primary: false },
|
||||
relationships: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
'First normalized item is primary'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -4,10 +4,31 @@ import fetch from 'fetch';
|
||||
|
||||
/**
|
||||
* Encapsulates common authenticator functionality.
|
||||
*
|
||||
* Upon session invalidation, deauthentication should be attempted at the URL
|
||||
* generated by `buildDeauthEndpointURL` on a best-effort basis.
|
||||
*
|
||||
* When a session is restored, a call should be made to the endpoint specified
|
||||
* by `buildTokenValidationEndpointURL`. If this endpoint responds with 401 or
|
||||
* 404, then the session is invalid and the restoration should be rejected.
|
||||
* All other responses should resolve the session restoration successfully.
|
||||
*/
|
||||
export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
||||
// =unimplemented methods
|
||||
|
||||
/**
|
||||
* Generates an auth method URL with which to authenticate.
|
||||
* @override
|
||||
* @param {object} options
|
||||
* @param {string} options.scope.scope_id
|
||||
* @param {string} options.authMethod.id
|
||||
* @return {string}
|
||||
*/
|
||||
buildAuthEndpointURL(/* {
|
||||
scope: { id: scopeID },
|
||||
authMethod: { id: authMethodID },
|
||||
} */) {}
|
||||
|
||||
/**
|
||||
* Generates a scope URL with which to deauthenticate.
|
||||
* @override
|
||||
@@ -51,9 +72,10 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
||||
*/
|
||||
async restore(data) {
|
||||
if (!data) return reject();
|
||||
return this.validateToken(data.token, data.id).then(() =>
|
||||
this.normalizeData(data)
|
||||
);
|
||||
return this.validateToken(
|
||||
data.attributes.token,
|
||||
data.attributes.id
|
||||
).then(() => this.normalizeData(data));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,6 +87,10 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
||||
* @return {object}
|
||||
*/
|
||||
normalizeData(data, username) {
|
||||
// Pull fields up from `data.attributes` for easier access in JavaScript.
|
||||
// The `attributes` field exists on the Go side for its convenience but is
|
||||
// unnecessary here.
|
||||
Object.assign(data, data.attributes);
|
||||
// Add booleans indicated the scope type
|
||||
data.isGlobal = data?.scope?.type === 'global';
|
||||
data.isOrg = data?.scope?.type === 'org';
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import BaseAuthenticator from './base';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { reject } from 'rsvp';
|
||||
import fetch from 'fetch';
|
||||
|
||||
/**
|
||||
* The OIDC base authenticator encapsulates the multistep OIDC flow.
|
||||
*
|
||||
* 1. Start authentication flow: this step is actually a combination of two
|
||||
* sub steps:
|
||||
* 1. Initiate a call via `startAuthentication` to the API at
|
||||
* `buildAuthEndpointURL` using the `start` command to fetch the URL of
|
||||
* the external authentication provider (`auth_url`).
|
||||
* 2. Open `auth_url`. This may be either inline (same tab)
|
||||
* or external in a new tab or browser window.
|
||||
* 2. Requests the token from `buildAuthEndpointURL` using the `token` command.
|
||||
* A token isn't guaranteed to be ready, so the OIDC flow can be in a
|
||||
* "pending" state.
|
||||
*
|
||||
* This authenticator should not be used directly because it does not specify
|
||||
* URL-building endpoints of its own.
|
||||
* To use, generate an application authenticator in your app
|
||||
* `authenticators/application.js` and extend this class.
|
||||
*/
|
||||
export default class OIDCAuthenticator extends BaseAuthenticator {
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Kicks-off the OIDC flow: calls the `authenticate:start` action on the
|
||||
* specified auth method, which returns some meta data along with an
|
||||
* authorization request URL. The response data is saved for later and
|
||||
* authentication enters a pending state.
|
||||
*
|
||||
* @param {boolean} requestCookies request cookie tokens (default `true`)
|
||||
* @param {object} options
|
||||
* @return {object}
|
||||
*/
|
||||
async startAuthentication(options) {
|
||||
const url = this.buildAuthEndpointURL(options);
|
||||
const body = JSON.stringify({ command: 'start' });
|
||||
const response = await fetch(url, { method: 'post', body });
|
||||
const json = await response.json();
|
||||
if (response.status < 400) {
|
||||
// Store meta about the pending OIDC flow
|
||||
this.session.set('data.pending', {
|
||||
oidc: json,
|
||||
});
|
||||
return json;
|
||||
} else {
|
||||
return reject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OIDC is a _process_ and does not have a singular "authenticate" step. Thus
|
||||
* this authenticate method simply normalizes the data passed into it once
|
||||
* `attemptFetchToken` completes. Notably, no requests are made by calling
|
||||
* authenticate. A Boundary token must already have been obtained.
|
||||
* @param {object} json
|
||||
* @return {object}
|
||||
*/
|
||||
async authenticate(json) {
|
||||
return this.normalizeData(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* This request attempts to fetch a Boundary token. This method represents a
|
||||
* single request the polling sequence used to wait for authentication.
|
||||
* There are three possible outcomes:
|
||||
*
|
||||
* 1. If the token is ready, it is persisted into the session, the session
|
||||
* enters an authenticated state, and this method returns `true`.
|
||||
* 2. If the token is not yet ready, this method returns false.
|
||||
* 3. If the request errors, this method returns to a rejection,
|
||||
* requiring appropriate error handling.
|
||||
*/
|
||||
async attemptFetchToken(options) {
|
||||
// Get the URL and setup the request body
|
||||
const url = this.buildAuthEndpointURL(options);
|
||||
const body = JSON.stringify({
|
||||
command: 'token',
|
||||
attributes: {
|
||||
//state: this.session.get('data.pending.oidc.attributes.state'),
|
||||
token_id: this.session.get('data.pending.oidc.attributes.token_id'),
|
||||
},
|
||||
});
|
||||
// Fetch the endpoint and get the response JSON
|
||||
const response = await fetch(url, { method: 'post', body });
|
||||
if (response.status === 202) {
|
||||
// The token isn't ready yet, keep trying.
|
||||
return false;
|
||||
} else if (response.status < 400) {
|
||||
// Response was successful, meaning a token was obtained.
|
||||
// Authenticate with the session service using the response JSON.
|
||||
const json = await response.json();
|
||||
await this.session.authenticate('authenticator:oidc', json);
|
||||
return true;
|
||||
} else {
|
||||
// Response errors, return a rejection for further handling.
|
||||
return reject();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,21 +34,6 @@ import fetch from 'fetch';
|
||||
*
|
||||
*/
|
||||
export default class PasswordAuthenticator extends BaseAuthenticator {
|
||||
// =unimplemented methods
|
||||
|
||||
/**
|
||||
* Generates an auth method URL with which to authenticate.
|
||||
* @override
|
||||
* @param {object} options
|
||||
* @param {string} options.scope.scope_id
|
||||
* @param {string} options.authMethod.id
|
||||
* @return {string}
|
||||
*/
|
||||
buildAuthEndpointURL(/* {
|
||||
scope: { id: scopeID },
|
||||
authMethod: { id: authMethodID },
|
||||
} */) {}
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
@@ -71,8 +56,9 @@ export default class PasswordAuthenticator extends BaseAuthenticator {
|
||||
options
|
||||
) {
|
||||
const body = JSON.stringify({
|
||||
command: 'login',
|
||||
token_type: requestCookies ? 'cookie' : null,
|
||||
credentials: { login_name, password },
|
||||
attributes: { login_name, password },
|
||||
});
|
||||
const authEndpointURL = this.buildAuthEndpointURL(options);
|
||||
const response = await fetch(authEndpointURL, { method: 'post', body });
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import OIDCAuthenticator from 'auth/authenticators/oidc';
|
||||
|
||||
export default class ApplicationOIDCAuthenticator extends OIDCAuthenticator {
|
||||
buildTokenValidationEndpointURL(id) {
|
||||
return `/validate/${id}`;
|
||||
}
|
||||
|
||||
buildAuthEndpointURL() {
|
||||
return '/authenticate';
|
||||
}
|
||||
|
||||
buildDeauthEndpointURL() {
|
||||
return '/deauthenticate';
|
||||
}
|
||||
}
|
||||
@@ -32,29 +32,25 @@ module('Unit | Authenticator | base', function (hooks) {
|
||||
|
||||
test('it passes token validation on restoration if receiving 200', async function (assert) {
|
||||
assert.expect(1);
|
||||
const mockData = { id: 'token_123' };
|
||||
const id = 'token_123';
|
||||
const mockData = { attributes: { id } };
|
||||
const authenticator = this.owner.lookup('authenticator:base');
|
||||
this.server.get(
|
||||
authenticator.buildTokenValidationEndpointURL(mockData.id),
|
||||
() => {
|
||||
assert.ok(true, 'token validation was requested');
|
||||
return new Response(200);
|
||||
}
|
||||
);
|
||||
this.server.get(authenticator.buildTokenValidationEndpointURL(id), () => {
|
||||
assert.ok(true, 'token validation was requested');
|
||||
return new Response(200);
|
||||
});
|
||||
await authenticator.restore(mockData);
|
||||
});
|
||||
|
||||
test('it fails token validation on restoration if receiving 401', async function (assert) {
|
||||
assert.expect(2);
|
||||
const mockData = { id: 'token_123' };
|
||||
const id = 'token_123';
|
||||
const mockData = { attributes: { id } };
|
||||
const authenticator = this.owner.lookup('authenticator:base');
|
||||
this.server.get(
|
||||
authenticator.buildTokenValidationEndpointURL(mockData.id),
|
||||
() => {
|
||||
assert.ok(true, 'token validation was requested');
|
||||
return new Response(401);
|
||||
}
|
||||
);
|
||||
this.server.get(authenticator.buildTokenValidationEndpointURL(id), () => {
|
||||
assert.ok(true, 'token validation was requested');
|
||||
return new Response(401);
|
||||
});
|
||||
try {
|
||||
await authenticator.restore(mockData);
|
||||
} catch (e) {
|
||||
@@ -64,15 +60,13 @@ module('Unit | Authenticator | base', function (hooks) {
|
||||
|
||||
test('it fails token validation on restoration if receiving 404', async function (assert) {
|
||||
assert.expect(2);
|
||||
const mockData = { id: 'token_123' };
|
||||
const id = 'token_123';
|
||||
const mockData = { attributes: { id } };
|
||||
const authenticator = this.owner.lookup('authenticator:base');
|
||||
this.server.get(
|
||||
authenticator.buildTokenValidationEndpointURL(mockData.id),
|
||||
() => {
|
||||
assert.ok(true, 'token validation was requested');
|
||||
return new Response(404);
|
||||
}
|
||||
);
|
||||
this.server.get(authenticator.buildTokenValidationEndpointURL(id), () => {
|
||||
assert.ok(true, 'token validation was requested');
|
||||
return new Response(404);
|
||||
});
|
||||
try {
|
||||
await authenticator.restore(mockData);
|
||||
} catch (e) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { Response } from 'miragejs';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
|
||||
module('Unit | Authenticator | OIDC', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
test('`startAuthentication` initiates the authentication flow and returns the response', async function (assert) {
|
||||
assert.expect(2);
|
||||
const authenticator = this.owner.lookup('authenticator:oidc');
|
||||
this.server.post(
|
||||
authenticator.buildAuthEndpointURL(),
|
||||
() => new Response(200, {}, { foo: 'bar' })
|
||||
);
|
||||
const response = await authenticator.startAuthentication();
|
||||
assert.ok(true, 'authentication succeeded');
|
||||
assert.deepEqual(response, { foo: 'bar' });
|
||||
});
|
||||
|
||||
test('`startAuthentication` rejects/errors if API returns an error response', async function (assert) {
|
||||
assert.expect(1);
|
||||
const authenticator = this.owner.lookup('authenticator:oidc');
|
||||
this.server.post(
|
||||
authenticator.buildAuthEndpointURL(),
|
||||
() => new Response(400)
|
||||
);
|
||||
// Because assert.throws doesn't work with async errors/promise rejections
|
||||
try {
|
||||
await authenticator.startAuthentication();
|
||||
} catch (e) {
|
||||
assert.ok(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -47,8 +47,9 @@ module('Unit | Authenticator | password', function (hooks) {
|
||||
(schema, request) => {
|
||||
const json = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(json, {
|
||||
command: 'login',
|
||||
token_type: 'cookie',
|
||||
credentials: {
|
||||
attributes: {
|
||||
login_name: 'foo',
|
||||
password: 'bar',
|
||||
},
|
||||
|
||||
+9
-21
@@ -1,17 +1,15 @@
|
||||
import BasePasswordAuthenticator from 'auth/authenticators/password';
|
||||
import BaseOIDCAuthenticator from 'auth/authenticators/oidc';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* A username/password authenticator that authenticates with
|
||||
* a scope/auth-method endpoint. Authentication occurs on an auth method
|
||||
* endpoint, while deauthentication occurs on a scope endpoint.
|
||||
* An OIDC authenticator that handles flow kick-off and polling.
|
||||
*/
|
||||
export default class PasswordAuthenticator extends BasePasswordAuthenticator {
|
||||
export default class OIDCAuthenticator extends BaseOIDCAuthenticator {
|
||||
// =services
|
||||
|
||||
@service store;
|
||||
|
||||
// =attributes
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Generates an auth method URL with which to authenticate.
|
||||
@@ -30,19 +28,6 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator {
|
||||
return adapter.buildURL('auth-method', authMethodID, options, 'findRecord');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a scope URL with which to deauthenticate.
|
||||
* @override
|
||||
* @param {object} options
|
||||
* @param {string} scopeID
|
||||
* @return {string}
|
||||
*/
|
||||
buildDeauthEndpointURL({ scope: { id: scopeID } }) {
|
||||
const adapter = this.store.adapterFor('application');
|
||||
const options = { adapterOptions: { method: 'deauthenticate' } };
|
||||
return adapter.buildURL('scope', scopeID, options, 'findRecord');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an auth token validation URL used to check tokens on restoration.
|
||||
* @override
|
||||
@@ -62,7 +47,7 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator {
|
||||
*/
|
||||
authenticate() {
|
||||
return super.authenticate(...arguments).then((data) => {
|
||||
const token = data?.token;
|
||||
const token = data?.attributes?.token;
|
||||
this.addTokenToAuthorization(token);
|
||||
return data;
|
||||
});
|
||||
@@ -76,7 +61,7 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator {
|
||||
* @return {object}
|
||||
*/
|
||||
restore(data) {
|
||||
const token = data?.token;
|
||||
const token = data?.attributes?.token;
|
||||
this.addTokenToAuthorization(token);
|
||||
return super.restore(data);
|
||||
}
|
||||
@@ -85,6 +70,9 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator {
|
||||
* Assigns a token string all future requests via the `Authorization` header
|
||||
* on the application adapter prototype. Only application and adapters that
|
||||
* extend the application adapter receive this header (which is most).
|
||||
*
|
||||
* TODO: if cookies were requested, this step should be skipped.
|
||||
*
|
||||
* @param {string} token
|
||||
*/
|
||||
addTokenToAuthorization(token) {
|
||||
@@ -62,7 +62,7 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator {
|
||||
*/
|
||||
authenticate() {
|
||||
return super.authenticate(...arguments).then((data) => {
|
||||
const token = data?.token;
|
||||
const token = data?.attributes?.token;
|
||||
this.addTokenToAuthorization(token);
|
||||
return data;
|
||||
});
|
||||
@@ -76,7 +76,7 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator {
|
||||
* @return {object}
|
||||
*/
|
||||
restore(data) {
|
||||
const token = data?.token;
|
||||
const token = data?.attributes?.token;
|
||||
this.addTokenToAuthorization(token);
|
||||
return super.restore(data);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<Rose::Form
|
||||
class="full-width"
|
||||
@onSubmit={{@submit}}
|
||||
@cancel={{false}}
|
||||
@disabled={{@disabled}}
|
||||
as |form|
|
||||
>
|
||||
|
||||
<form.actions
|
||||
@submitText={{t "actions.authenticate"}}
|
||||
@showCancel={{false}} />
|
||||
|
||||
</Rose::Form>
|
||||
@@ -5,7 +5,7 @@ import { getOwner } from '@ember/application';
|
||||
* confirmation via the confirmation service. If the user accepts, the
|
||||
* original method is executed. If the user denies, it is not.
|
||||
*/
|
||||
export function confirm(confirmationTextKey) {
|
||||
export function confirm(confirmationTextKey, options) {
|
||||
return function (_target, _propertyKey, desc) {
|
||||
const method = desc.value;
|
||||
desc.value = function () {
|
||||
@@ -15,7 +15,7 @@ export function confirm(confirmationTextKey) {
|
||||
const text = intl.t(confirmationTextKey);
|
||||
return (
|
||||
confirmService
|
||||
.confirm(text)
|
||||
.confirm(text, options)
|
||||
// If user confirmed, execute the decorated method
|
||||
.then(() => method.apply(this, arguments))
|
||||
// If the user denied, gracefully handle promise rejection with no-op
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'core/authenticators/oidc';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'core/components/form/authenticate/oidc';
|
||||
@@ -37,6 +37,7 @@
|
||||
"ember-loading": "^0.4.0",
|
||||
"ember-page-title": "^6.0.3",
|
||||
"ember-route-action-helper": "^2.0.8",
|
||||
"ember-feature-flags": "^6.0.0",
|
||||
"rose": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -71,7 +72,7 @@
|
||||
"eslint-plugin-ember": "^10.1.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"faker": "^5.1.0",
|
||||
"faker": "^5.5.3",
|
||||
"loader.js": "^4.7.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.2.1",
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 377 132">
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M125.45,41.34V35.68h-5.18v5.66h-2.64V27.75h2.64v5.7h5.18v-5.7h2.64V41.34Zm12.32,0h-2.09l-.19-.67a5.69,5.69,0,0,1-3,.87c-1.87,0-2.66-1.23-2.66-2.92,0-2,.9-2.75,3-2.75h2.46v-1c0-1.09-.32-1.47-1.95-1.47a14.85,14.85,0,0,0-2.83.3l-.32-1.87a14,14,0,0,1,3.5-.46c3.21,0,4.15,1.08,4.15,3.54Zm-2.55-3.7h-1.89c-.84,0-1.07.22-1.07,1s.23,1,1,1a4.1,4.1,0,0,0,1.93-.51Zm7.91,3.9a12.53,12.53,0,0,1-3.52-.56L140,39.1a11.5,11.5,0,0,0,3,.43c1.13,0,1.3-.24,1.3-1s-.12-.9-1.78-1.29c-2.49-.58-2.79-1.18-2.79-3.08s.9-2.83,3.82-2.83a14.07,14.07,0,0,1,3.06.34l-.25,2a19,19,0,0,0-2.81-.28c-1.11,0-1.3.24-1.3.84,0,.79.06.85,1.44,1.19,2.85.73,3.13,1.09,3.13,3.1S146.21,41.54,143.13,41.54Zm11.7-.2V34.5c0-.53-.23-.79-.81-.79a7.22,7.22,0,0,0-2.67.83v6.8H148.8V27.56l2.55.38v4.34a9.38,9.38,0,0,1,3.73-.94c1.7,0,2.31,1.14,2.31,2.89v7.11Zm4.71-11.19v-2.4h2.55v2.4Zm0,11.19v-9.8h2.55v9.8Zm4.6-9.72c0-2.46,1.48-3.88,5-3.88a16.63,16.63,0,0,1,3.8.44l-.3,2.19a21.49,21.49,0,0,0-3.41-.34c-1.83,0-2.41.6-2.41,2v5.15c0,1.43.58,2,2.41,2a21.57,21.57,0,0,0,3.41-.35l.3,2.2a16.63,16.63,0,0,1-3.8.44c-3.48,0-5-1.43-5-3.88Zm14.41,9.92c-3.5,0-4.45-1.85-4.45-3.86V35.2c0-2,1-3.86,4.45-3.86S183,33.19,183,35.2v2.48C183,39.69,182.05,41.54,178.55,41.54Zm0-8.11c-1.37,0-1.89.58-1.89,1.69v2.64c0,1.1.52,1.69,1.89,1.69s1.88-.59,1.88-1.69V35.12C180.43,34,179.91,33.43,178.55,33.43Zm11.64.16A20.64,20.64,0,0,0,187.48,35v6.32h-2.55v-9.8h2.16l.16,1.09a11.23,11.23,0,0,1,2.69-1.29Zm10.22,4.49c0,2.17-1,3.46-3.38,3.46a14.74,14.74,0,0,1-2.72-.28v4l-2.56.38V31.54h2l.25.82a5.54,5.54,0,0,1,3.22-1c2.06,0,3.15,1.16,3.15,3.4Zm-6.1,1.11a11.85,11.85,0,0,0,2.26.26c.93,0,1.28-.43,1.28-1.31V34.68c0-.81-.31-1.25-1.26-1.25a3.66,3.66,0,0,0-2.28.89Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M139.64,70.53c3.08-1.33,4.48-3.21,4.48-6.53V59.82c0-5.45-3.09-8.59-10.72-8.59H117.55V91.52h16.94c7.33,0,11-3.56,11-9.25V78C145.51,74,143.33,71.31,139.64,70.53Zm-14.46-13h7.32c2.78,0,4,1,4,3.27v3.57c0,2.36-1.08,3.75-4.06,3.75h-7.25ZM137.89,81.3c0,3-1.46,3.93-4.91,3.93h-7.8v-11h8.47c3,0,4.24,1.33,4.24,4.06Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M161.54,61.45c-10.11,0-12.82,5.57-12.82,11.62v7.44c0,6,2.71,11.62,12.82,11.62s12.83-5.57,12.83-11.62V73.07C174.37,67,171.64,61.45,161.54,61.45ZM167,80.76c0,3.32-1.51,5.08-5.45,5.08s-5.44-1.76-5.44-5.08V72.83c0-3.33,1.51-5.08,5.44-5.08S167,69.5,167,72.83Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M196.51,62.06V82.51c-2.66,1.39-5.87,2.48-7.68,2.48s-2.36-.78-2.36-2.36V62.06h-7.38V83.42c0,5.26,1.75,8.71,6.65,8.71A29.44,29.44,0,0,0,197.54,89l.72,2.48h5.64V62.06Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M228.4,61.45a29.61,29.61,0,0,0-11.8,3.09l-.72-2.48h-5.64V91.52h7.39V71.08c2.66-1.39,5.87-2.48,7.68-2.48s2.36.78,2.36,2.35V91.52h7.38V70.17C235.05,64.9,233.3,61.45,228.4,61.45Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M257.5,50V62.31a40.19,40.19,0,0,0-7.87-.86c-6.83,0-9.74,3.88-9.74,10.41v10c0,6.71,3.15,10.22,9.08,10.22A15.49,15.49,0,0,0,258.29,89l.72,2.48h5.87V49Zm0,33.16a10.43,10.43,0,0,1-6.59,2.66c-2.73,0-3.63-1.33-3.63-3.75V71.68c0-2.66,1-3.93,3.68-3.93a32.33,32.33,0,0,1,6.54.79Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M280.8,61.45a38.34,38.34,0,0,0-10.11,1.4l.91,5.62a41.47,41.47,0,0,1,8.17-.9c4.71,0,5.62,1.15,5.62,4.42v3.08h-7.08c-6,0-8.59,2.3-8.59,8.29,0,5.08,2.3,8.77,7.68,8.77a16.18,16.18,0,0,0,8.78-2.6l.54,2h6.06V72.1C292.78,64.72,290.05,61.45,280.8,61.45Zm4.59,23.3a11.57,11.57,0,0,1-5.56,1.51c-2.3,0-3-.91-3-3,0-2.23.67-2.9,3.09-2.9h5.44Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M313,61.45a33,33,0,0,0-7.74,3.88l-.48-3.27h-6.24V91.52h7.38v-19a57.85,57.85,0,0,1,7.81-4.3Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M335.67,62.06,328.9,84.44l-6.72-22.38h-7.56l9.32,29.46h2.59l-3.68,11.92h7l3.93-11.92,9.43-29.46Z">
|
||||
</path>
|
||||
|
||||
<polygon
|
||||
fill="#f04e54"
|
||||
points="44.94 102.47 44.94 97.64 54.96 97.64 54.96 94.67 50.1 94.67 50.1 89.82 73.03 89.82 62.38 71.38 73.03 52.93 49.53 52.93 49.53 78.51 36.88 78.51 36.88 40.29 80.4 40.29 87.67 52.87 76.98 71.38 87.88 90.26 80.84 102.47 44.94 102.47">
|
||||
</polygon>
|
||||
|
||||
<rect
|
||||
fill="#f04e54"
|
||||
x="42.04"
|
||||
y="89.82"
|
||||
width="4.85"
|
||||
height="4.85">
|
||||
</rect>
|
||||
|
||||
<rect
|
||||
fill="#f04e54"
|
||||
x="36.88"
|
||||
y="97.61"
|
||||
width="4.85"
|
||||
height="4.85">
|
||||
</rect>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -5,7 +5,7 @@
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="logomark">
|
||||
<g class="logomark">
|
||||
<path
|
||||
d="M0 7.19579L11.4553 0.58728V5.55192L4.30398 9.68052V25.5537L0 23.069V7.19579Z"
|
||||
fill="currentColor">
|
||||
|
||||
|
Before Width: | Height: | Size: 633 B After Width: | Height: | Size: 636 B |
@@ -26,13 +26,11 @@ module('Unit | Authenticator | password', function (hooks) {
|
||||
|
||||
test('it adds an authorization header to application adapter on restore', async function (assert) {
|
||||
assert.expect(1);
|
||||
const mockData = { id: 'token1234', token: 'token1234' };
|
||||
this.server.get(
|
||||
authenticator.buildTokenValidationEndpointURL(mockData.id),
|
||||
() => {
|
||||
return new Response(200);
|
||||
}
|
||||
);
|
||||
const id = 'token1234';
|
||||
const mockData = { attributes: { id, token: 'token1234' } };
|
||||
this.server.get(authenticator.buildTokenValidationEndpointURL(id), () => {
|
||||
return new Response(200);
|
||||
});
|
||||
authenticator.restore(mockData);
|
||||
assert.equal(applicationAdapter.headers.Authorization, 'Bearer token1234');
|
||||
});
|
||||
|
||||
@@ -16,10 +16,11 @@ back: Back
|
||||
retry: Retry
|
||||
get-topic-help: Get help for this topic
|
||||
copy-to-clipboard: Copy to Clipboard
|
||||
copy-error-detail-to-clipboard: Copy error detail to clipboard
|
||||
authenticate: Authenticate
|
||||
deauthenticate: Deauthenticate
|
||||
choose-org: Choose Scope
|
||||
choose-different-org: Choose a Different Scope
|
||||
choose-different-org: Choose a different scope
|
||||
add-host-set: Create Host Set
|
||||
add-host: Create Host
|
||||
add-host-sets: Add Host Sets
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
auth:
|
||||
titles:
|
||||
no-scopes: No Scopes Available
|
||||
flow-success: Successful authentication
|
||||
flow-error: Could not authenticate
|
||||
descriptions:
|
||||
no-scopes: No scopes were found. To authenticate, ask your administrator to create an scope and grant you access.
|
||||
choose-org: Choose a scope to authenticate.
|
||||
authenticating-with-org: "You are authenticating with {name}."
|
||||
authenticating-with-org: "Scope: You are authenticating with organization \"{name}\""
|
||||
flow-success: You've successfully authenticated. You may now close this window and return to the client.
|
||||
flow-error: Something went wrong while authenticating. Please close this window and try again. If the problem persists, notify your administrator.
|
||||
origin:
|
||||
connected-to: Connected to
|
||||
titles:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
create-success: Created succesfully.
|
||||
save-success: Saved succesfully.
|
||||
create-success: Created successfully.
|
||||
save-success: Saved successfully.
|
||||
delete-success: Deleted successfully.
|
||||
add-success: Added successfully.
|
||||
remove-success: Removed successfully.
|
||||
|
||||
@@ -16,7 +16,7 @@ account:
|
||||
auth-method:
|
||||
title: Auth Method
|
||||
title_plural: Auth Methods
|
||||
description: Auth methods govern how an org's users can login to Boundary. Boundary’s default auth method is username & passwords.
|
||||
description: Auth methods allow users to authenticate within a scope.
|
||||
messages:
|
||||
welcome:
|
||||
title: Welcome to Auth Methods
|
||||
@@ -24,11 +24,29 @@ auth-method:
|
||||
none:
|
||||
title: No Auth Methods
|
||||
description: No auth methods were found for this scope.
|
||||
no-primary-auth-method:
|
||||
title: "Recommendation: Select a Primary Auth Method"
|
||||
description: A primary auth method auto-creates users when they authenticate for the first time. Scopes without primary auth methods may experience authentication failures.
|
||||
pending:
|
||||
title: Authentication Pending
|
||||
description: Complete authentication in the newly-opened window.
|
||||
types:
|
||||
password: Password
|
||||
oidc: OIDC
|
||||
actions:
|
||||
create: Create Auth Method
|
||||
delete: Delete Auth Method
|
||||
make-primary: Make Primary
|
||||
remove-as-primary: Remove as Primary
|
||||
notifications:
|
||||
make-primary-success: Auth method successfully elected to primary.
|
||||
remove-as-primary-success: Auth method successfully removed as primary.
|
||||
questions:
|
||||
make-primary: Make primary auth method?
|
||||
remove-as-primary: Remove primary auth method?
|
||||
make-primary-confirm: "A primary auth method auto-creates users on their first authentication attempt if needed. Note: if you change the primary auth method, the previous primary will no longer auto-create users."
|
||||
remove-as-primary-confirm: Removing the primary auth method may lead to authentication failures for this scope.
|
||||
no-see-window: Don't see the new window?
|
||||
group:
|
||||
title: Group
|
||||
title_plural: Groups
|
||||
|
||||
@@ -7,3 +7,4 @@ copied: Copied
|
||||
connection:
|
||||
success:
|
||||
description: "Local proxy address ({protocol}): {address}:{port}"
|
||||
primary: Primary
|
||||
@@ -1,3 +1,5 @@
|
||||
const namedBlockPolyfill = require('ember-named-blocks-polyfill/lib/named-blocks-polyfill-plugin');
|
||||
|
||||
module.exports = {
|
||||
stories: [
|
||||
'../addon/components/**/*.stories.(js|mdx)',
|
||||
@@ -7,5 +9,8 @@ module.exports = {
|
||||
'@storybook/addon-knobs',
|
||||
'@storybook/addon-actions',
|
||||
'@storybook/addon-docs',
|
||||
]
|
||||
],
|
||||
emberOptions: {
|
||||
polyfills: [namedBlockPolyfill],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class="rose-dialog-cover-modal-overlay"></div>
|
||||
|
||||
<section
|
||||
...attributes
|
||||
role="dialog"
|
||||
class="rose-dialog-cover"
|
||||
id={{this.id}}
|
||||
aria-describedby={{concat "dialog-cover-header-" this.id}}
|
||||
tabindex="0"
|
||||
{{focus-trap}}
|
||||
>
|
||||
<header
|
||||
class="rose-dialog-cover-header"
|
||||
id={{concat "dialog-cover-header-" this.id}}
|
||||
>
|
||||
{{yield to="header"}}
|
||||
</header>
|
||||
<div class="rose-dialog-cover-body">
|
||||
{{yield to="body"}}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
<Meta title="Rose|Dialog|Cover" component="Cover" />
|
||||
|
||||
# Dialog Cover
|
||||
|
||||
A template-only dialog component that covers the viewport and accepts arbitrary
|
||||
content. Note that this component is unique to Boundary and is not part of the
|
||||
Structure design system.
|
||||
|
||||
Prerequisite: `ember-named-blocks-polyfill` must be installed in your app's
|
||||
`dependencies` rather than `devDependencies`.
|
||||
|
||||
<Preview>
|
||||
<Story name="Cover" height="15rem">{{
|
||||
template: hbs`
|
||||
<Rose::Dialog::Cover>
|
||||
<:header>
|
||||
<h1>Cover Header</h1>
|
||||
</:header>
|
||||
<:body>
|
||||
<p>Cover body</p>
|
||||
</:body>
|
||||
</Rose::Dialog::Cover>
|
||||
`
|
||||
}}</Story>
|
||||
</Preview>
|
||||
|
||||
|
||||
```htmlbars
|
||||
<Rose::Dialog::Cover>
|
||||
<:header>
|
||||
<h2>Cover Header</h2>
|
||||
</:header>
|
||||
<:body>
|
||||
<p>Cover body</p>
|
||||
</:body>
|
||||
</Rose::Dialog::Cover>
|
||||
```
|
||||
@@ -0,0 +1,11 @@
|
||||
<section ...attributes class="rose-frame">
|
||||
|
||||
<header class="rose-frame-header">
|
||||
{{yield to="header"}}
|
||||
</header>
|
||||
|
||||
<div class="rose-frame-body">
|
||||
{{yield to="body"}}
|
||||
</div>
|
||||
|
||||
</section>
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
<Meta title="Rose|Frame" component="Frame" />
|
||||
|
||||
# Frame
|
||||
|
||||
A simple frame component with named blocks `:header` and `:body`. Note that
|
||||
this component is unique to Boundary and is not part of the Structure design
|
||||
system.
|
||||
|
||||
Prerequisite: `ember-named-blocks-polyfill` must be installed in your app's
|
||||
`dependencies` rather than `devDependencies`.
|
||||
|
||||
<Preview>
|
||||
<Story name="Frame" height="15rem">{{
|
||||
template: hbs`
|
||||
<Rose::Frame>
|
||||
<:header>
|
||||
<h2>Frame Header</h2>
|
||||
</:header>
|
||||
<:body>
|
||||
<p>Frame body</p>
|
||||
</:body>
|
||||
</Rose::Frame>
|
||||
`
|
||||
}}</Story>
|
||||
</Preview>
|
||||
|
||||
|
||||
```htmlbars
|
||||
<Rose::Frame>
|
||||
<:header>
|
||||
<h2>Frame Header</h2>
|
||||
</:header>
|
||||
<:body>
|
||||
<p>Frame body</p>
|
||||
</:body>
|
||||
</Rose::Frame>
|
||||
```
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
&.rose-badge-outline {
|
||||
--border-color: var(--ui-gray-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
|
||||
&.rose-badge-dark {
|
||||
@@ -40,6 +41,7 @@
|
||||
&.rose-badge-informational-outline {
|
||||
--color: var(--info-starker-1);
|
||||
--border-color: var(--info-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
|
||||
&.rose-badge-alert {
|
||||
@@ -50,6 +52,7 @@
|
||||
&.rose-badge-alert-outline {
|
||||
--color: var(--alert-starker-1);
|
||||
--border-color: var(--alert-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
|
||||
&.rose-badge-error {
|
||||
@@ -60,6 +63,7 @@
|
||||
&.rose-badge-error-outline {
|
||||
--color: var(--failure-starker-1);
|
||||
--border-color: var(--failure-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
|
||||
&.rose-badge-success {
|
||||
@@ -70,6 +74,7 @@
|
||||
&.rose-badge-success-outline {
|
||||
--color: var(--success-starker-1);
|
||||
--border-color: var(--success-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
|
||||
&.rose-badge-warning {
|
||||
@@ -80,5 +85,6 @@
|
||||
&.rose-badge-warning-outline {
|
||||
--color: var(--warning-starker-1);
|
||||
--border-color: var(--warning-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
@use '../variables/sizing';
|
||||
@use '../utilities/type';
|
||||
@use '../variables/media';
|
||||
|
||||
$l: sizing.rems(l); // 16
|
||||
$m: sizing.rems(m); // 16
|
||||
$s: sizing.rems(s); // 12
|
||||
$xs: sizing.rems(xs); // 8
|
||||
$xxs: sizing.rems(xxs); // 4
|
||||
$xxxs: sizing.rems(xxxs); // 2
|
||||
$xxxxs: sizing.rems(xxxxs); // 1
|
||||
|
||||
.rose-frame {
|
||||
--headerColor: var(--ui-gray-starker-3);
|
||||
--headerIconColor: var(--headerColor);
|
||||
--headerBackgroundColor: var(--ui-gray-subtler-5);
|
||||
--headerBorderColor: var(--ui-gray-subtler-3);
|
||||
|
||||
@include type.type(s);
|
||||
border: $xxxxs solid var(--ui-gray-subtler-3);
|
||||
border-radius: $xxxs;
|
||||
//margin: $m;
|
||||
box-shadow: 0 $xxs $xxs rgba(var(--stark-components), var(--opacity-4));
|
||||
background-color: var(--background-color);
|
||||
|
||||
.rose-frame-header {
|
||||
padding: ($xs + $xxxs) $l;
|
||||
min-height: $l;
|
||||
color: var(--headerColor);
|
||||
background-color: var(--headerBackgroundColor);
|
||||
border-bottom: $xxxxs solid var(--headerBorderColor);
|
||||
border-top-left-radius: $xxxs;
|
||||
border-top-right-radius: $xxxs;
|
||||
}
|
||||
|
||||
.rose-frame-body {
|
||||
$bodyPadding: $l;
|
||||
|
||||
padding: $bodyPadding;
|
||||
|
||||
> .rose-separator {
|
||||
margin-left: -$bodyPadding;
|
||||
margin-right: -$bodyPadding;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
@import 'dialog';
|
||||
@import 'dropdown';
|
||||
@import 'form';
|
||||
@import 'frame';
|
||||
@import 'header';
|
||||
@import 'footer';
|
||||
@import 'layout';
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
.rose-dialog-cover {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 200;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--white);
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.rose-dialog-cover-header,
|
||||
.rose-dialog-cover-body {
|
||||
position: relative;
|
||||
top: -10vh;
|
||||
max-width: 650px;
|
||||
}
|
||||
}
|
||||
// overlay
|
||||
.rose-dialog-cover-modal-overlay {
|
||||
content: ' ';
|
||||
background-color: var(--black);
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
opacity: 0.85;
|
||||
}
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
@use '../variables/sizing';
|
||||
@use '../utilities/type';
|
||||
@use '../variables/media';
|
||||
@use '../../variables/sizing';
|
||||
@use '../../utilities/type';
|
||||
@use '../../variables/media';
|
||||
|
||||
$m: sizing.rems(m); // 16
|
||||
$s: sizing.rems(s); // 12
|
||||
@@ -0,0 +1,2 @@
|
||||
@import 'cover';
|
||||
@import 'dialog';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'rose/components/rose/dialog/cover';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'rose/components/rose/frame';
|
||||
@@ -33,9 +33,11 @@
|
||||
"ember-cli-babel": "^7.23.0",
|
||||
"ember-cli-htmlbars": "^5.3.1",
|
||||
"ember-focus-trap": "^0.4.0",
|
||||
"ember-named-blocks-polyfill": "^0.2.4",
|
||||
"ember-radio-button": "^2.0.1",
|
||||
"ember-uuid": "^2.1.0",
|
||||
"emberx-select": "github:adopted-ember-addons/emberx-select#bc5f774"
|
||||
"emberx-select": "github:adopted-ember-addons/emberx-select#bc5f774",
|
||||
"ember-named-blocks-polyfill": "^0.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ember/optional-features": "^2.0.0",
|
||||
|
||||
@@ -79,3 +79,15 @@
|
||||
</Rose::Dropdown>
|
||||
</form.radioGroup>
|
||||
</Rose::Form>
|
||||
|
||||
|
||||
<Rose::Dialog::Cover>
|
||||
<:header>
|
||||
<h1>Authentication Pending</h1>
|
||||
</:header>
|
||||
<:body>
|
||||
<p>Complete authentication in the newly-opened window.</p>
|
||||
<p>Don't see the new window? <a href="#">Try again.</a></p>
|
||||
<p><a href="#">Cancel</a></p>
|
||||
</:body>
|
||||
</Rose::Dialog::Cover>
|
||||
@@ -0,0 +1,29 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, find } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | rose/dialog/cover', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
await render(hbs`
|
||||
<Rose::Dialog::Cover>
|
||||
<:header>
|
||||
<h1>Authentication Pending</h1>
|
||||
</:header>
|
||||
<:body>
|
||||
<p>Complete authentication in the newly-opened window.</p>
|
||||
<p>Don't see the new window? <a href="#">Try again.</a></p>
|
||||
<p><a href="#">Cancel</a></p>
|
||||
</:body>
|
||||
</Rose::Dialog::Cover>
|
||||
`);
|
||||
|
||||
assert.ok(find('.rose-dialog-cover'));
|
||||
assert.ok(find('.rose-dialog-cover-header'));
|
||||
assert.ok(find('.rose-dialog-cover-body'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, find } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | rose/frame', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
await render(hbs`
|
||||
<Rose::Frame>
|
||||
<:header>Header</:header>
|
||||
<:body>Body</:body>
|
||||
</Rose::Frame>
|
||||
`);
|
||||
|
||||
assert.ok(find('section > header'));
|
||||
assert.ok(find('section > div'));
|
||||
});
|
||||
});
|
||||
+5
-2
@@ -50,13 +50,16 @@
|
||||
"request": "~2.68.0",
|
||||
"tunnel-agent": "~0.6.0",
|
||||
"cryptiles": "~4.1.2",
|
||||
"prismjs": "~1.21.0",
|
||||
"prismjs": "~1.23.0",
|
||||
"npm-user-validate": "^1.0.1",
|
||||
"caniuse-lite": "^1.0.30001157",
|
||||
"immer": "^8.0.1",
|
||||
"node-notifier": "^8.0.1",
|
||||
"highlight.js": "^10.4.1",
|
||||
"hoek": "~4.2.0"
|
||||
"hoek": "~4.2.0",
|
||||
"elliptic": "~6.5.4",
|
||||
"react-dev-utils": "~11.0.4",
|
||||
"yargs-parser": "~20.2.7"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class ScopesScopeAuthMethodsIndexController extends Controller {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* True if the current scope has a primary auth method set.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get hasPrimaryAuthMethod() {
|
||||
return Boolean(this.scopeModel.primary_auth_method_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ScopesScopeAuthenticateMethodOidcController extends Controller {
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Authetnication URL for the pending OIDC flow, if any.
|
||||
* @type {?string}
|
||||
*/
|
||||
get authURL() {
|
||||
return this.session.data.pending.oidc.attributes.auth_url;
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,15 @@ export default class Router extends EmberRouter {
|
||||
}
|
||||
|
||||
Router.map(function () {
|
||||
this.route('authentication-complete');
|
||||
this.route('authentication-error');
|
||||
|
||||
this.route('scopes', function () {
|
||||
this.route('scope', { path: ':scope_id' }, function () {
|
||||
this.route('authenticate', function () {
|
||||
this.route('method', { path: ':auth_method_id' });
|
||||
this.route('method', { path: ':auth_method_id' }, function () {
|
||||
this.route('oidc');
|
||||
});
|
||||
});
|
||||
|
||||
this.route('scopes', function () {});
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class AuthenticationCompleteRoute extends Route {}
|
||||
@@ -0,0 +1,12 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class AuthenticationErrorRoute extends Route {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
model() {
|
||||
return window.location.toString();
|
||||
}
|
||||
}
|
||||
@@ -71,8 +71,67 @@ export default class ScopesScopeAuthMethodsRoute extends Route {
|
||||
@notifyError(({ message }) => message, { catch: true })
|
||||
@notifySuccess('notifications.delete-success')
|
||||
async delete(authMethod) {
|
||||
const scopeModel = this.modelFor('scopes.scope');
|
||||
await authMethod.destroyRecord();
|
||||
// Reload the scope, since this is where the primary_auth_method_id is
|
||||
// stored. An auth method deletion could affect this field.
|
||||
await scopeModel.reload();
|
||||
await this.replaceWith('scopes.scope.auth-methods');
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Elects the specified auth method to primary for the current scope.
|
||||
* @param {AuthMethodModel} authMethod
|
||||
* @param {string} authMethod.id
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@confirm('resources.auth-method.questions.make-primary-confirm', {
|
||||
title: 'resources.auth-method.questions.make-primary',
|
||||
})
|
||||
@notifyError(({ message }) => message, { catch: true })
|
||||
@notifySuccess('resources.auth-method.notifications.make-primary-success')
|
||||
async makePrimary({ id }) {
|
||||
const scopeModel = this.modelFor('scopes.scope');
|
||||
scopeModel.primary_auth_method_id = id;
|
||||
// Attempt to save the change to the scope. If this operation fails,
|
||||
// we rollback the change and rethrow the error so the user can be notified.
|
||||
try {
|
||||
await scopeModel.save();
|
||||
} catch (e) {
|
||||
scopeModel.rollbackAttributes();
|
||||
throw e;
|
||||
}
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the `primary_auth_method_id` field to null for the current scope
|
||||
* and saves it if and only if the specified auth method is in fact primary
|
||||
* for the current scope.
|
||||
* @param {AuthMethodModel} authMethod
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@confirm('resources.auth-method.questions.remove-as-primary-confirm', {
|
||||
title: 'resources.auth-method.questions.remove-as-primary',
|
||||
})
|
||||
@notifyError(({ message }) => message, { catch: true })
|
||||
@notifySuccess(
|
||||
'resources.auth-method.notifications.remove-as-primary-success'
|
||||
)
|
||||
async removeAsPrimary(authMethod) {
|
||||
const scopeModel = this.modelFor('scopes.scope');
|
||||
scopeModel.primary_auth_method_id = null;
|
||||
// Attempt to save the change to the scope. If this operation fails,
|
||||
// we rollback the change and rethrow the error so the user can be notified.
|
||||
try {
|
||||
await scopeModel.save();
|
||||
} catch (e) {
|
||||
scopeModel.rollbackAttributes();
|
||||
throw e;
|
||||
}
|
||||
await authMethod.reload();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,15 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ScopesScopeAuthMethodsIndexRoute extends Route {}
|
||||
export default class ScopesScopeAuthMethodsIndexRoute extends Route {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Adds the scope to the controller context.
|
||||
* @param {Controller} controller
|
||||
*/
|
||||
setupController(controller) {
|
||||
super.setupController(...arguments);
|
||||
const scopeModel = this.modelFor('scopes.scope');
|
||||
controller.scopeModel = scopeModel;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { action } from '@ember/object';
|
||||
import loading from 'ember-loading/decorator';
|
||||
import { notifyError } from 'core/decorators/notify';
|
||||
@@ -20,7 +21,28 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route {
|
||||
*/
|
||||
model({ auth_method_id: id }) {
|
||||
const adapterOptions = { scopeID: this.modelFor('scopes.scope').id };
|
||||
return this.store.findRecord('auth-method', id, adapterOptions);
|
||||
return this.store.peekRecord('auth-method', id, adapterOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
async startOIDCAuthentication(authenticatorName, options) {
|
||||
const oidc = getOwner(this).lookup(authenticatorName);
|
||||
// TODO: delegate this call from the session service so that we don't have
|
||||
// to look up the authenticator directly
|
||||
const json = await oidc.startAuthentication(options);
|
||||
await this.openExternalOIDCFlow(json.attributes.auth_url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the specified URL in a new tab or window. By default this uses
|
||||
* `window.open`, but may be overriden.
|
||||
* @param {string} url
|
||||
*/
|
||||
async openExternalOIDCFlow(url) {
|
||||
// TODO don't use window directly
|
||||
await window.open(url);
|
||||
}
|
||||
|
||||
// =actions
|
||||
@@ -37,10 +59,23 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route {
|
||||
const authMethod = this.modelFor('scopes.scope.authenticate.method');
|
||||
const authenticatorName = `authenticator:${authMethod.type}`;
|
||||
const requestCookies = false;
|
||||
await this.session.authenticate(authenticatorName, creds, requestCookies, {
|
||||
scope,
|
||||
authMethod,
|
||||
});
|
||||
this.transitionTo('index');
|
||||
switch (authMethod.type) {
|
||||
case 'password':
|
||||
await this.session.authenticate(
|
||||
authenticatorName,
|
||||
creds,
|
||||
requestCookies,
|
||||
{ scope, authMethod }
|
||||
);
|
||||
this.transitionTo('index');
|
||||
break;
|
||||
case 'oidc':
|
||||
await this.startOIDCAuthentication(authenticatorName, {
|
||||
scope,
|
||||
authMethod,
|
||||
});
|
||||
this.transitionTo('scopes.scope.authenticate.method.oidc');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ScopesScopeAuthenticateMethodIndexRoute extends Route {}
|
||||
@@ -0,0 +1,82 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { task, timeout } from 'ember-concurrency';
|
||||
import { action } from '@ember/object';
|
||||
import { notifyError } from 'core/decorators/notify';
|
||||
import config from '../../../../../config/environment';
|
||||
|
||||
const POLL_TIMEOUT_SECONDS = config.oidcPollingTimeoutSeconds;
|
||||
|
||||
export default class ScopesScopeAuthenticateMethodOidcRoute extends Route {
|
||||
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* A simple Ember Concurrency-based polling task that refreshes the route
|
||||
* every POLL_TIMEOUT_SECONDS seconds. This is necessary to display changes
|
||||
* to session `status` that may occur.
|
||||
*
|
||||
* NOTE: tasks are sort of attributes and sort of methods, but they are not
|
||||
* language-level constructs. Thus we annotate this task as if it
|
||||
* is an attribute.
|
||||
* @type {Task}
|
||||
*/
|
||||
@task(function * () {
|
||||
while(!this.session.isAuthenticated) {
|
||||
yield timeout(POLL_TIMEOUT_SECONDS * 1000);
|
||||
yield this.refresh();
|
||||
}
|
||||
/* eslint-disable-next-line prettier/prettier */
|
||||
}).drop() poller;
|
||||
|
||||
// =methods
|
||||
|
||||
model() {
|
||||
const scope = this.modelFor('scopes.scope');
|
||||
const authMethod = this.modelFor('scopes.scope.authenticate.method');
|
||||
const authenticatorName = `authenticator:${authMethod.type}`;
|
||||
const oidc = getOwner(this).lookup(authenticatorName);
|
||||
// TODO: delegate this call from the session service so that we don't have
|
||||
// to look up the authenticator directly
|
||||
return oidc.attemptFetchToken({ scope, authMethod });
|
||||
}
|
||||
|
||||
/**
|
||||
* If authenticated, redirects to index for further processing.
|
||||
*/
|
||||
redirect() {
|
||||
if (this.session.isAuthenticated) this.replaceWith('index');
|
||||
}
|
||||
|
||||
/**
|
||||
* When this route is activated (entered), begin polling for changes.
|
||||
*/
|
||||
activate() {
|
||||
this.poller.perform();
|
||||
}
|
||||
|
||||
/**
|
||||
* When this route is deactivated (exited), stop polling for changes.
|
||||
*/
|
||||
deactivate() {
|
||||
this.poller.cancelAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* An error in this route indicates authentication failed. The user is
|
||||
* notified and returned to the index.
|
||||
*/
|
||||
@action
|
||||
@notifyError(() => 'errors.authentication-failed.title', { catch: true })
|
||||
error(e) {
|
||||
this.transitionTo('scopes.scope.authenticate.method.index');
|
||||
// rethrow the error to activate the notifyError decorator
|
||||
throw e;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,6 +4,12 @@
|
||||
@import 'rose';
|
||||
@import 'notify';
|
||||
|
||||
.rose-layout-global {
|
||||
.rose-layout-global-body {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.rose-layout-centered {
|
||||
> .rose-layout-page {
|
||||
width: sizing.rems(l) * 18;
|
||||
@@ -132,6 +138,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.rose-frame {
|
||||
.rose-form-actions {
|
||||
.rose-button-primary {
|
||||
display: block;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
|
||||
.rose-button-wrapper {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyable {
|
||||
white-space: nowrap;
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
</Rose::Dialog>
|
||||
{{else}}
|
||||
<Rose::Dialog
|
||||
@heading={{t "actions.confirm"}}
|
||||
@heading={{if confirmation.options.title (t confirmation.options.title) (t "actions.confirm")}}
|
||||
@dismissButtonText={{t "actions.cancel"}}
|
||||
@icon="alert-triangle"
|
||||
@style="warning"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{{page-title (t 'auth.titles.flow-success')}}
|
||||
|
||||
<main>
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Notification
|
||||
@style="success"
|
||||
@heading={{t "auth.titles.flow-success"}}
|
||||
>
|
||||
{{t "auth.descriptions.flow-success"}}
|
||||
</Rose::Notification>
|
||||
</Rose::Layout::Centered>
|
||||
</main>
|
||||
@@ -0,0 +1,20 @@
|
||||
{{page-title (t "auth.titles.flow-error")}}
|
||||
|
||||
<main>
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Notification
|
||||
@style="error"
|
||||
@heading={{t "auth.titles.flow-error"}}
|
||||
>
|
||||
{{t "auth.descriptions.flow-error"}}
|
||||
<br>
|
||||
<Copyable
|
||||
@text={{@model}}
|
||||
@buttonText={{t "actions.copy-error-detail-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
{{t "actions.copy-error-detail-to-clipboard"}}
|
||||
</Copyable>
|
||||
</Rose::Notification>
|
||||
</Rose::Layout::Centered>
|
||||
</main>
|
||||
@@ -1,4 +1,22 @@
|
||||
<Rose::Dropdown @text="{{t 'actions.manage'}}" @dropdownRight={{true}} as |dropdown|>
|
||||
{{#if (feature-flag 'primary-auth-method')}}
|
||||
{{#unless @model.isPrimary}}
|
||||
<dropdown.button
|
||||
@disabled={{@model.canSave}}
|
||||
{{on "click" (route-action "makePrimary" @model)}}
|
||||
>
|
||||
{{t 'resources.auth-method.actions.make-primary'}}
|
||||
</dropdown.button>
|
||||
{{/unless}}
|
||||
{{#if @model.isPrimary}}
|
||||
<dropdown.button
|
||||
@disabled={{@model.canSave}}
|
||||
{{on "click" (route-action "removeAsPrimary" @model)}}
|
||||
>
|
||||
{{t 'resources.auth-method.actions.remove-as-primary'}}
|
||||
</dropdown.button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<dropdown.link
|
||||
@route="scopes.scope.auth-methods.auth-method.accounts.new"
|
||||
@model={{@model}}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
<h2>
|
||||
{{t "resources.auth-method.title"}}
|
||||
{{#if (feature-flag 'primary-auth-method')}}
|
||||
{{#if @model.isPrimary}}
|
||||
|
||||
<Rose::Badge @style="success-outline">
|
||||
{{t 'states.primary'}}
|
||||
</Rose::Badge>
|
||||
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<DocLink @doc="auth-method" @iconSize="large" />
|
||||
</h2>
|
||||
<p>{{t "resources.auth-method.description"}}</p>
|
||||
|
||||
@@ -30,12 +30,27 @@
|
||||
<page.body>
|
||||
{{#if @model}}
|
||||
|
||||
{{#if (feature-flag 'primary-auth-method')}}
|
||||
{{#unless this.hasPrimaryAuthMethod}}
|
||||
<Rose::Notification
|
||||
@style="warning"
|
||||
@heading={{t "resources.auth-method.messages.no-primary-auth-method.title"}}
|
||||
>
|
||||
{{t "resources.auth-method.messages.no-primary-auth-method.description"}}
|
||||
</Rose::Notification>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell>{{t 'form.name.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.type.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.id.label'}}</row.headerCell>
|
||||
{{#if (feature-flag 'primary-auth-method')}}
|
||||
<row.cell></row.cell>
|
||||
<row.headerCell>{{t 'titles.actions'}}</row.headerCell>
|
||||
{{/if}}
|
||||
</header.row>
|
||||
</table.header>
|
||||
|
||||
@@ -43,12 +58,30 @@
|
||||
{{#each @model as |authMethod|}}
|
||||
<body.row as |row|>
|
||||
<row.headerCell>
|
||||
<LinkTo
|
||||
@route="scopes.scope.auth-methods.auth-method"
|
||||
@model={{authMethod.id}}
|
||||
>
|
||||
{{authMethod.displayName}}
|
||||
</LinkTo>
|
||||
{{!
|
||||
Show link to auth method route if non-OIDC and when
|
||||
oidc-crud feature is enabled, otherwise show just the name
|
||||
(when the method is OIDC and oidc-crud is disabled)
|
||||
}}
|
||||
{{#if authMethod.isOIDC}}
|
||||
{{#if (feature-flag 'oidc-crud')}}
|
||||
<LinkTo
|
||||
@route="scopes.scope.auth-methods.auth-method"
|
||||
@model={{authMethod.id}}
|
||||
>
|
||||
{{authMethod.displayName}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
{{authMethod.displayName}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<LinkTo
|
||||
@route="scopes.scope.auth-methods.auth-method"
|
||||
@model={{authMethod.id}}
|
||||
>
|
||||
{{authMethod.displayName}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
<p>{{authMethod.description}}</p>
|
||||
</row.headerCell>
|
||||
<row.cell>
|
||||
@@ -65,6 +98,39 @@
|
||||
<code>{{authMethod.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
{{#if (feature-flag 'primary-auth-method')}}
|
||||
<row.cell>
|
||||
{{#if authMethod.isPrimary}}
|
||||
<Rose::Badge @style="success-outline">
|
||||
{{t 'states.primary'}}
|
||||
</Rose::Badge>
|
||||
{{/if}}
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Dropdown
|
||||
@text={{t "actions.manage"}}
|
||||
@dropdownRight={{true}}
|
||||
@iconOnly={{true}}
|
||||
@showCaret={{false}}
|
||||
@icon="more-horizontal"
|
||||
as |dropdown|>
|
||||
{{#unless authMethod.isPrimary}}
|
||||
<dropdown.button
|
||||
{{on "click" (route-action "makePrimary" authMethod)}}
|
||||
>
|
||||
{{t 'resources.auth-method.actions.make-primary'}}
|
||||
</dropdown.button>
|
||||
{{/unless}}
|
||||
{{#if authMethod.isPrimary}}
|
||||
<dropdown.button
|
||||
{{on "click" (route-action "removeAsPrimary" authMethod)}}
|
||||
>
|
||||
{{t 'resources.auth-method.actions.remove-as-primary'}}
|
||||
</dropdown.button>
|
||||
{{/if}}
|
||||
</Rose::Dropdown>
|
||||
</row.cell>
|
||||
{{/if}}
|
||||
</body.row>
|
||||
{{/each}}
|
||||
</table.body>
|
||||
|
||||
@@ -3,58 +3,75 @@
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Layout::Page as |page|>
|
||||
|
||||
<page.header>
|
||||
<h2>{{t "actions.authenticate"}}</h2>
|
||||
<p>{{t "auth.descriptions.authenticating-with-org" name=@model.scope.displayName}}</p>
|
||||
</page.header>
|
||||
|
||||
<page.navigation>
|
||||
|
||||
<Rose::Dropdown
|
||||
@text={{t "actions.choose-different-org"}}
|
||||
@icon="user-organization"
|
||||
as |dropdown|>
|
||||
<dropdown.link @route="scopes.scope.authenticate" @model="global">
|
||||
{{t "titles.global"}}
|
||||
</dropdown.link>
|
||||
{{#each this.sortedScopes as |scope|}}
|
||||
<dropdown.link @route="scopes.scope.authenticate" @model={{scope.id}}>
|
||||
{{scope.displayName}}
|
||||
</dropdown.link>
|
||||
{{/each}}
|
||||
</Rose::Dropdown>
|
||||
|
||||
<Rose::Separator />
|
||||
|
||||
<Rose::Nav::Tabs as |nav|>
|
||||
{{#each @model.authMethods as |authMethod|}}
|
||||
<nav.link @route="scopes.scope.authenticate.method" @model={{authMethod.id}}>
|
||||
{{#if authMethod.name}}
|
||||
{{authMethod.name}}
|
||||
{{else}}
|
||||
{{t (concat "resources.auth-method.types." authMethod.type)}}
|
||||
{{/if}}
|
||||
</nav.link>
|
||||
{{/each}}
|
||||
</Rose::Nav::Tabs>
|
||||
</page.navigation>
|
||||
|
||||
<page.body>
|
||||
{{#if @model.authMethods}}
|
||||
{{outlet}}
|
||||
{{/if}}
|
||||
{{#unless @model.authMethods}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message
|
||||
@title={{t "resources.auth-method.messages.none.title"}}
|
||||
@icon="alert-circle-outline"
|
||||
as |message|>
|
||||
<message.description>
|
||||
{{t "resources.auth-method.messages.none.description"}}
|
||||
</message.description>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
|
||||
<Rose::Frame>
|
||||
<:header>
|
||||
<h2>{{t "actions.authenticate"}}</h2>
|
||||
</:header>
|
||||
<:body>
|
||||
<p>{{t "auth.descriptions.authenticating-with-org" name=@model.scope.displayName}}</p>
|
||||
<Rose::Dropdown
|
||||
@text={{t "actions.choose-different-org"}}
|
||||
@icon="user-organization"
|
||||
as |dropdown|>
|
||||
<dropdown.link @route="scopes.scope.authenticate" @model="global">
|
||||
{{t "titles.global"}}
|
||||
</dropdown.link>
|
||||
{{#each this.sortedScopes as |scope|}}
|
||||
<dropdown.link @route="scopes.scope.authenticate" @model={{scope.id}}>
|
||||
{{scope.displayName}}
|
||||
</dropdown.link>
|
||||
{{/each}}
|
||||
</Rose::Dropdown>
|
||||
|
||||
<Rose::Separator />
|
||||
|
||||
<Rose::Nav::Tabs as |nav|>
|
||||
{{#if (feature-flag 'oidc')}}
|
||||
{{! OIDC enable: list all auth methods }}
|
||||
{{#each @model.authMethods as |authMethod|}}
|
||||
<nav.link @route="scopes.scope.authenticate.method" @model={{authMethod.id}}>
|
||||
{{#if authMethod.name}}
|
||||
{{authMethod.name}}
|
||||
{{else}}
|
||||
{{t (concat "resources.auth-method.types." authMethod.type)}}
|
||||
{{/if}}
|
||||
</nav.link>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{! OIDC disabled: filter out OIDC auth methods}}
|
||||
{{#each @model.authMethods as |authMethod|}}
|
||||
{{#unless authMethod.isOIDC}}
|
||||
<nav.link @route="scopes.scope.authenticate.method" @model={{authMethod.id}}>
|
||||
{{#if authMethod.name}}
|
||||
{{authMethod.name}}
|
||||
{{else}}
|
||||
{{t (concat "resources.auth-method.types." authMethod.type)}}
|
||||
{{/if}}
|
||||
</nav.link>
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</Rose::Nav::Tabs>
|
||||
|
||||
{{#if @model.authMethods}}
|
||||
{{outlet}}
|
||||
{{/if}}
|
||||
{{#unless @model.authMethods}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message
|
||||
@title={{t "resources.auth-method.messages.none.title"}}
|
||||
@icon="alert-circle-outline"
|
||||
as |message|>
|
||||
<message.description>
|
||||
{{t "resources.auth-method.messages.none.description"}}
|
||||
</message.description>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
</:body>
|
||||
</Rose::Frame>
|
||||
</page.body>
|
||||
|
||||
</Rose::Layout::Page>
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
<Form::Authenticate
|
||||
@model={{@model}}
|
||||
@submit={{route-action "authenticate"}}
|
||||
@disabled={{is-loading}}
|
||||
/>
|
||||
{{outlet}}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{#if (feature-flag 'oidc')}}
|
||||
{{! OIDC enabled: show form for any auth method}}
|
||||
<Form::Authenticate
|
||||
@model={{@model}}
|
||||
@submit={{route-action "authenticate"}}
|
||||
@disabled={{is-loading}}
|
||||
/>
|
||||
{{else}}
|
||||
{{! OIDC disabled: hide OIDC auth form}}
|
||||
{{#unless @model.isOIDC}}
|
||||
<Form::Authenticate
|
||||
@model={{@model}}
|
||||
@submit={{route-action "authenticate"}}
|
||||
@disabled={{is-loading}}
|
||||
/>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Rose::Dialog::Cover>
|
||||
<:header>
|
||||
{{inline-svg "logo-app-full"}}
|
||||
<h1>{{t 'resources.auth-method.messages.pending.title'}}</h1>
|
||||
</:header>
|
||||
<:body>
|
||||
<p>{{t 'resources.auth-method.messages.pending.description'}}</p>
|
||||
<p>
|
||||
{{t 'resources.auth-method.questions.no-see-window'}}
|
||||
<a
|
||||
href="{{this.authURL}}"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{{t 'actions.retry'}}
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<LinkTo @route="scopes.scope.authenticate.method">
|
||||
{{t 'actions.cancel'}}
|
||||
</LinkTo>
|
||||
</p>
|
||||
</:body>
|
||||
</Rose::Dialog::Cover>
|
||||
@@ -36,6 +36,7 @@ module.exports = function (environment) {
|
||||
|
||||
notifyTimeout: 4000,
|
||||
sessionPollingTimeoutSeconds: 2.5,
|
||||
oidcPollingTimeoutSeconds: 1,
|
||||
|
||||
documentation: {
|
||||
baseURL: 'https://boundaryproject.io/help/admin-ui',
|
||||
@@ -83,6 +84,16 @@ module.exports = function (environment) {
|
||||
'ember-cli-mirage': {
|
||||
directory: '../../addons/api/mirage',
|
||||
},
|
||||
|
||||
featureFlags: {
|
||||
oidc: true,
|
||||
'oidc-crud': false,
|
||||
'primary-auth-method': true,
|
||||
search: false,
|
||||
filter: false,
|
||||
capabilities: false,
|
||||
'credential-store': false,
|
||||
},
|
||||
};
|
||||
|
||||
// Unsafe policy is necessary in development and test environments, but should
|
||||
@@ -115,6 +126,11 @@ module.exports = function (environment) {
|
||||
// rather than automatically include API_HOST. Changes to CSP should
|
||||
// be explicit.
|
||||
if (API_HOST) ENV.contentSecurityPolicy['connect-src'].push(API_HOST);
|
||||
|
||||
// Enable features in development
|
||||
ENV.featureFlags['primary-auth-method'] = true;
|
||||
ENV.featureFlags['oidc'] = true;
|
||||
//ENV.featureFlags['oidc-crud'] = true;
|
||||
}
|
||||
|
||||
if (environment === 'test') {
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
"test:ember": "COVERAGE=true ember exam --random",
|
||||
"posttest": "ember coverage-merge && yarn clean:coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"ember-named-blocks-polyfill": "^0.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.6",
|
||||
"@ember/optional-features": "^2.0.0",
|
||||
@@ -77,7 +80,7 @@
|
||||
"eslint-plugin-ember": "^10.1.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"faker": "^5.1.0",
|
||||
"faker": "^5.5.3",
|
||||
"loader.js": "^4.7.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.2.1",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
// //currentSession,
|
||||
// //invalidateSession,
|
||||
} from 'ember-simple-auth/test-support';
|
||||
import { enableFeature } from 'ember-feature-flags/test-support';
|
||||
|
||||
module('Acceptance | auth methods', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
@@ -270,4 +271,118 @@ module('Acceptance | auth methods', function (hooks) {
|
||||
'Displays field-level errors.'
|
||||
);
|
||||
});
|
||||
|
||||
test('user can make primary an auth method', async function (assert) {
|
||||
assert.expect(2);
|
||||
assert.notOk(
|
||||
instances.scopes.org.primaryAuthMethodId,
|
||||
'Primary auth method is not yet set.'
|
||||
);
|
||||
enableFeature('primary-auth-method');
|
||||
await visit(urls.authMethod);
|
||||
await click(
|
||||
'.rose-layout-page-actions .rose-dropdown-content [type="button"]:first-child'
|
||||
);
|
||||
const scope = this.server.schema.scopes.find(instances.scopes.org.id);
|
||||
assert.equal(
|
||||
scope.primaryAuthMethodId,
|
||||
instances.authMethod.id,
|
||||
'Primary auth method is set.'
|
||||
);
|
||||
});
|
||||
|
||||
test('user is notified of error on make primary an auth method', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.server.patch('/scopes/:id', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
code: 'fail',
|
||||
message: 'Sorry!',
|
||||
}
|
||||
);
|
||||
});
|
||||
assert.notOk(
|
||||
instances.scopes.org.primaryAuthMethodId,
|
||||
'Primary auth method is not yet set.'
|
||||
);
|
||||
enableFeature('primary-auth-method');
|
||||
await visit(urls.authMethod);
|
||||
await click(
|
||||
'.rose-layout-page-actions .rose-dropdown-content [type="button"]:first-child'
|
||||
);
|
||||
assert.ok(find('.rose-notification'));
|
||||
});
|
||||
|
||||
test('user can remove as primary an auth method', async function (assert) {
|
||||
assert.expect(2);
|
||||
instances.scopes.org.update({
|
||||
primaryAuthMethodId: instances.authMethod.id,
|
||||
});
|
||||
assert.ok(
|
||||
instances.scopes.org.primaryAuthMethodId,
|
||||
'Primary auth method is set.'
|
||||
);
|
||||
enableFeature('primary-auth-method');
|
||||
await visit(urls.authMethod);
|
||||
await click(
|
||||
'.rose-layout-page-actions .rose-dropdown-content [type="button"]:first-child'
|
||||
);
|
||||
const scope = this.server.schema.scopes.find(instances.scopes.org.id);
|
||||
assert.notOk(scope.primaryAuthMethodId, 'Primary auth method is unset.');
|
||||
});
|
||||
|
||||
test('user is notified of error on remove as primary an auth method', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.server.patch('/scopes/:id', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
code: 'fail',
|
||||
message: 'Sorry!',
|
||||
}
|
||||
);
|
||||
});
|
||||
instances.scopes.org.update({
|
||||
primaryAuthMethodId: instances.authMethod.id,
|
||||
});
|
||||
assert.ok(
|
||||
instances.scopes.org.primaryAuthMethodId,
|
||||
'Primary auth method is set.'
|
||||
);
|
||||
enableFeature('primary-auth-method');
|
||||
await visit(urls.authMethod);
|
||||
await click(
|
||||
'.rose-layout-page-actions .rose-dropdown-content [type="button"]:first-child'
|
||||
);
|
||||
assert.ok(find('.rose-notification'));
|
||||
});
|
||||
|
||||
test('user can make and remove primary auth methods from index', async function (assert) {
|
||||
assert.expect(3);
|
||||
assert.notOk(
|
||||
instances.scopes.org.primaryAuthMethodId,
|
||||
'Primary auth method is not yet set.'
|
||||
);
|
||||
enableFeature('primary-auth-method');
|
||||
await visit(urls.authMethods);
|
||||
await click(
|
||||
'.rose-table-body .rose-table-row:first-child .rose-dropdown-content [type="button"]:first-child'
|
||||
);
|
||||
let scope = this.server.schema.scopes.find(instances.scopes.org.id);
|
||||
assert.equal(
|
||||
scope.primaryAuthMethodId,
|
||||
instances.authMethod.id,
|
||||
'Primary auth method is set.'
|
||||
);
|
||||
await click(
|
||||
'.rose-table-body .rose-table-row:first-child .rose-dropdown-content [type="button"]:first-child'
|
||||
);
|
||||
scope = this.server.schema.scopes.find(instances.scopes.org.id);
|
||||
assert.notOk(scope.primaryAuthMethodId, 'Primary auth method is unset.');
|
||||
});
|
||||
});
|
||||
@@ -66,9 +66,12 @@ module('Acceptance | authentication', function (hooks) {
|
||||
scope = { id: orgScope.id, type: orgScope.type };
|
||||
globalAuthMethod = this.server.create('auth-method', {
|
||||
scope: globalScope,
|
||||
type: 'password',
|
||||
});
|
||||
authMethod = this.server.create('auth-method', {
|
||||
scope: orgScope,
|
||||
type: 'password',
|
||||
});
|
||||
this.server.create('user', { scope: globalScope });
|
||||
authMethod = this.server.create('auth-method', { scope: orgScope });
|
||||
orgScopeID = orgScope.id;
|
||||
globalAuthMethodID = globalAuthMethod.id;
|
||||
authMethodID = authMethod.id;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Controller | scopes/scope/auth-methods/index', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup(
|
||||
'controller:scopes/scope/auth-methods/index'
|
||||
);
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module(
|
||||
'Unit | Controller | scopes/scope/authenticate/method/oidc',
|
||||
function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup(
|
||||
'controller:scopes/scope/authenticate/method/oidc'
|
||||
);
|
||||
assert.ok(controller);
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | authentication-complete', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup('route:authentication-complete');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | authentication-error', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup('route:authentication-error');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module(
|
||||
'Unit | Route | scopes/scope/authenticate/method/index',
|
||||
function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup(
|
||||
'route:scopes/scope/authenticate/method/index'
|
||||
);
|
||||
assert.ok(route);
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module(
|
||||
'Unit | Route | scopes/scope/authenticate/method/oidc',
|
||||
function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup(
|
||||
'route:scopes/scope/authenticate/method/oidc'
|
||||
);
|
||||
assert.ok(route);
|
||||
});
|
||||
}
|
||||
);
|
||||
Loaded 100 of 141 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user