From 6892869bca99c33b0a909c353ece50c2bd6530c4 Mon Sep 17 00:00:00 2001 From: Andrew Connor Date: Tue, 8 Sep 2026 14:05:19 -0400 Subject: [PATCH 1/4] docs(contextual-access): document group-based tool access Adds the public configuration page for the group_access extension, alongside the existing rate-limiting page: how visibility gating is enforced, tool matcher precedence, the three identity backends, Dashboard and API setup, the staleness model, and fail-closed behavior. Two things get more space than their size suggests, because both fail silently rather than loudly: - identity matching. The backend looks a caller up by the identity the request carries, so an Arcade user ID that does not equal the directory login matches nothing, and fail-closed turns that into tools quietly missing. - SailPoint's API client rights. `idn:access-profiles:read` and `idn:roles:read` sound correct and are not sufficient; they authorize reading definitions rather than assignments, so a client with only those authenticates and then fails every lookup. Also records that a tool no rule matches stays visible, so a mistyped matcher governs nothing instead of hiding everything. Refs PLT-3234. Co-Authored-By: Claude Opus 5 --- .../governance/contextual-access/_meta.tsx | 3 + .../contextual-access/group-access/page.mdx | 203 ++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 app/en/operate/governance/contextual-access/group-access/page.mdx diff --git a/app/en/operate/governance/contextual-access/_meta.tsx b/app/en/operate/governance/contextual-access/_meta.tsx index e41331ffd..9c26acccb 100644 --- a/app/en/operate/governance/contextual-access/_meta.tsx +++ b/app/en/operate/governance/contextual-access/_meta.tsx @@ -7,6 +7,9 @@ export const meta: MetaRecord = { "rate-limiting": { title: "Rate Limiting", }, + "group-access": { + title: "Group-Based Tool Access", + }, examples: { title: "Running an Extension", }, diff --git a/app/en/operate/governance/contextual-access/group-access/page.mdx b/app/en/operate/governance/contextual-access/group-access/page.mdx new file mode 100644 index 000000000..553963bf9 --- /dev/null +++ b/app/en/operate/governance/contextual-access/group-access/page.mdx @@ -0,0 +1,203 @@ +--- +title: "Group-Based Tool Access" +description: "Vary which tools a caller can see and use based on their identity provider group membership, enforced natively by the Arcade Engine" +--- + +import { Callout, Steps } from "nextra/components"; + +# Group-Based Tool Access + +As a platform operator, you can vary which tools a caller sees based on who they are. Group-based access is built into the Arcade Engine: you define rules that require membership in an identity provider group, and the Engine resolves membership and filters the tool list before the caller ever sees it. Like rate limits, this runs natively inside Arcade, so there is no server to build or host. + +Use it when one shared MCP gateway serves more than one team or customer and they should not all see the same tools. Without it, the alternatives are a separate gateway per access boundary or a custom webhook extension. + +## How enforcement works + +A group access configuration has two parts: an **identity backend** that holds your groups, and a set of **rules**. Each rule combines two things: + +- A **tool matcher** that selects which tools the rule applies to +- One or more **required groups** the caller must belong to + +Enforcement runs at the tool access hook point, so it applies to both discovery and execution: + +- A caller who does not satisfy a matched rule has the tool **removed from the response** rather than being shown a tool that returns an error. +- Naming that tool directly is also refused, so hiding is not the only enforcement. A caller who guesses a tool name gains nothing. + +Membership is resolved by querying your identity provider, then cached per user and per group for five minutes by default. + + + Multiple required groups on one rule are combined with OR: the caller needs + membership in any one of them, not all of them. + + +### Tool matchers + +| Matcher | Example | Applies to | +| --- | --- | --- | +| Exact | `Slack.SendMessage` | One fully qualified tool | +| Toolkit | `Slack.*` | Every tool in the toolkit | +| Global | `*` | Every tool | + +When several rules match the same call, only the most specific rule applies: an exact match beats a toolkit match, and a toolkit match beats the global match. The caller is evaluated against that one rule only. + +**A tool no rule matches stays visible.** This matters when you write a matcher by hand: a typo produces a rule that governs nothing, rather than a rule that hides everything. Use a global `*` rule when you want a default that covers tools you have not named. + +## Choosing an identity backend + +One configuration uses one backend. All three are read-only integrations that never write to your directory. + +| Backend | You provide | Access it needs | +| --- | --- | --- | +| Okta | Org URL and an API token | Read access to users and groups | +| Microsoft Entra ID | Directory (tenant) ID, application (client) ID, client secret | The `GroupMember.Read.All` application permission, with administrator consent granted | +| SailPoint Identity Security Cloud | Tenant URL, client ID, client secret | See the SailPoint section below, because the rights that sound correct are not the ones that work | + +With Okta and Entra ID, a rule's required groups are group names. With SailPoint, they are access profile or role names. + +### Matching a caller to a directory user + +The backend looks the caller up in your directory **by the identity the request carries**. In practice the Arcade user ID has to equal the directory login exactly. + +This is the most common source of surprise in a real deployment. A caller whose Arcade identity is an internal ID, or an email that differs in case or domain from the directory login, matches nothing. Because the hook fails closed, that surfaces as tools quietly missing for everyone rather than as an error someone can act on. Confirm that the identity your gateway sends is the same string your directory knows the person by before you turn a rule on. + +## SailPoint Identity Security Cloud + +SailPoint support has constraints the other two backends do not. + +**Identity Security Cloud only.** IdentityIQ is a separate product with a different API surface and is not supported. + +**Rules match access profile and role names.** Not group names, and not raw entitlements or identity attributes. A rule requiring `Finance-Analyst` is satisfied when the caller holds an access profile or a role by that name. + +**Only provisioned access counts.** Access that has been requested or approved but not yet provisioned does not satisfy a rule. This is deliberate: a rule should reflect what someone actually has. + +### Required API client rights + +Create a read-only API client in Identity Security Cloud using the OAuth 2.0 client credentials grant. No interactive user is needed. Grant it exactly: + +- `idn:identity-history:read` +- `idn:public-identities:read` + + + `idn:access-profiles:read` and `idn:roles:read` are **not sufficient**, even + though they sound like the correct ones. They authorize reading access + profile and role *definitions*, not which identity has been assigned them. A + client granted only those authenticates successfully and then fails every + membership lookup, which fail-closed behavior turns into every gated tool + disappearing for every caller. + + +### Identity matching in SailPoint + +SailPoint's access read is keyed on an internal identity ID rather than a login, so an email caller is resolved to that ID first. Two cases are reported as errors rather than as a clean denial, so that a mapping mistake stays diagnosable instead of looking like a working deny: + +- No identity matches the caller +- More than one identity matches the caller + +### Consider gating on the provisioned groups instead + +Many organizations use SailPoint to provision access into Okta or Entra ID. If that describes you, pointing a rule at the resulting Okta or Entra group is simpler to operate and closer to real time, because you skip a layer of propagation. Reach for the SailPoint backend when the access you want to gate on lives only in SailPoint. + +## Configure in the Dashboard + + + +### Create a group access extension + +Navigate to **Contextual Access** in the Arcade Dashboard, click **Add Extension**, and choose the group access type. + +### Pick a scope + +Bind the configuration to the organization to apply it across all projects, or to a single project. + +### Select a backend and enter credentials + +Choose Okta, Microsoft Entra ID, or SailPoint ISC, then fill in that backend's fields. Credentials are stored write-only: they are never returned, so the form cannot show you a stored secret later. + +### Add rules + +Each rule row takes a tool matcher and a comma-separated list of required groups. You can add up to 100 rules, each matcher can appear only once, and each rule accepts up to 50 groups. + +### Activate + +The **Active** toggle controls enforcement. Inactive configurations are kept but not enforced, so you can stage rules before turning them on. + + + +Rules and credentials can both be edited after creation. Leave a credential field blank when editing to keep the stored value. + + + A project can have one group access configuration at a time. To combine + more than one set of rules, put them in one configuration. + + +## Configure via the API + +Create a configuration with the plugins API. The example below hides every Slack tool from callers outside two Okta groups, restricts one specific tool to a narrower group, and leaves every other tool visible: + +```bash +curl -s -X POST "https://api.arcade.dev/v1/orgs/{org_id}/projects/{project_id}/plugins" \ + -H "Authorization: Bearer $ARCADE_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "plugin_type": "group_access", + "name": "Team tool boundaries", + "group_access_config": { + "backend": "okta", + "okta": { + "org_url": "https://example.okta.com", + "api_token": "'"$OKTA_API_TOKEN"'" + }, + "rules": [ + { "match": "Slack.SendMessage", "required_groups": ["comms-team"] }, + { "match": "Slack.*", "required_groups": ["comms-team", "support-team"] } + ] + } + }' +``` + +Swap the `okta` block for `entra` or `sailpoint` to use another backend: + +```json +{ "backend": "entra", "entra": { "tenant_id": "...", "client_id": "...", "client_secret": "..." } } +``` + +```json +{ "backend": "sailpoint", "sailpoint": { "tenant_url": "https://acme.identitynow.com", "client_id": "...", "client_secret": "..." } } +``` + +To bind the configuration to the organization instead of a project, post to `/v1/orgs/{org_id}/plugins`. The [API reference](/references/api) documents the full plugins API, including listing, updating, and deleting. + +When you update a configuration, rules are replaced as a set rather than merged, because a rule has no identifier of its own. Send the complete list you want. Credentials behave the opposite way: omit one and the stored value is kept. + +## How quickly a change takes effect + +Effective delay is the membership cache lifetime plus however long your identity provider takes to converge on the change. Both directions are worth understanding: + +- **Granting access fails safe.** A caller stays without the tool until the change propagates. +- **Revoking access is bounded, not immediate.** A caller who is removed from a group can keep seeing a gated tool until the cached answer expires. + +Identity providers are themselves eventually consistent, and in testing a group removal took roughly ten seconds to appear in one provider's transitive membership data. If you need tighter propagation than the default five minutes, lower the cache lifetime on the hook. + +## When the platform cannot verify membership + +If membership cannot be determined, because the provider is unreachable or does not recognize the caller, the tool is **hidden**. A degraded provider must not silently stop enforcing the boundaries you rely on. + +For rules where availability matters more than the boundary, you can opt individual rules into the opposite behavior with `allow_on_unavailable`: + +```json +{ "match": "Slack.*", "required_groups": ["comms-team"], "allow_on_unavailable": true } +``` + + + Because unverifiable membership hides tools, a credential problem and a + revoked user look identical to the caller. If tools disappear unexpectedly + for everyone at once, check the backend credentials and the identity mapping + before looking at group membership. + + +## Next steps + +- [How hooks work](/operate/governance/contextual-access/how-hooks-work) - Where group access fits in the hook pipeline +- [Rate limiting](/operate/governance/contextual-access/rate-limiting) - Cap how often tools can be called +- [Build your own](/operate/governance/contextual-access/build-your-own) - Enforce custom policies from your own webhook server +- [API reference](/references/api) - Full plugins API documentation From b8e53c15892c9d0967ada477d1cba7136ad35030 Mon Sep 17 00:00:00 2001 From: "arcade-docs-bot[bot]" <321924871+arcade-docs-bot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:08:03 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=A4=96=20Regenerate=20LLMs.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/llms.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/llms.txt b/public/llms.txt index 29f0f7497..addc9a3df 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -1,4 +1,4 @@ - + # Arcade @@ -132,6 +132,7 @@ Arcade docs serve two audiences. Start with the path that matches your goal: - [Get Formatted Tool Definitions](https://docs.arcade.dev/en/build/tool-calling/custom-apps/get-tool-definitions): Documentation page - [Getting Your API Key](https://docs.arcade.dev/en/get-started/setup/api-keys): This documentation page guides users on how to obtain and manage their Arcade API key, detailing the steps for generating keys through both the Arcade dashboard and CLI. It emphasizes the importance of securely storing API keys, as they serve as administrator credentials that grant access to - [Governance](https://docs.arcade.dev/en/operate/governance): Documentation page +- [Group-Based Tool Access](https://docs.arcade.dev/en/operate/governance/contextual-access/group-access): Documentation page - [Handle Errors](https://docs.arcade.dev/en/build/create-tools/error-handling): Documentation page - [Hosting options overview](https://docs.arcade.dev/en/operate/deploy): Documentation page - [How Hooks Work](https://docs.arcade.dev/en/operate/governance/contextual-access/how-hooks-work): Documentation page From 3b4398686e88c711be8f89febfa84f5db6996693 Mon Sep 17 00:00:00 2001 From: Andrew Connor Date: Tue, 8 Sep 2026 15:03:41 -0400 Subject: [PATCH 3/4] docs(contextual-access): how to name an Entra group in a rule The page said a required group is a group name for both Okta and Entra ID. That is no longer accurate for Entra: a rule may carry the group's object ID, and a display name is honoured only for a security group that is not a Microsoft 365 group. The reason is worth an operator's attention rather than a footnote. Entra does not require display names to be unique and many tenants let ordinary members create groups, so a display-name rule can be satisfied by someone who creates a group with that name and joins it. Restricting the match to ordinary security groups removes the surface most members can add to but is not a guarantee, so the page recommends the object ID for anything sensitive and says where to copy it from. Also notes that this does not apply to Okta or SailPoint, where creating a group, access profile, or role is an administrative action. Refs PLT-3234. Co-Authored-By: Claude Opus 5 --- .../contextual-access/group-access/page.mdx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/en/operate/governance/contextual-access/group-access/page.mdx b/app/en/operate/governance/contextual-access/group-access/page.mdx index 553963bf9..4cc8b8d53 100644 --- a/app/en/operate/governance/contextual-access/group-access/page.mdx +++ b/app/en/operate/governance/contextual-access/group-access/page.mdx @@ -52,7 +52,26 @@ One configuration uses one backend. All three are read-only integrations that ne | Microsoft Entra ID | Directory (tenant) ID, application (client) ID, client secret | The `GroupMember.Read.All` application permission, with administrator consent granted | | SailPoint Identity Security Cloud | Tenant URL, client ID, client secret | See the SailPoint section below, because the rights that sound correct are not the ones that work | -With Okta and Entra ID, a rule's required groups are group names. With SailPoint, they are access profile or role names. +With Okta, a rule's required groups are group names. With Entra ID they are group names or group object IDs, which is a distinction worth reading below. With SailPoint, they are access profile or role names. + +### Naming an Entra group: name or object ID + +Entra does not require group display names to be unique. Its object ID is the only unique key, and many tenants let ordinary members create groups. A rule that requires the display name `finance-admins` could therefore be satisfied by someone who creates their own group with that name and joins it, without ever belonging to the group you meant. + +Two ways to name an Entra group in a rule: + +| Rule value | Matched against | Collision risk | +| --- | --- | --- | +| `finance-admins` | `displayName`, and only for a security group that is not a Microsoft 365 group | Possible, if your tenant lets members create security groups | +| `3f2504e0-4f89-11d3-9a0c-0305e82c3301` | the group's object ID | None. It is unique, and it follows the group through a rename | + +Restricting a display-name match to ordinary security groups removes the surface most members can add to, but it is not a guarantee. **For anything sensitive, put the group's object ID in the rule.** You can copy it from the group's overview page in Entra. + + + This does not apply to Okta or SailPoint. Group, access profile, and role + creation there are administrative actions, so an ordinary caller cannot + introduce a colliding name. + ### Matching a caller to a directory user From f1e4853a0c64914d96f0c88d0fe200afac5b12a5 Mon Sep 17 00:00:00 2001 From: Andrew Connor Date: Tue, 8 Sep 2026 15:49:19 -0400 Subject: [PATCH 4/4] docs(contextual-access): a SailPoint access profile is named by its ID The page said rules match access profile and role names. That is right for a role and wrong for an access profile, and wrong in a way that would send an operator to configure something that cannot work. SailPoint's access-items response documents `displayName` as the role display name on a role, but as the identity's display name on an access profile, and it carries no access profile name at all. There is nothing for a profile-name rule to match, so the rule carries the profile's ID. The page now states which value each rule shape matches, says where to copy the ID from, and explains why we do not resolve names for you: it would need `idn:access-profiles:read`, which the required-rights section deliberately excludes. Refs PLT-3234. Co-Authored-By: Claude Opus 5 --- .../contextual-access/group-access/page.mdx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/en/operate/governance/contextual-access/group-access/page.mdx b/app/en/operate/governance/contextual-access/group-access/page.mdx index 4cc8b8d53..85b8b8471 100644 --- a/app/en/operate/governance/contextual-access/group-access/page.mdx +++ b/app/en/operate/governance/contextual-access/group-access/page.mdx @@ -52,7 +52,7 @@ One configuration uses one backend. All three are read-only integrations that ne | Microsoft Entra ID | Directory (tenant) ID, application (client) ID, client secret | The `GroupMember.Read.All` application permission, with administrator consent granted | | SailPoint Identity Security Cloud | Tenant URL, client ID, client secret | See the SailPoint section below, because the rights that sound correct are not the ones that work | -With Okta, a rule's required groups are group names. With Entra ID they are group names or group object IDs, which is a distinction worth reading below. With SailPoint, they are access profile or role names. +With Okta, a rule's required groups are group names. With Entra ID they are group names or group object IDs, which is a distinction worth reading below. With SailPoint, they are role names or access profile IDs, which is also covered below. ### Naming an Entra group: name or object ID @@ -85,7 +85,17 @@ SailPoint support has constraints the other two backends do not. **Identity Security Cloud only.** IdentityIQ is a separate product with a different API surface and is not supported. -**Rules match access profile and role names.** Not group names, and not raw entitlements or identity attributes. A rule requiring `Finance-Analyst` is satisfied when the caller holds an access profile or a role by that name. +**Rules match roles and access profiles, not groups.** Raw entitlements and identity attributes do not count either. How you name each one differs: + +| Rule value | Matched against | +| --- | --- | +| `Finance-Approver` | a role's name | +| a role's ID | that role | +| an access profile's ID | that access profile | + +**An access profile has to be named by its ID.** SailPoint's access-items response documents `displayName` as the *role* display name on a role, but as *the identity's* display name on an access profile, and it carries no access profile name at all, so a profile-name rule has nothing to match against. Copy the ID from the access profile's page in Identity Security Cloud. + +Resolving profile names to IDs for you would mean reading `/v3/access-profiles`, which needs `idn:access-profiles:read`. The section below deliberately excludes that right, so the rule carries the ID instead. **Only provisioned access counts.** Access that has been requested or approved but not yet provisioned does not satisfy a rule. This is deliberate: a rule should reflect what someone actually has.