Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/content/docs/api-reference/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ A read-only key cannot change workspace resources or run, resume, or cancel work

A refused request fails with `403` and the message `This API key is read-only and cannot perform this operation`. Where the surface carries a code, it is `READ_ONLY_API_KEY`: in `error.details.code` on the v2 API and the Sim MCP server, in `details.code` on the v1 API and workflow execution, and in `error.data.code` on a workflow MCP server.

Access narrows a key; it never grants more than the key's identity already has. Access is fixed at creation, so create a new key to change it. Keys created before access levels existed have full access. Keys created from the deploy modal or for a workflow MCP server always have full access, because they exist to run workflows. If **Access** isn't shown when you create a key, read-only keys aren't available on your deployment yet.
Access narrows a key; it never grants more than the key's identity already has. Access is fixed at creation, so create a new key to change it. Keys created before access levels existed have full access. Keys created from the deploy modal or for a workflow MCP server always have full access, because they exist to run workflows.

### Expiration [#key-expiration]

Expand Down
47 changes: 0 additions & 47 deletions apps/sim/app/account/settings/layout.test.tsx

This file was deleted.

7 changes: 1 addition & 6 deletions apps/sim/app/account/settings/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { redirect } from 'next/navigation'
import { StandaloneSettingsShell } from '@/components/settings/standalone-settings-shell'
import { isReadOnlyApiKeyCreationEnabled } from '@/lib/api-key/feature-flag'
import { getSession } from '@/lib/auth'
import { resolveDeploymentShape } from '@/lib/core/config/deployment-shape'
import { isPlatformAdmin } from '@/lib/permissions/super-user'
Expand All @@ -9,10 +8,7 @@ import { FeatureFlagsProvider } from '@/app/workspace/[workspaceId]/providers/fe
export default async function AccountSettingsLayout({ children }: { children: React.ReactNode }) {
const session = await getSession()
if (!session?.user) redirect('/login')
const [isSuperUser, readOnlyApiKeysEnabled] = await Promise.all([
isPlatformAdmin(session.user.id),
isReadOnlyApiKeyCreationEnabled(),
])
const isSuperUser = await isPlatformAdmin(session.user.id)

/** Account settings host no workspace surface, so only the flags its sections read are on. */
return (
Expand All @@ -23,7 +19,6 @@ export default async function AccountSettingsLayout({ children }: { children: Re
changelog: false,
'mothership-model-selector': false,
'mothership-plan-mode': false,
'api-key-read-only': readOnlyApiKeysEnabled,
}}
>
<StandaloneSettingsShell
Expand Down
4 changes: 0 additions & 4 deletions apps/sim/app/o/[organizationId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { SettingsNavigationProvider } from '@/components/settings/settings-navigation-provider'
import { isReadOnlyApiKeyCreationEnabled } from '@/lib/api-key/feature-flag'
import { getSession } from '@/lib/auth'
import { getActiveOrganizationId } from '@/lib/auth/session-response'
import { canUseBenchmarks } from '@/lib/benchmarks/config'
Expand Down Expand Up @@ -90,7 +89,6 @@ export default async function OrganizationLayout({
simLinksEnabled,
peopleEnabled,
simLinkOrganizationId,
readOnlyApiKeysEnabled,
] = await Promise.all([
prefetchOrganizationSidebar(
queryClient,
Expand All @@ -115,7 +113,6 @@ export default async function OrganizationLayout({
userId: session.user.id,
sessionId: session.session.id,
}),
isReadOnlyApiKeyCreationEnabled(),
])
const initialSidebarCollapsed = cookieStore.get('sidebar_collapsed')?.value === '1'

Expand All @@ -131,7 +128,6 @@ export default async function OrganizationLayout({
'mothership-plan-mode': benchmarkEnabled,
'mothership-memory-spaces': benchmarkEnabled,
projects: projectsEnabled,
'api-key-read-only': readOnlyApiKeysEnabled,
}}
>
<OrganizationProvider context={context}>
Expand Down
4 changes: 0 additions & 4 deletions apps/sim/app/workspace/[workspaceId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { SettingsNavigationProvider } from '@/components/settings/settings-navigation-provider'
import type { WorkspaceHostContext } from '@/lib/api/contracts/workspaces'
import { isReadOnlyApiKeyCreationEnabled } from '@/lib/api-key/feature-flag'
import { getSession } from '@/lib/auth'
import { getActiveOrganizationId } from '@/lib/auth/session-response'
import { isChangelogEnabled } from '@/lib/changelog/feature-flag'
Expand Down Expand Up @@ -138,7 +137,6 @@ async function WorkspaceContent({
computerUseEnabled,
projectsEnabled,
simLinkOrganizationId,
readOnlyApiKeysEnabled,
] = await Promise.all([
cookies(),
orgSettings,
Expand All @@ -159,7 +157,6 @@ async function WorkspaceContent({
isComputerUseAvailable(hostContext.hostOrganizationId),
isProjectsEnabled(),
resolveSimLinkOrganization(principal),
isReadOnlyApiKeyCreationEnabled(),
prefetchWorkspaceAccess(queryClient, workspaceId, principal),
prefetchWorkspaceForkAvailability(queryClient, workspaceId, principal, hostContext),
])
Expand All @@ -176,7 +173,6 @@ async function WorkspaceContent({
'mothership-computer-use': computerUseEnabled,
'mothership-plan-mode': planModeEnabled,
projects: projectsEnabled,
'api-key-read-only': readOnlyApiKeysEnabled,
}}
>
<WorkspaceHostProvider workspaceId={workspaceId} initialContext={hostContext}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export interface WorkspaceFeatureFlags {
'mothership-computer-use'?: boolean
'mothership-memory-spaces'?: boolean
projects?: boolean
'api-key-read-only'?: boolean
}

const FeatureFlagsContext = createContext<WorkspaceFeatureFlags | null>(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type ApiKeyAccessLevel, DAY_MS } from '@/lib/api/contracts/api-keys'
import { useFeatureFlag } from '@/app/workspace/[workspaceId]/providers/feature-flags-provider'
import { type CreatedApiKey, useCreateApiKey } from '@/hooks/queries/api-keys'

const logger = createLogger('CreateApiKeyModal')
Expand Down Expand Up @@ -75,8 +74,7 @@ export function CreateApiKeyModal({
const [newKey, setNewKey] = useState<CreatedApiKey | null>(null)
const [showNewKeyDialog, setShowNewKeyDialog] = useState(false)
const createApiKeyMutation = useCreateApiKey()
const readOnlyKeysEnabled = useFeatureFlag('api-key-read-only')
const canChooseAccess = !fullAccessOnly && readOnlyKeysEnabled
const canChooseAccess = !fullAccessOnly

/**
* The form is seeded when the modal OPENS, not when this component mounts.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
} from '@sim/db/schema'
import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures'
import { authMock, authMockFns } from '@sim/testing'
import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock'
import { featureFlagsMock } from '@sim/testing/mocks/feature-flags.mock'
import { generateId } from '@sim/utils/id'
import { eq } from 'drizzle-orm'
import { NextRequest } from 'next/server'
Expand Down Expand Up @@ -60,7 +60,6 @@ const expiredKey = `sk-sim-fixture-${generateId()}`

let readPersonalKey = ''
let readWorkspaceKey = ''
let readOnlyKeysEnabled = false

type RouteHandler = (request: NextRequest, context: { params: Promise<never> }) => Promise<Response>

Expand Down Expand Up @@ -176,9 +175,6 @@ describe('read-only and expiring API keys in PostgreSQL', () => {
displayName: 'Topic',
fieldType: 'text',
})
featureFlagsMockFns.mockIsFeatureEnabled.mockImplementation(
async (flag) => flag === 'api-key-read-only' && readOnlyKeysEnabled
)
await db.insert(apiKey).values([
{
id: generateId(),
Expand Down Expand Up @@ -231,22 +227,6 @@ describe('read-only and expiring API keys in PostgreSQL', () => {
expect(after).toHaveLength(before.length)
})

it('refuses to create a read-only key while the api-key-read-only flag is off', async () => {
const before = await db.select({ id: apiKey.id }).from(apiKey).where(eq(apiKey.userId, userId))
const personal = await call(createPersonalKey, '/api/users/me/api-keys', {
body: { name: `Flag off personal ${generateId()}`, access: 'read' },
})
expect(personal.status).toBe(400)
expect(personal.body).toEqual({ error: 'Read-only API keys are not available yet' })
const scoped = await call(createWorkspaceKey, `/api/workspaces/${workspaceId}/api-keys`, {
body: { name: `Flag off workspace ${generateId()}`, access: 'read' },
params: { id: workspaceId },
})
expect(scoped.status).toBe(400)
const after = await db.select({ id: apiKey.id }).from(apiKey).where(eq(apiKey.userId, userId))
expect(after).toHaveLength(before.length)
})

it('reserves the delegation key-name prefix', async () => {
const created = await call(createPersonalKey, '/api/users/me/api-keys', {
body: { name: `mothership-delegation:${userId}`, access: 'write' },
Expand All @@ -255,7 +235,6 @@ describe('read-only and expiring API keys in PostgreSQL', () => {
})

it('mints read-only personal and workspace keys that report their access and expiry', async () => {
readOnlyKeysEnabled = true
const expiresAt = new Date(Date.now() + 30 * DAY_MS).toISOString()
const personal = await call(createPersonalKey, '/api/users/me/api-keys', {
body: { name: `Read personal ${generateId()}`, access: 'read', expiresAt },
Expand Down
11 changes: 0 additions & 11 deletions apps/sim/lib/api-key/feature-flag.ts

This file was deleted.

12 changes: 4 additions & 8 deletions apps/sim/lib/api-key/orchestration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { and, eq } from 'drizzle-orm'
import { DAY_MS } from '@/lib/api/contracts/api-keys'
import { createApiKey, createWorkspaceApiKey } from '@/lib/api-key/auth'
import { hashApiKey } from '@/lib/api-key/crypto'
import { isReadOnlyApiKeyCreationEnabled } from '@/lib/api-key/feature-flag'
import { DELEGATION_KEY_NAME_PREFIX } from '@/lib/api-key/reserved-names'
import { PlatformEvents } from '@/lib/core/telemetry'

Expand Down Expand Up @@ -70,16 +69,13 @@ type ResolvedIssueSettings =
| { ok: false; error: string }

/** Validates what a caller asked a new key to be, before anything is written. */
async function resolveIssueSettings(
function resolveIssueSettings(
params: ApiKeyIssueSettings & { name: string }
): Promise<ResolvedIssueSettings> {
): ResolvedIssueSettings {
if (params.name.startsWith(DELEGATION_KEY_NAME_PREFIX)) {
return { ok: false, error: `API key names may not start with "${DELEGATION_KEY_NAME_PREFIX}"` }
}
const access = params.access ?? 'write'
if (access === 'read' && !(await isReadOnlyApiKeyCreationEnabled())) {
return { ok: false, error: 'Read-only API keys are not available yet' }
}
const expiry = resolveApiKeyExpiry(params.expiresAt)
if (!expiry.ok) return expiry
return { ok: true, access, expiresAt: expiry.expiresAt }
Expand Down Expand Up @@ -131,7 +127,7 @@ export interface PerformCreatePersonalApiKeyResult {
export async function performCreatePersonalApiKey(
params: PerformCreatePersonalApiKeyParams
): Promise<PerformCreatePersonalApiKeyResult> {
const settings = await resolveIssueSettings(params)
const settings = resolveIssueSettings(params)
if (!settings.ok) return { success: false, errorCode: 'validation', error: settings.error }
const { access, expiresAt } = settings
try {
Expand Down Expand Up @@ -216,7 +212,7 @@ export async function performCreatePersonalApiKey(
export async function performCreateWorkspaceApiKey(
params: PerformCreateWorkspaceApiKeyParams
): Promise<PerformCreateWorkspaceApiKeyResult> {
const settings = await resolveIssueSettings(params)
const settings = resolveIssueSettings(params)
if (!settings.ok) return { success: false, errorCode: 'validation', error: settings.error }
const { access, expiresAt } = settings
try {
Expand Down
1 change: 0 additions & 1 deletion apps/sim/lib/core/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,6 @@ export const env = createEnv({
CHANGELOG: z.boolean().optional(), // Fallback for the `changelog` feature flag off AppConfig
SIM_LINKS: z.boolean().optional(), // Fallback for the `sim-links` feature flag off AppConfig
PEOPLE: z.boolean().optional(), // Fallback for the `people` feature flag off AppConfig
API_KEY_READ_ONLY: z.boolean().optional(), // Fallback for the `api-key-read-only` feature flag off AppConfig
MSHIP_MODEL_SELECTOR: z.boolean().optional(),
INBOX_ENABLED: z.boolean().optional(), // Enable inbox (Sim Mailer) on self-hosted (bypasses hosted requirements)
SANDBOXES_ENABLED: z.boolean().optional(), // Enable custom sandboxes on self-hosted (bypasses hosted requirements)
Expand Down
5 changes: 0 additions & 5 deletions apps/sim/lib/core/config/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,6 @@ const FEATURE_FLAGS = {
'Enable the organization People directory, org chart, profile imports, and directory sync. Supports organization rollout; disabled by default.',
fallback: 'PEOPLE',
},
'api-key-read-only': {
description:
'Allow creating read-only API keys. Turn on only after every pod runs a release that enforces key access, since older pods treat a read-only key as full access. Before rolling back below the release that added read-only keys, turn this off and revoke or replace every existing read-only key: turning the flag off only stops new ones, and that older code serves existing read-only keys as full access. Enforcement of existing read-only keys never depends on this flag. Global; disabled by default.',
fallback: 'API_KEY_READ_ONLY',
},
'mothership-model-selector': {
description:
'Show the Mothership model selector, model-specific effort levels, and Fast for supported ' +
Expand Down
Loading