Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions ui/app/components/identity/item-groups.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
}}
{{#each @model.direct_group_ids as |gid|}}
<LinkTo @route="vault.cluster.access.identity.show" @models={{array "groups" gid "details"}} class="list-item-row">
<Icon @name="folder" class="has-text-grey-light" />{{gid}}
<Icon @name="folder" class="has-text-grey-light" />
<Identity::ItemName @id={{gid}} @items={{@model.groups}} />
</LinkTo>
{{/each}}
{{#each @model.inherited_group_ids as |gid|}}
<LinkedBlock @params={{array "vault.cluster.access.identity.show" "groups" gid "details"}} class="list-item-row">
<LinkTo @route="vault.cluster.access.identity.show" @models={{array "groups" gid "details"}} class="has-text-black">
<Icon @name="folder" class="has-text-grey-light" />{{gid}}
<Icon @name="folder" class="has-text-grey-light" />
<Identity::ItemName @id={{gid}} @items={{@model.groups}} />
</LinkTo>
<Hds::Badge @text="inherited" />
</LinkedBlock>
Expand Down
6 changes: 4 additions & 2 deletions ui/app/components/identity/item-members.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
@models={{array "groups" gid "details"}}
class="is-block has-text-black has-text-weight-semibold"
>
<Icon @name="folder" class="has-text-grey-light" />{{gid}}
<Icon @name="folder" class="has-text-grey-light" />
<Identity::ItemName @id={{gid}} @items={{@model.groups}} />
</LinkTo>
</LF.Item>
<LF.Item>
Expand All @@ -33,7 +34,8 @@
@models={{array "entities" gid "details"}}
class="is-block has-text-black has-text-weight-semibold"
>
<Icon @name="user" class="has-text-grey-light" />{{gid}}
<Icon @name="user" class="has-text-grey-light" />
<Identity::ItemName @id={{gid}} @items={{@model.entities}} />
</LinkTo>
</LF.Item>
<LF.Item>
Expand Down
8 changes: 8 additions & 0 deletions ui/app/components/identity/item-name.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{{!
Copyright IBM Corp. 2016, 2026
SPDX-License-Identifier: BUSL-1.1
}}

{{#let (find-by "id" @id @items) as |item|}}
<span title={{if item.name @id}} data-test-identity-item-name={{@id}}>{{or item.name @id}}</span>
{{/let}}
3 changes: 2 additions & 1 deletion ui/app/components/identity/item-parent-groups.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
@models={{array "groups" gid "details"}}
class="is-block has-text-black has-text-weight-semibold"
>
<Icon @name="folder" class="has-text-grey-light" />{{gid}}
<Icon @name="folder" class="has-text-grey-light" />
<Identity::ItemName @id={{gid}} @items={{@model.groups}} />
</LinkTo>
</LF.Item>
</Hds::Layout::Flex>
Expand Down
49 changes: 46 additions & 3 deletions ui/app/routes/vault/cluster/access/identity/show.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ import { hash } from 'rsvp';
import Route from '@ember/routing/route';
import { TABS } from 'vault/helpers/tabs-for-identity-show';
import { service } from '@ember/service';
import { fetchIdentityItems } from 'vault/utils/identity-helpers';

const RELATED_IDENTITIES = {
entity: {
groups: [
{
identityType: 'group',
modelKey: 'groups',
idKeys: ['group_ids', 'direct_group_ids', 'inherited_group_ids'],
},
],
},
group: {
members: [
{ identityType: 'group', modelKey: 'groups', idKeys: ['member_group_ids'] },
{ identityType: 'entity', modelKey: 'entities', idKeys: ['member_entity_ids'] },
],
'parent-groups': [{ identityType: 'group', modelKey: 'groups', idKeys: ['parent_group_ids'] }],
},
};

export default class IdentityShowRoute extends Route {
@service router;
Expand All @@ -25,15 +45,38 @@ export default class IdentityShowRoute extends Route {
}

const methodType = itemType === 'entity' ? 'entityReadById' : 'groupReadById';
const { data } = await this.api.identity[methodType](params.item_id);
const canAddAlias = (await this.capabilities.for('groupAlias').canCreate) || false;
const [response, canAddAlias] = await Promise.all([
this.api.identity[methodType](params.item_id),
this.capabilities.for('groupAlias').canCreate,
]);
const relatedIdentities = await this.fetchRelatedIdentities(itemType, section, response.data);

return hash({
model: { ...data, identityType: itemType, canAddAlias },
model: {
...response.data,
...relatedIdentities,
identityType: itemType,
canAddAlias: canAddAlias || false,
},
section,
});
}

async fetchRelatedIdentities(itemType, section, model) {
const identities = (RELATED_IDENTITIES[itemType]?.[section] || []).filter(({ idKeys }) =>
idKeys.some((key) => model[key]?.length)
);
const results = await Promise.allSettled(
identities.map(({ identityType }) => fetchIdentityItems({ identityType, api: this.api }))
);

return identities.reduce((related, { modelKey }, index) => {
const result = results[index];
related[modelKey] = result.status === 'fulfilled' ? result.value : [];
return related;
}, {});
}

afterModel(resolvedModel) {
const { section, model } = resolvedModel;

Expand Down
108 changes: 108 additions & 0 deletions ui/tests/acceptance/access/identity/shared-identity-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { capitalize } from '@ember/string';
import { singularize } from 'ember-inflector';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { Response } from 'miragejs';

// Helper to create an entity or group
async function createEntityOrGroup(itemType, name) {
Expand Down Expand Up @@ -148,4 +149,111 @@ module('Acceptance | Create groups and entities alias test', function (hooks) {
assert.dom(GENERAL.latestFlashContent).includesText('Successfully deleted');
});
}

test('entity groups: it displays group names and keeps IDs in tooltips', async function (assert) {
const entityId = 'entity-id';
const directGroupId = 'direct-group-id';
const inheritedGroupId = 'inherited-group-id';
let groupListRequests = 0;

this.server.get(`/identity/entity/id/${entityId}`, () => ({
data: {
id: entityId,
name: 'example entity',
direct_group_ids: [directGroupId],
inherited_group_ids: [inheritedGroupId],
},
}));
this.server.get('/identity/group/id', () => {
groupListRequests++;
return {
data: {
keys: [directGroupId, inheritedGroupId],
key_info: {
[directGroupId]: { name: 'direct group' },
[inheritedGroupId]: { name: 'inherited group' },
},
},
};
});

await visit(`/vault/access/identity/entities/${entityId}/groups`);

assert
.dom(`[data-test-identity-item-name="${directGroupId}"]`)
.hasText('direct group')
.hasAttribute('title', directGroupId);
assert
.dom(`[data-test-identity-item-name="${inheritedGroupId}"]`)
.hasText('inherited group')
.hasAttribute('title', inheritedGroupId);
assert.strictEqual(groupListRequests, 1, 'loads all group names with one list request');
});

test('group members: it displays names for member groups and entities', async function (assert) {
const groupId = 'group-id';
const memberGroupId = 'member-group-id';
const memberEntityId = 'member-entity-id';
let groupListRequests = 0;
let entityListRequests = 0;

this.server.get(`/identity/group/id/${groupId}`, () => ({
data: {
id: groupId,
name: 'example group',
type: 'internal',
member_group_ids: [memberGroupId],
member_entity_ids: [memberEntityId],
},
}));
this.server.get('/identity/group/id', () => {
groupListRequests++;
return {
data: {
keys: [memberGroupId],
key_info: { [memberGroupId]: { name: 'member group' } },
},
};
});
this.server.get('/identity/entity/id', () => {
entityListRequests++;
return {
data: {
keys: [memberEntityId],
key_info: { [memberEntityId]: { name: 'member entity' } },
},
};
});

await visit(`/vault/access/identity/groups/${groupId}/members`);

assert
.dom(`[data-test-identity-item-name="${memberGroupId}"]`)
.hasText('member group')
.hasAttribute('title', memberGroupId);
assert
.dom(`[data-test-identity-item-name="${memberEntityId}"]`)
.hasText('member entity')
.hasAttribute('title', memberEntityId);
assert.strictEqual(groupListRequests, 1, 'loads all member group names with one list request');
assert.strictEqual(entityListRequests, 1, 'loads all member entity names with one list request');
});

test('related identity names: it falls back to IDs when the list request fails', async function (assert) {
const entityId = 'entity-id';
const groupId = 'group-id';

this.server.get(`/identity/entity/id/${entityId}`, () => ({
data: {
id: entityId,
name: 'example entity',
direct_group_ids: [groupId],
},
}));
this.server.get('/identity/group/id', () => new Response(403, {}, { errors: ['permission denied'] }));

await visit(`/vault/access/identity/entities/${entityId}/groups`);

assert.dom(`[data-test-identity-item-name="${groupId}"]`).hasText(groupId);
});
});