Authorization Governance Design Baseline
This document is a design and review baseline. The portable authorization contract described here is not available yet and must not be presented as a released TeaQL capability.
Applications remain responsible for authentication, authorization, policy administration, and production security until the runtime contract and six-language conformance evidence are published.
Why authorization belongs in the application harness
Authorization is often implemented as controller conditions or manually appended query filters. That can protect one endpoint while leaving aggregates, relation loads, background jobs, federation, or a newly generated endpoint with different behavior.
TeaQL already has the ingredients for a stronger boundary:
- a generated model describing entities, fields, relations, and actions;
- requests that preserve query intent before execution;
- a trusted
UserContextpassed to every execute and save operation; - runtime-controlled data access and mutation paths;
- purpose, comment, audit reason, row audit events, and application audit sinks;
- six runtimes that can execute one portable semantic contract.
The goal is to turn authorization from repeated application code into an explicit, model-driven runtime policy that can be governed centrally and enforced locally.
This design develops the use case recorded in teaql-code-gen issue #64: restricting Employee rows to an allowed organization and removing Salary from the selected fields. The issue captures the correct requirement. The portable design must additionally prevent the restriction from being omitted or weakened on other execution paths.
Goals
The authorization system should provide:
- one trusted authorization state in
UserContext; - model-derived identities for protected resources and operations;
- row-, field-, relation-, mutation-, and action-level decisions;
- mandatory enforcement below normal application query code;
- identical observable semantics in Java, Rust, Go, Python, C#/.NET, and TypeScript;
- local enforcement without a synchronous control-plane dependency;
- versioned, explainable, auditable policy decisions;
- centralized policy authoring, review, publication, evidence, and rollback;
- safe composition with dynamic input, caching, federation, internationalization, and audit;
- application extension points without allowing accidental policy bypass.
Non-goals
The first portable contract will not:
- authenticate users or issue identity tokens;
- replace an identity provider, enterprise directory, or secrets system;
- treat client-provided roles, permissions, identity, or policy as trusted;
- promise a universal policy language before the decision model is validated;
- authorize arbitrary raw SQL that bypasses TeaQL runtime enforcement;
- require every application to use one organizational role model;
- make the governance Console a synchronous dependency of database access;
- infer permission from the existence of a generated API or provider;
- replace database, network, operating-system, or deployment security controls.
Authorization belongs to UserContext
All trusted authorization state used for one execution belongs to the real UserContext. Query, mutation, validation, and translation APIs must not receive a second identity, permission, policy, service, or provider argument.
The logical shape is:
UserContext
└── AuthorizationContext
├── verified subject and claims
├── policy set identity and version
├── authorization evaluator
├── decision and obligation state
└── decision observer / audit sink
The application may initialize this state from authentication middleware, a service account, a background-job identity, a test fixture, or another trusted integration. The TeaQL authorization layer does not authenticate those sources. It consumes the verified state that the application placed in UserContext.
Dynamic JSON, generated API input, ordinary federation payloads, and entity fields must not supply or replace the subject, claims, roles, permissions, policy set, evaluator, or enforcement mode.
Generated resource and operation descriptors
Authorization rules should target stable generated identities rather than language-specific class names or endpoint paths.
entity.Employee
entity.Employee.field.salary
entity.Employee.field.organization
entity.Employee.relation.manager
entity.Employee.action.update
entity.Employee.action.delete
action.Employee.approveExpense
operation.Employee.select
operation.Employee.aggregate
The generator can emit portable descriptors containing:
- stable resource and operation IDs;
- entity inheritance and relation metadata;
- field type and sensitivity classification;
- read/write/create/update characteristics;
- generated actions and mutation capabilities;
- model and descriptor versions;
- source provenance for impact analysis.
Generated descriptors are inputs to policy authoring and enforcement. Generated files must not contain application role assignments or environment-specific policy decisions.
The runtime authorization decision
Before executing an operation, the runtime evaluates a canonical request intent against AuthorizationContext and produces either a denial or an AuthorizationPlan.
authorize(
UserContext,
OperationDescriptor,
ResourceDescriptor,
RequestIntent
) → Deny | AuthorizationPlan
An authorization plan may contain:
- an additional row predicate;
- an allowed projection or denied-field set;
- relation traversal and nested-load constraints;
- allowed aggregation and grouping fields;
- mutation field constraints;
- action conditions;
- masking or redaction obligations;
- maximum scope or result obligations;
- decision ID, policy version, reason codes, and audit classification.
This is a logical cross-runtime contract, not a requirement that every language expose identical class or method names.
Request composition must only narrow access
Application query intent and authorization constraints are not interchangeable. The effective request is formed using monotonic rules that cannot broaden access:
| Capability | Composition rule |
|---|---|
| Row filters | Application predicate AND authorization predicate |
| Selected fields | Intersection with allowed projection; denied fields remain denied |
| Relation loads | Apply authorization recursively at each protected relation boundary |
| Sort and group fields | Require explicit authorization; reject forbidden fields |
| Aggregates and facets | Execute over the same authorized active filter and allowed dimensions |
| Count | Count only authorized rows; never reveal the unrestricted total |
| Updates | Require row authorization and validate every changed field |
| Creates | Validate supplied fields, derived ownership, and create constraints |
| Deletes/recoveries | Authorize the target row and operation before mutation |
| Actions | Authorize the named action and its resource conditions |
For example, an application request for all active employees combined with a policy limiting the subject to organization 42 becomes:
employee.active = true
AND employee.organization_id = 42
If Salary is denied, selecting all fields must not restore it. Asking to sort, group, filter, or aggregate by Salary may also leak information and must be explicitly authorized rather than merely hiding the returned column.
Unknown, malformed, or unsupported policy input fails closed. A policy compilation or evaluation failure must not silently remove the authorization predicate.
Enforcement location
Policy enforcement must occur below normal generated and handwritten application requests, immediately before provider execution and again at mutation boundaries where required.
application request
↓
purpose/comment and request validation
↓
mandatory authorization evaluation
↓
authorized provider plan
↓
database execution
Convenience helpers may let application code inspect capabilities or hide unavailable UI actions, but those helpers are not the security boundary. Omitting a UI check or controller condition must not bypass runtime enforcement.
Provider escape hatches that cannot preserve the authorization contract must be clearly classified as privileged operations, disabled by default for ordinary application execution, and separately audited.
Fields, masking, and inference
Field authorization is broader than response serialization. A denied or masked field can leak through:
- filters and existence checks;
- sorting and pagination order;
- grouping and facets;
- aggregates;
- relation predicates;
- error messages and logs;
- cache keys or cache sharing;
- optimistic-lock and mutation responses.
Policy must therefore distinguish capabilities such as read, filter, sort, group, aggregate, write, and reveal. Removing a field from the final JSON is not sufficient authorization.
Masking is an obligation applied after permission has been evaluated, not a substitute for a denial. Sensitive values must remain classified so raw SQL traces, structured messages, audit events, and application sinks do not expose them accidentally.
Mutations and time-of-check behavior
Mutation authorization must evaluate the operation against trusted persisted state as well as proposed changes when policy depends on ownership, organization, status, or another protected attribute.
The runtime must prevent a caller from:
- assigning a protected ownership or scope field during create;
- moving an entity out of the authorized scope through update;
- modifying fields that were not loaded or authorized;
- using a stale entity version to bypass a newer policy-relevant state;
- deleting or recovering a row that is outside the authorized scope.
Authorization does not replace optimistic locking. Both checks must succeed, and the resulting mutation must retain its audit reason.
Federation boundary
A federated caller may transport business request intent and portable subject evidence only through a separately specified trusted protocol. Ordinary request JSON cannot send an effective permission set or an authorization plan.
The receiving service constructs its own UserContext, selects its locally trusted policy set, and enforces authorization before local provider execution. A remote service cannot request a weaker policy, enable a privileged mode, replace the evaluator, or remove an authorization predicate.
Direct and federated execution with equivalent trusted context must produce equivalent allow/deny and data-scope semantics. Policy identities may differ between services, so the trace must record which service made each decision.
Cache and continuous-query safety
Any cached query, aggregation, or continuous-pagination state whose result depends on authorization must include a stable authorization scope identity and policy version in its cache identity. Sharing cached results based only on SQL text, locale, or page number can expose data across subjects.
Policy publication must define whether affected caches are invalidated, version-separated, or allowed to expire. A cached authorization decision must never outlive its documented validity or revocation policy.
Decision explanation and audit
Every decision should have a stable, non-sensitive explanation record:
{
"decisionId": "authz_01...",
"outcome": "ALLOW_WITH_CONSTRAINTS",
"operation": "operation.Employee.select",
"policySet": "workforce-access",
"policyVersion": "2026.08.1",
"reasonCodes": ["ORG_SCOPE_APPLIED", "SALARY_FIELD_DENIED"]
}
Normal clients do not receive internal policy expressions, hidden values, or sensitive subject claims. Authorized operators can use the decision ID to inspect a richer explanation in the governance system.
Authorization decision events complement the immutable row audit path and customizable application audit sink:
- authorization explains why an operation was allowed or denied;
- row audit records persisted data changes;
- application audit records business meaning and workflow events.
One must not impersonate another. Feature-written log lines are not substitutes for runtime authorization evidence or immutable mutation audit.
Shared authorization governance Console
TeaQL proposes authorization as another module of the same governance control plane used for terminology and localization.
generated model and operation descriptors
↓
TeaQL Governance Console
author → simulate → review → approve → publish
↓
signed/versioned policy artifacts and evidence
↓
Java · Rust · Go · Python · .NET · TypeScript
The Console can provide:
- resource, field, relation, operation, and action policy authoring;
- role-based, attribute-based, and relationship-derived rule composition;
- subject and policy-set integration without becoming an identity provider;
- policy simulation against masked fixtures;
- conflict, unreachable-rule, and privilege-expansion analysis;
- review, approval, separation of duties, and change justification;
- environment promotion, staged rollout, rollback, and emergency revocation;
- descriptor compatibility and affected-application reports;
- runtime policy-version inventory and conformance status;
- decision search, explanation, audit export, and compliance evidence;
- cloud-hosted and self-hosted enterprise operation.
The Console is the control plane. It publishes immutable, content-addressed policy artifacts. Runtime enforcement is the data plane and must continue with an already accepted policy artifact if the Console is temporarily unavailable, subject to explicit expiry and revocation rules.
Applications must be able to identify the artifact version and hash used for a decision. Draft or merely uploaded policy must never become executable without publication.
Open runtime and commercial governance boundary
The portable runtime contract should remain capable of correct local authorization without a paid online service. Applications should be able to load a documented policy artifact, provide their own evaluator, and export their policy data.
Commercial Cloud or self-hosted capabilities can provide the higher-value enterprise experience:
- collaborative policy design;
- organizational workflows and SSO;
- simulation and impact analysis;
- managed publication and fleet distribution;
- decision observability and compliance reporting;
- operational support and service guarantees.
The commercial value is governance at organizational scale, not disabling correct authorization in the open runtime. Existing published policies should not stop enforcing merely because the Console is disconnected or a commercial subscription changes state.
Six-runtime conformance suite
One shared corpus must verify Java, Rust, Go, Python, C#/.NET, and TypeScript behavior. It should cover:
- allow and explicit deny;
- row predicates and inheritance;
- field read/filter/sort/group/aggregate/write capabilities;
- nested relations and per-parent loading;
- count, aggregate, and facet non-disclosure;
- create, update, delete, recover, and action authorization;
- protected ownership and scope changes;
- dynamic-input override attempts;
- cache separation and policy version changes;
- direct and federated parity;
- policy compilation/evaluation failure closed behavior;
- decision explanation, masking, and audit evidence;
- identical canonical results across all six runtimes.
A static validator, controller-only check, or persistence-only test is not a complete authorization pass. Evidence requires real provider execution and adversarial cases that attempt to bypass each enforcement boundary.
AI-assisted implementation guardrails
Coding agents must not solve a local authorization request by scattering role checks through controllers or appending optional filters at selected call sites. The maintained implementation rules should require:
- trusted authorization state only in
UserContext; - no second identity, permission, policy, provider, or service argument;
- generated stable descriptors rather than language-specific resource names;
- mandatory runtime enforcement for every provider path;
- monotonic request composition that can only narrow access;
- explicit rejection of unknown or forbidden input;
- structural tests for count, aggregates, relations, mutations, cache, and federation;
- changes to generator/runtime contracts instead of application-layer SQL workarounds.
After review, this subset should be copied into the code-generator repository's maintained AGENTS.md and enforced by generated compile fixtures and the shared authorization corpus.
Proposed delivery stages
- Review stable resource, operation, subject, decision, and authorization-plan schemas.
- Define the first portable policy subset and monotonic request-composition rules.
- Implement reference row and field enforcement in Java and Rust.
- Add mutation, relation, aggregate, cache, and decision-audit coverage.
- Implement Go, Python, C#/.NET, and TypeScript parity.
- Add federation semantics and adversarial cross-runtime fixtures.
- Build policy artifact tooling and a minimal local authoring workflow.
- Add Governance Console collaboration, simulation, publication, and fleet evidence.
- Publish runtime APIs only after six-language conformance evidence is complete.
Current status
This page is the review baseline for future work. Existing TeaQL extension points can support application-specific filters, field removal, and mutation checks, but they do not yet constitute the portable, centrally governed, six-runtime authorization system described here.
Until that system is released, applications must treat authorization as application-owned security logic, test every execution path, and avoid representing the proposed Governance Console or portable policy contract as currently available.