diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 2ba6e9db43..a552979a52 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.45.3", + "version": "7.45.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.45.3", + "version": "7.45.4", "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 086a1697e3..5121d3ac99 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.45.3", + "version": "7.45.4", "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 868983d30b..9427176c43 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.4 +*Released*: 7 July 2026 +- GitHub Issue #1846: Add role-restriction option to API key dialog + ### version 7.45.3 *Released*: 4 July 2026 - GitHub Issue #1134: Support plating from find by samples 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..67469a4582 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,17 @@ export const KeyGeneratorModal: FC = props => { })(); }, [type, onGenerateKey]); + useEffect(() => { + if (type !== 'apikey') return; + (async () => { + try { + setRoles(await api.security.getApiKeyRoles()); + } catch (e) { + console.error(e); + } + })(); + }, [api.security, type]); + const onCopyKey = useCallback(() => { const handleCopy = (event: ClipboardEvent): void => { setCopyValue(event, keyValue); @@ -137,6 +151,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 && ( @@ -342,11 +378,12 @@ export const APIKeysPanel: FC = props => {
API Keys

- API keys are used to authorize client code using one of the{' '} + API keys are used to authorize desktop AI agents using LabKey's MCP and client code using one of the{' '} LabKey Client APIs. API keys are appropriate for authenticating ad hoc interactions within statistical tools (e.g., R, RStudio, SAS) or programming languages (e.g., Java, Python), as well as authenticating API use from automated scripts. A valid API key provides - complete access to your data and actions, so it should be kept secret. + access to your data and actions, so it should be kept secret. We recommend restricting API keys to a + specific security role at creation time, imposing appropriate limits on AI agents and scripts.

{apiEnabled && ( diff --git a/packages/components/src/theme/user.scss b/packages/components/src/theme/user.scss index 7766b43647..f6add16279 100644 --- a/packages/components/src/theme/user.scss +++ b/packages/components/src/theme/user.scss @@ -30,6 +30,5 @@ div:has(> .user-detail-modal) > .modal-backdrop { } .api-key__input { - width: 90%; display: inline-block; }