From 3130831ae0171f50c8a96d6ba7e62b9314908f87 Mon Sep 17 00:00:00 2001 From: Zhihe Li Date: Fri, 26 Aug 2022 11:50:53 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20create=20worker=20?= =?UTF-8?q?page=20(#1244)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 Add create worker page ✅ Closes: ICU-5246 --- addons/api/addon/models/worker.js | 5 +- addons/api/addon/serializers/worker.js | 2 +- addons/api/mirage/config.js | 10 ++ addons/api/tests/unit/models/worker-test.js | 18 ++- .../api/tests/unit/serializers/worker-test.js | 7 +- addons/core/translations/resources/en-us.yaml | 35 ++++- .../rose/components/code-editor/_index.scss | 3 +- .../styles/rose/components/layout/_page.scss | 1 + .../form/worker/create-worker-led/index.hbs | 127 ++++++++++++++++++ .../form/worker/create-worker-led/index.js | 68 ++++++++++ ui/admin/app/routes/scopes/scope/workers.js | 15 +-- .../app/routes/scopes/scope/workers/index.js | 33 +++-- .../app/routes/scopes/scope/workers/new.js | 29 +++- ui/admin/app/styles/app.scss | 57 ++++++++ .../templates/scopes/scope/workers/index.hbs | 45 ++++--- .../templates/scopes/scope/workers/new.hbs | 23 +++- ui/admin/config/environment.js | 2 +- ui/admin/ember-cli-build.js | 3 + .../tests/acceptance/workers/create-test.js | 86 ++++++++++++ .../tests/acceptance/workers/list-test.js | 4 +- 20 files changed, 507 insertions(+), 66 deletions(-) create mode 100644 ui/admin/app/components/form/worker/create-worker-led/index.hbs create mode 100644 ui/admin/app/components/form/worker/create-worker-led/index.js create mode 100644 ui/admin/tests/acceptance/workers/create-test.js diff --git a/addons/api/addon/models/worker.js b/addons/api/addon/models/worker.js index e08909020..a210ff597 100644 --- a/addons/api/addon/models/worker.js +++ b/addons/api/addon/models/worker.js @@ -37,12 +37,13 @@ export default class WorkerModel extends GeneratedWorkerModel { config_tags; /** - * Method to modify the adpater to handle custom POST route for creating worker. + * Method to modify the adapter to handle custom POST route for creating worker. + * @param {string} workerGeneratedAuthToken * @param {object} options * @param {object} options.adapterOptions * @return {Promise} */ - addWorkerLed(workerGeneratedAuthToken, options = { adapterOptions: {} }) { + createWorkerLed(workerGeneratedAuthToken, options = { adapterOptions: {} }) { const defaultAdapterOptions = { method: 'create:worker-led', workerGeneratedAuthToken, diff --git a/addons/api/addon/serializers/worker.js b/addons/api/addon/serializers/worker.js index 54f8099a2..f7545cc39 100644 --- a/addons/api/addon/serializers/worker.js +++ b/addons/api/addon/serializers/worker.js @@ -30,7 +30,7 @@ export default class WorkerSerializer extends ApplicationSerializer { */ serializeWithGeneratedToken(snapshot, workerGeneratedAuthToken) { return { - version: snapshot.attr('version'), + scope_id: snapshot.attr('scope').scope_id, worker_generated_auth_token: workerGeneratedAuthToken, }; } diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js index 33b310494..a200b4260 100644 --- a/addons/api/mirage/config.js +++ b/addons/api/mirage/config.js @@ -645,6 +645,16 @@ export default function () { this.get('/workers/:id'); this.del('/workers/:id'); this.patch('/workers/:id'); + this.post('/workers:create:worker-led', ({ workers, scopes }) => { + const globalScope = scopes.find('global'); + + // This POST only takes in a token so we need to generate a random worker to return + const newWorker = this.create('worker', { + type: 'pki', + scope: globalScope, + }); + return workers.create(newWorker.attrs); + }); /* Uncomment the following line and the Response import above * Then change the response code to simulate error responses. diff --git a/addons/api/tests/unit/models/worker-test.js b/addons/api/tests/unit/models/worker-test.js index 9e0bd5292..a43d2cabb 100644 --- a/addons/api/tests/unit/models/worker-test.js +++ b/addons/api/tests/unit/models/worker-test.js @@ -6,19 +6,29 @@ module('Unit | Model | worker', function (hooks) { setupTest(hooks); setupMirage(hooks); - test('it has an `addWorkerLed` method that targets a specific POST API', async function (assert) { + test('it has a `createWorkerLed` method that targets a specific POST API', async function (assert) { assert.expect(2); + + const scopeId = 'global'; + const workerGeneratedAuthToken = 'token'; this.server.post('/v1/workers:create:worker-led', (schema, request) => { const body = JSON.parse(request.requestBody); assert.deepEqual(body, { - worker_generated_auth_token: 'token', + scope_id: scopeId, + worker_generated_auth_token: workerGeneratedAuthToken, }); return { id: '123abc' }; }); const store = this.owner.lookup('service:store'); - const model = store.createRecord('worker', { type: 'pki' }); - await model.addWorkerLed('token'); + const model = store.createRecord('worker', { + type: 'pki', + scope: { + scope_id: scopeId, + type: 'global', + }, + }); + await model.createWorkerLed(workerGeneratedAuthToken); const worker = store.peekRecord('worker', '123abc'); assert.ok(worker); }); diff --git a/addons/api/tests/unit/serializers/worker-test.js b/addons/api/tests/unit/serializers/worker-test.js index 5b93be295..5bfdeb000 100644 --- a/addons/api/tests/unit/serializers/worker-test.js +++ b/addons/api/tests/unit/serializers/worker-test.js @@ -23,12 +23,17 @@ module('Unit | Serializer | worker', function (hooks) { }); test('it serializes using `adapterOptions.workerGeneratedAuthToken`', function (assert) { + const scopeId = 'global'; const store = this.owner.lookup('service:store'); const serializer = store.serializerFor('worker'); const record = store.createRecord('worker', { name: 'worker', description: 'Description', version: 1, + scope: { + scope_id: scopeId, + type: 'global', + }, }); const snapshot = record._createSnapshot(); snapshot.adapterOptions = { @@ -36,7 +41,7 @@ module('Unit | Serializer | worker', function (hooks) { }; const serializedRecord = serializer.serialize(snapshot); assert.deepEqual(serializedRecord, { - version: 1, + scope_id: scopeId, worker_generated_auth_token: '123-abc', }); }); diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index d8dca572b..80ce5e0fa 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -26,7 +26,7 @@ auth-method: title: No Auth Methods description: No auth methods were found for this scope. no-primary-auth-method: - title: "Recommendation: Select a 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 @@ -52,7 +52,7 @@ auth-method: 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." + 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: @@ -436,9 +436,40 @@ worker: title: Worker title_plural: Workers description: Workers are the server components that perform the session handling. + actions: + register: Register Worker messages: welcome: title: Welcome to Workers table: session_count: Session Count ip_address: IP Address + form: + cluster_id: + label: Boundary Cluster ID + help: The cluster ID is the UUID in the HCP Boundary Controller URL + ip_address: + label: IP Address + help: The public IP address of the server + config_file_path: + label: Config file path + help: The path on your server where you will store your configuration file + worker_tags: + label: Worker Tags + help: Boundary can use tags to define key-value pairs which targets can use to determine where they should route connections + steps: + 1: + title: 1. Create a config file + create_directory: Create a configuration file in a new directory on your server + create_config: 'Open the file with a text editor, such as Vi. Paste the following configuration into the worker config file:' + save_config: Save the configuration in your `pki-worker.hcl` file. + 2: + title: 2. Install & start Boundary on your server + run_command: 'Run this command on your server to install Boundary:' + copy_registration_request: When the server starts, it will print details related to this worker. Copy the value for + 3: + title: 3. Confirm the worker when they connect + worker_auth_registration_request: + label: Worker Auth Registration Request + help: The registration key provided by this worker when it started + registered: Registered diff --git a/addons/rose/addon/styles/rose/components/code-editor/_index.scss b/addons/rose/addon/styles/rose/components/code-editor/_index.scss index 962da8eff..2c6fb86b9 100644 --- a/addons/rose/addon/styles/rose/components/code-editor/_index.scss +++ b/addons/rose/addon/styles/rose/components/code-editor/_index.scss @@ -31,8 +31,6 @@ display: flex; justify-content: space-between; align-items: center; - background-color: var(--ui-gray-subtler-6); - border-top: 1px solid var(--ui-gray-subtler-3); border-bottom: 1px solid var(--ui-gray-subtler-3); height: 2.75rem; @@ -72,6 +70,7 @@ margin-right: sizing.rems(xs); margin-left: sizing.rems(s); height: sizing.rems(xl); + white-space: nowrap; &-icon { margin-right: sizing.rems(xs); diff --git a/addons/rose/addon/styles/rose/components/layout/_page.scss b/addons/rose/addon/styles/rose/components/layout/_page.scss index 568e1b776..b6abb4efc 100644 --- a/addons/rose/addon/styles/rose/components/layout/_page.scss +++ b/addons/rose/addon/styles/rose/components/layout/_page.scss @@ -32,6 +32,7 @@ .rose-layout-page-body { grid-area: body; + width: 100%; } } diff --git a/ui/admin/app/components/form/worker/create-worker-led/index.hbs b/ui/admin/app/components/form/worker/create-worker-led/index.hbs new file mode 100644 index 000000000..6f12393f0 --- /dev/null +++ b/ui/admin/app/components/form/worker/create-worker-led/index.hbs @@ -0,0 +1,127 @@ + + + + + + + + + +

{{t 'resources.worker.form.steps.1.title'}}

+
+ + + <:action> + {{t 'resources.worker.form.steps.1.create_directory'}} + + + + + + + + <:action> + {{t 'resources.worker.form.steps.1.create_config'}} + + + + +

{{t 'resources.worker.form.steps.1.save_config'}}

+
+ +

{{t 'resources.worker.form.steps.2.title'}}

+
+ + + <:action> + {{t 'resources.worker.form.steps.2.run_command'}} + + + + + {{t 'resources.worker.form.steps.2.copy_registration_request'}} + {{t + 'resources.worker.form.steps.3.worker_auth_registration_request.label' + }} + +
+ +

{{t 'resources.worker.form.steps.3.title'}}

+
+
+ + {{#if (can 'save worker' @model)}} + + {{t 'resources.worker.actions.register'}} + + {{/if}} +
+ + {{#if @model.cannotSave}} +
+
+ +

{{this.generatedWorkerAuthToken}}

+
+
+ +

{{t 'resources.worker.form.steps.3.registered'}}

+
+
+ {{/if}} +
+ + + {{t 'actions.done'}} + +
\ No newline at end of file diff --git a/ui/admin/app/components/form/worker/create-worker-led/index.js b/ui/admin/app/components/form/worker/create-worker-led/index.js new file mode 100644 index 000000000..501fe2846 --- /dev/null +++ b/ui/admin/app/components/form/worker/create-worker-led/index.js @@ -0,0 +1,68 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +export default class FormWorkerCreateWorkerLedComponent extends Component { + // =attributes + + @tracked generatedWorkerAuthToken; + @tracked clusterId; + @tracked ipAddress; + @tracked configFilePath; + @tracked workerTags; + + get createConfigText() { + return `mkdir ${this.configFilePath || ''}/ ;\\ +touch ${this.configFilePath || ''}/pki-worker.hcl`; + } + + get workerConfigText() { + return `hcp_boundary_cluster_id = "${this.clusterId || ''}" + +listener "tcp" { + address = "0.0.0.0:9202" + purpose = "proxy" +} + +worker { + public_addr = "${this.ipAddress || ''}" + auth_storage_path = "${this.configFilePath || ''}/worker1" + tags { + type = [${ + this.workerTags + ? this.workerTags + .split(',') + .filter((tag) => tag.trim()) + .map((tag) => `"${tag?.trim()}"`) + .join(', ') + : '"", ""' + }] + } +}`; + } + + get installBoundaryText() { + return `curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - ;\\ +sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" ;\\ +sudo apt-get update && sudo apt-get install boundary ;\\ +boundary server -config="${this.configFilePath || ''}/pki-worker.hcl"`; + } + + get shellCodeEditor() { + return { + mode: 'shell', + readOnly: true, + lineNumbers: false, + cursorBlinkRate: -1, + styleActiveLine: false, + }; + } + + get hclCodeEditor() { + return { + mode: 'hcl', + readOnly: true, + cursorBlinkRate: -1, + styleActiveLine: false, + }; + } +} diff --git a/ui/admin/app/routes/scopes/scope/workers.js b/ui/admin/app/routes/scopes/scope/workers.js index f59453172..4d629acc5 100644 --- a/ui/admin/app/routes/scopes/scope/workers.js +++ b/ui/admin/app/routes/scopes/scope/workers.js @@ -5,7 +5,6 @@ import { action } from '@ember/object'; export default class ScopesScopeWorkersRoute extends Route { // =services - @service can; @service session; @service router; @@ -18,23 +17,11 @@ export default class ScopesScopeWorkersRoute extends Route { if (!this.session.isAuthenticated) this.router.transitionTo('index'); } - /** - * Load all workers. - * @return {WorkerModel} - */ - model() { - const scope = this.modelFor('scopes.scope'); - const { id: scope_id } = scope; - if (this.can.can('list worker', scope, { collection: 'workers' })) { - return this.store.query('worker', { scope_id }); - } - } - /** * Refreshes worker data. */ @action - refreshWorkers() { + refresh() { return super.refresh(...arguments); } } diff --git a/ui/admin/app/routes/scopes/scope/workers/index.js b/ui/admin/app/routes/scopes/scope/workers/index.js index 6d65b459b..56a333f92 100644 --- a/ui/admin/app/routes/scopes/scope/workers/index.js +++ b/ui/admin/app/routes/scopes/scope/workers/index.js @@ -1,16 +1,19 @@ import Route from '@ember/routing/route'; import { action } from '@ember/object'; import { resourceFilter } from 'core/decorators/resource-filter'; +import { inject as service } from '@ember/service'; export default class ScopesScopeWorkersIndexRoute extends Route { // =attributes + @service can; @resourceFilter({ allowed: (route) => { const configTags = route - .modelFor('scopes.scope.workers') + .modelFor('scopes.scope.workers.index') .toArray() - .flatMap((worker) => worker.config_tags.type); + .flatMap((worker) => worker.config_tags?.type) + .filter(Boolean); // Filter out duplicate tags return [...new Set(configTags)]; @@ -21,18 +24,24 @@ export default class ScopesScopeWorkersIndexRoute extends Route { // =methods model() { - const workers = this.modelFor('scopes.scope.workers'); + const scope = this.modelFor('scopes.scope'); + const { id: scope_id } = scope; + if (this.can.can('list worker', scope, { collection: 'workers' })) { + const workers = this.store.query('worker', { scope_id }); - if (this.tags?.length) { - // Return workers that have config tags that have at - // least one intersection with the filter tags - return workers.filter( - (worker) => - worker.config_tags.type.filter(Set.prototype.has, new Set(this.tags)) - .length > 0 - ); + if (this.tags?.length) { + // Return workers that have config tags that have at + // least one intersection with the filter tags + return workers.filter( + (worker) => + worker.config_tags?.type.filter( + Set.prototype.has, + new Set(this.tags) + ).length > 0 + ); + } + return workers; } - return workers; } /** diff --git a/ui/admin/app/routes/scopes/scope/workers/new.js b/ui/admin/app/routes/scopes/scope/workers/new.js index 876ce8a77..d925cb1e1 100644 --- a/ui/admin/app/routes/scopes/scope/workers/new.js +++ b/ui/admin/app/routes/scopes/scope/workers/new.js @@ -1,3 +1,30 @@ import Route from '@ember/routing/route'; +import { action } from '@ember/object'; +import { loading } from 'ember-loading'; +import { notifyError, notifySuccess } from 'core/decorators/notify'; -export default class ScopesScopeWorkersNewRoute extends Route {} +export default class ScopesScopeWorkersNewRoute extends Route { + // =methods + + /** + * Creates a new unsaved worker. + * @return {WorkerModel} + */ + model() { + const scopeModel = this.modelFor('scopes.scope'); + return this.store.createRecord('worker', { scopeModel }); + } + + /** + * Created the worker and notifies the user of success or error. + * @param {WorkerModel} worker + * @param {string} workerGeneratedAuthToken + */ + @action + @loading + @notifyError(({ message }) => message, { catch: true }) + @notifySuccess('notifications.add-success') + async createWorkerLed(worker, workerGeneratedAuthToken) { + await worker.createWorkerLed(workerGeneratedAuthToken); + } +} diff --git a/ui/admin/app/styles/app.scss b/ui/admin/app/styles/app.scss index 5dbef9350..adf9d611a 100644 --- a/ui/admin/app/styles/app.scss +++ b/ui/admin/app/styles/app.scss @@ -394,3 +394,60 @@ margin-top: sizing.rems(xs); } } + +.worker-create-section { + border: sizing.rems(xxxxs) solid var(--ui-gray-subtler-3); + border-radius: sizing.rems(xxs); + padding: sizing.rems(l); + margin-bottom: sizing.rems(xl); + + > div { + margin-bottom: sizing.rems(l); + } + + > code { + color: var(--failure); + background-color: var(--ui-gray-subtler-5); + border: sizing.rems(xxxxs) solid var(--ui-gray-subtler-2); + } + + .worker-auth-token { + display: flex; + gap: sizing.rems(m); + align-items: flex-end; + } + + .worker-success { + display: flex; + align-items: center; + justify-content: space-between; + + > div { + display: flex; + gap: sizing.rems(xs); + align-items: center; + p { + margin-bottom: sizing.rems(xxs); + } + } + + > div:first-child { + width: 40%; + p { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + > div:last-child { + p { + color: var(--success); + } + } + + .rose-icon { + color: var(--success); + } + } +} diff --git a/ui/admin/app/templates/scopes/scope/workers/index.hbs b/ui/admin/app/templates/scopes/scope/workers/index.hbs index f750cb703..d583ed1d8 100644 --- a/ui/admin/app/templates/scopes/scope/workers/index.hbs +++ b/ui/admin/app/templates/scopes/scope/workers/index.hbs @@ -30,29 +30,30 @@ (resource-filter 'scopes.scope.workers.index' 'tags') as |filter| }} - - - - - - - + + + + + + + {{/if}} {{/with}} - {{#if (has-resource-filter-selections 'scopes.scope.workers.index' 'tags' @@ -69,7 +70,7 @@ - + diff --git a/ui/admin/app/templates/scopes/scope/workers/new.hbs b/ui/admin/app/templates/scopes/scope/workers/new.hbs index 1110d4e5f..48492b7b5 100644 --- a/ui/admin/app/templates/scopes/scope/workers/new.hbs +++ b/ui/admin/app/templates/scopes/scope/workers/new.hbs @@ -1,2 +1,23 @@ {{page-title (t 'titles.new')}} -{{outlet}} \ No newline at end of file + + + + + + + + +

+ {{t 'titles.new'}} + +

+
+ + + + + +
\ No newline at end of file diff --git a/ui/admin/config/environment.js b/ui/admin/config/environment.js index 95417a4b2..b58ae8e27 100644 --- a/ui/admin/config/environment.js +++ b/ui/admin/config/environment.js @@ -93,7 +93,7 @@ module.exports = function (environment) { 'api-client.cli': '/api-client/cli', 'api-client.api': '/api-client/api', worker: '/workers', - 'worker.new': '/workers/new', + 'worker.manage-workers': '/workers/manage-workers-on-hcp', }, }, diff --git a/ui/admin/ember-cli-build.js b/ui/admin/ember-cli-build.js index a7b391110..475c97184 100644 --- a/ui/admin/ember-cli-build.js +++ b/ui/admin/ember-cli-build.js @@ -34,6 +34,9 @@ module.exports = function (defaults) { app.import('node_modules/clipboard/dist/clipboard.min.js'); } + app.import('node_modules/codemirror/lib/codemirror.css'); + app.import('node_modules/codemirror/theme/monokai.css'); + // Use `app.import` to add additional libraries to the generated // output files. // diff --git a/ui/admin/tests/acceptance/workers/create-test.js b/ui/admin/tests/acceptance/workers/create-test.js new file mode 100644 index 000000000..667e7a853 --- /dev/null +++ b/ui/admin/tests/acceptance/workers/create-test.js @@ -0,0 +1,86 @@ +import { module, test } from 'qunit'; +import { visit, fillIn, click, find } from '@ember/test-helpers'; +import { setupApplicationTest } from 'ember-qunit'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { Response } from 'miragejs'; +import { + authenticateSession, + // These are left here intentionally for future reference. + //currentSession, + //invalidateSession, +} from 'ember-simple-auth/test-support'; + +module('Acceptance | workers | create', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + let globalScope; + let workersURL; + let newWorkerURL; + let getWorkersCount; + + hooks.beforeEach(function () { + globalScope = this.server.create('scope', { id: 'global' }); + + workersURL = `/scopes/global/workers`; + newWorkerURL = `${workersURL}/new`; + getWorkersCount = () => this.server.schema.workers.all().length; + + authenticateSession({}); + }); + + test('can create new workers', async function (assert) { + assert.expect(1); + const workersCount = getWorkersCount(); + await visit(newWorkerURL); + await fillIn('[name="worker_auth_registration_request"]', 'token'); + await click('[type="submit"]'); + assert.strictEqual(getWorkersCount(), workersCount + 1); + }); + + test('Users can navigate to new workers route with proper authorization', async function (assert) { + assert.expect(2); + await visit(newWorkerURL); + assert.ok( + globalScope.authorized_collection_actions.workers.includes( + 'create:worker-led' + ) + ); + assert.dom(`[href="${newWorkerURL}"]`).isVisible(); + }); + + test('Users cannot navigate to new workers route without proper authorization', async function (assert) { + assert.expect(2); + globalScope.authorized_collection_actions.workers = []; + await visit(workersURL); + assert.notOk( + globalScope.authorized_collection_actions.users.includes( + 'create:worker-led' + ) + ); + assert.dom(`[href="${newWorkerURL}"]`).isNotVisible(); + }); + + test('saving a new user with invalid fields displays error messages', async function (assert) { + assert.expect(1); + this.server.post('/workers:create:worker-led', () => { + return new Response( + 500, + {}, + { + status: 500, + code: 'api_error', + message: 'rpc error: code = Unknown', + } + ); + }); + await visit(newWorkerURL); + await fillIn('[name="worker_auth_registration_request"]', 'token'); + await click('[type="submit"]'); + assert.strictEqual( + find('.rose-notification-body').textContent.trim(), + 'rpc error: code = Unknown', + 'Displays primary error message.' + ); + }); +}); diff --git a/ui/admin/tests/acceptance/workers/list-test.js b/ui/admin/tests/acceptance/workers/list-test.js index 0f7c5976d..46e76ef30 100644 --- a/ui/admin/tests/acceptance/workers/list-test.js +++ b/ui/admin/tests/acceptance/workers/list-test.js @@ -26,9 +26,7 @@ module('Acceptance | workers | list', function (hooks) { hooks.beforeEach(function () { instances.scopes.global = this.server.create('scope', { id: 'global' }); - instances.user = this.server.create('user', { - scope: instances.scopes.global, - }); + urls.globalScope = `/scopes/global/scopes`; urls.workers = `/scopes/global/workers`; authenticateSession({});