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
30 changes: 27 additions & 3 deletions apps/docs/content/docs/platform/enterprise/access-control.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,23 @@ Organization owners and admins manage groups under **Settings → Organization
Changes to workspace scope, the default-group switch, and membership take effect immediately. **Discard** only discards the buffered name, description, and permission changes.
</Callout>

In the provider, block, and platform permission lists, a checked box allows the item. Clear it to restrict the item. The **Project Issues** project checklist is an exception: checked projects are restricted.
Choose an access state in the provider, block, and platform permission lists:

| State | What members can do |
| --- | --- |
| **Allowed** | Use the item, subject to their workspace role and deployment settings. |
| **Can request** | Discover the item and ask an administrator for access when their role and deployment settings permit it. Use stays blocked until approved. |
| **Unavailable** | Cannot use or request the item. It is omitted from normal discovery; direct links remain restricted. |

Use **Unavailable** for software your organization has not procured. Existing groups retain their permissions and request behavior until you change them. **Project Issues** keeps its separate checklist: checked projects are restricted.

Search and the state filter narrow each list. **Set shown to…** changes only the visible, editable rows and offers choices supported by all of them. Changes remain a draft until **Save**; **Discard** restores the saved configuration.

A restricted parent also restricts its children. Expand a provider, integration, or platform feature to inspect its models, tools, connectors, or authentication modes. Allow the parent before editing its children. Child choices are preserved when a parent changes. An unavailable child stays unavailable even if its parent allows requests. Requests cannot bypass an unavailable parent.

The info icon beside an item explains its restriction and, for inherited choices, names the parent to change. Hover or focus the icon to read the help, or click or tap to keep it open. Press Escape or click outside to dismiss it.

These choices configure the group's policy, independently of the deployment's current availability. **Allowed** and **Can request** cannot enable a deployment-disabled feature or bypass a workspace role. Members only discover and request items available to their deployment and role. Custom blocks, private dynamically discovered models, and other items outside the public request catalog do not offer **Can request**.

### General

Expand All @@ -47,7 +63,7 @@ The **Agent default** chooses the initial model for new Agent blocks. Choose an

### Blocks

**Core Blocks** contains blocks such as Agent, API, and Function; **Tools** contains integration blocks. All are allowed by default. Select a subset to restrict block types, then expand an integration to deny individual operations—for example, allow reads while refusing deletes.
**Core Blocks** contains blocks such as Agent, API, and Function; **Integrations and Triggers** contains integration blocks. All are allowed by default. Select a subset to restrict block types, then expand an integration to deny individual operations—for example, allow reads while refusing deletes.

The `start_trigger` entry block is always allowed. Existing workflows are not rewritten when a restriction changes. A run fails if it reaches a blocked type, provider, model, or tool operation.

Expand Down Expand Up @@ -104,7 +120,7 @@ Restricting **Tool Auto-Approval** prevents remembering approval for future call

#### Organization-level permissions

**Workspace Creation** and **Member Directory** read only the organization default, because those actions have no workspace scope. Other groups show them as inactive with an **Organization** tag, and **Select All** skips them. A newly created workspace uses the organization default until you add it to a workspace group.
**Workspace Creation** and **Member Directory** read only the organization default, because those actions have no workspace scope. Other groups show them as inactive with an **Organization** tag, and **Set shown to…** skips them. A newly created workspace uses the organization default until you add it to a workspace group.

**Integrations**, **API Keys**, **Invitations**, **Personal API Keys**, **CLI Access**, and **OAuth App Access** use the selected workspace group for workspace operations. Account-level operations use the organization default: minting a personal key, sending organization invitations, approving CLI login, authorizing or refreshing OAuth, and account-level billing or audit reads.

Expand All @@ -116,6 +132,14 @@ A **workspace API key** acts as the workspace. It has workspace-role and operati

