From a03a3bed264d9d6e9169746a0e028a4375ed9e0b Mon Sep 17 00:00:00 2001 From: Benjamin Shafii Date: Sat, 13 Jun 2026 18:01:21 -0700 Subject: [PATCH] fix(den-api): restrict api key management to owners --- ee/apps/den-api/src/routes/org/api-keys.ts | 6 +++--- ee/apps/den-api/src/routes/org/shared.ts | 8 ++++++-- ee/apps/den-api/test/org-identity-access.test.ts | 16 ++++++++++++++++ packages/docs/cloud/security-and-operations.mdx | 2 +- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/ee/apps/den-api/src/routes/org/api-keys.ts b/ee/apps/den-api/src/routes/org/api-keys.ts index ce1b93561..ab40c2d2e 100644 --- a/ee/apps/den-api/src/routes/org/api-keys.ts +++ b/ee/apps/den-api/src/routes/org/api-keys.ts @@ -131,7 +131,7 @@ export function registerOrgApiKeyRoutes Or } } - if (payload.currentMember.isOwner || memberHasRole(payload.currentMember.role, "admin")) { + if (canManageApiKeys(payload)) { return { ok: true as const } } @@ -169,11 +169,15 @@ export function ensureApiKeyManager(c: { get: (key: "organizationContext") => Or ok: false as const, response: { error: "forbidden", - message: "Only workspace owners and admins can manage API keys.", + message: "Only workspace owners can manage API keys.", }, } } +export function canManageApiKeys(payload: { currentMember: { isOwner: boolean; role?: string } } | null | undefined) { + return payload?.currentMember.isOwner === true +} + export function canManageIdentityConfiguration(payload: { currentMember: { isOwner: boolean; role?: string } } | null | undefined) { return payload?.currentMember.isOwner === true } diff --git a/ee/apps/den-api/test/org-identity-access.test.ts b/ee/apps/den-api/test/org-identity-access.test.ts index 8659f0be1..b3a2e287f 100644 --- a/ee/apps/den-api/test/org-identity-access.test.ts +++ b/ee/apps/den-api/test/org-identity-access.test.ts @@ -29,3 +29,19 @@ test("identity configuration management is owner-only", () => { expect(sharedModule.canManageIdentityConfiguration(null)).toBe(false) }) + +test("api key management is owner-only", () => { + expect(sharedModule.canManageApiKeys({ + currentMember: { isOwner: true, role: "owner" }, + })).toBe(true) + + expect(sharedModule.canManageApiKeys({ + currentMember: { isOwner: false, role: "admin" }, + })).toBe(false) + + expect(sharedModule.canManageApiKeys({ + currentMember: { isOwner: false, role: "member" }, + })).toBe(false) + + expect(sharedModule.canManageApiKeys(null)).toBe(false) +}) diff --git a/packages/docs/cloud/security-and-operations.mdx b/packages/docs/cloud/security-and-operations.mdx index 19e45d2fc..7ecc1b501 100644 --- a/packages/docs/cloud/security-and-operations.mdx +++ b/packages/docs/cloud/security-and-operations.mdx @@ -49,7 +49,7 @@ For production organizations, use SSO with identity-provider MFA as the primary - Enable enforced SSO for organizations that have owners, admins, SCIM, SSO, API keys, or shared production providers. - Require MFA, phishing-resistant factors where available, and conditional-access policy in the identity provider before users reach OpenWork Cloud. -- Keep SSO and SCIM configuration owner-only. Treat owner-only changes, custom-role changes, member role changes, API-key creation, and emergency access as privileged actions that require a fresh IdP-authenticated session. +- Keep SSO, SCIM, and API-key management owner-only. Treat owner-only changes, custom-role changes, member role changes, API-key creation, and emergency access as privileged actions that require a fresh IdP-authenticated session. - Do not rely on standalone email/password accounts for production owner access unless the deployment has an equivalent MFA and recovery control outside OpenWork. Native factor enrollment and in-product reauthentication should be treated as product work before replacing SSO-enforced MFA as the production control.