diff --git a/acs-admin/src/pages/AccessControl/Permissions/GroupDropdown.vue b/acs-admin/src/pages/AccessControl/Permissions/GroupDropdown.vue index d441076a1..d222421a2 100644 --- a/acs-admin/src/pages/AccessControl/Permissions/GroupDropdown.vue +++ b/acs-admin/src/pages/AccessControl/Permissions/GroupDropdown.vue @@ -26,16 +26,19 @@ function copy(id: string) { } function handleDelete() { + const who = props.row.original.principal.kerberos + ?? props.row.original.principal.name + ?? props.row.original.principal.uuid useDialog({ title: 'Remove from group?', - message: `Are you sure that you want to remove ${props.row.original.principal.kerberos} from the ${props.row.original.name} group? The user will lose all permissions associated with the group.`, + message: `Are you sure that you want to remove ${who} from the ${props.row.original.name} group? The user will lose all permissions associated with the group.`, confirmText: 'Remove from Group', onConfirm: () => { s.client.Auth.remove_from_group(props.row.original.uuid, props.row.original.principal.uuid).then(() => { - toast.success(`${props.row.original.principal.kerberos} has been removed from the ${props.row.original.name} group`) + toast.success(`${who} has been removed from the ${props.row.original.name} group`) }).catch((err) => { - toast.error(`Unable to remove ${props.row.original.principal.kerberos} from ${props.row.original.name}`) - console.error(`Unable to remove ${props.row.original.principal.kerberos} from ${props.row.original.name}`, err) + toast.error(`Unable to remove ${who} from ${props.row.original.name}`) + console.error(`Unable to remove ${who} from ${props.row.original.name}`, err) }) } }); diff --git a/acs-admin/src/pages/AccessControl/Principals/PrincipalList.vue b/acs-admin/src/pages/AccessControl/Principals/PrincipalList.vue index bc2d6ef5e..c5eb05555 100644 --- a/acs-admin/src/pages/AccessControl/Principals/PrincipalList.vue +++ b/acs-admin/src/pages/AccessControl/Principals/PrincipalList.vue @@ -60,7 +60,7 @@ export default { value: p, } }), - principals: this.p.data.map((p) => p.kerberos).filter((v, i, a) => a.indexOf(v) === i).map((p) => { + principals: this.p.data.map((p) => p.kerberos).filter(Boolean).filter((v, i, a) => a.indexOf(v) === i).map((p) => { return { label: p, value: p, diff --git a/acs-admin/src/pages/AccessControl/Principals/PrincipalManagementSidebar.vue b/acs-admin/src/pages/AccessControl/Principals/PrincipalManagementSidebar.vue index 3044510e7..ac06c8ca8 100644 --- a/acs-admin/src/pages/AccessControl/Principals/PrincipalManagementSidebar.vue +++ b/acs-admin/src/pages/AccessControl/Principals/PrincipalManagementSidebar.vue @@ -7,7 +7,7 @@ {{principal.name}} {{ principal.class.name ?? "Principal" }} - Principal - + {{principal.kerberos}} diff --git a/acs-admin/src/pages/AccessControl/Principals/principalListColumns.ts b/acs-admin/src/pages/AccessControl/Principals/principalListColumns.ts index c695be97f..f46ff9171 100644 --- a/acs-admin/src/pages/AccessControl/Principals/principalListColumns.ts +++ b/acs-admin/src/pages/AccessControl/Principals/principalListColumns.ts @@ -13,7 +13,7 @@ import {toast} from "vue-sonner"; export interface PrincipalMapping { uuid: string - kerberos: string + kerberos: string | null name: string class: { uuid: string @@ -56,7 +56,12 @@ export const columns: ColumnDef[] = [{ }), cell: ({row}) => { - return h('span', {class: 'max-w-[500px] truncate font-medium'}, row.getValue('kerberos')) + const krb = row.getValue('kerberos') + // Identity-less principals (OIDC service accounts) have no + // kerberos name; show a muted placeholder rather than a blank. + return krb + ? h('span', {class: 'max-w-[500px] truncate font-medium'}, krb) + : h('span', {class: 'max-w-[500px] truncate italic text-gray-400'}, 'No identity') }, filterFn: (row, id, value) => { return value.includes(row.getValue(id)) diff --git a/acs-admin/src/store/usePrincipalStore.js b/acs-admin/src/store/usePrincipalStore.js index aae8e79f6..a3804b015 100644 --- a/acs-admin/src/store/usePrincipalStore.js +++ b/acs-admin/src/store/usePrincipalStore.js @@ -51,11 +51,29 @@ export const usePrincipalStore = defineStore('principal', { this.loading = true; /* XXX This API needs fixing */ fetch.cache = "reload"; - const princs = await auth.list_principals(); - const rv = Promise.all(princs.map(uuid => + /* The Auth service only lists principals holding an + * identity (kerberos etc.). Principals can exist without + * one - service-setup creates identity-less principals for + * OIDC service accounts - so union in the members of the + * ConfigDB Principal class, or grants can never be edited + * for them here. */ + const [princs, members] = await Promise.all([ + auth.list_principals(), + client.ConfigDB.class_members(UUIDs.Class.Principal) + .catch(err => { + console.error("Principal class fetch failed: %o", err); + return []; + }), + ]); + const rv = await Promise.all(princs.map(uuid => auth.get_identity(uuid, "kerberos") .catch(ServiceError.check(403, 404)) .then(krb => [uuid, krb ?? "UNKNOWN"]))); + const known = new Set(princs); + for (const uuid of members) { + if (!known.has(uuid)) + rv.push([uuid, null]); + } fetch.cache = "default"; return rv; }),