Chat filters restricted blocks from its available block catalog. Generated steps can still reference a restricted MCP tool, custom tool, or skill; execution rejects the restricted operation when reached. After changing a group, verify both the person's available UI and a representative authorized run. For an existing workflow that fails, remove the restricted item or change the governing policy.

## Access requests

Members can request items marked **Can request** from a locked feature or **Settings → Requests → Browse access**. Administrators review the request and its impact before approving. Approval updates the governing permission group, so it affects everyone governed by that group; it is not a personal exception.

An organization administrator can pause requests in **Settings → Requests → Review requests**. Requestable features remain discoverable, but new requests and approvals are disabled. The group's saved choices, request history, and cancellation remain available. Changing an item to **Unavailable** also prevents approval of an older pending request for that item or a dependent child. Administrators can still decline those requests.

The API keeps the existing access fields. The optional `deniedAccessRequests` list stores canonical target keys such as `integration:salesforce` and `feature:hideTablesTab`. An absent or empty list preserves the previous request behavior. Partial configuration updates retain unspecified choices. Request approval never changes this list. Member credit-limit requests remain separate from permission-group access settings.

## Self-hosted setup

On billing-disabled deployments, enable permission groups through the enterprise feature configuration or explicitly set:
Expand Down
68 changes: 58 additions & 10 deletions apps/docs/openapi-v2-resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -6953,7 +6953,8 @@
"disableOAuthAppAccess": false,
"disableKnowledgeBaseExport": false,
"deniedPartialAccessProjectIssues": [],
"defaultAgentModel": null
"defaultAgentModel": null,
"deniedAccessRequests": []
},
"isDefault": false,
"membershipMode": "inherit",
Expand Down Expand Up @@ -7113,7 +7114,8 @@
"disableOAuthAppAccess": false,
"disableKnowledgeBaseExport": false,
"deniedPartialAccessProjectIssues": [],
"defaultAgentModel": null
"defaultAgentModel": null,
"deniedAccessRequests": []
},
"isDefault": false,
"membershipMode": "inherit",
Expand Down Expand Up @@ -7273,7 +7275,8 @@
"disableOAuthAppAccess": false,
"disableKnowledgeBaseExport": false,
"deniedPartialAccessProjectIssues": [],
"defaultAgentModel": null
"defaultAgentModel": null,
"deniedAccessRequests": []
},
"isDefault": false,
"membershipMode": "inherit",
Expand Down Expand Up @@ -7441,7 +7444,8 @@
"disableOAuthAppAccess": false,
"disableKnowledgeBaseExport": false,
"deniedPartialAccessProjectIssues": [],
"defaultAgentModel": null
"defaultAgentModel": null,
"deniedAccessRequests": []
},
"isDefault": false,
"membershipMode": "inherit",
Expand Down Expand Up @@ -21256,6 +21260,16 @@
}
],
"description": "Default model for new Agent blocks. Null uses the platform default."
},
"deniedAccessRequests": {
"default": [],
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 1024
},
"description": "Access requests are unavailable for the listed canonical target keys when access is restricted."
}
},
"required": [
Expand Down Expand Up @@ -21302,7 +21316,8 @@
"disableOAuthAppAccess",
"disableKnowledgeBaseExport",
"deniedPartialAccessProjectIssues",
"defaultAgentModel"
"defaultAgentModel",
"deniedAccessRequests"
],
"additionalProperties": false,
"description": "Resolved group settings. True disables a boolean capability; null allowlists permit every value and empty allowlists permit none. defaultAgentModel selects the default for new Agent blocks without restricting model selection."
Expand Down Expand Up @@ -21434,7 +21449,8 @@
"disableOAuthAppAccess": false,
"disableKnowledgeBaseExport": false,
"deniedPartialAccessProjectIssues": [],
"defaultAgentModel": null
"defaultAgentModel": null,
"deniedAccessRequests": []
},
"isDefault": false,
"membershipMode": "inherit",
Expand Down Expand Up @@ -21511,7 +21527,8 @@
"disableOAuthAppAccess": false,
"disableKnowledgeBaseExport": false,
"deniedPartialAccessProjectIssues": [],
"defaultAgentModel": null
"defaultAgentModel": null,
"deniedAccessRequests": []
},
"isDefault": false,
"membershipMode": "inherit",
Expand Down Expand Up @@ -21786,6 +21803,15 @@
}
],
"description": "Default model for new Agent blocks. Null uses the platform default."
},
"deniedAccessRequests": {
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 1024
},
"description": "Access requests are unavailable for the listed canonical target keys when access is restricted."
}
},
"additionalProperties": false,
Expand Down Expand Up @@ -21879,7 +21905,8 @@
"disableOAuthAppAccess": false,
"disableKnowledgeBaseExport": false,
"deniedPartialAccessProjectIssues": [],
"defaultAgentModel": null
"defaultAgentModel": null,
"deniedAccessRequests": []
},
"isDefault": false,
"membershipMode": "inherit",
Expand Down Expand Up @@ -21954,7 +21981,8 @@
"disableOAuthAppAccess": false,
"disableKnowledgeBaseExport": false,
"deniedPartialAccessProjectIssues": [],
"defaultAgentModel": null
"defaultAgentModel": null,
"deniedAccessRequests": []
},
"isDefault": false,
"membershipMode": "inherit",
Expand Down Expand Up @@ -22236,6 +22264,15 @@
}
],
"description": "Default model for new Agent blocks. Null uses the platform default."
},
"deniedAccessRequests": {
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 1024
},
"description": "Access requests are unavailable for the listed canonical target keys when access is restricted."
}
},
"additionalProperties": false,
Expand Down Expand Up @@ -24600,6 +24637,16 @@
}
],
"description": "Default model for new Agent blocks. Null uses the platform default."
},
"deniedAccessRequests": {
"default": [],
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 1024
},
"description": "Access requests are unavailable for the listed canonical target keys when access is restricted."
}
},
"required": [
Expand Down Expand Up @@ -24646,7 +24693,8 @@
"disableOAuthAppAccess",
"disableKnowledgeBaseExport",
"deniedPartialAccessProjectIssues",
"defaultAgentModel"
"defaultAgentModel",
"deniedAccessRequests"
],
"additionalProperties": false
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,9 @@ interface IntegrationBlockDetailProps {
}

