diff --git a/addons/api/addon/generated/models/host.js b/addons/api/addon/generated/models/host.js index 929f9c934..221a629a3 100644 --- a/addons/api/addon/generated/models/host.js +++ b/addons/api/addon/generated/models/host.js @@ -54,4 +54,10 @@ export default class GeneratedHostModel extends BaseModel { readOnly: true, }) plugin; + + @attr({ + emptyArrayIfMissing: true, + readOnly: true, + }) + host_set_ids; } diff --git a/addons/api/mirage/factories/auth-method.js b/addons/api/mirage/factories/auth-method.js index 46ac67090..f8b38fc13 100644 --- a/addons/api/mirage/factories/auth-method.js +++ b/addons/api/mirage/factories/auth-method.js @@ -9,6 +9,7 @@ import permissions from '../helpers/permissions'; import generateId from '../helpers/id'; import { + TYPE_AUTH_METHOD_PASSWORD, TYPE_AUTH_METHOD_OIDC, TYPE_AUTH_METHOD_LDAP, TYPES_AUTH_METHOD, @@ -23,11 +24,17 @@ export default factory.extend({ 'delete', 'authenticate', ], - authorized_collection_actions: () => { - return { - accounts: ['create', 'list'], - 'managed-groups': ['create', 'list'], - }; + authorized_collection_actions() { + switch (this.type) { + case TYPE_AUTH_METHOD_PASSWORD: + return { accounts: ['create', 'list'] }; + case TYPE_AUTH_METHOD_LDAP: + case TYPE_AUTH_METHOD_OIDC: + return { + accounts: ['create', 'list'], + 'managed-groups': ['create', 'list'], + }; + } }, id: () => generateId('am_'), diff --git a/addons/api/mirage/factories/host-catalog.js b/addons/api/mirage/factories/host-catalog.js index efb485446..caf8410e8 100644 --- a/addons/api/mirage/factories/host-catalog.js +++ b/addons/api/mirage/factories/host-catalog.js @@ -85,7 +85,16 @@ export default factory.extend({ afterCreate(hostCatalog, server) { const { scope, type } = hostCatalog; const hosts = server.createList('host', 10, { scope, hostCatalog, type }); - server.createList('host-set', 3, { scope, hostCatalog, hosts, type }); + const hostSets = server.createList('host-set', 3, { + scope, + hostCatalog, + hosts, + type, + }); + const hostSetIds = hostSets.map((hostSet) => hostSet.id); + hosts.forEach((host) => { + host.update({ hostSetIds }); + }); }, }), }); diff --git a/addons/api/mirage/factories/scope.js b/addons/api/mirage/factories/scope.js index d94b84186..b19a00f64 100644 --- a/addons/api/mirage/factories/scope.js +++ b/addons/api/mirage/factories/scope.js @@ -22,14 +22,8 @@ export default factory.extend({ authorized_collection_actions() { const collectionActions = { scopes: ['create', 'list'], - users: ['create', 'list'], - sessions: ['list'], groups: ['create', 'list'], roles: ['create', 'list'], - targets: ['create', 'list'], - 'credential-stores': ['create', 'list'], - 'auth-methods': ['create', 'list'], - 'host-catalogs': ['create', 'list'], }; // Worker permissions only available on the global scope @@ -41,6 +35,15 @@ export default factory.extend({ if (this.type === 'global' || this.type === 'org') { collectionActions['storage-buckets'] = ['create', 'list']; collectionActions['session-recordings'] = ['list']; + collectionActions.users = ['create', 'list']; + collectionActions['auth-methods'] = ['create', 'list']; + } + + if (this.type === 'project') { + collectionActions['credential-stores'] = ['create', 'list']; + collectionActions['host-catalogs'] = ['create', 'list']; + collectionActions.sessions = ['list']; + collectionActions.targets = ['create', 'list']; } return collectionActions; diff --git a/addons/api/mirage/serializers/host.js b/addons/api/mirage/serializers/host.js index f36307794..9d9dabe3a 100644 --- a/addons/api/mirage/serializers/host.js +++ b/addons/api/mirage/serializers/host.js @@ -7,4 +7,13 @@ import ApplicationSerializer from './application'; export default ApplicationSerializer.extend({ modelName: 'host', + + _hashForModel(model) { + const json = ApplicationSerializer.prototype._hashForModel.apply( + this, + arguments + ); + json.host_catalog_id = model.hostCatalogId; + return json; + }, }); diff --git a/addons/api/tests/unit/serializers/host-test.js b/addons/api/tests/unit/serializers/host-test.js index 60b666298..2e6f5bdf5 100644 --- a/addons/api/tests/unit/serializers/host-test.js +++ b/addons/api/tests/unit/serializers/host-test.js @@ -57,6 +57,7 @@ module('Unit | Serializer | host', function (hooks) { address: '10.0.0.1', dns_names: [], ip_addresses: [], + host_set_ids: [], }, relationships: {}, }, @@ -78,6 +79,7 @@ module('Unit | Serializer | host', function (hooks) { authorized_actions: ['no-op', 'read'], ip_addresses: ['10.0.0.1', '10.0.0.2', '10.0.0.3'], dns_names: ['test.example.internal', 'test.example.external'], + host_set_ids: ['hst_12345'], }; const normalized = serializer.normalizeSingleResponse(store, host, payload); assert.deepEqual(normalized, { @@ -92,6 +94,7 @@ module('Unit | Serializer | host', function (hooks) { ip_addresses: ['10.0.0.1', '10.0.0.2', '10.0.0.3'], dns_names: ['test.example.internal', 'test.example.external'], plugin: { name: 'aws' }, + host_set_ids: ['hst_12345'], }, relationships: {}, }, diff --git a/ui/admin/app/routes/scopes/scope/auth-methods/auth-method.js b/ui/admin/app/routes/scopes/scope/auth-methods/auth-method.js index baedad4ba..f676a72c0 100644 --- a/ui/admin/app/routes/scopes/scope/auth-methods/auth-method.js +++ b/ui/admin/app/routes/scopes/scope/auth-methods/auth-method.js @@ -7,10 +7,14 @@ import Route from '@ember/routing/route'; import { action } from '@ember/object'; import { notifySuccess, notifyError } from 'core/decorators/notify'; import { inject as service } from '@ember/service'; +import { paramValueFinder } from 'admin/utils/param-value-finder'; + export default class ScopesScopeAuthMethodsAuthMethodRoute extends Route { // =services @service store; + @service can; + @service router; // =methods @@ -18,10 +22,30 @@ export default class ScopesScopeAuthMethodsAuthMethodRoute extends Route { * Load an auth method by ID. * @param {object} params * @param {string} params.auth_method_id - * @return {authMethodModel} + * @return {Promise{AuthMethodModel}} */ async model({ auth_method_id }) { - return this.store.findRecord('auth-method', auth_method_id); + return this.store.findRecord('auth-method', auth_method_id, { + reload: true, + }); + } + + /** + * Redirects to route with correct scope id if incorrect. + * @param {AuthMethodModel} authMethod + * @param {object} transition + */ + redirect(authMethod, transition) { + const scope = this.modelFor('scopes.scope'); + if (authMethod.scopeID !== scope.id) { + let paramValues = paramValueFinder('auth-method', transition.to.parent); + this.router.replaceWith( + transition.to.name, + authMethod.scopeID, + authMethod.id, + ...paramValues + ); + } } // =actions diff --git a/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/accounts/account.js b/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/accounts/account.js index 6342ce4da..100342f67 100644 --- a/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/accounts/account.js +++ b/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/accounts/account.js @@ -10,6 +10,8 @@ export default class ScopesScopeAuthMethodsAuthMethodAccountsAccountRoute extend // =services @service store; + @service can; + @service router; // =methods @@ -17,9 +19,22 @@ export default class ScopesScopeAuthMethodsAuthMethodAccountsAccountRoute extend * Load an account by ID. * @param {object} params * @param {string} params.account_id - * @return {AccountModel} + * @return {Promise{AccountModel}} */ - model({ account_id }) { + async model({ account_id }) { return this.store.findRecord('account', account_id, { reload: true }); } + + /** + * Redirects to route with correct auth-method id if incorrect. + * @param {AccountModel} account + * @param {object} transition + */ + redirect(account, transition) { + const authMethod = this.modelFor('scopes.scope.auth-methods.auth-method'); + const { auth_method_id } = account; + if (auth_method_id !== authMethod.id) { + this.router.replaceWith(transition.to.name, auth_method_id, account.id); + } + } } diff --git a/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/accounts/new.js b/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/accounts/new.js index b603dbcec..8a192dbea 100644 --- a/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/accounts/new.js +++ b/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/accounts/new.js @@ -10,9 +10,23 @@ export default class ScopesScopeAuthMethodsAuthMethodAccountsNewRoute extends Ro // =services @service store; + @service can; + @service router; // =methods + /** + * Redirect to parent route when auth-method does not have create authorized action. + */ + beforeModel() { + const authMethod = this.modelFor('scopes.scope.auth-methods.auth-method'); + if ( + this.can.cannot('create model', authMethod, { collection: 'accounts' }) + ) { + this.router.replaceWith('scopes.scope.auth-methods.auth-method.accounts'); + } + } + /** * Creates a new unsaved account in current scope. * @return {AccountModel} diff --git a/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/managed-groups/managed-group.js b/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/managed-groups/managed-group.js index 4cd6eb30a..2df321e0f 100644 --- a/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/managed-groups/managed-group.js +++ b/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/managed-groups/managed-group.js @@ -11,6 +11,8 @@ export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsManagedGroupRo // =services @service store; + @service can; + @service router; // =methods @@ -18,14 +20,31 @@ export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsManagedGroupRo * Load managed group by ID. * @param {object} params * @param {string} params.managed_group_id - * @returns {ManagedGroupModel} + * @returns {Promise{ManagedGroupModel}} */ - model({ managed_group_id }) { + async model({ managed_group_id }) { return this.store.findRecord('managed-group', managed_group_id, { reload: true, }); } + /** + * Redirects to route with correct auth-method id if incorrect. + * @param {ManagedGroupModel} managedGroup + * @param {object} transition + */ + redirect(managedGroup, transition) { + const authMethod = this.modelFor('scopes.scope.auth-methods.auth-method'); + const { auth_method_id } = managedGroup; + if (auth_method_id !== authMethod.id) { + this.router.replaceWith( + transition.to.name, + auth_method_id, + managedGroup.id + ); + } + } + /** * Copies the contents of string array fields in order to force the instance * into a dirty state. This ensures that `model.rollbackAttributes()` reverts diff --git a/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/managed-groups/new.js b/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/managed-groups/new.js index 8e400b617..30349673c 100644 --- a/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/managed-groups/new.js +++ b/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/managed-groups/new.js @@ -10,8 +10,27 @@ export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsNewRoute exten // =services @service store; + @service can; + @service router; // =methods + + /** + * Redirect to parent route when auth-method does not have create authorized action. + */ + beforeModel() { + const authMethod = this.modelFor('scopes.scope.auth-methods.auth-method'); + if ( + this.can.cannot('create model', authMethod, { + collection: 'managed-groups', + }) + ) { + this.router.replaceWith( + 'scopes.scope.auth-methods.auth-method.managed-groups' + ); + } + } + model() { const { id: auth_method_id, type } = this.modelFor( 'scopes.scope.auth-methods.auth-method' diff --git a/ui/admin/app/routes/scopes/scope/auth-methods/new.js b/ui/admin/app/routes/scopes/scope/auth-methods/new.js index 1a1c9799d..37a2c624d 100644 --- a/ui/admin/app/routes/scopes/scope/auth-methods/new.js +++ b/ui/admin/app/routes/scopes/scope/auth-methods/new.js @@ -10,6 +10,8 @@ export default class ScopesScopeAuthMethodsNewRoute extends Route { // =services @service store; + @service can; + @service router; // =attributes @@ -21,6 +23,20 @@ export default class ScopesScopeAuthMethodsNewRoute extends Route { // =methods + /** + * Redirect to parent route when scope does not have create authorized action. + */ + beforeModel() { + const scopeModel = this.modelFor('scopes.scope'); + if ( + this.can.cannot('create model', scopeModel, { + collection: 'auth-methods', + }) + ) { + this.router.replaceWith('scopes.scope.auth-methods'); + } + } + /** * Create a new unsaved auth-method. * @return {AuthMethodModel} diff --git a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store.js b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store.js index 20d546bc8..d0f5b94f5 100644 --- a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store.js +++ b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store.js @@ -5,17 +5,43 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; +import { paramValueFinder } from 'admin/utils/param-value-finder'; export default class ScopesScopeCredentialStoresCredentialStoreRoute extends Route { // =services @service store; + @service can; + @service router; /** * Load a specific credential store in current scope - * @return {Promise[CredentialStoreModel]} + * @return {Promise{CredentialStoreModel}} */ async model({ credential_store_id }) { - return this.store.findRecord('credential-store', credential_store_id); + return this.store.findRecord('credential-store', credential_store_id, { + reload: true, + }); + } + + /** + * Redirects to route with correct scope id if incorrect. + * @param {CredentialStoreModel} credentialStore + * @param {object} transition + */ + redirect(credentialStore, transition) { + const scope = this.modelFor('scopes.scope'); + if (credentialStore.scopeID !== scope.id) { + let paramValues = paramValueFinder( + 'credential-store', + transition.to.parent + ); + this.router.replaceWith( + transition.to.name, + credentialStore.scopeID, + credentialStore.id, + ...paramValues + ); + } } } diff --git a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries/credential-library.js b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries/credential-library.js index 8a0ced025..68a678901 100644 --- a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries/credential-library.js +++ b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries/credential-library.js @@ -11,6 +11,8 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari // =services @service store; + @service can; + @service router; // =methods @@ -18,7 +20,7 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari * Load a credential-library using current credential store and its parent scope. * @param {object} params * @param {string} params.credential_library_id - * @return {CredentialLibraryModel} + * @return {Promise{CredentialLibraryModel}} */ async model({ credential_library_id }) { return this.store.findRecord('credential-library', credential_library_id, { @@ -26,6 +28,24 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari }); } + /** + * Redirects to route with correct credential-store id if incorrect. + * @param {CredentialLibraryModel} credentialLibrary + */ + redirect(credentialLibrary) { + const credentialStore = this.modelFor( + 'scopes.scope.credential-stores.credential-store' + ); + const { credential_store_id } = credentialLibrary; + if (credential_store_id !== credentialStore.id) { + this.router.replaceWith( + 'scopes.scope.credential-stores.credential-store.credential-libraries.credential-library', + credential_store_id, + credentialLibrary.id + ); + } + } + /** * Copies the contents of array fields in order to force the instance * into a dirty state. This ensures that `model.rollbackAttributes()` reverts diff --git a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries/new.js b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries/new.js index c77aa0fe3..076dccccb 100644 --- a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries/new.js +++ b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries/new.js @@ -14,6 +14,7 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari @service store; @service router; @service features; + @service can; // =attributes @@ -25,6 +26,24 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari // =methods + /** + * Redirect to parent route when credential-store does not have create authorized action. + */ + beforeModel() { + const credentialStore = this.modelFor( + 'scopes.scope.credential-stores.credential-store' + ); + if ( + this.can.cannot('create model', credentialStore, { + collection: 'credential-libraries', + }) + ) { + this.router.replaceWith( + 'scopes.scope.credential-stores.credential-store.credential-libraries' + ); + } + } + /** * Creates a new unsaved credential library in current credential store. * @return {CredentialLibraryModel} diff --git a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credentials/credential.js b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credentials/credential.js index ce0c8fce7..0fa2709e3 100644 --- a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credentials/credential.js +++ b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credentials/credential.js @@ -10,6 +10,8 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialsCreden // =services @service store; + @service can; + @service router; // =methods @@ -17,11 +19,29 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialsCreden * Load a credential using current credential store and its parent scope. * @param {object} params * @param {string} params.credential_id - * @return {CredentialModel} + * @return {Promise{CredentialModel}} */ async model({ credential_id }) { return this.store.findRecord('credential', credential_id, { reload: true, }); } + + /** + * Redirects to route with correct credential-store id if incorrect. + * @param {CredentialModel} credential + */ + redirect(credential) { + const credentialStore = this.modelFor( + 'scopes.scope.credential-stores.credential-store' + ); + const { credential_store_id } = credential; + if (credential_store_id !== credentialStore.id) { + this.router.replaceWith( + 'scopes.scope.credential-stores.credential-store.credentials.credential', + credential_store_id, + credential.id + ); + } + } } diff --git a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credentials/new.js b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credentials/new.js index c6f6333f5..a7a9583f7 100644 --- a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credentials/new.js +++ b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credentials/new.js @@ -12,6 +12,7 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialsNewRou @service store; @service router; + @service can; // =attributes @@ -23,6 +24,24 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialsNewRou // =methods + /** + * Redirect to parent route when credential-store does not have create authorized action. + */ + beforeModel() { + const credentialStore = this.modelFor( + 'scopes.scope.credential-stores.credential-store' + ); + if ( + this.can.cannot('create model', credentialStore, { + collection: 'credentials', + }) + ) { + this.router.replaceWith( + 'scopes.scope.credential-stores.credential-store.credentials' + ); + } + } + /** * Creates a new unsaved credential in current credential store. * Also rollback/destroy any new, unsaved instances from this route before diff --git a/ui/admin/app/routes/scopes/scope/credential-stores/new.js b/ui/admin/app/routes/scopes/scope/credential-stores/new.js index b43b6d377..77b1fe396 100644 --- a/ui/admin/app/routes/scopes/scope/credential-stores/new.js +++ b/ui/admin/app/routes/scopes/scope/credential-stores/new.js @@ -13,6 +13,7 @@ export default class ScopesScopeCredentialStoresNewRoute extends Route { @service store; @service router; @service features; + @service can; // =attributes @@ -23,6 +24,21 @@ export default class ScopesScopeCredentialStoresNewRoute extends Route { }; // =methods + + /** + * Redirect to parent route when scope does not have create authorized action. + */ + beforeModel() { + const scopeModel = this.modelFor('scopes.scope'); + if ( + this.can.cannot('create model', scopeModel, { + collection: 'credential-stores', + }) + ) { + this.router.replaceWith('scopes.scope.credential-stores'); + } + } + /** * Creates a new unsaved credential-store * Also rollback/destroy any new, unsaved instances from this route before diff --git a/ui/admin/app/routes/scopes/scope/groups/group.js b/ui/admin/app/routes/scopes/scope/groups/group.js index 3d48f0a9c..74f9941ae 100644 --- a/ui/admin/app/routes/scopes/scope/groups/group.js +++ b/ui/admin/app/routes/scopes/scope/groups/group.js @@ -10,6 +10,8 @@ export default class ScopesScopeGroupsGroupRoute extends Route { // =services @service store; + @service can; + @service router; // =methods @@ -17,9 +19,21 @@ export default class ScopesScopeGroupsGroupRoute extends Route { * Load a group in current scope. * @param {object} params * @param {string} params.group_id - * @return {groupModel} + * @return {Promise{GroupModel}} */ - model({ group_id }) { + async model({ group_id }) { return this.store.findRecord('group', group_id, { reload: true }); } + + /** + * Redirects to route with correct scope id if incorrect. + * @param {GroupModel} credentialStore + * @param {object} transition + */ + redirect(group, transition) { + const scope = this.modelFor('scopes.scope'); + if (group.scopeID !== scope.id) { + this.router.replaceWith(transition.to.name, group.scopeID, group.id); + } + } } diff --git a/ui/admin/app/routes/scopes/scope/groups/new.js b/ui/admin/app/routes/scopes/scope/groups/new.js index d96205a48..42e77c87a 100644 --- a/ui/admin/app/routes/scopes/scope/groups/new.js +++ b/ui/admin/app/routes/scopes/scope/groups/new.js @@ -10,9 +10,21 @@ export default class ScopesScopeGroupsNewRoute extends Route { // =services @service store; + @service can; + @service router; // =methods + /** + * Redirect to parent route when scope does not have create authorized action. + */ + beforeModel() { + const scopeModel = this.modelFor('scopes.scope'); + if (this.can.cannot('create model', scopeModel, { collection: 'groups' })) { + this.router.replaceWith('scopes.scope.groups'); + } + } + /** * Creates a new unsaved group. * @return {GroupModel} diff --git a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog.js b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog.js index c1f14298a..7cc912195 100644 --- a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog.js +++ b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog.js @@ -5,11 +5,14 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; +import { paramValueFinder } from 'admin/utils/param-value-finder'; export default class ScopesScopeHostCatalogsHostCatalogRoute extends Route { // =services @service store; + @service can; + @service router; // =methods @@ -17,9 +20,29 @@ export default class ScopesScopeHostCatalogsHostCatalogRoute extends Route { * Load a host catalog. * @param {object} params * @param {string} params.host_catalog_id - * @return {HostCatalogModel} + * @return {Promise{HostCatalogModel}} */ async model({ host_catalog_id }) { - return this.store.findRecord('host-catalog', host_catalog_id); + return this.store.findRecord('host-catalog', host_catalog_id, { + reload: true, + }); + } + + /** + * Redirects to route with correct scope id if incorrect. + * @param {HostCatalogModel} hostCatalog + * @param {object} transition + */ + redirect(hostCatalog, transition) { + const scope = this.modelFor('scopes.scope'); + if (hostCatalog.scopeID !== scope.id) { + let paramValues = paramValueFinder('host-catalog', transition.to.parent); + this.router.replaceWith( + transition.to.name, + hostCatalog.scopeID, + hostCatalog.id, + ...paramValues + ); + } } } diff --git a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/host-set.js b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/host-set.js index ddec38882..faee25eb3 100644 --- a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/host-set.js +++ b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/host-set.js @@ -6,10 +6,14 @@ import Route from '@ember/routing/route'; import { action } from '@ember/object'; import { inject as service } from '@ember/service'; +import { paramValueFinder } from 'admin/utils/param-value-finder'; + export default class ScopesScopeHostCatalogsHostCatalogHostSetsHostSetRoute extends Route { // =services @service store; + @service can; + @service router; // =methods @@ -17,12 +21,33 @@ export default class ScopesScopeHostCatalogsHostCatalogHostSetsHostSetRoute exte * Load a host-set using current host-catalog and its parent scope. * @param {object} params * @param {string} params.host_set_id - * @return {HostSetModel} + * @return {Promise{HostSetModel}} */ async model({ host_set_id }) { return this.store.findRecord('host-set', host_set_id, { reload: true }); } + /** + * Redirects to route with correct host-catalog id if incorrect. + * @param {HostSetModel} hostSet + * @param {object} transition + */ + redirect(hostSet, transition) { + const hostCatalog = this.modelFor( + 'scopes.scope.host-catalogs.host-catalog' + ); + const { host_catalog_id } = hostSet; + if (host_catalog_id !== hostCatalog.id) { + let paramValues = paramValueFinder('host-set', transition.to.parent); + this.router.replaceWith( + transition.to.name, + host_catalog_id, + hostSet.id, + ...paramValues + ); + } + } + /** * Copies the contents of string array fields in order to force the instance * into a dirty state. This ensures that `model.rollbackAttributes()` reverts diff --git a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/host-set/create-and-add-host.js b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/host-set/create-and-add-host.js index 37e824012..6e8cededc 100644 --- a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/host-set/create-and-add-host.js +++ b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/host-set/create-and-add-host.js @@ -15,9 +15,24 @@ export default class ScopesScopeHostCatalogsHostCatalogHostSetsHostSetCreateAndA @service store; @service intl; @service router; + @service can; // =methods + /** + * Redirect to parent route when host-catalog does not have create authorized action. + */ + beforeModel() { + const hostCatalog = this.modelFor( + 'scopes.scope.host-catalogs.host-catalog' + ); + if (this.can.cannot('create model', hostCatalog, { collection: 'hosts' })) { + this.router.replaceWith( + 'scopes.scope.host-catalogs.host-catalog.host-sets.host-set.hosts' + ); + } + } + /** * Creates a new unsaved host in current host catalog scope. * @return {Promise{HostSetModel,HostModel}} diff --git a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/host-set/hosts/host.js b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/host-set/hosts/host.js index acfd23207..31cb4b1a9 100644 --- a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/host-set/hosts/host.js +++ b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/host-set/hosts/host.js @@ -10,6 +10,8 @@ export default class ScopesScopeHostCatalogsHostCatalogHostSetsHostSetHostsHostR // =services @service store; + @service can; + @service router; // =methods @@ -17,9 +19,26 @@ export default class ScopesScopeHostCatalogsHostCatalogHostSetsHostSetHostsHostR * Load a host in current scope. * @param {object} params * @param {string} params.host_id - * @return {HostModel} + * @return {Promise{HostModel}} */ async model({ host_id }) { return this.store.findRecord('host', host_id, { reload: true }); } + + /** + * Redirects to route with correct host-catalog id if incorrect. + * @param {HostModel} host + */ + redirect(host) { + const hostSet = this.modelFor( + 'scopes.scope.host-catalogs.host-catalog.host-sets.host-set' + ); + if (host.host_set_ids !== hostSet.id) { + this.router.replaceWith( + 'scopes.scope.host-catalogs.host-catalog.host-sets.host-set.hosts.host', + host.host_set_ids[0], + host.id + ); + } + } } diff --git a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/new.js b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/new.js index e2f4d0b1b..04bdc1ae9 100644 --- a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/new.js +++ b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/host-sets/new.js @@ -11,8 +11,26 @@ export default class ScopesScopeHostCatalogsHostCatalogHostSetsNewRoute extends @service store; @service router; + @service can; // =methods + + /** + * Redirect to parent route when host-catalog does not have create authorized action. + */ + beforeModel() { + const hostCatalog = this.modelFor( + 'scopes.scope.host-catalogs.host-catalog' + ); + if ( + this.can.cannot('create model', hostCatalog, { collection: 'host-sets' }) + ) { + this.router.replaceWith( + 'scopes.scope.host-catalogs.host-catalog.host-sets' + ); + } + } + /** * Creates a new unsaved host set in current host catalog scope. * @return {HostSetModel} diff --git a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/hosts/host.js b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/hosts/host.js index 9ca5a26ac..16f51069f 100644 --- a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/hosts/host.js +++ b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/hosts/host.js @@ -10,6 +10,8 @@ export default class ScopesScopeHostCatalogsHostCatalogHostsHostRoute extends Ro // =services @service store; + @service can; + @service router; // =methods @@ -17,9 +19,27 @@ export default class ScopesScopeHostCatalogsHostCatalogHostsHostRoute extends Ro * Load a host using current host-catalog and it's parent scope. * @param {object} params * @param {string} params.host_id - * @return {HostModel} + * @return {Promise{HostModel}} */ async model({ host_id }) { return this.store.findRecord('host', host_id, { reload: true }); } + + /** + * Redirects to route with correct host-catalog id if incorrect. + * @param {HostModel} host + */ + redirect(host) { + const hostCatalog = this.modelFor( + 'scopes.scope.host-catalogs.host-catalog' + ); + const { host_catalog_id } = host; + if (host_catalog_id !== hostCatalog.id) { + this.router.replaceWith( + 'scopes.scope.host-catalogs.host-catalog.hosts.host', + host_catalog_id, + host.id + ); + } + } } diff --git a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/hosts/new.js b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/hosts/new.js index 26feeb30e..9b2289ca0 100644 --- a/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/hosts/new.js +++ b/ui/admin/app/routes/scopes/scope/host-catalogs/host-catalog/hosts/new.js @@ -10,9 +10,23 @@ export default class ScopesScopeHostCatalogsHostCatalogHostsNewRoute extends Rou // =services @service store; + @service can; + @service router; // =methods + /** + * Redirect to parent route when host-catalog does not have create authorized action. + */ + beforeModel() { + const hostCatalog = this.modelFor( + 'scopes.scope.host-catalogs.host-catalog' + ); + if (this.can.cannot('create model', hostCatalog, { collection: 'hosts' })) { + this.router.replaceWith('scopes.scope.host-catalogs.host-catalog.hosts'); + } + } + /** * Creates a new unsaved host in current host catalog scope. * @return {HostModel} diff --git a/ui/admin/app/routes/scopes/scope/host-catalogs/new.js b/ui/admin/app/routes/scopes/scope/host-catalogs/new.js index de1a5c9df..bb0fc3a8b 100644 --- a/ui/admin/app/routes/scopes/scope/host-catalogs/new.js +++ b/ui/admin/app/routes/scopes/scope/host-catalogs/new.js @@ -12,6 +12,7 @@ export default class ScopesScopeHostCatalogsNewRoute extends Route { @service store; @service router; + @service can; // =attributes @@ -23,6 +24,20 @@ export default class ScopesScopeHostCatalogsNewRoute extends Route { // =methods + /** + * Redirect to parent route when scope does not have create authorized action. + */ + beforeModel() { + const scopeModel = this.modelFor('scopes.scope'); + if ( + this.can.cannot('create model', scopeModel, { + collection: 'host-catalogs', + }) + ) { + this.router.replaceWith('scopes.scope.host-catalogs'); + } + } + /** * Creates a new unsaved host catalog belonging to the current scope. * Also rollback/destroy any new, unsaved instances from this route before diff --git a/ui/admin/app/routes/scopes/scope/roles/new.js b/ui/admin/app/routes/scopes/scope/roles/new.js index 40eb717b5..100d336c0 100644 --- a/ui/admin/app/routes/scopes/scope/roles/new.js +++ b/ui/admin/app/routes/scopes/scope/roles/new.js @@ -10,9 +10,21 @@ export default class ScopesScopeRolesNewRoute extends Route { // =services @service store; + @service can; + @service router; // =methods + /** + * Redirect to parent route when scope does not have create authorized action. + */ + beforeModel() { + const scopeModel = this.modelFor('scopes.scope'); + if (this.can.cannot('create model', scopeModel, { collection: 'roles' })) { + this.router.replaceWith('scopes.scope.roles'); + } + } + /** * Creates a new unsaved role. * @return {RoleModel} diff --git a/ui/admin/app/routes/scopes/scope/roles/role.js b/ui/admin/app/routes/scopes/scope/roles/role.js index f86ede75e..a32dd419d 100644 --- a/ui/admin/app/routes/scopes/scope/roles/role.js +++ b/ui/admin/app/routes/scopes/scope/roles/role.js @@ -11,6 +11,8 @@ export default class ScopesScopeRolesRoleRoute extends Route { // =services @service store; + @service can; + @service router; // =methods @@ -18,9 +20,9 @@ export default class ScopesScopeRolesRoleRoute extends Route { * Load a role in current scope. * @param {object} params * @param {string} params.role_id - * @return {RoleModel} + * @return {Promise{RoleModel}} */ - model({ role_id }) { + async model({ role_id }) { const { id: scopeID } = this.modelFor('scopes.scope'); return this.store.findRecord('role', role_id, { reload: true, @@ -28,6 +30,18 @@ export default class ScopesScopeRolesRoleRoute extends Route { }); } + /** + * Redirects to route with correct scope id if incorrect. + * @param {RoleModel} role + * @param {object} transition + */ + redirect(role, transition) { + const scope = this.modelFor('scopes.scope'); + if (role.scopeID !== scope.id) { + this.router.replaceWith(transition.to.name, role.scopeID, role.id); + } + } + // =actions /** * Rollback changes on a role. diff --git a/ui/admin/app/routes/scopes/scope/session-recordings/session-recording.js b/ui/admin/app/routes/scopes/scope/session-recordings/session-recording.js index 5637ff46d..b4ad1484f 100644 --- a/ui/admin/app/routes/scopes/scope/session-recordings/session-recording.js +++ b/ui/admin/app/routes/scopes/scope/session-recordings/session-recording.js @@ -5,12 +5,14 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; +import { paramValueFinder } from 'admin/utils/param-value-finder'; export default class ScopesScopeSessionRecordingsSessionRecordingRoute extends Route { // =services @service store; @service session; @service router; + @service can; // =methods @@ -36,4 +38,25 @@ export default class ScopesScopeSessionRecordingsSessionRecordingRoute extends R return sessionRecording; } + + /** + * Redirects to route with correct scope id if incorrect. + * @param {SessionRecordingModel} sessionRecording + * @param {object} transition + */ + redirect(sessionRecording, transition) { + const scope = this.modelFor('scopes.scope'); + if (sessionRecording.scopeID !== scope.id) { + const paramValues = paramValueFinder( + 'session-recording', + transition.to.parent + ); + this.router.replaceWith( + transition.to.name, + sessionRecording.scopeID, + sessionRecording.id, + ...paramValues + ); + } + } } diff --git a/ui/admin/app/routes/scopes/scope/session-recordings/session-recording/channels-by-connection/channel.js b/ui/admin/app/routes/scopes/scope/session-recordings/session-recording/channels-by-connection/channel.js index f80137479..0d3f057fa 100644 --- a/ui/admin/app/routes/scopes/scope/session-recordings/session-recording/channels-by-connection/channel.js +++ b/ui/admin/app/routes/scopes/scope/session-recordings/session-recording/channels-by-connection/channel.js @@ -9,6 +9,8 @@ import { inject as service } from '@ember/service'; export default class ScopesScopeSessionRecordingsSessionRecordingChannelsByConnectionChannelRoute extends Route { // =services @service store; + @service can; + @service router; // =methods /** @@ -31,4 +33,21 @@ export default class ScopesScopeSessionRecordingsSessionRecordingChannelsByConne storageBucket, }; } + + /** + * Redirects to route with correct session-recording id if incorrect. + * @param {channelRecording: Object, sessionRecording: Object, storageBucket: Object} model + */ + redirect(model) { + const { channelRecording, sessionRecording } = model; + const session_recording_id = + channelRecording.connection_recording.session_recording.id; + if (session_recording_id !== sessionRecording.id) { + this.router.replaceWith( + 'scopes.scope.session-recordings.session-recording.channels-by-connection.channel', + session_recording_id, + channelRecording.id + ); + } + } } diff --git a/ui/admin/app/routes/scopes/scope/storage-buckets/new.js b/ui/admin/app/routes/scopes/scope/storage-buckets/new.js index 94cf7b315..fce816c0a 100644 --- a/ui/admin/app/routes/scopes/scope/storage-buckets/new.js +++ b/ui/admin/app/routes/scopes/scope/storage-buckets/new.js @@ -14,9 +14,25 @@ export default class ScopesScopeStorageBucketsNewRoute extends Route { // =services @service store; + @service can; + @service router; // =methods + /** + * Redirect to parent route when scope does not have create authorized action. + */ + beforeModel() { + const scopeModel = this.modelFor('scopes.scope'); + if ( + this.can.cannot('create scope', scopeModel, { + collection: 'storage-buckets', + }) + ) { + this.router.replaceWith('scopes.scope.storage-buckets'); + } + } + /** * Creates a new unsaved storage bucket. * Also rollback/destroy any new, unsaved instances from this route before diff --git a/ui/admin/app/routes/scopes/scope/storage-buckets/storage-bucket.js b/ui/admin/app/routes/scopes/scope/storage-buckets/storage-bucket.js index 997a3e2fe..5332a10d7 100644 --- a/ui/admin/app/routes/scopes/scope/storage-buckets/storage-bucket.js +++ b/ui/admin/app/routes/scopes/scope/storage-buckets/storage-bucket.js @@ -10,6 +10,8 @@ export default class ScopesScopeStorageBucketsStorageBucketRoute extends Route { // =services @service store; + @service can; + @service router; // =methods @@ -17,9 +19,26 @@ export default class ScopesScopeStorageBucketsStorageBucketRoute extends Route { * Load a storage bucket in current scope. * @param {object} params * @param {string} params.storage_bucket_id - * @return {StorageBucketModel} + * @return {Promise{StorageBucketModel}} */ - model({ storage_bucket_id }) { - return this.store.findRecord('storage-bucket', storage_bucket_id); + async model({ storage_bucket_id }) { + return this.store.findRecord('storage-bucket', storage_bucket_id, { + reload: true, + }); + } + + /** + * Redirects to route with correct scope id if incorrect. + * @param {StorageBucketModel} storageBucket + */ + redirect(storageBucket) { + const scope = this.modelFor('scopes.scope'); + if (storageBucket.scopeID !== scope.id) { + this.router.replaceWith( + 'scopes.scope.storage-buckets.storage-bucket', + storageBucket.scopeID, + storageBucket.id + ); + } } } diff --git a/ui/admin/app/routes/scopes/scope/targets/new.js b/ui/admin/app/routes/scopes/scope/targets/new.js index 036442ac6..229f24a53 100644 --- a/ui/admin/app/routes/scopes/scope/targets/new.js +++ b/ui/admin/app/routes/scopes/scope/targets/new.js @@ -14,8 +14,7 @@ export default class ScopesScopeTargetsNewRoute extends Route { @service store; @service router; @service features; - - // =methods + @service can; // =attributes queryParams = { @@ -23,6 +22,21 @@ export default class ScopesScopeTargetsNewRoute extends Route { refreshModel: true, }, }; + + // =methods + + /** + * Redirect to parent route when scope does not have create authorized action. + */ + beforeModel() { + const scopeModel = this.modelFor('scopes.scope'); + if ( + this.can.cannot('create model', scopeModel, { collection: 'targets' }) + ) { + this.router.replaceWith('scopes.scope.targets'); + } + } + /** * Creates a new unsaved target in current scope. * Also rollback/destroy any new, unsaved instances from this route before diff --git a/ui/admin/app/routes/scopes/scope/targets/target.js b/ui/admin/app/routes/scopes/scope/targets/target.js index 74f2d14f1..deb0c0f9d 100644 --- a/ui/admin/app/routes/scopes/scope/targets/target.js +++ b/ui/admin/app/routes/scopes/scope/targets/target.js @@ -10,6 +10,8 @@ export default class ScopesScopeTargetsTargetRoute extends Route { // =services @service store; + @service can; + @service router; // =methods @@ -17,9 +19,21 @@ export default class ScopesScopeTargetsTargetRoute extends Route { * Load a target. * @param {object} params * @param {string} params.target_id - * @return {TargetModel} + * @return {Promise{TargetModel}} */ async model({ target_id }) { return this.store.findRecord('target', target_id, { reload: true }); } + + /** + * Redirects to route with correct scope id if incorrect. + * @param {TargetModel} target + * @param {object} transition + */ + redirect(target, transition) { + const scope = this.modelFor('scopes.scope'); + if (target.scopeID !== scope.id) { + this.router.replaceWith(transition.to.name, target.scopeID, target.id); + } + } } diff --git a/ui/admin/app/routes/scopes/scope/users/new.js b/ui/admin/app/routes/scopes/scope/users/new.js index 8d10cbcbe..7d1f5b62f 100644 --- a/ui/admin/app/routes/scopes/scope/users/new.js +++ b/ui/admin/app/routes/scopes/scope/users/new.js @@ -10,9 +10,21 @@ export default class ScopesScopeUsersNewRoute extends Route { // =services @service store; + @service can; + @service router; // =methods + /** + * Redirect to parent route when scope does not have create authorized action. + */ + beforeModel() { + const scopeModel = this.modelFor('scopes.scope'); + if (this.can.cannot('create model', scopeModel, { collection: 'users' })) { + this.router.replaceWith('scopes.scope.users'); + } + } + /** * Creates a new unsaved user. * @return {UserModel} diff --git a/ui/admin/app/routes/scopes/scope/users/user.js b/ui/admin/app/routes/scopes/scope/users/user.js index 31621188c..24d3d4311 100644 --- a/ui/admin/app/routes/scopes/scope/users/user.js +++ b/ui/admin/app/routes/scopes/scope/users/user.js @@ -10,6 +10,8 @@ export default class ScopesScopeUsersUserRoute extends Route { // =services @service store; + @service can; + @service router; // =methods @@ -17,9 +19,21 @@ export default class ScopesScopeUsersUserRoute extends Route { * Load a user in current scope. * @param {object} params * @param {string} params.user_id - * @return {UserModel} + * @return {Promise{UserModel}} */ - model({ user_id }) { - return this.store.findRecord('user', user_id); + async model({ user_id }) { + return this.store.findRecord('user', user_id, { reload: true }); + } + + /** + * Redirects to route with correct scope id if incorrect. + * @param {UserModel} user + * @param {object} transition + */ + redirect(user, transition) { + const scope = this.modelFor('scopes.scope'); + if (user.scopeID !== scope.id) { + this.router.replaceWith(transition.to.name, user.scopeID, user.id); + } } } diff --git a/ui/admin/app/routes/scopes/scope/workers/new.js b/ui/admin/app/routes/scopes/scope/workers/new.js index c05fc19ed..ddaa5453b 100644 --- a/ui/admin/app/routes/scopes/scope/workers/new.js +++ b/ui/admin/app/routes/scopes/scope/workers/new.js @@ -13,9 +13,25 @@ export default class ScopesScopeWorkersNewRoute extends Route { // =services @service store; + @service can; + @service router; // =methods + /** + * Redirect to parent route when scope does not have create authorized action. + */ + beforeModel() { + const scopeModel = this.modelFor('scopes.scope'); + if ( + this.can.cannot('create worker led worker', scopeModel, { + collection: 'workers', + }) + ) { + this.router.replaceWith('scopes.scope.workers'); + } + } + /** * Creates a new unsaved worker. * @return {WorkerModel} diff --git a/ui/admin/app/routes/scopes/scope/workers/worker.js b/ui/admin/app/routes/scopes/scope/workers/worker.js index 835f6b357..db805b770 100644 --- a/ui/admin/app/routes/scopes/scope/workers/worker.js +++ b/ui/admin/app/routes/scopes/scope/workers/worker.js @@ -10,6 +10,8 @@ export default class ScopesScopeWorkersWorkerRoute extends Route { // =services @service store; + @service can; + @service router; // =methods @@ -17,9 +19,24 @@ export default class ScopesScopeWorkersWorkerRoute extends Route { * Load a worker in current scope. * @param {object} params * @param {string} params.worker_id - * @return {WorkerModel} + * @return {Promise{WorkerModel}} */ - model({ worker_id }) { - return this.store.findRecord('worker', worker_id); + async model({ worker_id }) { + return this.store.findRecord('worker', worker_id, { reload: true }); + } + + /** + * Redirects to route with correct scope id if incorrect. + * @param {WorkerModel} worker + */ + redirect(worker) { + const scope = this.modelFor('scopes.scope'); + if (worker.scopeID !== scope.id) { + this.router.replaceWith( + 'scopes.scope.workers.worker', + worker.scopeID, + worker.id + ); + } } } diff --git a/ui/admin/app/utils/param-value-finder.js b/ui/admin/app/utils/param-value-finder.js new file mode 100644 index 000000000..277353374 --- /dev/null +++ b/ui/admin/app/utils/param-value-finder.js @@ -0,0 +1,14 @@ +// Traverses through a RouteInfo object in order to find +// the dynamic segments of a route and stops traversing when +// the route name of the caller is reached. In the RouteInfo +// object the params object contains the dynamic segment of +// a particular route. + +export function paramValueFinder(routeName, parentInfo) { + if (!parentInfo?.params || parentInfo.localName === routeName) { + return []; + } + + let otherParam = Object.values(parentInfo.params); + return [...paramValueFinder(routeName, parentInfo.parent), ...otherParam]; +} diff --git a/ui/admin/tests/acceptance/accounts/create-test.js b/ui/admin/tests/acceptance/accounts/create-test.js index 262307238..1a4d0ea09 100644 --- a/ui/admin/tests/acceptance/accounts/create-test.js +++ b/ui/admin/tests/acceptance/accounts/create-test.js @@ -26,11 +26,13 @@ module('Acceptance | accounts | create', function (hooks) { org: null, }, authMethods: null, + authMethod: null, account: null, }; const urls = { orgScope: null, authMethods: null, + authMethod: null, accounts: null, newAccount: null, account: null, @@ -190,4 +192,21 @@ module('Acceptance | accounts | create', function (hooks) { 'Displays field-level errors.' ); }); + + test('users cannot directly navigate to new account route without proper authorization', async function (assert) { + assert.expect(2); + instances.authMethod.authorized_collection_actions.accounts = + instances.authMethod.authorized_collection_actions.accounts.filter( + (item) => item !== 'create' + ); + + await visit(urls.newAccount); + + assert.false( + instances.authMethod.authorized_collection_actions.accounts.includes( + 'create' + ) + ); + assert.strictEqual(currentURL(), urls.accounts); + }); }); diff --git a/ui/admin/tests/acceptance/accounts/read-test.js b/ui/admin/tests/acceptance/accounts/read-test.js index 4d422ac14..d0a0ed77c 100644 --- a/ui/admin/tests/acceptance/accounts/read-test.js +++ b/ui/admin/tests/acceptance/accounts/read-test.js @@ -18,6 +18,7 @@ import { module('Acceptance | accounts | read', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + const ACCOUNT_SELECTOR = 'main tbody .hds-table__td:nth-child(1) a'; const instances = { @@ -26,11 +27,13 @@ module('Acceptance | accounts | read', function (hooks) { org: null, }, authMethods: null, + authMethod: null, account: null, }; const urls = { orgScope: null, authMethods: null, + authMethod: null, accounts: null, account: null, }; @@ -80,4 +83,22 @@ module('Acceptance | accounts | read', function (hooks) { await visit(urls.accounts); assert.dom(ACCOUNT_SELECTOR).doesNotExist(); }); + + test('user can navigate to account and incorrect url autocorrects', async function (assert) { + assert.expect(2); + const authMethod = this.server.create('auth-method', { + scope: instances.scopes.org, + }); + const account = this.server.create('account', { + scope: instances.scopes.org, + authMethod, + }); + const incorrectUrl = `${urls.accounts}/${account.id}`; + const correctUrl = `${urls.orgScope}/auth-methods/${authMethod.id}/accounts/${account.id}`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), correctUrl); + }); }); diff --git a/ui/admin/tests/acceptance/auth-methods/create-test.js b/ui/admin/tests/acceptance/auth-methods/create-test.js index 394f62792..76723cf2f 100644 --- a/ui/admin/tests/acceptance/auth-methods/create-test.js +++ b/ui/admin/tests/acceptance/auth-methods/create-test.js @@ -444,4 +444,18 @@ module('Acceptance | auth-methods | create', function (hooks) { .dom(FIELD_ERROR_TEXT_SELECTOR) .hasText('At least one URL is required.'); }); + + test('users cannot directly navigate to new auth method route without proper authorization', async function (assert) { + assert.expect(2); + instances.orgScope.authorized_collection_actions['auth-methods'] = ['list']; + + await visit(urls.newAuthMethod); + + assert.false( + instances.orgScope.authorized_collection_actions['auth-methods'].includes( + 'create' + ) + ); + assert.strictEqual(currentURL(), urls.authMethods); + }); }); diff --git a/ui/admin/tests/acceptance/auth-methods/delete-test.js b/ui/admin/tests/acceptance/auth-methods/delete-test.js index 84e2d3071..591d7e26f 100644 --- a/ui/admin/tests/acceptance/auth-methods/delete-test.js +++ b/ui/admin/tests/acceptance/auth-methods/delete-test.js @@ -17,7 +17,7 @@ import { } from 'ember-simple-auth/test-support'; import { TYPE_AUTH_METHOD_LDAP } from 'api/models/auth-method'; -module('Acceptance | auth methods | delete', function (hooks) { +module('Acceptance | auth-methods | delete', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); diff --git a/ui/admin/tests/acceptance/auth-methods/oidc-test.js b/ui/admin/tests/acceptance/auth-methods/oidc-test.js index 00c62238f..3c8ddf32c 100644 --- a/ui/admin/tests/acceptance/auth-methods/oidc-test.js +++ b/ui/admin/tests/acceptance/auth-methods/oidc-test.js @@ -25,7 +25,7 @@ import { import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method'; -module('Acceptance | auth methods | oidc', function (hooks) { +module('Acceptance | auth-methods | oidc', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); diff --git a/ui/admin/tests/acceptance/auth-methods/read-test.js b/ui/admin/tests/acceptance/auth-methods/read-test.js index b46e82272..8b2c6e8e5 100644 --- a/ui/admin/tests/acceptance/auth-methods/read-test.js +++ b/ui/admin/tests/acceptance/auth-methods/read-test.js @@ -20,7 +20,7 @@ import { TYPE_AUTH_METHOD_PASSWORD, } from 'api/models/auth-method'; -module('Acceptance | auth methods | read', function (hooks) { +module('Acceptance | auth-methods | read', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); const AUTH_LINK_SELECTOR = 'main tbody .hds-table__td:nth-child(1) a'; @@ -183,4 +183,22 @@ module('Acceptance | auth methods | read', function (hooks) { assert.dom(LDAP_AUTH_TYPE_SELECTOR).hasText('LDAP'); assert.dom(LDAP_AUTH_ACTIONS_SELECTOR).doesNotExist(); }); + + test('users can navigate to auth method and incorrect url autocorrects', async function (assert) { + assert.expect(2); + const orgScope = this.server.create('scope', { + type: 'org', + scope: { id: 'global', type: 'global' }, + }); + const authMethod = this.server.create('auth-method', { + scope: orgScope, + }); + const incorrectUrl = `${urls.globalAuthMethods}/${authMethod.id}/accounts`; + const correctUrl = `/scopes/${orgScope.id}/auth-methods/${authMethod.id}/accounts`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), correctUrl); + }); }); diff --git a/ui/admin/tests/acceptance/auth-methods/update-test.js b/ui/admin/tests/acceptance/auth-methods/update-test.js index 54190a6a9..a5331d9b1 100644 --- a/ui/admin/tests/acceptance/auth-methods/update-test.js +++ b/ui/admin/tests/acceptance/auth-methods/update-test.js @@ -20,7 +20,7 @@ import { TYPE_AUTH_METHOD_LDAP, } from 'api/models/auth-method'; -module('Acceptance | auth methods | update', function (hooks) { +module('Acceptance | auth-methods | update', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); diff --git a/ui/admin/tests/acceptance/credential-library/create-test.js b/ui/admin/tests/acceptance/credential-library/create-test.js index dc9cfe2d1..94c5a7250 100644 --- a/ui/admin/tests/acceptance/credential-library/create-test.js +++ b/ui/admin/tests/acceptance/credential-library/create-test.js @@ -233,4 +233,22 @@ module('Acceptance | credential-libraries | create', function (hooks) { assert.false(featuresService.isEnabled('ssh-target')); assert.dom('[value="vault-ssh-certificate"]').doesNotExist(); }); + + test('users cannot directly navigate to new credential library route without proper authorization', async function (assert) { + assert.expect(2); + instances.credentialStore.authorized_collection_actions[ + 'credential-libraries' + ] = instances.credentialStore.authorized_collection_actions[ + 'credential-libraries' + ].filter((item) => item !== 'create'); + + await visit(urls.newCredentialLibrary); + + assert.false( + instances.credentialStore.authorized_collection_actions[ + 'credential-libraries' + ].includes('create') + ); + assert.strictEqual(currentURL(), urls.credentialLibraries); + }); }); diff --git a/ui/admin/tests/acceptance/credential-library/read-test.js b/ui/admin/tests/acceptance/credential-library/read-test.js index 3194db43a..dc8dff7d6 100644 --- a/ui/admin/tests/acceptance/credential-library/read-test.js +++ b/ui/admin/tests/acceptance/credential-library/read-test.js @@ -106,4 +106,22 @@ module('Acceptance | credential-libraries | read', function (hooks) { await a11yAudit(); assert.ok(find('.rose-message-subtitle').textContent.trim(), 'Error 404'); }); + + test('users can navigate to credential library and incorrect url autocorrects', async function (assert) { + assert.expect(2); + const credentialStore = this.server.create('credential-store', { + scope: instances.scopes.project, + }); + const credentialLibrary = this.server.create('credential-library', { + scope: instances.scopes.project, + credentialStore, + }); + const incorrectUrl = `${urls.credentialLibraries}/${credentialLibrary.id}`; + const correctUrl = `${urls.projectScope}/credential-stores/${credentialStore.id}/credential-libraries/${credentialLibrary.id}`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), correctUrl); + }); }); diff --git a/ui/admin/tests/acceptance/credential-store/create-test.js b/ui/admin/tests/acceptance/credential-store/create-test.js index 6fc90f3f9..2c2bef5e8 100644 --- a/ui/admin/tests/acceptance/credential-store/create-test.js +++ b/ui/admin/tests/acceptance/credential-store/create-test.js @@ -153,4 +153,22 @@ module('Acceptance | credential-stores | create', function (hooks) { ) ); }); + + test('users cannot directly navigate to new credential store route without proper authorization', async function (assert) { + assert.expect(2); + instances.scopes.project.authorized_collection_actions[ + 'credential-stores' + ] = instances.scopes.project.authorized_collection_actions[ + 'credential-stores' + ].filter((item) => item !== 'create'); + + await visit(urls.newCredentialStore); + + assert.false( + instances.scopes.project.authorized_collection_actions[ + 'credential-stores' + ].includes('create') + ); + assert.strictEqual(currentURL(), urls.credentialStores); + }); }); diff --git a/ui/admin/tests/acceptance/credential-store/credentials/create-test.js b/ui/admin/tests/acceptance/credential-store/credentials/create-test.js index ff4529c69..d844f98bc 100644 --- a/ui/admin/tests/acceptance/credential-store/credentials/create-test.js +++ b/ui/admin/tests/acceptance/credential-store/credentials/create-test.js @@ -336,5 +336,22 @@ module( .dom('.hds-form-radio-card:nth-child(2) input') .hasAttribute('value', 'ssh_private_key'); }); + + test('users cannot directly navigate to new credential route without proper authorization', async function (assert) { + assert.expect(2); + instances.staticCredentialStore.authorized_collection_actions.credentials = + instances.staticCredentialStore.authorized_collection_actions.credentials.filter( + (item) => item !== 'create' + ); + + await visit(urls.newCredential); + + assert.false( + instances.staticCredentialStore.authorized_collection_actions.credentials.includes( + 'create' + ) + ); + assert.strictEqual(currentURL(), urls.credentials); + }); } ); diff --git a/ui/admin/tests/acceptance/credential-store/credentials/read-test.js b/ui/admin/tests/acceptance/credential-store/credentials/read-test.js index 09ccfb301..8e5e32f31 100644 --- a/ui/admin/tests/acceptance/credential-store/credentials/read-test.js +++ b/ui/admin/tests/acceptance/credential-store/credentials/read-test.js @@ -165,4 +165,22 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) { find(`[href="https://boundaryproject.io/help/admin-ui/credentials"]`) ); }); + + test('users can navigate to credential and incorrect url autocorrects', async function (assert) { + assert.expect(2); + const credentialStore = this.server.create('credential-store', { + scope: instances.scopes.project, + }); + const credential = this.server.create('credential', { + scope: instances.scopes.project, + credentialStore, + }); + const incorrectUrl = `${urls.credentials}/${credential.id}`; + const correctUrl = `${urls.projectScope}/credential-stores/${credentialStore.id}/credentials/${credential.id}`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), correctUrl); + }); }); diff --git a/ui/admin/tests/acceptance/credential-store/read-test.js b/ui/admin/tests/acceptance/credential-store/read-test.js index f8d7bc38f..14f3aaba5 100644 --- a/ui/admin/tests/acceptance/credential-store/read-test.js +++ b/ui/admin/tests/acceptance/credential-store/read-test.js @@ -142,4 +142,22 @@ module('Acceptance | credential-stores | read', function (hooks) { ) .exists(); }); + + test('users can navigate to credential store and incorrect url autocorrects', async function (assert) { + assert.expect(2); + const projectScope = this.server.create('scope', { + type: 'project', + scope: { id: instances.scopes.org.id, type: 'org' }, + }); + const credentialStore = this.server.create('credential-store', { + scope: projectScope, + }); + const incorrectUrl = `${urls.credentialStores}/${credentialStore.id}/credentials`; + const correctUrl = `/scopes/${projectScope.id}/credential-stores/${credentialStore.id}/credentials`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), correctUrl); + }); }); diff --git a/ui/admin/tests/acceptance/groups/create-test.js b/ui/admin/tests/acceptance/groups/create-test.js index 682ed5c64..044a409e0 100644 --- a/ui/admin/tests/acceptance/groups/create-test.js +++ b/ui/admin/tests/acceptance/groups/create-test.js @@ -21,11 +21,9 @@ module('Acceptance | groups | create', function (hooks) { const instances = { scopes: { - global: null, org: null, }, group: null, - orgScope: null, }; const urls = { groups: null, @@ -35,7 +33,7 @@ module('Acceptance | groups | create', function (hooks) { hooks.beforeEach(function () { authenticateSession({}); - instances.orgScope = this.server.create( + instances.scopes.org = this.server.create( 'scope', { type: 'org', @@ -44,9 +42,9 @@ module('Acceptance | groups | create', function (hooks) { 'withChildren' ); instances.group = this.server.create('group', { - scope: instances.orgScope, + scope: instances.scopes.org, }); - urls.groups = `/scopes/${instances.orgScope.id}/groups`; + urls.groups = `/scopes/${instances.scopes.org.id}/groups`; urls.group = `${urls.groups}/${instances.group.id}`; urls.newGroup = `${urls.groups}/new`; }); @@ -64,17 +62,21 @@ module('Acceptance | groups | create', function (hooks) { assert.expect(2); await visit(urls.groups); assert.ok( - instances.orgScope.authorized_collection_actions.groups.includes('create') + instances.scopes.org.authorized_collection_actions.groups.includes( + 'create' + ) ); assert.ok(find(`[href="${urls.newGroup}"]`)); }); test('cannot navigate to new groups route without proper authorization', async function (assert) { assert.expect(2); - instances.orgScope.authorized_collection_actions.groups = []; + instances.scopes.org.authorized_collection_actions.groups = []; await visit(urls.groups); - assert.notOk( - instances.orgScope.authorized_collection_actions.groups.includes('create') + assert.false( + instances.scopes.org.authorized_collection_actions.groups.includes( + 'create' + ) ); assert.notOk(find(`[href="${urls.newGroup}"]`)); }); @@ -123,4 +125,21 @@ module('Acceptance | groups | create', function (hooks) { 'Displays field-level errors.' ); }); + + test('users cannot directly navigate to new group route without proper authorization', async function (assert) { + assert.expect(2); + instances.scopes.org.authorized_collection_actions.groups = + instances.scopes.org.authorized_collection_actions.groups.filter( + (item) => item !== 'create' + ); + + await visit(urls.newGroup); + + assert.false( + instances.scopes.org.authorized_collection_actions.groups.includes( + 'create' + ) + ); + assert.strictEqual(currentURL(), urls.groups); + }); }); diff --git a/ui/admin/tests/acceptance/groups/read-test.js b/ui/admin/tests/acceptance/groups/read-test.js index 054b3ed05..b10095711 100644 --- a/ui/admin/tests/acceptance/groups/read-test.js +++ b/ui/admin/tests/acceptance/groups/read-test.js @@ -62,4 +62,22 @@ module('Acceptance | groups | read', function (hooks) { await visit(urls.group); assert.notOk(find('main tbody .rose-table-header-cell:nth-child(1) a')); }); + + test('users can navigate to group and incorrect url autocorrects', async function (assert) { + assert.expect(2); + const orgScope = this.server.create('scope', { + type: 'org', + scope: { id: 'global', type: 'global' }, + }); + const group = this.server.create('group', { + scope: orgScope, + }); + const incorrectUrl = `${urls.groups}/${group.id}/members`; + const correctUrl = `/scopes/${orgScope.id}/groups/${group.id}/members`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), correctUrl); + }); }); diff --git a/ui/admin/tests/acceptance/host-catalogs/create-test.js b/ui/admin/tests/acceptance/host-catalogs/create-test.js index 62b35f05b..68135a028 100644 --- a/ui/admin/tests/acceptance/host-catalogs/create-test.js +++ b/ui/admin/tests/acceptance/host-catalogs/create-test.js @@ -201,4 +201,21 @@ module('Acceptance | host-catalogs | create', function (hooks) { 'Name is required.' ); }); + + test('users cannot directly navigate to new host catalog route without proper authorization', async function (assert) { + assert.expect(2); + instances.scopes.project.authorized_collection_actions['host-catalogs'] = + instances.scopes.project.authorized_collection_actions[ + 'host-catalogs' + ].filter((item) => item !== 'create'); + + await visit(urls.newHostCatalog); + + assert.false( + instances.scopes.project.authorized_collection_actions[ + 'host-catalogs' + ].includes('create') + ); + assert.strictEqual(currentURL(), urls.hostCatalogs); + }); }); diff --git a/ui/admin/tests/acceptance/host-catalogs/host-sets/create-test.js b/ui/admin/tests/acceptance/host-catalogs/host-sets/create-test.js index f59237c97..a6801de54 100644 --- a/ui/admin/tests/acceptance/host-catalogs/host-sets/create-test.js +++ b/ui/admin/tests/acceptance/host-catalogs/host-sets/create-test.js @@ -224,4 +224,21 @@ module('Acceptance | host-catalogs | host sets | create', function (hooks) { 'Name is required.' ); }); + + test('users cannot directly navigate to new host set route without proper authorization', async function (assert) { + assert.expect(2); + instances.hostCatalog.authorized_collection_actions['host-sets'] = + instances.hostCatalog.authorized_collection_actions['host-sets'].filter( + (item) => item !== 'create' + ); + + await visit(urls.newHostSet); + + assert.false( + instances.hostCatalog.authorized_collection_actions['host-sets'].includes( + 'create' + ) + ); + assert.strictEqual(currentURL(), urls.hostSets); + }); }); diff --git a/ui/admin/tests/acceptance/host-catalogs/host-sets/hosts-test.js b/ui/admin/tests/acceptance/host-catalogs/host-sets/hosts-test.js index 0468545f4..d625ba0eb 100644 --- a/ui/admin/tests/acceptance/host-catalogs/host-sets/hosts-test.js +++ b/ui/admin/tests/acceptance/host-catalogs/host-sets/hosts-test.js @@ -278,4 +278,29 @@ module('Acceptance | host-catalogs | host-sets | hosts', function (hooks) { await click('form [type="submit"]'); assert.ok(find('[role="alert"]')); }); + + test('users can navigate to host and incorrect url autocorrects', async function (assert) { + assert.expect(2); + const hostCatalog = this.server.create('host-catalog', { + scope: instances.scopes.project, + type: 'plugin', + plugin: { name: 'aws' }, + }); + const hostSet = this.server.create('host-set', { + scope: instances.scopes.project, + hostCatalog, + }); + const host = this.server.create('host', { + scope: instances.scopes.project, + hostCatalog, + hostSetIds: [hostSet.id], + }); + const incorrectUrl = `${urls.hostCatalogs}/${hostCatalog.id}/host-sets/${instances.hostSet.id}/hosts/${host.id}`; + const correctUrl = `${urls.hostCatalogs}/${hostCatalog.id}/host-sets/${hostSet.id}/hosts/${host.id}`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), correctUrl); + }); }); diff --git a/ui/admin/tests/acceptance/host-catalogs/host-sets/read-test.js b/ui/admin/tests/acceptance/host-catalogs/host-sets/read-test.js index a10a5578f..709afd0e5 100644 --- a/ui/admin/tests/acceptance/host-catalogs/host-sets/read-test.js +++ b/ui/admin/tests/acceptance/host-catalogs/host-sets/read-test.js @@ -15,7 +15,7 @@ import { //invalidateSession, } from 'ember-simple-auth/test-support'; -module('Acceptance | host-catalogs | host sets | read', function (hooks) { +module('Acceptance | host-catalogs | host-sets | read', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); @@ -113,4 +113,23 @@ module('Acceptance | host-catalogs | host sets | read', function (hooks) { .dom(`[href="https://boundaryproject.io/help/admin-ui/host-sets"]`) .exists(); }); + + test('users can navigate to host set and incorrect url autocorrects', async function (assert) { + assert.expect(2); + const hostCatalog = this.server.create('host-catalog', { + scope: instances.scopes.project, + type: 'static', + }); + const hostSet = this.server.create('host-set', { + scope: instances.scopes.project, + hostCatalog, + }); + const incorrectUrl = `${urls.hostSets}/${hostSet.id}`; + const correctUrl = `${urls.hostCatalogs}/${hostCatalog.id}/host-sets/${hostSet.id}`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), correctUrl); + }); }); diff --git a/ui/admin/tests/acceptance/host-catalogs/hosts/create-test.js b/ui/admin/tests/acceptance/host-catalogs/hosts/create-test.js index 0b72457e1..ccc133fa0 100644 --- a/ui/admin/tests/acceptance/host-catalogs/hosts/create-test.js +++ b/ui/admin/tests/acceptance/host-catalogs/hosts/create-test.js @@ -54,6 +54,7 @@ module('Acceptance | host-catalogs | hosts | create', function (hooks) { scope: { id: instances.scopes.org.id, type: 'org' }, }); instances.hostCatalog = this.server.create('host-catalog', { + type: 'static', scope: instances.scopes.project, }); instances.host = this.server.create('host', { @@ -160,4 +161,21 @@ module('Acceptance | host-catalogs | hosts | create', function (hooks) { 'Name is required.' ); }); + + test('users cannot directly navigate to new host route without proper authorization', async function (assert) { + assert.expect(2); + instances.hostCatalog.authorized_collection_actions.hosts = + instances.hostCatalog.authorized_collection_actions.hosts.filter( + (item) => item !== 'create' + ); + + await visit(urls.newHost); + + assert.false( + instances.hostCatalog.authorized_collection_actions.hosts.includes( + 'create' + ) + ); + assert.strictEqual(currentURL(), urls.hosts); + }); }); diff --git a/ui/admin/tests/acceptance/host-catalogs/hosts/read-test.js b/ui/admin/tests/acceptance/host-catalogs/hosts/read-test.js index e911a5241..e2aa8f199 100644 --- a/ui/admin/tests/acceptance/host-catalogs/hosts/read-test.js +++ b/ui/admin/tests/acceptance/host-catalogs/hosts/read-test.js @@ -113,4 +113,23 @@ module('Acceptance | host-catalogs | hosts | read', function (hooks) { .dom(`[href="https://boundaryproject.io/help/admin-ui/hosts"]`) .exists(); }); + + test('users can navigate to host and incorrect url autocorrects', async function (assert) { + assert.expect(2); + const hostCatalog = this.server.create('host-catalog', { + scope: instances.scopes.project, + type: 'static', + }); + const host = this.server.create('host', { + scope: instances.scopes.project, + hostCatalog, + }); + const incorrectUrl = `${urls.hosts}/${host.id}`; + const correctUrl = `${urls.hostCatalogs}/${hostCatalog.id}/hosts/${host.id}`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), correctUrl); + }); }); diff --git a/ui/admin/tests/acceptance/host-catalogs/read-test.js b/ui/admin/tests/acceptance/host-catalogs/read-test.js index 1a94d1a9a..4f18ae6c2 100644 --- a/ui/admin/tests/acceptance/host-catalogs/read-test.js +++ b/ui/admin/tests/acceptance/host-catalogs/read-test.js @@ -105,4 +105,22 @@ module('Acceptance | host-catalogs | read', function (hooks) { .dom(`[href="https://boundaryproject.io/help/admin-ui/host-catalogs"]`) .exists(); }); + + test('users can navigate to host catalog and incorrect url autocorrects', async function (assert) { + assert.expect(2); + const projectScope = this.server.create('scope', { + type: 'project', + scope: { id: instances.scopes.org.id, type: 'org' }, + }); + const hostCatalog = this.server.create('host-catalog', { + scope: projectScope, + }); + const incorrectUrl = `${urls.hostCatalogs}/${hostCatalog.id}/host-sets`; + const correctUrl = `/scopes/${projectScope.id}/host-catalogs/${hostCatalog.id}/host-sets`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), correctUrl); + }); }); diff --git a/ui/admin/tests/acceptance/managed-groups/create-test.js b/ui/admin/tests/acceptance/managed-groups/create-test.js index 4a591266e..a0f9450cf 100644 --- a/ui/admin/tests/acceptance/managed-groups/create-test.js +++ b/ui/admin/tests/acceptance/managed-groups/create-test.js @@ -242,4 +242,21 @@ module('Acceptance | managed-groups | create', function (hooks) { assert.dom(ERROR_MSG_SELECTOR).hasText('The request was invalid.'); assert.dom(FIELD_ERROR_TEXT_SELECTOR).hasText('Name is required.'); }); + + test('Users cannot directly navigate to a new managed group route without proper authorization', async function (assert) { + assert.expect(2); + instances.authMethod.authorized_collection_actions['managed-groups'] = + instances.authMethod.authorized_collection_actions[ + 'managed-groups' + ].filter((item) => item !== 'create'); + + await visit(urls.newManagedGroup); + + assert.false( + instances.authMethod.authorized_collection_actions[ + 'managed-groups' + ].includes('create') + ); + assert.strictEqual(currentURL(), urls.managedGroups); + }); }); diff --git a/ui/admin/tests/acceptance/managed-groups/read-test.js b/ui/admin/tests/acceptance/managed-groups/read-test.js index 8a36675df..f778f5518 100644 --- a/ui/admin/tests/acceptance/managed-groups/read-test.js +++ b/ui/admin/tests/acceptance/managed-groups/read-test.js @@ -118,4 +118,23 @@ module('Acceptance | managed-groups | read', function (hooks) { assert.dom(`[href="${urls.ldapManagedGroup}"]`).doesNotExist(); }); + + test('User can navigate to managed group and incorrect url autocorrects', async function (assert) { + assert.expect(2); + const authMethod = this.server.create('auth-method', { + scope: instances.scopes.org, + type: TYPE_AUTH_METHOD_LDAP, + }); + const managedGroup = this.server.create('managed-group', { + scope: instances.scopes.org, + authMethod, + }); + const incorrectUrl = `${urls.managedGroups}/${managedGroup.id}`; + const correctUrl = `${urls.orgScope}/auth-methods/${authMethod.id}/managed-groups/${managedGroup.id}`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), correctUrl); + }); }); diff --git a/ui/admin/tests/acceptance/roles/create-test.js b/ui/admin/tests/acceptance/roles/create-test.js index 7f4573f2d..c76fa59a5 100644 --- a/ui/admin/tests/acceptance/roles/create-test.js +++ b/ui/admin/tests/acceptance/roles/create-test.js @@ -27,7 +27,6 @@ module('Acceptance | roles | create', function (hooks) { project: null, }, role: null, - orgScope: null, }; const urls = { roles: null, @@ -38,7 +37,7 @@ module('Acceptance | roles | create', function (hooks) { hooks.beforeEach(function () { authenticateSession({}); - instances.orgScope = this.server.create( + instances.scopes.org = this.server.create( 'scope', { type: 'org', @@ -52,19 +51,19 @@ module('Acceptance | roles | create', function (hooks) { // mechanism. instances.scopes.project = this.server.create('scope', { type: 'project', - scope: { id: instances.orgScope.id, type: instances.orgScope.type }, + scope: { id: instances.scopes.org.id, type: instances.scopes.org.type }, }); instances.role = this.server.create( 'role', { - scope: instances.orgScope, + scope: instances.scopes.org, }, 'withPrincipals' ); - urls.roles = `/scopes/${instances.orgScope.id}/roles`; + urls.roles = `/scopes/${instances.scopes.org.id}/roles`; urls.role = `${urls.roles}/${instances.role.id}`; urls.newRole = `${urls.roles}/new`; - urls.orgScope = `/scopes/${instances.orgScope.id}`; + urls.orgScope = `/scopes/${instances.scopes.org.id}`; }); test('can create new role', async function (assert) { @@ -80,17 +79,21 @@ module('Acceptance | roles | create', function (hooks) { assert.expect(2); await visit(urls.roles); assert.ok( - instances.orgScope.authorized_collection_actions.roles.includes('create') + instances.scopes.org.authorized_collection_actions.roles.includes( + 'create' + ) ); assert.ok(find(`[href="${urls.newRole}"]`)); }); test('Users cannot navigate to new roles route without proper authorization', async function (assert) { assert.expect(2); - instances.orgScope.authorized_collection_actions.roles = []; + instances.scopes.org.authorized_collection_actions.roles = []; await visit(urls.roles); assert.notOk( - instances.orgScope.authorized_collection_actions.roles.includes('create') + instances.scopes.org.authorized_collection_actions.roles.includes( + 'create' + ) ); assert.notOk(find(`[href="${urls.newRole}"]`)); }); @@ -133,4 +136,21 @@ module('Acceptance | roles | create', function (hooks) { assert.ok(find('[role="alert"]')); assert.ok(find('.hds-form-error__message')); }); + + test('users cannot directly navigate to new role route without proper authorization', async function (assert) { + assert.expect(2); + instances.scopes.org.authorized_collection_actions.roles = + instances.scopes.org.authorized_collection_actions.roles.filter( + (item) => item !== 'create' + ); + + await visit(urls.newRole); + + assert.false( + instances.scopes.org.authorized_collection_actions.roles.includes( + 'create' + ) + ); + assert.strictEqual(currentURL(), urls.roles); + }); }); diff --git a/ui/admin/tests/acceptance/roles/read-test.js b/ui/admin/tests/acceptance/roles/read-test.js index 4d1e5732a..8b450016f 100644 --- a/ui/admin/tests/acceptance/roles/read-test.js +++ b/ui/admin/tests/acceptance/roles/read-test.js @@ -96,4 +96,22 @@ module('Acceptance | roles | read', function (hooks) { await a11yAudit(); assert.strictEqual(currentURL(), urls.role); }); + + test('users can navigate to role and incorrect url autocorrects', async function (assert) { + assert.expect(2); + const orgScope = this.server.create('scope', { + type: 'org', + scope: { id: 'global', type: 'global' }, + }); + const role = this.server.create('role', { + scope: orgScope, + }); + const incorrectUrl = `${urls.roles}/${role.id}/grants`; + const correctUrl = `/scopes/${orgScope.id}/roles/${role.id}/grants`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), correctUrl); + }); }); diff --git a/ui/admin/tests/acceptance/session-recordings/read-test.js b/ui/admin/tests/acceptance/session-recordings/read-test.js index e7f2267e8..4f0fef200 100644 --- a/ui/admin/tests/acceptance/session-recordings/read-test.js +++ b/ui/admin/tests/acceptance/session-recordings/read-test.js @@ -10,7 +10,7 @@ import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { authenticateSession } from 'ember-simple-auth/test-support'; -module('Acceptance | session recordings | read', function (hooks) { +module('Acceptance | session-recordings | read', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); @@ -113,4 +113,15 @@ module('Acceptance | session recordings | read', function (hooks) { await click(LIST_CHANNEL_RECORDING_BUTTON); assert.strictEqual(currentURL(), urls.channelRecording); }); + + test('users can navigate to session recording and incorrect url autocorrects', async function (assert) { + assert.expect(2); + featuresService.enable('ssh-session-recording'); + const incorrectUrl = `/scopes/${instances.scopes.org.id}/session-recordings/${instances.sessionRecording.id}/channels-by-connection`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), urls.sessionRecording); + }); }); diff --git a/ui/admin/tests/acceptance/session-recordings/session-recording/channels-by-connection/channel-test.js b/ui/admin/tests/acceptance/session-recordings/session-recording/channels-by-connection/channel-test.js index d51bb25f7..fbb07214a 100644 --- a/ui/admin/tests/acceptance/session-recordings/session-recording/channels-by-connection/channel-test.js +++ b/ui/admin/tests/acceptance/session-recordings/session-recording/channels-by-connection/channel-test.js @@ -11,11 +11,13 @@ import { authenticateSession } from 'ember-simple-auth/test-support'; import { Response } from 'miragejs'; module( - 'Acceptance | session recordings | session recording | channels by connection | channel', + 'Acceptance | session-recordings | session-recording | channels-by-connection | channel', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + let featuresService; + // Instances const instances = { scopes: { @@ -32,6 +34,7 @@ module( globalScope: null, sessionRecordings: null, sessionRecording: null, + channelRecording: null, }; hooks.beforeEach(function () { @@ -55,22 +58,23 @@ module( urls.globalScope = `/scopes/global`; urls.sessionRecordings = `${urls.globalScope}/session-recordings`; urls.sessionRecording = `${urls.sessionRecordings}/${instances.sessionRecording.id}/channels-by-connection`; - urls.connectionRecording = `${urls.sessionRecording}/${instances.channelRecording.id}`; + urls.channelRecording = `${urls.sessionRecording}/${instances.channelRecording.id}`; + featuresService = this.owner.lookup('service:features'); authenticateSession({}); }); test('user can navigate to a channel', async function (assert) { assert.expect(1); // Visit channel - await visit(urls.connectionRecording); - assert.strictEqual(currentURL(), urls.connectionRecording); + await visit(urls.channelRecording); + assert.strictEqual(currentURL(), urls.channelRecording); }); test('user can view recording with proper authorization', async function (assert) { assert.expect(1); // Visit channel - await visit(urls.connectionRecording); + await visit(urls.channelRecording); // if authorized player will render assert.dom('.session-recording-player').exists(); @@ -81,7 +85,7 @@ module( instances.channelRecording.mime_types = []; // Visit channel - await visit(urls.connectionRecording); + await visit(urls.channelRecording); // if unauthorized player will not render assert.dom('.session-recording-player').doesNotExist(); @@ -95,7 +99,7 @@ module( ); // Visit channel - await visit(urls.connectionRecording); + await visit(urls.channelRecording); // if unauthorized player will not render assert.dom('.session-recording-player').doesNotExist(); @@ -118,7 +122,7 @@ module( ); // Visit channel - await visit(urls.connectionRecording); + await visit(urls.channelRecording); // if there was an error player will not render assert.dom('.session-recording-player').doesNotExist(); @@ -130,11 +134,25 @@ module( test('user can navigate back to session recording screen', async function (assert) { assert.expect(1); // Visit channel - await visit(urls.connectionRecording); + await visit(urls.channelRecording); // click "Back to channels" link in player header await click('.session-recording-player-header > a'); assert.strictEqual(currentURL(), urls.sessionRecording); }); + + test('users can navigate to channel recording and incorrect url autocorrects', async function (assert) { + assert.expect(2); + featuresService.enable('ssh-session-recording'); + const sessionRecording = this.server.create('session-recording', { + scope: instances.scopes.global, + }); + const incorrectUrl = `${urls.sessionRecordings}/${sessionRecording.id}/channels-by-connection/${instances.channelRecording.id}`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), urls.channelRecording); + }); } ); diff --git a/ui/admin/tests/acceptance/storage-buckets/create-test.js b/ui/admin/tests/acceptance/storage-buckets/create-test.js index ad4058347..b75447bc1 100644 --- a/ui/admin/tests/acceptance/storage-buckets/create-test.js +++ b/ui/admin/tests/acceptance/storage-buckets/create-test.js @@ -200,4 +200,21 @@ module('Acceptance | storage-buckets | create', function (hooks) { assert.dom(ALERT_TEXT_SELECTOR).hasText('The request was invalid.'); assert.dom(FIELD_ERROR_TEXT_SELECTOR).hasText('Name is required.'); }); + + test('users cannot directly navigate to new storage bucket route without proper authorization', async function (assert) { + assert.expect(2); + instances.scopes.global.authorized_collection_actions['storage-buckets'] = + instances.scopes.global.authorized_collection_actions[ + 'storage-buckets' + ].filter((item) => item !== 'create'); + + await visit(urls.newStorageBucket); + + assert.false( + instances.scopes.global.authorized_collection_actions[ + 'storage-buckets' + ].includes('create') + ); + assert.strictEqual(currentURL(), urls.storageBuckets); + }); }); diff --git a/ui/admin/tests/acceptance/storage-buckets/read-test.js b/ui/admin/tests/acceptance/storage-buckets/read-test.js index bf09e6f38..a265d86a8 100644 --- a/ui/admin/tests/acceptance/storage-buckets/read-test.js +++ b/ui/admin/tests/acceptance/storage-buckets/read-test.js @@ -87,4 +87,14 @@ module('Acceptance | storage-buckets | read', function (hooks) { assert.dom(MESSAGE_SELECTOR).hasText('Error 404'); }); + + test('users can navigate to storage bucket and incorrect url autocorrects', async function (assert) { + assert.expect(2); + const incorrectUrl = `/scopes/${instances.scopes.org.id}/storage-buckets/${instances.storageBucket.id}`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), urls.storageBucket); + }); }); diff --git a/ui/admin/tests/acceptance/targets/create-test.js b/ui/admin/tests/acceptance/targets/create-test.js index 86f3ced0d..d890a9ba0 100644 --- a/ui/admin/tests/acceptance/targets/create-test.js +++ b/ui/admin/tests/acceptance/targets/create-test.js @@ -443,4 +443,21 @@ module('Acceptance | targets | create', function (hooks) { assert.false(featuresService.isEnabled('target-network-address')); assert.dom('[name="address"]').doesNotExist(); }); + + test('users cannot directly navigate to new storage bucket route without proper authorization', async function (assert) { + assert.expect(2); + instances.scopes.project.authorized_collection_actions.targets = + instances.scopes.project.authorized_collection_actions.targets.filter( + (item) => item !== 'create' + ); + + await visit(urls.newSSHTarget); + + assert.false( + instances.scopes.project.authorized_collection_actions.targets.includes( + 'create' + ) + ); + assert.strictEqual(currentURL(), urls.targets); + }); }); diff --git a/ui/admin/tests/acceptance/targets/enable-session-recording-test.js b/ui/admin/tests/acceptance/targets/enable-session-recording-test.js index b81ac5018..644bdda6e 100644 --- a/ui/admin/tests/acceptance/targets/enable-session-recording-test.js +++ b/ui/admin/tests/acceptance/targets/enable-session-recording-test.js @@ -49,6 +49,7 @@ module('Acceptance | targets | enable session recording', function (hooks) { target: null, enableSessionRecording: null, storageBuckets: null, + storageBucket: null, newStorageBucket: null, }; @@ -78,13 +79,13 @@ module('Acceptance | targets | enable session recording', function (hooks) { storageBucketTwo = instances.storageBucket[1]; // Generate route URLs for resources - urls.globalScope = `/scopes/global/scopes`; + urls.globalScope = `/scopes/global`; urls.orgScope = `/scopes/${instances.scopes.org.id}/scopes`; urls.projectScope = `/scopes/${instances.scopes.project.id}`; urls.targets = `${urls.projectScope}/targets`; urls.target = `${urls.targets}/${instances.target.id}`; urls.enableSessionRecording = `${urls.target}/enable-session-recording`; - urls.storageBuckets = `${urls.projectScope}/storage-buckets`; + urls.storageBuckets = `${urls.globalScope}/storage-buckets`; urls.newStorageBucket = `${urls.enableSessionRecording}/create-storage-bucket`; urls.storageBucket = `${urls.storageBuckets}/${storageBucketOne.id}`; diff --git a/ui/admin/tests/acceptance/targets/enable-session-recording/index-test.js b/ui/admin/tests/acceptance/targets/enable-session-recording/index-test.js index b81ac5018..644bdda6e 100644 --- a/ui/admin/tests/acceptance/targets/enable-session-recording/index-test.js +++ b/ui/admin/tests/acceptance/targets/enable-session-recording/index-test.js @@ -49,6 +49,7 @@ module('Acceptance | targets | enable session recording', function (hooks) { target: null, enableSessionRecording: null, storageBuckets: null, + storageBucket: null, newStorageBucket: null, }; @@ -78,13 +79,13 @@ module('Acceptance | targets | enable session recording', function (hooks) { storageBucketTwo = instances.storageBucket[1]; // Generate route URLs for resources - urls.globalScope = `/scopes/global/scopes`; + urls.globalScope = `/scopes/global`; urls.orgScope = `/scopes/${instances.scopes.org.id}/scopes`; urls.projectScope = `/scopes/${instances.scopes.project.id}`; urls.targets = `${urls.projectScope}/targets`; urls.target = `${urls.targets}/${instances.target.id}`; urls.enableSessionRecording = `${urls.target}/enable-session-recording`; - urls.storageBuckets = `${urls.projectScope}/storage-buckets`; + urls.storageBuckets = `${urls.globalScope}/storage-buckets`; urls.newStorageBucket = `${urls.enableSessionRecording}/create-storage-bucket`; urls.storageBucket = `${urls.storageBuckets}/${storageBucketOne.id}`; diff --git a/ui/admin/tests/acceptance/targets/read-test.js b/ui/admin/tests/acceptance/targets/read-test.js index c48f89e86..a3075ce8d 100644 --- a/ui/admin/tests/acceptance/targets/read-test.js +++ b/ui/admin/tests/acceptance/targets/read-test.js @@ -193,4 +193,14 @@ module('Acceptance | targets | read', function (hooks) { .dom(`[href="https://boundaryproject.io/help/admin-ui/targets"]`) .exists(); }); + + test('users can navigate to target and incorrect url autocorrects', async function (assert) { + assert.expect(2); + const incorrectUrl = `/scopes/${instances.scopes.org.id}/targets/${instances.sshTarget.id}`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), urls.sshTarget); + }); }); diff --git a/ui/admin/tests/acceptance/users/create-test.js b/ui/admin/tests/acceptance/users/create-test.js index c1d11e3c0..e705556f0 100644 --- a/ui/admin/tests/acceptance/users/create-test.js +++ b/ui/admin/tests/acceptance/users/create-test.js @@ -145,4 +145,21 @@ module('Acceptance | users | create', function (hooks) { assert.dom('.rose-notification-body').hasText('The request was invalid.'); assert.dom('.rose-form-error-message').hasText('Name is required.'); }); + + test('users cannot directly navigate to new user route without proper authorization', async function (assert) { + assert.expect(2); + instances.scopes.org.authorized_collection_actions.users = + instances.scopes.org.authorized_collection_actions.users.filter( + (item) => item !== 'create' + ); + + await visit(urls.newUser); + + assert.false( + instances.scopes.org.authorized_collection_actions.users.includes( + 'create' + ) + ); + assert.strictEqual(currentURL(), urls.users); + }); }); diff --git a/ui/admin/tests/acceptance/users/read-test.js b/ui/admin/tests/acceptance/users/read-test.js index 3d19f046e..939b67ce7 100644 --- a/ui/admin/tests/acceptance/users/read-test.js +++ b/ui/admin/tests/acceptance/users/read-test.js @@ -90,4 +90,14 @@ module('Acceptance | users | read', function (hooks) { .dom(`[href="https://boundaryproject.io/help/admin-ui/users"]`) .exists(); }); + + test('users can navigate to user and incorrect url autocorrects', async function (assert) { + assert.expect(2); + const incorrectUrl = `/scopes/global/users/${instances.user.id}`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), urls.user); + }); }); diff --git a/ui/admin/tests/acceptance/workers/create-test.js b/ui/admin/tests/acceptance/workers/create-test.js index fad34d3ef..fca2aa0f4 100644 --- a/ui/admin/tests/acceptance/workers/create-test.js +++ b/ui/admin/tests/acceptance/workers/create-test.js @@ -4,7 +4,14 @@ */ import { module, test } from 'qunit'; -import { visit, fillIn, click, find, findAll } from '@ember/test-helpers'; +import { + visit, + fillIn, + click, + find, + findAll, + currentURL, +} from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import { Response } from 'miragejs'; @@ -144,4 +151,21 @@ module('Acceptance | workers | create', function (hooks) { 'Displays primary error message.' ); }); + + test('users cannot directly navigate to new worker route without proper authorization', async function (assert) { + assert.expect(2); + globalScope.authorized_collection_actions.workers = + globalScope.authorized_collection_actions.workers.filter( + (item) => item !== 'create:worker-led' + ); + + await visit(newWorkerURL); + + assert.false( + globalScope.authorized_collection_actions.workers.includes( + 'create:worker-led' + ) + ); + assert.strictEqual(currentURL(), workersURL); + }); }); diff --git a/ui/admin/tests/acceptance/workers/read-test.js b/ui/admin/tests/acceptance/workers/read-test.js index f81fc9d5d..dc4909d3e 100644 --- a/ui/admin/tests/acceptance/workers/read-test.js +++ b/ui/admin/tests/acceptance/workers/read-test.js @@ -86,4 +86,18 @@ module('Acceptance | workers | read', function (hooks) { assert.dom('main tbody .rose-table-header-cell:nth-child(1) a').isVisible(); }); + + test('users can navigate to worker and incorrect url autocorrects', async function (assert) { + assert.expect(2); + const orgScope = this.server.create('scope', { + type: 'org', + scope: { id: 'global', type: 'global' }, + }); + const incorrectUrl = `/scopes/${orgScope.id}/workers/${instances.worker.id}`; + + await visit(incorrectUrl); + + assert.notEqual(currentURL(), incorrectUrl); + assert.strictEqual(currentURL(), urls.worker); + }); }); diff --git a/ui/admin/tests/unit/utils/param-value-finder-test.js b/ui/admin/tests/unit/utils/param-value-finder-test.js new file mode 100644 index 000000000..3f4544636 --- /dev/null +++ b/ui/admin/tests/unit/utils/param-value-finder-test.js @@ -0,0 +1,36 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { paramValueFinder } from 'admin/utils/param-value-finder'; + +module('Unit | Utility | param-value-finder', function (hooks) { + setupTest(hooks); + + let router; + + hooks.beforeEach(function () { + router = this.owner.lookup('service:router'); + }); + + test('returns empty array when null or empty routeInfo object is used', function (assert) { + assert.expect(2); + let result = paramValueFinder('scopes', null); + + assert.deepEqual(result, []); + + result = paramValueFinder('scopes', {}); + + assert.deepEqual(result, []); + }); + + test('returns correct array when routeInfo object is used', function (assert) { + assert.expect(2); + let routeInfo = router.recognize('/scopes'); + let result = paramValueFinder('scopes', routeInfo.parent); + + assert.deepEqual(result, []); + + routeInfo = router.recognize('/scopes/global/auth-methods'); + result = paramValueFinder('scopes', routeInfo.parent); + assert.deepEqual(result, ['global']); + }); +});