ataccess
v0.0.1
Published
Standalone RBAC/ACL authorization engine with URN-based permissions
Maintainers
Readme
ataccess
Authoritative contract for the ataccess authorization library.
This README is the single source of truth for responsibilities, guarantees, public API, and integration rules. When it conflicts with any other document, this README overrides.
1. Overview
What ataccess is
A standalone authorization library for Node.js and for browser bundles (same public API; used by SPAs for client-side checks) that implements:
- RBAC (role-based access control)
- HRBAC (hierarchical roles with inheritance)
- URN-based ACL (permissions as
resource:action:targetwith wildcards) - ABAC (attribute-based conditions on permissions when a resource context is provided)
- Target semantics for
:own(resource owner) and:tenant(resource in user’s tenant)
It answers one question: “Is this user allowed to perform this action on this resource (or target)?” by:
- Resolving the user’s effective permissions (roles + direct permissions)
- Matching a required URN against those permissions
- Applying target rules (
:own,:tenant) and optional ABAC conditions when a resource object is provided
What problems it solves
- Centralized authorization logic: One place to define and evaluate permissions.
- Deterministic decisions: Same inputs always yield the same allow/deny result (subject to config).
- URN-based permissions: Consistent, parseable format (
resource:action:target) with wildcard matching. - Multi-tenancy and ownership: Built-in handling for “own” and “tenant” targets when resource context is supplied.
- Extensible audit: Optional
auditLoggerhook for logging access decisions.
What it explicitly does NOT do
- Authentication: It does not identify users or validate tokens. The consumer must supply a
User(or null). - Persistence: It does not store roles or permissions. The consumer registers roles (and optional permissions) in memory or from their own store.
- Policy resolution from external systems: It does not fetch policies from external services. All decisions are driven by in-memory roles and permission definitions.
- Implicit grants: Access is never granted by any path other than an explicit matching permission (or the explicit
defaultAllowconfig).
2. Responsibilities & Guarantees
Authorization scope
- ataccess is responsible only for:
- Resolving a user’s effective permissions from roles and direct permissions.
- Matching a required URN against those permissions (with defined wildcard and normalization rules).
- Applying
:ownand:tenantchecks when a resource object is provided. - Evaluating ABAC conditions on permission definitions when a resource is provided.
- It is not responsible for: authentication, session management, rate limiting, or business logic beyond the above.
Deterministic behavior
- For a given
(user, requiredUrn, resource?)and engine configuration (including registered roles and permissions), the result ofcheck/checkSyncis deterministic. - URN normalization (e.g. lowercase, trim) is deterministic and applied consistently for matching.
Security invariants (non-negotiable)
These invariants must never be weakened. They define the security semantics of the library.
Deny by default
If no permission matches the required URN, or the user is null/undefined, the result is deny unless the engine is configured withdefaultAllow === true. For security-critical use, configuredefaultAllow: false.URN semantics
- Permissions and required URNs have exactly three segments:
resource:action:target. - Matching is component-wise;
*in a segment matches any value in that segment. - Normalization is deterministic and documented (see URN utilities).
- Permissions and required URNs have exactly three segments:
:ownand:tenanttarget semantics- For target
own: access is allowed only if the resource’s owner matches the user. Owner is resolved in this order:userId→ownerId→createdBy(first present on the resource object). No other field order is supported. - For target
tenant: access is allowed only ifresource.tenantIdmatchesuser.tenantId. - These field orders and semantics are fixed; changing them would be a breaking change.
- For target
Role resolution
- Inheritance is acyclic (cycle detection via visited set).
- Recursion depth is bounded by
maxDepth. - Inactive or expired
UserRoleentries are excluded from resolution.
No implicit grant
Access is granted only by:- An explicit permission that matches the required URN (and passes any target or ABAC checks), or
- The explicit configuration
defaultAllow: true.
There are no other code paths that grant access.
3. Feature Set
- RBAC: Roles with a list of permission URNs; user has a list of role names (or
UserRole[]). Effective permissions are the union of role permissions plus user’s direct permissions. - HRBAC: Roles may declare
inherits: [roleName, ...]. Resolution is acyclic and depth-limited.UserRolecan carrycontext,active, andexpiresAt; inactive or expired roles are excluded. - URN-based ACL: Permissions are strings in the form
resource:action:target(e.g.profile:read:own,*:*:*). Matching is component-wise with*as wildcard. UsebuildUrn,matchUrn,matchAnyUrnfor construction and matching. - Policy evaluation model: The primary decision is
engine.check(user, requiredUrn, resource?). The engine:- Resolves the user’s effective permissions (via internal use of role resolution and direct permissions).
- Checks if any permission matches the required URN.
- If the URN target is
ownortenantandresourceis provided, applies the corresponding check (owner or tenant). - If a matching permission has ABAC
conditionsandresourceis provided, evaluates conditions (with$user.*placeholder support). - Returns an
AccessResult:{ allowed, reason?, matchedBy?, matchedUrn?, duration? }.
4. Public API Contract
Inputs
- User:
{ userId: string; tenantId?: string; roles: …; permissions: string[]; metadata?: Record<string, unknown> }. Supplied by the consumer; may be null/undefined (then result is deny unlessdefaultAllowis true). The exportedUsertype declaresrolesasstring[]; at runtimerolesmay bestring[]orUserRole[](seeRoleResolver.resolveUserPermissionsandgetPermissionsForUser). UsetoRoleNameswhen you need a flat role-name array from either form. - requiredUrn: Non-empty string in the form
resource:action:target. Invalid format behavior is controlled bystrictMode(see config). - resource (optional): Plain object. Used for
:own/:tenantand for ABAC conditions. Owner fields:userId,ownerId,createdBy(first present wins). Tenant field:tenantId. Other fields may be used by ABAC conditions.
Outputs
- AccessResult:
{ allowed: boolean; reason?: string; matchedBy?: string; matchedUrn?: string; duration?: number }.allowedis the decision; other fields are informational.
Decision semantics
- Allow: At least one effective permission matches the required URN, and any applicable
:own/:tenantcheck passes, and any applicable ABAC conditions evaluate to true. - Deny: Otherwise (no match, or user null, or target/condition check failed). Unless
defaultAllow === true, no match or null user yields deny.
Error behavior
- Invalid URN format: If
strictModeis true, the engine denies immediately without matching. IfstrictModeis false, behavior is defined by the implementation (see URN utilities for normalization). - The library does not throw for “permission denied”; it returns
{ allowed: false, reason?: string, ... }. Throwing is reserved for programming errors (e.g. invalid arguments) as defined by the implementation.
Stable surface (contract)
- Decision:
AccessEngine#check(user, requiredUrn, resource?),checkSync(user, requiredUrn, resource?);AccessResultshape as above. - URN:
parseUrn,buildUrn,matchUrn,matchAnyUrn,isValidUrn,normalizeUrn; formatresource:action:target; wildcard*. - Types:
User,Role,UserRole,AccessResult,ParsedUrn,AccessEngineConfig, and other exported types as in the package. - RoleResolver:
constructor(roles),resolveUserPermissions(user, options)returningResolvedPermissions(e.g. permissions Set, roles Set, hasWildcard). - Config:
AccessEngineConfig(e.g.defaultAllow,strictMode,enableCache,enableAudit,auditLogger, …). - Resource helpers:
getResourceOwnerId(resource),getResourceTenantId(resource)(same field order as:own/:tenant). - Factories:
createAccessEngine(config?),createAccessEngineWithDefaults(config?),createAccessEngineWithRoles(roles, config?).
Consumers must rely only on this stable surface and the documented invariants. Do not depend on unexported or @internal APIs.
5. Integration Guide
Backend vs frontend
The library is the same package everywhere; how you import it depends on the tier:
| Tier | Import rule | Typical use |
|------|-------------|-------------|
| Backend (Node services, gateway) | Use your shared backend access layer—the module that re-exports this library together with persistence, JWT/catalog resolution, gateway middleware, policies, and ACL grants. Do not import this package directly in microservice application code; import buildUrn, rule helpers, and types from that layer so checks stay aligned with the store and gateway. | Facade-first: seed roles, resolve JWT to a RoleResolver, call the facade check(resource, action, { target, resourceContext }) (or requirePermission in resolvers) instead of reaching into store/engine internals. Low-level paths still use buildUrn + engine.check (often via a single checkAccess-style helper). |
| Frontend (browser) | Import this package directly (public API only). | Map your UI user to User; use buildUrn, toRoleNames, and rule helpers (can, canAny, canAll, urnOrRoles, …). Prefer a thin wrapper (e.g. canDo(user, resource, action, target) = buildUrn + can) so URNs are never hand-built. Permissions shown in the UI usually mirror the JWT/session token. |
Standalone or tests may call createAccessEngine / createAccessEngineWithRoles / createAccessEngineWithDefaults and engine.check directly.
How a backend service must consume the library (via the shared layer)
Single decision path
For “can user do X?” use one path: build the required URN withbuildUrn(resourceType, action, target)(e.g. target'*'or'own'or'tenant') and callengine.check(user, urn, resourceContext?), or use your backend’s facade/helper that does the same internally. Do not construct URNs via string concatenation or template literals; usebuildUrnonly.Optional resource context
When the check involves:own,:tenant, or ABAC, pass the resource object as the third argument toengine.check(user, urn, resource). The engine will usegetResourceOwnerId/getResourceTenantIdand ABAC condition evaluation when applicable. Production backends often passresourceContextinto facadecheckso ACL grants can match onresourceProperty/resourceValue.Role and permission loading
This package does not persist data. The backend loads roles (and optionally permission definitions) from its own store, maps them to the engine’sRoleshape if needed, and registers them withengine.addRole(...)or by constructing an engine withcreateAccessEngineWithRoles(roles, config).User shape
The consumer must supply aUserthat matches the contract (e.g.userId,tenantId?,roles,permissions). Normalize role names from your user model withtoRoleNames(roles)when adaptingstring[] | UserRole[]to the format the engine expects.
Responsibilities of the consuming backend
- Authentication: Provide a valid
Useror null; this library does not authenticate. - Persistence: Load and persist roles/permissions; register them with the engine (or your access store behind the facade).
- URN construction: Use only
buildUrnfor building URNs. - Single gate: Route all “can user do X?” checks through one helper that calls
buildUrnandengine.check(and optionally passes resource context), or through the facade equivalent. - No use of internals: Do not import or rely on unexported APIs or
@internalimplementation details (e.g. cache, private methods).
Explicit anti-patterns (what must NOT be done)
- Do not build URNs with template literals or string concatenation. Use
buildUrn(resource, action, target)only. - Do not call internal or unexported APIs. Use only the public, documented surface.
- Do not assume access is granted when
useris null unless you have explicitly setdefaultAllow: true(not recommended for security-critical use). - Do not rely on any behavior that is not documented in this README (e.g. undocumented field order for
:own/:tenant, or undocumented normalization). - Do not use this library to enforce non-authorization concerns (e.g. rate limiting or business rules that are not “can this user perform this action on this resource?”).
6. Extensibility Model
What can be extended safely
- auditLogger (config): Set
auditLoggerinAccessEngineConfigto a function that receivesAuditEvent(e.g. user, urn, resource, result). Invoked after each check; return value is ignored. - PermissionRule and rule helpers: Use
allow,deny,hasRole,can,isOwner,sameTenant,and,or,not,custom,rule(),rateLimit, etc., in your own code (e.g. gateway or middleware). They are not invoked inside the engine forcheck; they are for composing rules outside the engine. (UseAccessEngine.checkonly for allow/deny on URNs; userateLimitand similar helpers in composed rules, not as a substitute for engine checks.) - ABAC conditions: Attach
conditionsto permission definitions and pass aresourceintoengine.check; conditions may reference$user.*in values. Operators and semantics are defined in the implementation (see package types and engine implementation).
What is intentionally fixed and immutable
- URN format: Exactly three segments
resource:action:target; no other structure is supported. - Owner field order for
:own:userId→ownerId→createdByonly. - Tenant field for
:tenant:resource.tenantIdanduser.tenantIdonly. - Deny-by-default: The invariant that “no match or null user ⇒ deny” unless
defaultAllow === truecannot be relaxed without a breaking change. - No external policy resolvers: The engine does not accept plug-in policy resolvers; decisions are driven only by in-memory roles and permission definitions.
7. Non-Goals
- Legacy or backward-compatibility layers: The library does not provide compatibility shims or fallbacks for deprecated behavior. Use the current public API only.
- Fallback behavior: There is no fallback path that grants access when the primary path denies. The only way to allow is an explicit match (or
defaultAllow). - Backward compatibility guarantees: Contract changes that break the documented stable surface are made only with a major version bump. Within that, consumers must not rely on undocumented or internal behavior.
8. Documentation Map
- This README is the only place for authorization contract, invariants, and integration (including backend vs frontend). Other repository documents must link here and must not duplicate those rules.
- Tests:
test/ataccess.spec.ts— contract tests (invariants, stable API) and feature coverage; tests use only the public API. - Repo coding standards may still describe non-contract topics (for example gateway
authorization/folder layout); those sections should point here for anything that touches this library’s semantics.
This README is the authoritative source for ataccess behavior and integration.
