From 838415cb6e1d6e200d549b65b79af9f49f029d9b Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 30 Jun 2026 11:57:43 -0700 Subject: [PATCH 1/5] Role-restricted API keys --- packages/components/package.json | 2 +- .../components/releaseNotes/components.md | 4 +++ .../components/security/APIWrapper.ts | 6 ++-- .../internal/components/user/APIKeysPanel.tsx | 36 +++++++++++++++++-- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/packages/components/package.json b/packages/components/package.json index 61c93c2765..8eddfcbc21 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.45.2", + "version": "7.45.3-fb-role-restricted-api-keys.0", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index a441459736..e5b34a897f 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -1,6 +1,10 @@ # @labkey/components Components, models, actions, and utility functions for LabKey applications and pages +### version 7.45.3 +*Released*: TBD +- Add role restriction option to API key dialog + ### version 7.45.2 *Released*: 29 June 2026 - GitHub Issue #1023: Add redirect() helper that uses core-safeRedirect when necessary to check url before redirecting diff --git a/packages/components/src/internal/components/security/APIWrapper.ts b/packages/components/src/internal/components/security/APIWrapper.ts index da3acbc47a..3aca61018b 100644 --- a/packages/components/src/internal/components/security/APIWrapper.ts +++ b/packages/components/src/internal/components/security/APIWrapper.ts @@ -68,7 +68,7 @@ interface AuthenticationConfigurationResponse { export interface SecurityAPIWrapper { addGroupMembers: (groupId: number, principalIds: number[], projectPath: string) => Promise; - createApiKey: (type?: string, description?: string) => Promise; + createApiKey: (type?: string, description?: string, role?: string) => Promise; createGroup: (groupName: string, projectPath: string) => Promise; deleteApiKeys: (selections: Set) => Promise; deleteContainer: (options: DeleteContainerOptions) => Promise>; @@ -133,11 +133,11 @@ export class ServerSecurityAPIWrapper implements SecurityAPIWrapper { }); }; - createApiKey = async (type = 'apikey', description?: string): Promise => { + createApiKey = async (type = 'apikey', description?: string, role?: string): Promise => { const response = await request<{ apikey: string }>({ url: ActionURL.buildURL('security', 'createApiKey.api'), method: 'POST', - jsonData: { type, description }, + jsonData: { type, description, role }, errorLogMsg: 'Problem generating the apiKey for this user.', }); diff --git a/packages/components/src/internal/components/user/APIKeysPanel.tsx b/packages/components/src/internal/components/user/APIKeysPanel.tsx index 776d129c64..bd524aaa22 100644 --- a/packages/components/src/internal/components/user/APIKeysPanel.tsx +++ b/packages/components/src/internal/components/user/APIKeysPanel.tsx @@ -3,6 +3,7 @@ * any form or by any electronic or mechanical means without written permission from LabKey Corporation. */ import React, { ChangeEvent, FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { List } from 'immutable'; import { ActionURL } from '@labkey/api'; @@ -31,6 +32,7 @@ import { GridPanel } from '../../../public/QueryModel/GridPanel'; import { Modal } from '../../Modal'; import { getHelpLink, HelpLink } from '../../util/helpLinks'; import { biologicsIsPrimaryApp } from '../../app/products'; +import { SecurityRole } from '../permissions/models'; const API_KEYS_QUERY_HREF = ActionURL.buildURL('query', 'executeQuery.view', '/', { schemaName: 'core', @@ -101,19 +103,21 @@ interface ModalProps extends KeyGeneratorProps { export const KeyGeneratorModal: FC = props => { const { type, afterCreate, noun, onClose } = props; const [description, setDescription] = useState(); + const [role, setRole] = useState(); + const [roles, setRoles] = useState>(List()); const { api } = useAppContext(); const [error, setError] = useState(false); const [keyValue, setKeyValue] = useState(undefined); const onGenerateKey = useCallback(async () => { try { - const key = await api.security.createApiKey(type, description); + const key = await api.security.createApiKey(type, description, role); setKeyValue(key); afterCreate?.(); } catch (e) { setError(true); } - }, [api.security, type, afterCreate, description]); + }, [api.security, type, afterCreate, description, role]); useEffect(() => { (async () => { @@ -123,6 +127,12 @@ export const KeyGeneratorModal: FC = props => { })(); }, [type, onGenerateKey]); + useEffect(() => { + if (type === 'apikey') { + api.security.fetchRoles().then(setRoles).catch(() => {}); + } + }, [api.security, type]); + const onCopyKey = useCallback(() => { const handleCopy = (event: ClipboardEvent): void => { setCopyValue(event, keyValue); @@ -137,6 +147,10 @@ export const KeyGeneratorModal: FC = props => { setDescription(event.target.value); }, []); + const changeRole = useCallback((event: ChangeEvent) => { + setRole(event.target.value || undefined); + }, []); + return ( = props => { autoFocus placeholder="Enter description of key usage (optional)" /> + {roles.size > 0 && ( +
+ + +
+ )} )} {!!keyValue && ( From 635868d895df84a82708b02e814891bb100fa138 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 30 Jun 2026 12:19:01 -0700 Subject: [PATCH 2/5] update test --- .../src/internal/components/user/APIKeysPanel.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/components/src/internal/components/user/APIKeysPanel.test.tsx b/packages/components/src/internal/components/user/APIKeysPanel.test.tsx index 6f354d851d..d8625b0ccd 100644 --- a/packages/components/src/internal/components/user/APIKeysPanel.test.tsx +++ b/packages/components/src/internal/components/user/APIKeysPanel.test.tsx @@ -4,6 +4,7 @@ */ import React from 'react'; +import { List } from 'immutable'; import { waitFor } from '@testing-library/dom'; import { renderWithAppContext } from '../../test/reactTestLibraryHelpers'; @@ -80,6 +81,7 @@ describe('KeyGeneratorModal', () => { api: { security: { createApiKey: apiKeyFn, + fetchRoles: jest.fn().mockResolvedValue(List()), }, }, }, From 854433bbdbd7278b6c151997f676d7cf73b1a29a Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 30 Jun 2026 14:47:24 -0700 Subject: [PATCH 3/5] @labkey/components@7.45-3-fb-role-restricted-api-keys.1 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 7c223a9bf4..8314602183 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.45.2", + "version": "7.45.3-fb-role-restricted-api-keys.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.45.2", + "version": "7.45.3-fb-role-restricted-api-keys.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 8eddfcbc21..42c92a222f 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.45.3-fb-role-restricted-api-keys.0", + "version": "7.45.3-fb-role-restricted-api-keys.1", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 46358e2907b81441c43b182285468e12057b8a59 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 30 Jun 2026 16:18:22 -0700 Subject: [PATCH 4/5] Use custom API action to convey a relevant subset of roles --- .../internal/components/security/APIWrapper.ts | 14 ++++++++++++++ .../components/user/APIKeysPanel.test.tsx | 3 +-- .../src/internal/components/user/APIKeysPanel.tsx | 15 +++++++-------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/components/src/internal/components/security/APIWrapper.ts b/packages/components/src/internal/components/security/APIWrapper.ts index 3aca61018b..f75757f191 100644 --- a/packages/components/src/internal/components/security/APIWrapper.ts +++ b/packages/components/src/internal/components/security/APIWrapper.ts @@ -56,6 +56,11 @@ export interface RemoveGroupMembersResponse { removed: number[]; } +export interface ApiKeyRole { + displayName: string; + uniqueName: string; +} + export interface AuthenticationConfiguration { description: string; reauthUrl: string; @@ -69,6 +74,7 @@ interface AuthenticationConfigurationResponse { export interface SecurityAPIWrapper { addGroupMembers: (groupId: number, principalIds: number[], projectPath: string) => Promise; createApiKey: (type?: string, description?: string, role?: string) => Promise; + getApiKeyRoles: () => Promise; createGroup: (groupName: string, projectPath: string) => Promise; deleteApiKeys: (selections: Set) => Promise; deleteContainer: (options: DeleteContainerOptions) => Promise>; @@ -144,6 +150,13 @@ export class ServerSecurityAPIWrapper implements SecurityAPIWrapper { return response.apikey; }; + getApiKeyRoles = (): Promise => { + return request({ + url: ActionURL.buildURL('security', 'getApiKeyRoles.api'), + errorLogMsg: 'Failed to load API key roles.', + }); + }; + deleteApiKeys(selections: Set): Promise { const rows = []; selections.forEach(selection => { @@ -453,6 +466,7 @@ export function getSecurityTestAPIWrapper( return { addGroupMembers: mockFn(), createApiKey: mockFn(), + getApiKeyRoles: mockFn(), deleteApiKeys: mockFn(), createGroup: mockFn(), deleteContainer: mockFn(), diff --git a/packages/components/src/internal/components/user/APIKeysPanel.test.tsx b/packages/components/src/internal/components/user/APIKeysPanel.test.tsx index d8625b0ccd..58709d20fc 100644 --- a/packages/components/src/internal/components/user/APIKeysPanel.test.tsx +++ b/packages/components/src/internal/components/user/APIKeysPanel.test.tsx @@ -4,7 +4,6 @@ */ import React from 'react'; -import { List } from 'immutable'; import { waitFor } from '@testing-library/dom'; import { renderWithAppContext } from '../../test/reactTestLibraryHelpers'; @@ -81,7 +80,7 @@ describe('KeyGeneratorModal', () => { api: { security: { createApiKey: apiKeyFn, - fetchRoles: jest.fn().mockResolvedValue(List()), + getApiKeyRoles: jest.fn().mockResolvedValue([]), }, }, }, diff --git a/packages/components/src/internal/components/user/APIKeysPanel.tsx b/packages/components/src/internal/components/user/APIKeysPanel.tsx index bd524aaa22..a7466a72c3 100644 --- a/packages/components/src/internal/components/user/APIKeysPanel.tsx +++ b/packages/components/src/internal/components/user/APIKeysPanel.tsx @@ -3,7 +3,6 @@ * any form or by any electronic or mechanical means without written permission from LabKey Corporation. */ import React, { ChangeEvent, FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { List } from 'immutable'; import { ActionURL } from '@labkey/api'; @@ -32,7 +31,7 @@ import { GridPanel } from '../../../public/QueryModel/GridPanel'; import { Modal } from '../../Modal'; import { getHelpLink, HelpLink } from '../../util/helpLinks'; import { biologicsIsPrimaryApp } from '../../app/products'; -import { SecurityRole } from '../permissions/models'; +import { ApiKeyRole } from '../security/APIWrapper'; const API_KEYS_QUERY_HREF = ActionURL.buildURL('query', 'executeQuery.view', '/', { schemaName: 'core', @@ -104,7 +103,7 @@ export const KeyGeneratorModal: FC = props => { const { type, afterCreate, noun, onClose } = props; const [description, setDescription] = useState(); const [role, setRole] = useState(); - const [roles, setRoles] = useState>(List()); + const [roles, setRoles] = useState([]); const { api } = useAppContext(); const [error, setError] = useState(false); const [keyValue, setKeyValue] = useState(undefined); @@ -129,7 +128,7 @@ export const KeyGeneratorModal: FC = props => { useEffect(() => { if (type === 'apikey') { - api.security.fetchRoles().then(setRoles).catch(() => {}); + api.security.getApiKeyRoles().then(setRoles).catch(() => {}); } }, [api.security, type]); @@ -171,17 +170,17 @@ export const KeyGeneratorModal: FC = props => { autoFocus placeholder="Enter description of key usage (optional)" /> - {roles.size > 0 && ( + {roles.length > 0 && (
- +