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
19 changes: 18 additions & 1 deletion apps/sim/app/o/[organizationId]/settings/[section]/page.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { user } from '@sim/db/schema'
import { authMockFns } from '@sim/testing/mocks/auth.mock'
import { queueTableRows, resetDbChainMock } from '@sim/testing/mocks/database.mock'
import { nextNavigationMock } from '@sim/testing/mocks/next-navigation.mock'
import { beforeEach, describe, expect, it, vi } from 'vitest'

Expand All @@ -21,8 +23,9 @@ import OrganizationSettingsSectionPage from '@/app/o/[organizationId]/settings/[

const mockGetSession = authMockFns.mockGetSession

describe('organization request settings routing', () => {
describe('organization settings access', () => {
beforeEach(() => {
resetDbChainMock()
mockGetSession.mockResolvedValue({ user: { id: 'viewer' } })
mocks.authorize.mockResolvedValue(true)
})
Expand All @@ -36,4 +39,18 @@ describe('organization request settings routing', () => {
})
).rejects.toThrow('NEXT_NOT_FOUND')
})

it.each(['admin', 'mothership'])(
'conceals %s from organization admins without platform authority',
async (section) => {
const props = {
params: Promise.resolve({ organizationId: 'organization', section }),
}
queueTableRows(user, [{ role: 'admin' }])
await OrganizationSettingsSectionPage(props)

queueTableRows(user, [{ role: 'user' }])
await expect(OrganizationSettingsSectionPage(props)).rejects.toThrow('NEXT_NOT_FOUND')
}
)
})
9 changes: 7 additions & 2 deletions apps/sim/app/o/[organizationId]/settings/[section]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { prefetchStandaloneGeneral } from '@/components/settings/prefetch-standa
import { SettingsSectionProvider } from '@/components/settings/settings-panel'
import { getSession } from '@/lib/auth'
import { organizationRoutes } from '@/lib/navigation/paths'
import { isPlatformAdmin } from '@/lib/permissions/super-user'
import { authorizeOrganizationSettingsSection } from '@/lib/settings/application/organization-section-access'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect'
Expand Down Expand Up @@ -39,8 +40,8 @@ export async function generateMetadata({

/**
* One settings surface for the organization view. An organization section runs
* through the organization gate; an account section needs only the sign-in the
* layout already established, and renders through the account plane's renderer.
* through the organization gate; account sections use the account renderer,
* with platform administration restricted to platform admins.
*/
export default async function OrganizationSettingsSectionPage({
params,
Expand Down Expand Up @@ -81,6 +82,10 @@ export default async function OrganizationSettingsSectionPage({
return <OrganizationSettings section={resolved.section} />
}

if (resolved.section === 'admin' || resolved.section === 'mothership') {
if (!(await isPlatformAdmin(session.user.id))) notFound()
}

/**
* No Suspense boundary: a fallback mounted with the section puts React's 300ms reveal floor
* under every switch (see the workspace section layout).
Expand Down
13 changes: 8 additions & 5 deletions apps/sim/app/o/[organizationId]/settings/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ export type OrganizationSurfaceSettingsSection =
* The account sections the organization surface hosts, rendered by the account
* plane's own renderer. General is the one that belongs to the person alone: the
* personal Subscription gives way to the organization's, which sits beside
* General in the Account group, and the Desktop, Browser, and Terminal sections
* are bound to the workspace they are opened from.
* General in the Account group. Platform administration keeps the account
* plane's access rules; Desktop, Browser, and Terminal are workspace-bound.
*/
export const ORGANIZATION_SURFACE_ACCOUNT_ITEMS: SettingsNavigationItem<AccountSettingsSection>[] =
ACCOUNT_SETTINGS_ITEMS.filter((item) => item.id === 'general')
ACCOUNT_SETTINGS_ITEMS.filter((item) => item.id === 'general' || item.group === 'platform')

export function resolveOrganizationSettingsSection(
path: string
Expand Down Expand Up @@ -108,10 +108,13 @@ export function organizationSettingsNavigation(
export function organizationSurfaceSettingsNavigation(
isAdmin: boolean,
features: OrganizationSettingsFeatures,
availability: { connectedAccounts: boolean; search: boolean; knowledgeGraphs?: boolean }
availability: { connectedAccounts: boolean; search: boolean; knowledgeGraphs?: boolean },
isPlatformAdmin: boolean
): SettingsNavigationItem<AccountSettingsSection | OrganizationSettingsSection>[] {
return [
...ORGANIZATION_SURFACE_ACCOUNT_ITEMS,
...ORGANIZATION_SURFACE_ACCOUNT_ITEMS.filter(
(item) => item.group !== 'platform' || isPlatformAdmin
),
...organizationSettingsNavigation(isAdmin, features, availability),
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation'
import { ORGANIZATION_SETTINGS_GROUPS } from '@/components/settings/navigation'
import { SettingsSidebar } from '@/components/settings/settings-sidebar'
import { isApiClientError } from '@/lib/api/client/errors'
import { useSession } from '@/lib/auth/auth-client'
import { isEnterprise } from '@/lib/billing/plan-helpers'
import {
hasPaidSubscriptionStatus,
Expand All @@ -30,6 +31,7 @@ export function OrganizationSettingsSidebar(props: OrganizationSettingsSidebarPr
useOrganizationContext()
const knowledgeGraphs = useFeatureFlag('mothership-memory-spaces')
const pathname = usePathname()
const { data: session } = useSession()
const queryClient = useQueryClient()
const refreshPlan = viewer.isAdmin && settingsFeatures.hosted && settingsFeatures.billingEnabled
const { data: summary, error } = useOrganizationBillingSummary(organization.id, {
Expand Down Expand Up @@ -67,11 +69,16 @@ export function OrganizationSettingsSidebar(props: OrganizationSettingsSidebarPr
plane='organization'
activeSection={resolveOrganizationSurfaceSection(pathname ?? '')?.section ?? 'general'}
groups={ORGANIZATION_SETTINGS_GROUPS}
items={organizationSurfaceSettingsNavigation(isAdmin, features, {
connectedAccounts: connectedAccountsAvailable,
search: searchAccess.memberScoped,
knowledgeGraphs,
})}
items={organizationSurfaceSettingsNavigation(
isAdmin,
features,
{
connectedAccounts: connectedAccountsAvailable,
search: searchAccess.memberScoped,
knowledgeGraphs,
},
session?.user?.role === 'admin'
)}
hrefForSection={(section) => routes.settingsSection(section)}
onSectionIntent={(section) =>
warmOrganizationSettingsSectionQuery(
Expand Down
1 change: 1 addition & 0 deletions apps/sim/components/settings/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,7 @@ export const ORGANIZATION_SETTINGS_GROUPS = [
{ key: 'organization', title: 'Organization' },
{ key: 'governance', title: 'Governance' },
{ key: 'sim-search', title: 'Sim Search' },
{ key: 'platform', title: 'Platform' },
] as const

type OrganizationSettingsGroup = (typeof ORGANIZATION_SETTINGS_GROUPS)[number]['key']
Expand Down
Loading