/**
* Integration detail page, shown only when the viewer's access policy permits
* the integration (an unconfigured one included, which the page presents as
* unavailable). A direct link to a restricted one renders an access-required
* state instead of the connect actions, and nothing renders until the policy
* has loaded.
* Shows integration content only when the viewer's resolved policy permits it.
* Restricted integrations show access and request status, with loading and retry
* states while that status is being checked.
*/
export function IntegrationBlockDetail({ integration, workspaceId }: IntegrationBlockDetailProps) {
const { getAccess, availabilityError, refetchAvailability } = useIntegrationAccess(workspaceId)
Expand All @@ -93,6 +91,22 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
return <IntegrationBlockDetailContent integration={integration} workspaceId={workspaceId} />
}

if (access === 'requestable') {
if (accessRequests.isError) {
return (
<IntegrationAccessNotice
workspaceId={workspaceId}
title='Unable to check access'
description={accessRequests.error.message}
action={<Chip onClick={() => void accessRequests.refetch()}>Try again</Chip>}
/>
)
}
if (!accessRequests.data) {
return <IntegrationBlockDetailFallback workspaceId={workspaceId} />
}
}

const requestable = access === 'requestable' && accessRequests.data?.enabled === true

return (
Expand All @@ -102,7 +116,9 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
description={
requestable
? 'Ask an administrator for access.'
: 'Your organization restricts this integration.'
: access === 'requestable' && accessRequests.data?.enabled === false
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
? 'Access requests are paused for your organization.'
: 'Your organization restricts this integration.'
}
action={
requestable ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ interface IntegrationItemProps {
description?: string | null
icon: ComponentType<{ className?: string }>
unavailable?: boolean
restricted?: boolean
/** Set for an integration the viewer's policy restricts but lets them request. */
onRequestAccess?: () => void
}
Expand All @@ -92,9 +93,9 @@ function IntegrationItem({
description,
icon: Icon,
unavailable = false,
restricted = false,
onRequestAccess,
}: IntegrationItemProps) {
const restricted = onRequestAccess !== undefined
return (
<SettingsResourceRow
iconVariant='custom'
Expand All @@ -110,9 +111,9 @@ function IntegrationItem({
<Lock className='size-[14px] text-[var(--text-icon)]' aria-hidden />
) : undefined
}
href={restricted ? undefined : `/workspace/${workspaceId}/integrations/${slug}`}
href={onRequestAccess ? undefined : `/workspace/${workspaceId}/integrations/${slug}`}
onClick={onRequestAccess}
clickLabel={restricted ? `Request access to ${name}` : `Open ${name}`}
clickLabel={onRequestAccess ? `Request access to ${name}` : `Open ${name}`}
navigable={!unavailable && !restricted}
disabled={unavailable}
/>
Expand Down Expand Up @@ -248,15 +249,15 @@ function IntegrationsContent() {

/**
* Usable and unconfigured integrations are listed, the latter as unavailable;
* a requestable one is listed as a locked row while access requests are on.
* a requestable one stays listed as a locked row while requests are paused.
* Nothing is listed until the access policy has loaded.
*/
const isListed = useCallback(
(blockType: string) => {
const access = getAccess(blockType)
return isIntegrationPermitted(access) || (access === 'requestable' && accessRequestsEnabled)
return isIntegrationPermitted(access) || access === 'requestable'
},
[getAccess, accessRequestsEnabled]
[getAccess]
)

const listedFeatured = useMemo(
Expand Down Expand Up @@ -469,8 +470,9 @@ function IntegrationsContent() {
description={integration.description}
icon={Icon}
unavailable={access === 'unconfigured'}
restricted={access === 'requestable'}
onRequestAccess={
access === 'requestable'
access === 'requestable' && accessRequestsEnabled
? () => setRequestedIntegration(integration)
: undefined
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,13 @@ export const groupSearchUrlKeys = {
*/
export const groupStatusParam = {
key: 'group-status',
parser: parseAsStringLiteral(['all', 'enabled', 'disabled'] as const).withDefault('all'),
parser: parseAsStringLiteral([
'all',
'enabled',
'disabled',
'requestable',
'unavailable',
] as const).withDefault('all'),
} as const

/** Filter view-state: clean URLs, no back-stack churn. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ interface SettingsResourceRowProps {
* the bleed would force a horizontal scrollbar.
*/
flush?: boolean
/** Compact single-line controls retain a 40px row without changing the default resource layout. */
density?: 'default' | 'compact'
/** Renders the row as unavailable without an activation target. */
disabled?: boolean
}
Expand Down Expand Up @@ -133,6 +135,7 @@ export function SettingsResourceRow({
clickLabel,
navigable = false,
flush = false,
density = 'default',
disabled = false,
}: SettingsResourceRowProps) {
const describedById = useId()
Expand Down Expand Up @@ -202,7 +205,9 @@ export function SettingsResourceRow({
// mixing clickable and static rows keeps one height and one inset.
const rowClass = cn(
'flex items-center justify-between gap-2.5',
!flush && '-mx-2 rounded-lg p-2',
!flush && '-mx-2 rounded-lg',
!flush && (density === 'compact' ? 'px-2' : 'p-2'),
density === 'compact' && 'min-h-10 max-md:pointer-coarse:min-h-11',
disabled && 'opacity-50'
)

Expand Down
Loading
Loading