diff --git a/README.md b/README.md
index 78c103799e2..f25af677768 100644
--- a/README.md
+++ b/README.md
@@ -30,8 +30,6 @@
npx sim-setup
```
-Open [http://localhost:3000](http://localhost:3000)
-
### Desktop: [macOS](https://sim.ai/api/desktop/update/download)
diff --git a/apps/sim/app/api/organizations/[id]/permission-groups/utils.test.ts b/apps/sim/app/api/organizations/[id]/permission-groups/utils.test.ts
index 62c0ddb18cc..c426e067815 100644
--- a/apps/sim/app/api/organizations/[id]/permission-groups/utils.test.ts
+++ b/apps/sim/app/api/organizations/[id]/permission-groups/utils.test.ts
@@ -4,13 +4,15 @@
import { resetDbChainMock } from '@sim/testing'
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
-const { mockIsOrganizationAdminOrOwner, mockIsOrganizationOnEnterprisePlan } = vi.hoisted(() => ({
- mockIsOrganizationAdminOrOwner: vi.fn<() => Promise>(),
- mockIsOrganizationOnEnterprisePlan: vi.fn<() => Promise>(),
-}))
+const { mockIsOrganizationAdminOrOwner, mockIsOrganizationPermissionRegimeActive } = vi.hoisted(
+ () => ({
+ mockIsOrganizationAdminOrOwner: vi.fn<() => Promise>(),
+ mockIsOrganizationPermissionRegimeActive: vi.fn<() => Promise>(),
+ })
+)
-vi.mock('@/lib/billing', () => ({
- isOrganizationOnEnterprisePlan: mockIsOrganizationOnEnterprisePlan,
+vi.mock('@/lib/permission-groups/resolve.server', () => ({
+ isOrganizationPermissionRegimeActive: mockIsOrganizationPermissionRegimeActive,
}))
vi.mock('@/lib/workspaces/permissions/utils', () => ({
@@ -29,7 +31,7 @@ describe('authorizeOrgAccessControl', () => {
it('returns a 403 when the user is not an organization admin/owner', async () => {
mockIsOrganizationAdminOrOwner.mockResolvedValue(false)
- mockIsOrganizationOnEnterprisePlan.mockResolvedValue(true)
+ mockIsOrganizationPermissionRegimeActive.mockResolvedValue(true)
const response = await authorizeOrgAccessControl('user-1', 'org-1')
@@ -37,12 +39,12 @@ describe('authorizeOrgAccessControl', () => {
expect(response?.status).toBe(403)
await expect(response?.json()).resolves.toEqual({ error: 'Admin permissions required' })
// Entitlement is only checked after the admin gate passes.
- expect(mockIsOrganizationOnEnterprisePlan).not.toHaveBeenCalled()
+ expect(mockIsOrganizationPermissionRegimeActive).not.toHaveBeenCalled()
})
it('returns a 403 when the organization is not on an enterprise plan', async () => {
mockIsOrganizationAdminOrOwner.mockResolvedValue(true)
- mockIsOrganizationOnEnterprisePlan.mockResolvedValue(false)
+ mockIsOrganizationPermissionRegimeActive.mockResolvedValue(false)
const response = await authorizeOrgAccessControl('user-1', 'org-1')
@@ -54,7 +56,7 @@ describe('authorizeOrgAccessControl', () => {
it('returns null when the user is an admin and the org is entitled', async () => {
mockIsOrganizationAdminOrOwner.mockResolvedValue(true)
- mockIsOrganizationOnEnterprisePlan.mockResolvedValue(true)
+ mockIsOrganizationPermissionRegimeActive.mockResolvedValue(true)
const response = await authorizeOrgAccessControl('user-1', 'org-1')
diff --git a/apps/sim/app/api/organizations/[id]/permission-groups/utils.ts b/apps/sim/app/api/organizations/[id]/permission-groups/utils.ts
index cb19b17e163..026963a3a5a 100644
--- a/apps/sim/app/api/organizations/[id]/permission-groups/utils.ts
+++ b/apps/sim/app/api/organizations/[id]/permission-groups/utils.ts
@@ -2,12 +2,12 @@ import { db } from '@sim/db'
import { permissionGroup, permissionGroupWorkspace, workspace } from '@sim/db/schema'
import { and, asc, eq, inArray } from 'drizzle-orm'
import { NextResponse } from 'next/server'
-import { isOrganizationOnEnterprisePlan } from '@/lib/billing'
import type { DbOrTx } from '@/lib/db/types'
import type {
AllMembersConflict,
ScopeConflict,
} from '@/lib/permission-groups/application/group-membership'
+import { isOrganizationPermissionRegimeActive } from '@/lib/permission-groups/resolve.server'
import { isOrganizationAdminOrOwner } from '@/lib/workspaces/permissions/utils'
/** A workspace reference (id + display name). */
@@ -32,13 +32,12 @@ export async function authorizeOrgAccessControl(
}
/**
- * The feature gate, deliberately, not the governance reader: the Access Control settings page is
- * gated on the same plan check, so reading governance here would open the API for a past-due
- * organization whose page still 404s. Restrictions keep applying through a dunning window —
- * that is what the governance reader is for — but managing them follows the page.
+ * The active permission regime, which is what the Access Control page now reads too: an
+ * organization whose restrictions still apply has to be able to see and loosen them, and a
+ * deployment with Access Control switched off governs nobody, so neither should manage anything.
*/
- const entitled = await isOrganizationOnEnterprisePlan(organizationId)
- if (!entitled) {
+ const governed = await isOrganizationPermissionRegimeActive(organizationId)
+ if (!governed) {
return NextResponse.json({ error: 'Access Control is an Enterprise feature' }, { status: 403 })
}
diff --git a/apps/sim/app/o/[organizationId]/settings/navigation.test.ts b/apps/sim/app/o/[organizationId]/settings/navigation.test.ts
index 94ec37b962a..ba3a295e3e0 100644
--- a/apps/sim/app/o/[organizationId]/settings/navigation.test.ts
+++ b/apps/sim/app/o/[organizationId]/settings/navigation.test.ts
@@ -18,6 +18,7 @@ import {
const enterprise: OrganizationSettingsFeatures = {
billingEnabled: true,
hasEnterprisePlan: true,
+ governanceActive: true,
hosted: true,
selfHosted: {},
}
@@ -46,12 +47,27 @@ describe('organization settings navigation', () => {
expect(
organizationSettingsNavigation(
true,
- { ...enterprise, hasEnterprisePlan: false },
+ { ...enterprise, hasEnterprisePlan: false, governanceActive: false },
available
).map(({ id }) => id)
).toEqual(['billing', 'members', 'recently-deleted', 'search-mcp'])
})
+ /**
+ * A failing payment closes the plan gate while the organization's permission groups keep
+ * applying, so the page that edits them has to stay listed — otherwise its members are governed
+ * by rules nobody can reach until the invoice clears.
+ */
+ it('keeps Access Control listed while the organization is still governed', () => {
+ expect(
+ organizationSettingsNavigation(
+ true,
+ { ...enterprise, hasEnterprisePlan: false, governanceActive: true },
+ available
+ ).map(({ id }) => id)
+ ).toEqual(['billing', 'members', 'recently-deleted', 'access-control', 'search-mcp'])
+ })
+
it('honors individual self-hosted feature flags and hides billing when disabled', () => {
expect(
organizationSettingsNavigation(
diff --git a/apps/sim/app/o/[organizationId]/settings/organization-settings-sidebar.tsx b/apps/sim/app/o/[organizationId]/settings/organization-settings-sidebar.tsx
index ab5d7c8de34..6c03c4c8e5a 100644
--- a/apps/sim/app/o/[organizationId]/settings/organization-settings-sidebar.tsx
+++ b/apps/sim/app/o/[organizationId]/settings/organization-settings-sidebar.tsx
@@ -6,7 +6,10 @@ import { ORGANIZATION_SETTINGS_GROUPS } from '@/components/settings/navigation'
import { SettingsSidebar } from '@/components/settings/settings-sidebar'
import { isApiClientError } from '@/lib/api/client/errors'
import { isEnterprise } from '@/lib/billing/plan-helpers'
-import { hasUsableSubscriptionAccess } from '@/lib/billing/subscriptions/utils'
+import {
+ hasPaidSubscriptionStatus,
+ hasUsableSubscriptionAccess,
+} from '@/lib/billing/subscriptions/utils'
import { organizationRoutes, WORKSPACE_SETTINGS_PATH } from '@/lib/navigation/paths'
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
import {
@@ -42,6 +45,16 @@ export function OrganizationSettingsSidebar(props: OrganizationSettingsSidebarPr
isEnterprise(summary.data.subscriptionPlan) &&
hasUsableSubscriptionAccess(summary.data.subscriptionStatus, summary.data.billingBlocked)
: settingsFeatures.hasEnterprisePlan,
+ /**
+ * Refreshed from the same summary, or the item the plan gate just hid would reappear only on
+ * reload. Governance keeps its own rule — an entitled status, block state ignored — because a
+ * failing payment does not stop the organization's permission groups from applying.
+ */
+ governanceActive:
+ refreshPlan && summary
+ ? isEnterprise(summary.data.subscriptionPlan) &&
+ hasPaidSubscriptionStatus(summary.data.subscriptionStatus)
+ : settingsFeatures.governanceActive,
}
const routes = organizationRoutes(organization.id)
diff --git a/apps/sim/components/settings/navigation.test.ts b/apps/sim/components/settings/navigation.test.ts
index e469f904048..eab988612ea 100644
--- a/apps/sim/components/settings/navigation.test.ts
+++ b/apps/sim/components/settings/navigation.test.ts
@@ -231,6 +231,7 @@ describe('settings navigation boundaries', () => {
).toEqual({
billingEnabled: false,
hasEnterprisePlan: true,
+ governanceActive: true,
hosted: false,
selfHosted: {
'connected-accounts': true,
@@ -492,6 +493,7 @@ describe('settings navigation boundaries', () => {
const hostedFree = {
billingEnabled: true,
hasEnterprisePlan: false,
+ governanceActive: false,
hosted: true,
selfHosted: {},
}
diff --git a/apps/sim/components/settings/navigation.ts b/apps/sim/components/settings/navigation.ts
index e46615cf09f..c4919d321ba 100644
--- a/apps/sim/components/settings/navigation.ts
+++ b/apps/sim/components/settings/navigation.ts
@@ -1014,18 +1014,27 @@ export function resolveOrganizationSectionAccess({
export interface OrganizationSettingsFeatures {
billingEnabled: boolean
hasEnterprisePlan: boolean
+ /**
+ * Whether the organization's permission-group regime is in force, which outlives the plan gate
+ * through a failing payment — see `isOrganizationGovernanceActive`. Only Access Control reads
+ * it, because only that section edits something that keeps applying while the gate is closed.
+ */
+ governanceActive: boolean
hosted: boolean
selfHosted: Partial>
}
export function getOrganizationSettingsFeatures(
hasEnterprisePlan: boolean,
- deployment: DeploymentShape
+ deployment: DeploymentShape,
+ /** Defaults to the plan gate, so a caller with no reason to distinguish the two keeps its behavior. */
+ governanceActive: boolean = hasEnterprisePlan
): OrganizationSettingsFeatures {
const { features } = deployment
return {
billingEnabled: deployment.billingEnabled,
hasEnterprisePlan,
+ governanceActive,
hosted: deployment.hosted,
selfHosted: {
'connected-accounts': true,
@@ -1055,6 +1064,12 @@ export function isOrganizationSettingsSectionAvailable(
/* Sim Search itself is enterprise on the hosted product; self-hosted gates it by flag, not by section. */
if (section === 'integrations' || section === 'search-slack')
return !features.hosted || features.hasEnterprisePlan
+ /**
+ * Access Control follows governance rather than the plan gate: its restrictions keep applying
+ * through a failing payment, so hiding the page that edits them would leave an organization
+ * governed by rules it cannot see or loosen until the invoice clears.
+ */
+ if (section === 'access-control' && features.hosted) return features.governanceActive
if (features.hosted) return features.hasEnterprisePlan
return features.selfHosted[section] ?? false
}
diff --git a/apps/sim/lib/organizations/surface.test.ts b/apps/sim/lib/organizations/surface.test.ts
index 4a622a29d61..1a74e24ae06 100644
--- a/apps/sim/lib/organizations/surface.test.ts
+++ b/apps/sim/lib/organizations/surface.test.ts
@@ -16,6 +16,8 @@ vi.mock('@/lib/credential-groups/scoped-availability', () => ({
vi.mock('@/lib/permission-groups/resolve.server', () => ({
getUserPermissionConfigForOrganization: mockPermissionConfig,
+ /** The nav lists Access Control on the regime; these tests drive it from the plan knob. */
+ isOrganizationPermissionRegimeActive: mockEnterprisePlan,
}))
vi.mock('@/lib/billing/core/subscription', () => ({
isOrganizationOnEnterprisePlan: mockEnterprisePlan,
diff --git a/apps/sim/lib/organizations/surface.ts b/apps/sim/lib/organizations/surface.ts
index ee2c3283c5a..399a97520b0 100644
--- a/apps/sim/lib/organizations/surface.ts
+++ b/apps/sim/lib/organizations/surface.ts
@@ -18,7 +18,10 @@ import {
} from '@/lib/knowledge/access/availability'
import { getOrganizationSettingsAccess } from '@/lib/organizations/settings-access'
import { capabilityDeniedBy } from '@/lib/permission-groups/capability-assertions'
-import { getUserPermissionConfigForOrganization } from '@/lib/permission-groups/resolve.server'
+import {
+ getUserPermissionConfigForOrganization,
+ isOrganizationPermissionRegimeActive,
+} from '@/lib/permission-groups/resolve.server'
export interface OrganizationSurfaceOrganization {
id: string
@@ -77,19 +80,36 @@ async function resolveOrganizationSurfaceContext(
if (!row) return null
const deployment = getDeploymentShape()
- const [config, [{ memberCount }], connectedAccountsAvailable, searchAccess, hasEnterprisePlan] =
- await Promise.all([
- getUserPermissionConfigForOrganization(organizationId),
- db
- .select({ memberCount: count() })
- .from(member)
- .where(eq(member.organizationId, organizationId)),
- isScopedCredentialGroupsAvailable({ kind: 'organization', organizationId }),
- resolveKnowledgeAccessAvailability({ organizationId }),
- deployment.hosted && access.isAdmin
- ? isOrganizationOnEnterprisePlan(organizationId)
- : Promise.resolve(false),
- ])
+ const [
+ config,
+ [{ memberCount }],
+ connectedAccountsAvailable,
+ searchAccess,
+ hasEnterprisePlan,
+ governanceActive,
+ ] = await Promise.all([
+ getUserPermissionConfigForOrganization(organizationId),
+ db
+ .select({ memberCount: count() })
+ .from(member)
+ .where(eq(member.organizationId, organizationId)),
+ isScopedCredentialGroupsAvailable({ kind: 'organization', organizationId }),
+ resolveKnowledgeAccessAvailability({ organizationId }),
+ deployment.hosted && access.isAdmin
+ ? isOrganizationOnEnterprisePlan(organizationId)
+ : Promise.resolve(false),
+ /**
+ * Access Control stays listed while a payment is failing, because its rules still apply.
+ *
+ * Resolved rather than rejected on a read failure: this value only decides whether a nav item
+ * is drawn, and it is shared by every organization page — letting it throw would take home,
+ * chat and search down with the billing table. The page and the management API read the same
+ * regime and still fail closed, so a listed item cannot be used to reach anything.
+ */
+ deployment.hosted && access.isAdmin
+ ? isOrganizationPermissionRegimeActive(organizationId).catch(() => false)
+ : Promise.resolve(false),
+ ])
return {
organization: {
id: row.id,
@@ -112,7 +132,11 @@ async function resolveOrganizationSurfaceContext(
},
connectedAccountsAvailable,
searchAccess,
- settingsFeatures: getOrganizationSettingsFeatures(hasEnterprisePlan, deployment),
+ settingsFeatures: getOrganizationSettingsFeatures(
+ hasEnterprisePlan,
+ deployment,
+ governanceActive
+ ),
deployment,
}
}
diff --git a/apps/sim/lib/settings/application/organization-section-access.test.ts b/apps/sim/lib/settings/application/organization-section-access.test.ts
index f425f2c3604..d14162a91bc 100644
--- a/apps/sim/lib/settings/application/organization-section-access.test.ts
+++ b/apps/sim/lib/settings/application/organization-section-access.test.ts
@@ -7,6 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
canOpen: vi.fn(),
enterprise: vi.fn(),
+ governance: vi.fn(),
groups: vi.fn(),
search: vi.fn(),
}))
@@ -21,6 +22,7 @@ vi.mock('@/lib/organizations/settings-access', () => ({
}))
vi.mock('@/lib/billing/core/subscription', () => ({
isOrganizationOnEnterprisePlan: mocks.enterprise,
+ isOrganizationGovernanceActive: mocks.governance,
}))
import { authorizeOrganizationSettingsSection } from '@/lib/settings/application/organization-section-access'
@@ -31,6 +33,7 @@ describe('organization settings authorization', () => {
setEnvFlags({ isHosted: true, isBillingEnabled: true })
mocks.canOpen.mockResolvedValue(true)
mocks.enterprise.mockResolvedValue(true)
+ mocks.governance.mockResolvedValue(true)
mocks.groups.mockResolvedValue(true)
mocks.search.mockResolvedValue(true)
})
@@ -57,6 +60,49 @@ describe('organization settings authorization', () => {
}
)
+ /**
+ * Access Control configures restrictions that keep applying while a payment is failing, so the
+ * page that edits them has to stay reachable — otherwise an organization is governed by rules
+ * nobody can see or loosen until the invoice clears.
+ */
+ it('opens Access Control for an organization still being governed', async () => {
+ mocks.enterprise.mockResolvedValue(false)
+ mocks.governance.mockResolvedValue(true)
+
+ await expect(
+ authorizeOrganizationSettingsSection({
+ organizationId: 'target',
+ userId: 'viewer',
+ section: 'access-control',
+ })
+ ).resolves.toBe(true)
+ })
+
+ it('closes Access Control once nothing governs the organization', async () => {
+ mocks.enterprise.mockResolvedValue(false)
+ mocks.governance.mockResolvedValue(false)
+
+ await expect(
+ authorizeOrganizationSettingsSection({
+ organizationId: 'target',
+ userId: 'viewer',
+ section: 'access-control',
+ })
+ ).resolves.toBe(false)
+ })
+
+ /** Every other section keeps reading the plan gate, and pays no extra lookup for this one. */
+ it('reads governance for no section but Access Control', async () => {
+ await authorizeOrganizationSettingsSection({
+ organizationId: 'target',
+ userId: 'viewer',
+ section: 'audit-logs',
+ })
+
+ expect(mocks.governance).not.toHaveBeenCalled()
+ expect(mocks.enterprise).toHaveBeenCalledWith('target')
+ })
+
it.each([
{ groups: false, search: false, connectedAccounts: false, integrations: false },
{ groups: true, search: false, connectedAccounts: true, integrations: false },
diff --git a/apps/sim/lib/settings/application/organization-section-access.ts b/apps/sim/lib/settings/application/organization-section-access.ts
index bf762ea9e8a..89aa4602dbe 100644
--- a/apps/sim/lib/settings/application/organization-section-access.ts
+++ b/apps/sim/lib/settings/application/organization-section-access.ts
@@ -8,6 +8,7 @@ import { getDeploymentShape } from '@/lib/core/config/deployment-shape'
import { isScopedCredentialGroupsAvailable } from '@/lib/credential-groups/scoped-availability'
import { isKnowledgeMemberAccessAvailable } from '@/lib/knowledge/access/availability'
import { canOpenOrganizationSettingsSection } from '@/lib/organizations/settings-access'
+import { isOrganizationPermissionRegimeActive } from '@/lib/permission-groups/resolve.server'
interface AuthorizeOrganizationSettingsSectionInput {
organizationId: string
@@ -33,12 +34,20 @@ export async function authorizeOrganizationSettingsSection({
const deployment = getDeploymentShape()
const needsEnterprisePlan = deployment.hosted && section !== 'members' && section !== 'billing'
- const hasEnterprisePlan = needsEnterprisePlan
- ? await isOrganizationOnEnterprisePlan(organizationId)
- : false
+ /**
+ * Access Control's availability follows the permission regime rather than the plan gate, and no
+ * other section reads it — so each section pays for exactly one of the two lookups.
+ */
+ const readsRegime = needsEnterprisePlan && section === 'access-control'
+ const [hasEnterprisePlan, governanceActive] = await Promise.all([
+ needsEnterprisePlan && !readsRegime
+ ? isOrganizationOnEnterprisePlan(organizationId)
+ : Promise.resolve(false),
+ readsRegime ? isOrganizationPermissionRegimeActive(organizationId) : Promise.resolve(false),
+ ])
return isOrganizationSettingsSectionAvailable(
section,
- getOrganizationSettingsFeatures(hasEnterprisePlan, deployment)
+ getOrganizationSettingsFeatures(hasEnterprisePlan, deployment, governanceActive)
)
}
diff --git a/apps/sim/lib/settings/application/workspace-section-access.test.ts b/apps/sim/lib/settings/application/workspace-section-access.test.ts
index cb9724ac59e..6841e05fc1a 100644
--- a/apps/sim/lib/settings/application/workspace-section-access.test.ts
+++ b/apps/sim/lib/settings/application/workspace-section-access.test.ts
@@ -74,6 +74,10 @@ vi.mock('@/lib/credential-groups/scoped-availability', () => ({
vi.mock('@/lib/knowledge/access/availability', () => ({
isKnowledgeMemberAccessAvailable: mocks.isKnowledgeMemberAccessAvailable,
}))
+vi.mock('@/lib/permission-groups/resolve.server', () => ({
+ /** Access Control follows the regime; these tests drive it from the same plan knob. */
+ isOrganizationPermissionRegimeActive: mocks.isOrganizationOnEnterprisePlan,
+}))
vi.mock('@/lib/organizations/settings-access', () => ({
canOpenOrganizationSettingsSection: mocks.canOpenOrganizationSettingsSection,
}))
@@ -277,7 +281,27 @@ describe('authorizeWorkspaceSettingsSection', () => {
mocks.checkWorkspaceAccess.mockResolvedValue(ORGANIZATION_ACCESS)
await expect(authorize('access-control')).resolves.toEqual({ allowed: true })
- expect(mocks.getOrganizationSettingsFeatures).toHaveBeenCalledWith(true, mocks.deploymentShape)
+ /**
+ * Access Control is gated on the permission regime rather than the plan, so the plan lookup is
+ * skipped for it and the regime is what reaches the navigation gate.
+ */
+ expect(mocks.getOrganizationSettingsFeatures).toHaveBeenCalledWith(
+ false,
+ mocks.deploymentShape,
+ true
+ )
+ })
+
+ /**
+ * The workspace-scoped page reads the same regime as the organization one: an organization whose
+ * restrictions still apply during a failing payment must not have this page taken away.
+ */
+ it('keeps the workspace Access Control page open while the organization is governed', async () => {
+ mocks.checkWorkspaceAccess.mockResolvedValue(ORGANIZATION_ACCESS)
+ mocks.isOrganizationOnEnterprisePlan.mockResolvedValue(false)
+
+ await expect(authorize('access-control')).resolves.toEqual({ allowed: true })
+ expect(mocks.isOrganizationOnEnterprisePlan).toHaveBeenCalledTimes(1)
})
it('resolves the exact entitlement source only for gated workspace sections', async () => {
diff --git a/apps/sim/lib/settings/application/workspace-section-access.ts b/apps/sim/lib/settings/application/workspace-section-access.ts
index 2f441121f74..01cca335dfe 100644
--- a/apps/sim/lib/settings/application/workspace-section-access.ts
+++ b/apps/sim/lib/settings/application/workspace-section-access.ts
@@ -14,6 +14,7 @@ import { getDeploymentShape } from '@/lib/core/config/deployment-shape'
import { canOpenOrganizationSettingsSection } from '@/lib/organizations/settings-access'
import { isAccessRequestEnabled } from '@/lib/permission-access-requests/settings'
import type { BooleanPermissionGroupConfigKey } from '@/lib/permission-groups/features'
+import { isOrganizationPermissionRegimeActive } from '@/lib/permission-groups/resolve.server'
import { isPlatformAdmin } from '@/lib/permissions/super-user'
import { authorizeOrganizationSettingsSection } from '@/lib/settings/application/organization-section-access'
import { isCustomBlocksEligibleForOrganization } from '@/lib/workflows/custom-blocks/operations'
@@ -114,17 +115,26 @@ async function canOpenOrganizationSection(
}
const needsEnterprisePlan = organizationSection !== 'members' && organizationSection !== 'billing'
- const [canOpenSection, isEnterpriseOrganization] = await Promise.all([
+ /** Same split as the organization surface: Access Control follows the regime, everything else the plan. */
+ const readsRegime = needsEnterprisePlan && organizationSection === 'access-control'
+ const [canOpenSection, isEnterpriseOrganization, governanceActive] = await Promise.all([
canOpenOrganizationSettingsSection(workspace.organizationId, input.userId, organizationSection),
- needsEnterprisePlan
+ needsEnterprisePlan && !readsRegime
? isOrganizationOnEnterprisePlan(workspace.organizationId)
: Promise.resolve(false),
+ readsRegime
+ ? isOrganizationPermissionRegimeActive(workspace.organizationId)
+ : Promise.resolve(false),
])
return (
canOpenSection &&
isOrganizationSettingsSectionAvailable(
organizationSection,
- getOrganizationSettingsFeatures(needsEnterprisePlan && isEnterpriseOrganization, deployment)
+ getOrganizationSettingsFeatures(
+ needsEnterprisePlan && isEnterpriseOrganization,
+ deployment,
+ governanceActive
+ )
)
)
}