import { SmrtClassOptions } from '@happyvertical/smrt-core'; import { Membership } from '../models/Membership.js'; import { Tenant } from '../models/Tenant.js'; /** * Permission resolution result */ export interface PermissionResolutionResult { /** Set of granted permission slugs */ permissions: Set; /** Membership ID used for resolution */ membershipId: string | null; /** Role ID from membership */ roleId: string | null; /** Group IDs that contributed permissions */ groupIds: string[]; /** Permission IDs explicitly denied */ deniedPermissionIds: string[]; /** * Ancestor tenant id the resolving membership belongs to, when resolution * used opt-in hierarchical inheritance (no direct membership in the target * tenant; nearest ACTIVE ancestor membership whose role has * `inheritsToDescendants: true`). `null` for direct-membership resolution. */ inheritedFromTenantId: string | null; } export interface PermissionResolutionOptions { /** * Membership row already resolved by the caller for this user/tenant. * Passing this lets request-scoped session loaders avoid re-querying the * same membership row before resolving permissions. * * Pass the RAW lookup result: an inactive row pins resolution to the empty * set (a suspended/pending direct membership stays effective), while an * explicit `null` asserts "no direct membership row exists" and makes the * resolver consider opt-in ancestor-membership inheritance. Leaving it * `undefined` lets the resolver perform its own lookup. */ membership?: Membership | null; } /** * Tenant permission inheritance chain result */ export interface TenantPermissionInheritanceResult { /** Effective tenant permissions (slugs) after inheritance resolution */ permissions: Set; /** Tenant IDs that contributed to the inheritance chain */ contributingTenantIds: string[]; /** Whether inheritance was active (at least one tenant in chain had inheritPermissions: true) */ inheritanceActive: boolean; /** * Permission slugs explicitly DENY'd by a `TenantPermissionOverride` anywhere * in the tenant hierarchy. These are a HARD, tenant-wide block: * `resolvePermissions` subtracts them AFTER role + group grants are applied, * so a tenant-DENY overrides a permission a role/group otherwise grants. A * more-specific membership-override GRANT can still re-add a slug listed here; * a membership-override DENY stays absolute. (This set is independent of the * net `permissions` above, which only reflects DENY's effect on the inherited * cascade.) */ deniedPermissions: Set; } /** * PermissionResolver resolves the effective permissions for a user in a tenant. * * Resolution algorithm: * 1. Resolve tenant hierarchy permissions (if hierarchical tenants are used) * 2. Get user's membership in the tenant * 3. Get base permissions from membership's role * 4. Get user's groups in the tenant * 5. Add permissions from group roles * 6. Apply membership overrides (grant/deny) * * DENY overrides take precedence over GRANT overrides at every level. * * ## Hierarchical Tenant Permissions * * When tenants are organized hierarchically, permissions can cascade from * parent tenants to child tenants. The cascade is controlled by: * - Parent's `cascadePermissions`: If true, parent pushes permissions down * - Child's `inheritPermissions`: If true, child accepts parent's permissions * * Child tenants can override inherited permissions using TenantPermissionOverride: * - INHERIT: Use parent's value (default) * - GRANT: Explicitly grant at this level * - DENY: Explicitly block (even if parent grants) * * ## Hierarchical Membership-Role Inheritance (opt-in) * * Independent of the tenant-level cascade above, membership-role authority * can follow the hierarchy DOWN when a role is explicitly flagged * `inheritsToDescendants: true`: a user with no direct membership in the * target tenant resolves through the nearest ACTIVE ancestor membership * holding such a role. A direct membership row in the target tenant always * wins (including inactive rows, which resolve empty), child tenant-DENY * overrides still subtract from inherited grants, and with no flagged role * the resolver behaves exactly as before. See `resolvePermissions`. * * @example * ```typescript * const resolver = new PermissionResolver(options); * await resolver.initialize(); * * // Check single permission * const canCreate = await resolver.hasPermission(userId, tenantId, 'articles.create'); * * // Get all permissions * const result = await resolver.resolvePermissions(userId, tenantId); * console.log(result.permissions); // Set * * // Resolve tenant-level permissions only (without user context) * const tenantPerms = await resolver.resolveTenantPermissions(tenantId); * ``` */ export declare class PermissionResolver { private options; private membershipCollection; private roleCollection; private rolePermissionCollection; private membershipOverrideCollection; private groupMemberCollection; private groupRoleCollection; private permissionCollection; private tenantCollection; private tenantPermissionOverrideCollection; constructor(options: SmrtClassOptions); /** * Initialize collections * * Each collection is created via the inherited static `SmrtCollection.create()` * factory, which is generically typed to return the concrete subclass instance. */ initialize(): Promise; /** * Resolve effective permissions for a tenant, considering hierarchy inheritance. * * Algorithm: * 1. Get the tenant and its ancestors (from root to immediate parent) * 2. Batch fetch all permission overrides for the entire chain (single query) * 3. Walk down the chain, building up permissions: * - Start with root tenant's permissions * - For each child: if parent.cascadePermissions && child.inheritPermissions: * - Merge parent's permissions * - Apply child's overrides (GRANT adds, DENY removes) * 4. Return the final effective permission set */ resolveTenantPermissions(tenantId: string): Promise; /** * Get the inheritance chain for a tenant (for debugging/display purposes) */ getTenantInheritanceChain(tenantId: string): Promise>; /** * Resolve all effective permissions for a user in a tenant. * * Precedence (broad -> specific, most-specific wins): * tenant-inherited (cascade) * -> role * -> group roles * -> tenant-DENY (removes; overrides role/group grants, tenant-wide) * -> membership GRANT (re-adds; most specific, can win over a tenant-DENY) * -> membership DENY (absolute; always wins) * * ## Membership selection * * A direct membership row in the target tenant always pins resolution to * itself: active rows resolve normally, inactive (pending/suspended) rows * resolve to the empty set — an explicit direct membership is authoritative * even when it attenuates or suspends a user who holds broader authority on * an ancestor tenant. * * Only when NO direct membership row exists does the resolver consider * opt-in hierarchical inheritance: it walks the tenant's ancestors (nearest * first) and resolves through the nearest ACTIVE ancestor membership whose * role has `inheritsToDescendants: true`. All later layers then run * unchanged against the TARGET tenant — the tenant cascade and tenant-DENY * block come from the target tenant (so a child tenant can still carve * authority out of an inherited role), group roles remain exact-tenant * (only groups the user belongs to in the target tenant contribute), and * membership GRANT/DENY overrides travel with the ancestor membership used. * With no role flagged `inheritsToDescendants`, resolution is identical to * the pre-inheritance behavior. * * Algorithm: * 1. Get membership (direct, or nearest inheritable ancestor membership) * and collect all permission IDs from all sources * 2. Batch fetch all permissions in a single query * 3. Apply permissions from role, then groups * 4. Subtract tenant-level DENY'd slugs (hard tenant-wide block) * 5. Apply membership GRANT overrides (can re-add a tenant-DENY'd slug) * 6. Subtract membership DENY overrides (absolute precedence) */ resolvePermissions(userId: string, tenantId: string, options?: PermissionResolutionOptions): Promise; /** * Find the membership to resolve through when the user has no direct * membership row in the target tenant. * * Walks the target tenant's ancestor chain (from the materialized * `hierarchyPath`, nearest ancestor first) and returns the nearest ACTIVE * ancestor membership whose role is explicitly flagged * `inheritsToDescendants: true`. Ancestor memberships that are inactive or * hold an unflagged role are skipped — they neither confer nor block * inheritance from higher ancestors. Attenuating a user in a specific * tenant is expressed with a direct membership row there (which pins * resolution) or a tenant-level DENY, not with an intermediate unflagged * membership. * * Fails closed (returns null, resolving to the empty set) when the tenant * is missing or its `hierarchyPath` is malformed: deeper than * `MAX_TENANT_HIERARCHY_DEPTH`, self-referential, containing duplicate * ancestor ids, or inconsistent with the actual `parentTenantId` chain * (e.g. a stale path naming an unrelated tenant). */ private resolveInheritedMembership; /** * Check if a user has a specific permission in a tenant */ hasPermission(userId: string, tenantId: string, permissionSlug: string, options?: PermissionResolutionOptions): Promise; /** * Check if a user has all of the specified permissions */ hasAllPermissions(userId: string, tenantId: string, permissionSlugs: string[], options?: PermissionResolutionOptions): Promise; /** * Check if a user has any of the specified permissions */ hasAnyPermission(userId: string, tenantId: string, permissionSlugs: string[], options?: PermissionResolutionOptions): Promise; /** * Static factory method */ static create(options: SmrtClassOptions): Promise; } //# sourceMappingURL=PermissionResolver.d.ts.map