Skip to main content

validation

validatePolicy, validateRoles, validateRole, parsePolicyRow, parseRoleRow + the closed-set limits that the engine enforces at boot.

duck-iam ships standalone validators in @gentleduck/iam/core/validate for when you load policies and roles from sources you don't fully trust - admin dashboards, JSON files written by hand, columns in a shared database, a staging endpoint that previews proposed changes, or a migration tool that copies rows between adapters.

All validators return a IamValidate.IResult with valid: boolean and an array of IamValidate.IIssue records. Issues have closed-set code strings so callers can branch on them programmatically.

validatePolicy

Deep-validates an untrusted candidate policy object. Returns issues for missing required fields, invalid combining algorithm, malformed rules, invalid condition shapes (operator + value + group structure), and any field that exceeds the per-field limit.

import { validatePolicy } from '@gentleduck/iam/core/validate'

const result = validatePolicy(jsonFromDatabase)
if (!result.valid) {
  throw new Error(result.issues.map((i) => `${i.code}: ${i.message}`).join('\n'))
}
// Safe to feed the validated row to the engine
engine.admin.savePolicy(jsonFromDatabase as AccessControl.IPolicy)

Use this before:

  • inserting an admin-dashboard JSON into the database,
  • calling engine.admin.savePolicy() with externally-sourced data,
  • importing a policy bundle from a config file or git repo.

validateRoles

Validates an array of role definitions for cross-role concerns: duplicate ids, dangling inherits references, circular inheritance chains, and roles that have no permissions and no inheritance.

import { validateRoles } from '@gentleduck/iam/core/validate'

const result = validateRoles(rolesFromConfig)
if (!result.valid) console.error(result.issues)

Run this at boot time over the role bundle you load from your adapter or config file. Circular chains are reported as warnings since the runtime cuts them with a seen set; bad ids are errors.

validateRole

Shape guard for a single Role row. Mirrors validatePolicy for the RBAC side. Confirms id is a non-empty string, permissions is an array, and (when present) inherits is an array of strings. Adapters call this after JSON.parse to drop tampered rows before they reach the evaluator.

import { validateRole } from '@gentleduck/iam/core/validate'

const result = validateRole(row)
if (!result.valid) continue   // drop the row, move on

parsePolicyRow

Adapter-author helper. Wraps validatePolicy and returns the typed row on success or null on any validation failure - so the boundary between unknown (from JSON / SQL / Redis) and the typed domain crosses through a single function instead of a scatter of as AccessControl.IPolicy<...> casts.

import { parsePolicyRow } from '@gentleduck/iam/core/validate'

class MyAdapter implements IamAdapter.IAdapter {
  async listPolicies() {
    const rows = await this.myStore.fetchPolicies()
    const out: AccessControl.IPolicy[] = []
    for (const row of rows) {
      const policy = parsePolicyRow(row)
      if (policy !== null) out.push(policy)
      // else: drop the malformed row (log it if you want)
    }
    return out
  }
}

The TAction / TResource / TRole generics are TS-only constraints that runtime validation cannot verify; the adapter trusts the strings because the same library wrote them via savePolicy.

parseRoleRow

Mirror of parsePolicyRow for role rows. Same contract: returns the typed row on success, null on failure.

Limits enforced by validatePolicy

The validators reject inputs that would otherwise blow runtime budgets at evaluation time. The closed-set limits live alongside the validators so callers can introspect them if they want to surface the same caps in their UI.

ConstantValueWhat it caps
IAM_POLICY_LIMITS.rulesPerPolicy1_000rules per policy
IAM_POLICY_LIMITS.actionsPerRule100actions per rule
IAM_POLICY_LIMITS.resourcesPerRule100resources per rule
IAM_POLICY_LIMITS.cartesianPerRule1_000actions x resources expansion
IAM_MAX_FIELD_LENGTH256field-path strings
IAM_MAX_CONDITION_VALUE_LENGTH1024string condition values
MAX_INHERITANCE_DEPTH8role inherits chain depth
import { IAM_POLICY_LIMITS, IAM_MAX_FIELD_LENGTH } from '@gentleduck/iam/core/validate'

ReDoS defense

The matches operator runs the candidate string through a heuristic catastrophic-backtracking detector before compiling the pattern. detectCatastrophicRegex(pattern) returns { safe: boolean; reason?: string } and validatePolicy calls it on every matches condition.

The detector refuses common ReDoS shapes:

  • Nested quantifiers ((a+)+, (a*)*).
  • Alternation inside a quantifier ((a|a)+).
  • More than IAM_MAX_UNBOUNDED_QUANTIFIERS unbounded quantifiers (default 4) in one pattern.
  • Backreference followed by a quantifier ((\w+)\1+).
  • Bounded quantifier with large upper bound (a{1,1000000}); cap is IAM_MAX_BOUNDED_QUANTIFIER (default 1_000).
  • Lookaround group containing a quantifier ((?=(a+)+)).

Patterns deemed unsafe never compile, so the runtime never sees them. A false result from a matches operator inside a deny rule would flip the rule to "condition not met -> allow", so the validator's guarantee is load-bearing.

Closed-set codes

Every issue carries a stable, closed-set code string. Branch on them in your error handling so a code change is a deliberate event:

CodeMeaning
DUPLICATE_ROLE_IDTwo roles share the same id
MISSING_INHERITED_ROLEinherits references an unknown role id
CIRCULAR_INHERITANCERole chain forms a cycle
EMPTY_ROLERole has neither permissions nor inheritance
INVALID_TYPETop-level shape is not an object
MISSING_FIELDA required field is missing or empty
INVALID_EFFECTEffect is not 'allow' or 'deny'
INVALID_OPERATORCondition operator is not in the closed set
INVALID_CONDITIONCondition group structure is malformed
LIMIT_EXCEEDEDA IAM_POLICY_LIMITS cap was breached
UNSAFE_REGEXdetectCatastrophicRegex rejected a pattern

The full IamValidate.IIssue type is exported from @gentleduck/iam/core/validate.

See also

  • JSON schema - emit a JSON Schema document for editor tooling and out-of-band validators.
  • Adapter row parsers - the parsePolicyRow / parseRoleRow pattern in context.