Roles and Permissions — Access Control
Controlling who can see and do what usually ends up as a permissions spreadsheet nobody maintains — an incident waiting to happen. In DATTA every capability declares a named permission, permissions are grouped into roles you manage from the interface, and anything not granted is denied by default. You see the access map on a single screen, adjust it in a few clicks, and get an audit trail of every denial.
Concepts
- Permission — a named, specific capability (e.g.
CHAT_SEND,RULES_EDIT,PLATFORM_ADMIN). It is the smallest unit of control. The catalog is central: every permission in the platform lives in it, and creating a permission outside the catalog is a bug — nobody can grant what is not there. - Role — a manageable grouping of permissions. You assign roles to users; the platform checks permissions. That gives you granularity without multiplying management work.
- Sensitive permission — permissions flagged as sensitive (revealing secrets, destructive operations) require a reason recorded in the audit trail, a stricter usage limit, and a double confirmation in the interface.
- Deny by default — if the user lacks the permission, the action is blocked with a friendly message in Portuguese (HTTP 403) and the attempt is recorded in the audit trail.
Built-in profiles (roles)
The platform ships with four profiles that cover most teams:
| Role | For whom | What it covers |
|---|---|---|
ADMIN | Administrators | Every permission in the catalog, including the sensitive ones. |
ADVANCED_USER | Data curators and engineers | Analytical operations and curation: editing the catalog, ontology, glossary, lineage, pipelines, DATTABI, DATTAX, and source discovery. |
ANALISTA | Analysts | Data reading + running notebooks, DATTAX, and DATTABI dashboards. |
READ_ONLY | Consumers | Consuming dashboards, search, chat, and catalog reading. |
The built-in profiles are synchronized into the system database every time the platform starts, idempotently. Your own roles are preserved by that synchronization — nothing you created is overwritten.
Where it is administered
The management screen is , where the catalog appears grouped by category — the same grouping used throughout the interface. To grant access to a person, use : each user gets a base role and can receive additional direct permissions for edge cases. Account details are covered in the users guide.
Permission changes take effect on the user's next sign-in. Server-side blocking, however, is immediate: even if a stale screen still shows a button, the action is denied.
Catalog categories
Every permission belongs to a category. That grouping is what organizes the roles screen:
CHAT GRAPH DOCUMENT RULES PROCESSOS
USERS CONFIG MODELS AUDIT BACKUP
EMBEDDING SEARCH MASKING CATALOG ONTOLOGY
GLOSSARY LINEAGE PROFILING PIPELINE TWIN
JDBC DRIVER DISCOVERY DATTAX DATTABI
MCP PROMPT STATS NOTEBOOK SYSTEM
PLATFORM NEO4J OPENSEARCH VALKEY KAFKA
LLM SIQLCross-cutting permissions
These cover shared capabilities that many screens use, and therefore behave differently from the fine-grained functional permissions:
| Permission | Category | What it unlocks | Fine granularity |
|---|---|---|---|
LLM_INVOKE | LLM | Invoking language inference in the platform (chat, completion, and embedding generation). | Per-model limit/quota is a separate control. |
MCP_INVOKE | MCP | Access to the MCP channel: listing tools, opening a session, and invoking. | Each individual tool requires its own MCP_TOOL_*. |
PRESENCE_VIEW | CHAT | Real-time presence channel (who is online). | — |
PROMPT_VIEW | PROMPT | Viewing prompt templates (system, entity extraction, data extraction, investigation). | — |
PROMPT_EDIT | PROMPT | Editing, creating, and deleting templates — sensitive, because it affects AI behavior across the whole platform. | — |
PLATFORM_VIEW | PLATFORM | Reading infrastructure resources and metrics in the platform console. | — |
PLATFORM_ADMIN | PLATFORM | Non-destructive infrastructure changes (scale, restart, edit non-sensitive configuration). | — |
PLATFORM_SENSITIVE | PLATFORM | Revealing secrets, isolating or draining a node, force-removing a resource — always requires a reason. | — |
Default distribution per profile
A cross-section of the cross-cutting and most used permissions, and how the built-in profiles distribute them. The complete list is always visible on the roles screen itself — that is the up-to-date source, category by category.
| Permission | ADMIN | ADVANCED_USER | ANALISTA | READ_ONLY |
|---|---|---|---|---|
CHAT_SEND | ✓ | ✓ | ✓ | ✓ |
LLM_INVOKE | ✓ | ✓ | ✓ | ✓ |
MCP_INVOKE | ✓ | ✓ | ✓ | ✓ |
PRESENCE_VIEW | ✓ | ✓ | ✓ | ✓ |
PROMPT_VIEW | ✓ | ✓ | ✓ | ✗ |
PROMPT_EDIT | ✓ | ✗ | ✗ | ✗ |
SEARCH_EXECUTE | ✓ | ✓ | ✓ | ✓ |
CATALOG_VIEW | ✓ | ✓ | ✓ | ✓ |
CATALOG_EDIT | ✓ | ✓ | ✗ | ✗ |
RULES_VIEW | ✓ | ✓ | ✗ | ✓ |
RULES_EDIT | ✓ | ✓ | ✗ | ✗ |
RULES_DELETE | ✓ | ✓ | ✗ | ✗ |
AUDIT_VIEW | ✓ | ✓ | ✓ | ✓ |
PLATFORM_VIEW | ✓ | ✓ | ✓ | ✗ |
PLATFORM_ADMIN | ✓ | ✗ | ✗ | ✗ |
PLATFORM_SENSITIVE | ✓ | ✗ | ✗ | ✗ |
USERS_MANAGE_ROLES | ✓ | ✗ | ✗ | ✗ |
BACKUP_RESTORE | ✓ | ✗ | ✗ | ✗ |
DRIVER_MANAGE | ✓ | ✗ | ✗ | ✗ |
MASKING_CONFIGURE | ✓ | ✗ | ✗ | ✗ |
MODELS_MANAGE | ✓ | ✗ | ✗ | ✗ |
Layered security
- The interface hides what the user cannot do — nobody clicks a button just to receive an error.
- The platform blocks on the server, regardless of the interface. That is the real barrier: it never depends on the browser.
- The audit trail records every denial with the actor, the required permission, the resource, and the outcome. With the interface hiding the actions, a recorded denial now genuinely signals an anomalous attempt.
Every access denial generates the AUTHZ.PERMISSION_DENIED event, with actor, permission, resource, method, and outcome=denied. The event covers both denial paths: the route-level block — in which the permission comes through as (path-gate) and the resource is the route itself — and the block by the permission declared on the endpoint, in which the required permission appears by name and the resource identifies class and method.
The event is written as a structured log (logger datta.audit.authz, WARN level, prefix AUDIT event=AUTHZ.PERMISSION_DENIED) and carried by the platform's telemetry into the OpenSearch otel-logs-* index, queryable in Trace Analytics and in the audit console (/configurar/audit?type=AUTHZ). The choice of a structured log is deliberate: the platform does not expose an HTTP audit ingest endpoint today, so any publication through that path would fall into a silent error — the log is what actually guarantees the trail. Creating that ingest endpoint, or migrating the other publishers to the same log channel, is separate work.
Hands-on example — read-only mode in Triage Rules
Access control does not just block: it adapts the interface. The Regras de Triagem (Triage Rules) page (/regras_triagem) is the canonical case.
Previously, the page showed the write buttons to any authenticated user. Anyone without permission clicked, got the error translated into a red notice, and was left not understanding why — a failure of both experience and permission feedback. Today the page reads the logged-in user's permission and adapts: the write actions disappear, a Somente leitura (read-only) badge appears in the header next to the rule count, and rules open in view mode. The user sees and searches all the content, with no buttons that would lead to a denial.
The trigger is the presence of RULES_EDIT (or the ADMIN role) — not the presence of RULES_VIEW. If reading the permission fails for any reason, read-only is the assumed state: deny by default.
What changes on screen
| Element | With RULES_EDIT | Without RULES_EDIT |
|---|---|---|
| Header badge | — | Somente leitura (title="Voce nao tem permissao para alterar regras") |
| + Nova Regra | visible | hidden |
| Co-pilot (generate with AI) | visible | hidden |
| Detail fields (title, block, order, text, contexts) | editable | locked |
| Salvar / Descartar | appear while editing | never appear |
| Ativar/Desativar (enable/disable) and Excluir (delete) | visible | hidden |
| Mover (move, AI rule) | visible | hidden |
| Empty-detail text | "← Selecione uma regra para editar" | "← Selecione uma regra para visualizar" |
The badge only appears for a logged-in user without edit permission. The Manutenção (Maintenance) gear — delete all rules — is separate and requires strict ADMIN; RULES_EDIT is not enough.
AI-generated rules (the "Regras Automáticas" block) stay locked for everyone, including those with RULES_EDIT, until they are moved to a custom block. That lock is independent of the permission-based read-only mode and stacks with it: with either one active, the fields do not accept edits and the Salvar / Descartar bar never shows up.
What the server requires
Every rule mutation — create, update, move, delete, enable/disable, test normative references, and generate with AI, including cancelling the generation — requires RULES_EDIT or ROLE_ADMIN. The block is per route, in the security configuration of the rules service itself; the exact paths are in the API reference.
Reading the rules, today, requires only a valid token: any authenticated user lists the rules. RULES_VIEW exists in the catalog and governs the per-role distribution and the governance intent, but it is not yet enforced on reads. Hardening this would require adding a route-level block on the reads as well — open work.
One detail that matters to implementers: in the reactive services, a permission declared on the class is not intercepted on methods that return Mono/Flux — which is why the mutation block is done per route. The user's permission names reach the server through the token and become authorities; ROLE_ADMIN is never blocked. There is also a server-to-server channel (the X-Internal-Token header, which grants ROLE_INTERNAL_SERVICE plus RULES_VIEW and RULES_EDIT) so that batch triage — asynchronous, without a user token — can read the rules without hitting a 401.
Who lands in read-only mode
| Permission | ADMIN | ADVANCED_USER | ANALISTA | READ_ONLY |
|---|---|---|---|---|
RULES_VIEW | ✓ | ✓ | ✗ | ✓ |
RULES_EDIT | ✓ | ✓ | ✗ | ✗ |
In practice: READ_ONLY users (they have RULES_VIEW, not RULES_EDIT) and any custom role without RULES_EDIT. ADMIN and ADVANCED_USER edit normally.
ANALISTA does not have RULES_VIEW in the default distribution, but that does not hide the page: the Regras menu item (the "Processar" section) is not filtered by permission, and reading is open to every valid token. An ANALISTA who opens the page sees it in read-only mode, for lack of RULES_EDIT. The missing RULES_VIEW reflects the governance intent, which today is enforced neither in the menu nor on the server.
Step by step for the user
- Open Regras de Triagem. If you only have view permission, the Somente leitura badge appears next to the rule count.
- Browse, filter by block/status, and search by number, title, or text normally — reading is unrestricted.
- Select a rule: the detail panel opens with the fields locked, without the Save, Delete, or Enable/Disable buttons.
- To gain editing, ask the administrator for the
RULES_EDITpermission (or a role that contains it, such asADVANCED_USER).
Step by step for the administrator
- To grant editing: assign
RULES_EDITto the user — via a role or as a direct permission — in . The change takes effect at the person's next sign-in. - To keep someone view-only: ensure
RULES_VIEWwithoutRULES_EDIT. TheREAD_ONLYprofile already covers it. - The destructive "Apagar todas as regras" (delete all rules) operation — the Maintenance gear — remains exclusive to strict
ADMINand requires a reason of at least 10 characters, recorded in the audit trail, regardless ofRULES_EDIT.
Known limitations
- The permission is read when the page mounts; reducing a permission mid-session does not update the badge in real time. The server, however, already blocks any mutation regardless of the screen state.
- The interface is a visual hint; the barrier is the server. Even if the screen showed the actions by mistake, the mutation would be denied.
How a new permission enters the catalog
- Declare the permission in the central catalog, in the right category, with a pt-BR description and the sensitive flag. If the category does not exist yet, create it in the category list.
- Distribute it to the appropriate built-in profiles — remembering that
ADMINcovers everything automatically. - Annotate the endpoint(s) with
@RequirePermission("NOVA_PERMISSAO"). - Document it on this page when the permission is cross-cutting.
- Update the tests that validate the catalog, if any.
The annotation in practice
On a method:
@GetMapping("/datasets")
@RequirePermission("CATALOG_VIEW")
public Flux<DatasetDto> listDatasets() { ... }Declared on the controller class, covering all of its methods:
// declared on the controller class — applies to all of its methods
@RequirePermission("AUDIT_VIEW")The annotation works at both levels, and the method overrides the class — it does not stack: each method has exactly one requirement. Permissions ending in _ADMIN/:ADMIN or _VIEW/:VIEW are automatically inherited by ROLE_ADMIN, which avoids listing each one in the role. Equivalent alternative: @PreAuthorize("hasAuthority('X')").
Where the catalog is stored
The catalog is materialized in the datta system database every time the platform starts, idempotently:
(:Role {name, description, builtIn, createdAt, updatedAt})
-[:TEM_PERMISSAO]-> (:Permissao {name})
(:Usuario)-[:TEM_ROLE]->(:Role)
(:Usuario)-[:TEM_PERMISSAO_DIRETA]->(:Permissao) // grants individuaisThe source of truth is the code; the graph exists to feed the roles screen and to store your own roles and the direct grants.
The synchronization runs sequentially and is preceded by a hygiene routine: it deduplicates :Permissao nodes by name, removes nodes with no edges at all (permissions that left the catalog), and creates the uniqueness constraints on Permissao.name and Role.name.
Troubleshooting — repeated entries or legacy permissions on the roles screen. Just restart the authentication component: the hygiene runs at startup. Without redeploying, you can run the
scripts/neo4j-permissoes-dedup.cypherscript — the instructions are in the file's own header. History: until 2026-06-10 the synchronization was concurrent and had no uniqueness constraint, and simultaneous creation produced up to 4 copies per name; the SPAT→DATTA rebrand left 24 orphaned entries.
Current coverage (2026-05-09)
An architectural sweep on 2026-05-09 ensured that 209 out of 209 REST controllers have their permission declared, on the class or on the method. The 10 endpoints intentionally without access control are:
| Endpoint | Justification |
|---|---|
| Login | public by definition |
| Installer download | public distribution of the installer |
| Health checks (4 files) | availability probes for the platform |
| Error handlers (4 files) | expose no endpoints of their own |
The two permission interceptors — one for the reactive services, one for the traditional ones — support:
- Class-level annotation, covering all methods without repetition.
- Method-level override, when a method needs a higher permission than the class. Example: the search area requires
SEARCH_EXECUTEfor the whole class, but deleting documents by query requiresSEARCH_INDEX_DELETE. ROLE_ADMINinheritance of permissions ending in_ADMIN/:ADMINor_VIEW/:VIEW.
Anti-patterns rejected in review
- An endpoint without a declared permission.
- A permission declared with a name that does not exist in the catalog — impossible to grant, so it always denies.
- A role check hardcoded in the code (
hasRole('ADMIN')) instead of the specific permission, losing granularity. - A permission granted only in the interface (hiding the button) without a server-side block.
- A sensitive permission without an audit event or without a usage limit.
See also: Users · API overview · API reference