Skip to content

Commit 82958a6

Browse files
committed
fix(api-keys): provide feature flags to account settings and clarify meta scopes
- The account settings layout resolves api-key-read-only and wraps its sections in FeatureFlagsProvider, as the workspace and organization layouts do, so personal API key settings no longer throw from the create modal's flag read - The /api/v2/meta scopes description says read-only keys are also refused every operation that declares no API scope; OpenAPI regenerated
1 parent 0d6c376 commit 82958a6

4 files changed

Lines changed: 71 additions & 9 deletions

File tree

‎apps/docs/openapi-v2-resources.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18651,7 +18651,7 @@
1865118651
"type": "string",
1865218652
"enum": ["api:read", "api:write", "search:read"]
1865318653
},
18654-
"description": "API scopes the calling credential holds. A read-only API key holds only api:read and is refused every operation that changes state; a full-access key holds api:read and api:write; an OAuth access token holds what it was granted."
18654+
"description": "API scopes the calling credential holds. A read-only API key holds only api:read: it is refused every operation that changes state and every operation that declares no API scope, so api:read alone does not authorize those. A full-access key holds api:read and api:write; an OAuth access token holds what it was granted."
1865518655
}
1865618656
},
1865718657
"required": ["v2Enabled", "keyType", "expiresAt", "scopes"],
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { ReactNode } from 'react'
2+
import { authMockFns } from '@sim/testing'
3+
import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock'
4+
import { nextNavigationMock } from '@sim/testing/mocks/next-navigation.mock'
5+
import { renderToStaticMarkup } from 'react-dom/server'
6+
import { beforeEach, describe, expect, it, vi } from 'vitest'
7+
8+
vi.mock('next/navigation', () => nextNavigationMock)
9+
vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock)
10+
vi.mock('@/lib/permissions/super-user', () => ({ isPlatformAdmin: vi.fn(async () => false) }))
11+
vi.mock('@/components/settings/standalone-settings-shell', () => ({
12+
StandaloneSettingsShell: ({ children }: { children: ReactNode }) => children,
13+
}))
14+
15+
import AccountSettingsLayout from '@/app/account/settings/layout'
16+
import { useFeatureFlag } from '@/app/workspace/[workspaceId]/providers/feature-flags-provider'
17+
18+
/** Stands in for the personal API keys page, whose create modal reads this flag on every render. */
19+
function ReadOnlyKeysFlag() {
20+
return <span>{`read-only keys ${useFeatureFlag('api-key-read-only') ? 'on' : 'off'}`}</span>
21+
}
22+
23+
async function renderAccountSettings(): Promise<string> {
24+
const element = await AccountSettingsLayout({ children: <ReadOnlyKeysFlag /> })
25+
return renderToStaticMarkup(element)
26+
}
27+
28+
describe('AccountSettingsLayout', () => {
29+
beforeEach(() => {
30+
authMockFns.mockGetSession.mockResolvedValue({
31+
user: { id: 'viewer-1' },
32+
session: { id: 'session-1' },
33+
})
34+
})
35+
36+
it.each([
37+
[true, 'on'],
38+
[false, 'off'],
39+
])('gives account sections the read-only key flag (%s)', async (enabled, rendered) => {
40+
featureFlagsMockFns.mockIsFeatureEnabled.mockImplementation(
41+
async (flag) => flag === 'api-key-read-only' && enabled
42+
)
43+
expect(await renderAccountSettings()).toContain(`read-only keys ${rendered}`)
44+
})
45+
})
Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,38 @@
11
import { redirect } from 'next/navigation'
22
import { StandaloneSettingsShell } from '@/components/settings/standalone-settings-shell'
3+
import { isReadOnlyApiKeyCreationEnabled } from '@/lib/api-key/feature-flag'
34
import { getSession } from '@/lib/auth'
45
import { resolveDeploymentShape } from '@/lib/core/config/deployment-shape'
56
import { isPlatformAdmin } from '@/lib/permissions/super-user'
7+
import { FeatureFlagsProvider } from '@/app/workspace/[workspaceId]/providers/feature-flags-provider'
68

79
export default async function AccountSettingsLayout({ children }: { children: React.ReactNode }) {
810
const session = await getSession()
911
if (!session?.user) redirect('/login')
10-
const isSuperUser = await isPlatformAdmin(session.user.id)
12+
const [isSuperUser, readOnlyApiKeysEnabled] = await Promise.all([
13+
isPlatformAdmin(session.user.id),
14+
isReadOnlyApiKeyCreationEnabled(),
15+
])
1116

17+
/** Account settings host no workspace surface, so only the flags its sections read are on. */
1218
return (
13-
<StandaloneSettingsShell
14-
plane='account'
15-
isSuperUser={isSuperUser}
16-
deployment={resolveDeploymentShape()}
19+
<FeatureFlagsProvider
20+
flags={{
21+
dashboards: false,
22+
'workflow-tests': false,
23+
changelog: false,
24+
'mothership-model-selector': false,
25+
'mothership-plan-mode': false,
26+
'api-key-read-only': readOnlyApiKeysEnabled,
27+
}}
1728
>
18-
{children}
19-
</StandaloneSettingsShell>
29+
<StandaloneSettingsShell
30+
plane='account'
31+
isSuperUser={isSuperUser}
32+
deployment={resolveDeploymentShape()}
33+
>
34+
{children}
35+
</StandaloneSettingsShell>
36+
</FeatureFlagsProvider>
2037
)
2138
}

‎apps/sim/lib/api/contracts/v2/meta.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const v2MetaSchema = z
2525
scopes: z
2626
.array(z.enum(['api:read', 'api:write', 'search:read']))
2727
.describe(
28-
'API scopes the calling credential holds. A read-only API key holds only api:read and is refused every operation that changes state; a full-access key holds api:read and api:write; an OAuth access token holds what it was granted.'
28+
'API scopes the calling credential holds. A read-only API key holds only api:read: it is refused every operation that changes state and every operation that declares no API scope, so api:read alone does not authorize those. A full-access key holds api:read and api:write; an OAuth access token holds what it was granted.'
2929
),
3030
})
3131
.meta({

0 commit comments

Comments
 (0)