diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 7c223a9bf4..81d109217d 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.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.45.2", + "version": "7.45.3-fb-role-restricted-api-keys.2", "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 61c93c2765..553576072e 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.2", "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..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; @@ -68,7 +73,8 @@ 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; + getApiKeyRoles: () => Promise; createGroup: (groupName: string, projectPath: string) => Promise; deleteApiKeys: (selections: Set) => Promise; deleteContainer: (options: DeleteContainerOptions) => Promise>; @@ -133,17 +139,24 @@ 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.', }); 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 6f354d851d..58709d20fc 100644 --- a/packages/components/src/internal/components/user/APIKeysPanel.test.tsx +++ b/packages/components/src/internal/components/user/APIKeysPanel.test.tsx @@ -80,6 +80,7 @@ describe('KeyGeneratorModal', () => { api: { security: { createApiKey: apiKeyFn, + 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 776d129c64..a7466a72c3 100644 --- a/packages/components/src/internal/components/user/APIKeysPanel.tsx +++ b/packages/components/src/internal/components/user/APIKeysPanel.tsx @@ -31,6 +31,7 @@ import { GridPanel } from '../../../public/QueryModel/GridPanel'; import { Modal } from '../../Modal'; import { getHelpLink, HelpLink } from '../../util/helpLinks'; import { biologicsIsPrimaryApp } from '../../app/products'; +import { ApiKeyRole } from '../security/APIWrapper'; const API_KEYS_QUERY_HREF = ActionURL.buildURL('query', 'executeQuery.view', '/', { schemaName: 'core', @@ -101,19 +102,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([]); 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 +126,12 @@ export const KeyGeneratorModal: FC = props => { })(); }, [type, onGenerateKey]); + useEffect(() => { + if (type === 'apikey') { + api.security.getApiKeyRoles().then(setRoles).catch(() => {}); + } + }, [api.security, type]); + const onCopyKey = useCallback(() => { const handleCopy = (event: ClipboardEvent): void => { setCopyValue(event, keyValue); @@ -137,6 +146,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.length > 0 && ( +
+ + +
+ )} )} {!!keyValue && (