- name
- nocobase-acl-manage
- description
- NocoBase 2 only; never use in a NocoBase 3 project. Task-driven ACL governance through nb CLI for role lifecycle, global role mode, permission policy, Portal entry access, user-role membership, and risk assessment. Use when users describe business permission outcomes instead of raw command arguments, including which roles may enter a Portal.
# Goal
Turn ACL and permission governance into a task-driven workflow so users can ask for business outcomes while the skill handles:
- CLI command selection and argument shaping
- capability checks and safety guards
- write confirmation and readback evidence
- risk-oriented explanation for high-impact changes
# Scope
- This skill is CLI-first for reads and writes.
- Tasks are grouped into five domains:
- role
- global role mode
- permission, including Portal entry access
- user
- risk assessment
- Every write task follows `plan -> confirm -> apply -> readback`.
- `global role mode` is treated as a global system policy, not a per-role field.
# Non-Goals
- Do not bypass CLI by calling direct REST endpoints.
- Do not mutate ACL through ad-hoc database operations.
- Do not create temporary script files to execute ACL writes.
- Do not invoke other skills for env/plugin bootstrap; use direct `nb` commands in this skill.
- Do not hide high-impact blast radius (global mode, broad snippets, broad strategy actions).
- Do not claim one-click coverage for workflows that require governance review.
# Canonical Task Model
## A) Role Domain
| Task | User Outcome | Required Inputs | Optional Inputs |
|---|---|---|---|
| `role.audit-all` | list all roles with comparable policy summary | none | `data_source_key`, `output` |
| `role.create-blank` | create a role with default read-only baseline (single creation mode) | `role_name` | `role_title`, `description`, `hidden`, `allow_configure`, `allow_new_menu`, `portal_uid` or `portal_hint` when creating it for a resolved Portal |
| `role.compare` | explain differences between roles | `role_names[]` | `data_source_key`, `output` |
## B) Global Role-Mode Domain
| Task | User Outcome | Required Inputs | Optional Inputs |
|---|---|---|---|
| `global.role-mode.get` | read current global role mode | none | `output` |
| `global.role-mode.set` | switch global role mode | `role_mode` | `strict_mode` |
`role_mode` enum mapping:
- `default`: independent role usage (no union mode)
- `allow-use-union`: union mode available, role switching still allowed
- `only-use-union`: force union mode for multi-role users
## C) Permission Domain
| Task | User Outcome | Required Inputs | Optional Inputs |
|---|---|---|---|
| `permission.system-snippets.set` | set role-level system snippets | `role_name`, (`snippet_preset` or `snippets`) | none |
| `permission.route.desktop.set` | set desktop route permissions for a role | `role_name`, `route_ids[]` | `set_mode` (`set` or `add` or `remove`) |
| `permission.portal.access.set` | grant or revoke one role's access to an existing Portal | `role_name`, (`portal_uid` or `portal_hint`), `access` | none |
| `permission.data-source.global.set` | set global strategy actions for all tables in one data source | `role_name`, `global_actions[]` | `data_source_key` |
| `permission.data-source.resource.set` | set independent actions for one or more collections | `role_name`, (`collection_hint` or `collection_hints[]`), `actions[]` | `data_source_key`, `fields_map`, `scope_map`, `resource_scope` (`all` by default) |
| `permission.scope.manage` | create/update/list reusable scopes | `scope_task` | `data_source_key`, `scope_id`, `scope_payload` |
## D) User Domain
| Task | User Outcome | Required Inputs | Optional Inputs |
|---|---|---|---|
| `user.assign-role` | assign one role to one or many users | `role_name`, (`user_ids[]` or `user_filter`) | `allow_generic_association_write` |
| `user.unassign-role` | remove one role from one or many users | `role_name`, (`user_ids[]` or `user_filter`) | `allow_generic_association_write` |
| `user.audit-role-membership` | inspect users bound to roles | (`role_name` or `user_id`) | `output` |
## E) Risk Domain
| Task | User Outcome | Required Inputs | Optional Inputs |
|---|---|---|---|
| `risk.assess-role` | risk score and rationale for one role | `role_name` | `data_source_key`, `output` |
| `risk.assess-user` | risk score based on user-role-permission relationship | `user_id` | `data_source_key`, `output` |
| `risk.assess-system` | system-level ACL governance risk summary | none | `data_source_key`, `output` |
Role creation interaction policy:
- always create with the same default read-only baseline (`role.create-blank`)
- `role_name` must use NocoBase role uid format with `r_` prefix
- if user input has no `r_` prefix, normalize to `r_<normalized_name>` and show normalized value in confirmation/readback
- do not ask users to choose role archetypes (for example, "employee/auditor/manager/custom")
- if `role_name` is provided, execute creation directly
- when the request includes the current resolved Portal, resolve it before confirmation; after role creation, immediately grant that role explicit entry to the Portal through `permission.portal.access.set`, then read back both the role and exact Portal association
- treat a failed Portal grant as partial completion and do not claim the new role is ready for that Portal
- when no Portal context is available, do not guess or grant access to an unrelated Portal; after creation succeeds, move to permission assignment guidance
- permission follow-up options: system snippets, desktop routes, Portal entry access, data-source global strategy, data-source resource strategy
Resource permission interaction policy:
- before executing `permission.data-source.resource.set`, always confirm:
- data source key (`main` by default unless user specifies another)
- resolved target collection(s)
- action list
- data scope (`all` by default when user does not specify)
- disambiguate operation verbs from ACL action names:
- phrases like `add table permission` / `configure permission` describe the operation, not ACL action `create`
- only treat `create` as selected action when user clearly asks for data-creation capability (`can create records` / `can add data`)
- user does not need to provide exact technical collection names
- accept business names or keywords as `collection_hint(s)`
- resolve real collection names from the selected data source collection list
- if matching is ambiguous, present candidates and ask user to choose
- if no match is found, ask user for a clearer business keyword
- resolve scope binding before write:
- if user does not specify scope, default to `all`
- `all` -> built-in scope `key=all` id in target data source
- `own` -> built-in scope `key=own` id in target data source
- `custom` -> user-specified scope id (or resolved scope key)
- do not leave action scope as `null` when final scope is `all` or `own`
- write completeness (hard rule):
- for `permission.data-source.resource.set`, execute one complete write payload per target collection
- the write payload must include `usingActionsConfig: true`
- the same payload must include the final `actions[]` set, with explicit scope binding (`scopeId` or `scopeKey`) for `all|own` and explicit non-empty `fields` arrays for field-configurable actions
- do not stage writes as "set actions first, then patch fields/scope/usingActionsConfig"
- before any write, show confirmation summary (data source + resolved collections + actions + scope)
- when scope is defaulted, confirmation must explicitly state `scope=all (default)` and allow user override
- if any required item is missing or unresolved, ask user first and do not write
- default field rule: all fields
- full-field default must be written as explicit field-name lists resolved from target collection metadata
- do not use `fields=[]` as a full-field default marker
- if user did not provide field-level restrictions, apply full-field permission for each selected action
- apply full-field default to every selected action that supports field configuration (`create`, `view`, `update`, `export`, `importXlsx`)
- full-field default must include system fields returned by metadata (for example `sort`, `createdBy`, `createdById`, `updatedBy`, `updatedById`) unless user explicitly asks to exclude them
- use technical field names (`field.name`) for writes, never display titles
- do not auto-drop fields only because they are system/context/relation/hidden; only exclude when user intent explicitly restricts them
- `view` action must default to all fields for that collection
- if user explicitly asks for `all permissions` on a collection, resolve runtime available actions and confirm expanded action set before write
Portal permission interaction policy:
- manage only whether a role may enter an existing custom Portal; do not treat this task as Portal-internal route, menu, page, region, control, or data permission configuration
- resolve one real enabled Portal with `nb portal list -j` before planning a write; accept a uid, name, path, title, or business hint, but require disambiguation when matches are not unique
- use the official `roles.multiPortals:list/add/remove` association actions exposed as `nb api acl roles multi-portals ...`; do not write the `rolesMultiPortals` through table directly
- Portal access for a union role is allowed when any effective role has the explicit Portal grant; `root` can enter all enabled Portals
- `allowNewMultiPortal` is a separate future-Portal default policy and is not evidence that a role can enter a specific existing Portal
- after a write, read back the role's Portal association and verify the exact Portal uid is present or absent
- if the Multi-portal capability or `roles multi-portals` runtime commands are unavailable, stop with capability guidance; do not emulate Portal grants in another collection
# Input Contract
| Input | Required | Default | Validation | Clarification Question |
|---|---|---|---|---|
| `task` | yes | none | one of canonical tasks or alias | "Which ACL governance task should run?" |
| `role_name` | conditional | none | role exists for update/audit tasks; write tasks normalize to `r_*` uid | "Which role should be targeted?" |
| `role_mode` | conditional | none | one of `default/allow-use-union/only-use-union` | "Which global role mode should be set?" |
| `collection_hint` / `collection_hints[]` | conditional | none | required for `permission.data-source.resource.set`; business name/keyword input is allowed | "Which business table(s) should be configured?" |
| `resolved_collection_names[]` | conditional | runtime resolved | required before write for `permission.data-source.resource.set`; each collection must exist in selected data source | "I found these matching collections. Which should be used?" |
| `actions[]` | conditional | none | required for `permission.data-source.resource.set` | "Which actions should be granted on these collections?" |
| `portal_uid` / `portal_hint` | conditional | none | required for Portal permission tasks; must resolve to one enabled custom Portal | "Which Portal should this role access?" |
| `access` | conditional | none | `allow` or `deny` for `permission.portal.access.set` | "Should this role be allowed or denied entry to the Portal?" |
| `resource_scope` | no | `all` | one of `all` / `own` / `custom(scope_id or scope_filter)` | "Which data scope should be used? Default is all; choose own/custom only when needed." |
| `data_source_key` | no | `main` | must exist at runtime | "Which data source key should be used? (default: main)" |
| `strict_mode` | no | `safe` | `safe` or `fast` | "Use safe mode with full readback?" |
| `allow_generic_association_write` | no | `false` | boolean | "Allow guarded generic association writes for user-role assignment?" |
| `output` | no | `text+matrix` | `text`, `text+matrix`, `text+evidence` | "How detailed should the result be?" |
Default behavior when user says `you decide`:
- choose role tasks for role intent
- choose permission tasks for policy intent
- choose user tasks for assignment intent
- choose risk tasks for assessment intent
- for role creation intent, always use `role.create-blank` first
- `data_source_key=main`
- `strict_mode=safe`
## Filter Authoring Gate
Before authoring any reusable custom ACL scope, `scope_filter`, `user_filter`, or other persisted permission filter, load the `nocobase-utils` skill with topic `filter`, then read [Filter Condition Format](../nocobase-utils/references/filter/index.md) in the current task. Resolve every terminal field's live frontend interface/type and use only that group's operator allowlist. Date fields use date-specific comparison operators; `$lt`, `$lte`, `$gt`, and `$gte` remain number-only even when the backend accepts them. Do not apply this UI-displayable allowlist mechanically to fixed CLI locator objects such as `--filter-by-tk` or direct control filters documented by a command.
# Mandatory Clarification Gate
- max clarification rounds: `2`
- max questions per round: `3`
- never execute writes before required inputs are complete
- for `role.create-blank`, ask only for `role_name` when missing; do not ask role-type questions
- for write tasks with `role_name`, normalize to `r_*` uid and echo normalized value in confirmation
- for `permission.data-source.resource.set`, default `data_source_key=main` when omitted unless user explicitly provides another data source
- for `permission.data-source.resource.set`, if collection hint cannot be resolved or is ambiguous, ask follow-up questions before write
- for `permission.data-source.resource.set`, if actions are incomplete, ask follow-up questions before write
- for `permission.data-source.resource.set`, if scope is omitted, apply default `all` and require confirmation before write
- for `permission.data-source.resource.set`, if custom scope is requested but scope id/key is unresolved, ask follow-up questions before write
- for Portal writes, do not continue until one enabled custom Portal is resolved
- for `permission.data-source.resource.set`, if user has not confirmed the final write plan, do not write
- if user asks to set role mode for a specific role, clarify and normalize to global mode change
- if task implies writes and target identity is missing, stop and ask first
- if CLI returns auth errors (`401`, `403`, `Auth required`), stop and instruct the agent to hand off environment/auth recovery to `nocobase-env-manage`
# Workflow
1. Resolve intent and normalize task.
- map aliases to canonical tasks
- map natural-language role mode wording to `default/allow-use-union/only-use-union`
- normalize create-role wording to `role.create-blank` baseline first, then permission assignment
2. Capability gate (CLI).
- confirm direct `nb` CLI is available in PATH
- command assembly guard:
- command form must be `nb <command> [subcommand ...] [flags ...]`
- first token after `nb` must be a command (for example `env` or `api`), not a flag such as `-e`/`-t`/`-j`
- wrong: `nb -e local`
- correct: `nb api resource list --resource users -e local -j`
- raw JSON input guard (PowerShell/runtime):
- prefer structured body flags (for example `--resources`, `--actions`) over inline `--body` when possible
- if raw JSON body is required, prefer `--body-file` over inline `--body`
- `--body-file` content must be valid JSON encoded as UTF-8 without BOM
- if inline `--body` fails JSON parsing in PowerShell, regenerate payload as `--body-file` and retry once
- avoid Bash-style escaped JSON in PowerShell (for example `{\"k\":\"v\"}`), it may be parsed as invalid JSON
- policy payload guard (hard rule for independent resource writes):
- preflight must block `api acl roles data-source-resources create|update` and `api acl roles apply-data-permissions` when payload is missing or invalid (`--body-file` preferred, `--body` compatible)
- for those writes, payload must include `usingActionsConfig: true` and non-empty `actions[]`
- for actions `create/view/update/export/importXlsx`, each action must carry non-empty `fields[]`
- for every action item, scope binding must be explicit via one of:
- `scopeId` (for explicit id binding)
- `scopeKey` (for key-based resolution, such as `all`/`own`)
- `scope.{id|key}` (compatibility readback payload)
- if user intent is scope `all` or `own`, readback must show resolved non-null `scopeId`
- if guard fails, stop before CLI execution and return a fixable error
- parameter safety guard:
- command shape guard for resource permissions:
- `roles data-source-resources` only has `create|get|update`; do not call `list`
- for `roles data-source-resources get|update`, locator must be one of:
- `--filter-by-tk <resource_config_id>`
- `--data-source-key <data_source_key> --name <collection_name>`
- for action-level independent-permission readback, use `roles data-source-resources get ... --appends actions`
View on GitHub