Policies, Roles, and Permissions Architecture

Last modified: 12/10/2025

Policies, Roles, and Permissions Architecture

Overview

ngdpbase uses a Policy-Based Access Control (PBAC) system inspired by JSPWiki's security framework. This architecture provides fine-grained control over who can access what resources and perform which actions.

Terms

Core Components

1. PolicyEvaluator (src/managers/PolicyEvaluator.js)

The PolicyEvaluator is the central component that evaluates access requests against defined policies.

Key Method:

async evaluateAccess(context)

Parameters:

Returns:

{
  hasDecision: boolean,  // Whether a policy matched
  allowed: boolean,      // Whether access is granted
  reason: string,        // Human-readable reason
  policyName: string     // ID of the matching policy
}

2. Policy Structure

Policies are defined in config/app-default-config.json under ngdpbase.access.policies:

{
  "id": "admin-full-access",
  "name": "Administrator Full Access",
  "description": "Full system access for administrators",
  "priority": 100,
  "effect": "allow",
  "subjects": [
    {"type": "role", "value": "admin"}
  ],
  "resources": [
    {"type": "page", "pattern": "*"}
  ],
  "actions": [
    "page:read",
    "page:edit",
    "admin:users",
    ...
  ]
}

Policy Fields:

3. Roles

Roles are assigned to users and define their capabilities through policies.

Built-in Roles:

Special Roles:

4. Actions (Permissions)

Actions are namespaced strings representing operations:

Page Permissions:

Attachment Permissions:

Export Permissions:

Search Permissions:

Admin Permissions:

Default Policies

The system includes 7 default policies defined in config/app-default-config.json:

1. admin-full-access (Priority 100)

2. deny-anonymous-system-pages (Priority 90)

3. editor-permissions (Priority 80)

4. contributor-permissions (Priority 70)

5. reader-permissions (Priority 60)

6. anonymous-read-only (Priority 50)

7. default-view-for-all (Priority 1)

How Policies Are Evaluated

  1. Policy Loading:

    • Policies are loaded from configuration at startup
    • ACLManager loads policies into PolicyEvaluator
  2. Access Check Flow:

    text User Request → ACLManager.checkPagePermission() ↓ Action Mapping (view → page:read) ↓ PolicyEvaluator.evaluateAccess() ↓ Check Each Policy (by priority) ↓ First Match Wins → Return Decision

  3. Matching Logic:

    • Policies are checked in priority order (highest first)
    • For each policy, three checks are performed:
      • Subject Match: Does user have required role?
      • Resource Match: Does the page match the pattern?
      • Action Match: Is the requested action in the policy?
    • First policy where all three match determines the outcome
    • If no policy matches, access is denied
  4. Action Name Mapping:
    Legacy action names are mapped to policy actions in ACLManager:

    javascript const actionMap = { 'view': 'page:read', 'edit': 'page:edit', 'delete': 'page:delete', 'create': 'page:create', 'rename': 'page:rename', 'upload': 'attachment:upload' };

Integration Points

UserManager.hasPermission()

Used for generic permission checks (admin routes, features):

async hasPermission(username, action) {
  // Builds user context with roles
  const userContext = {
    username: user.username,
    roles: [...user.roles, 'Authenticated', 'All'],
    isAuthenticated: true
  };

  // Evaluates using PolicyEvaluator with generic page '*'
  const result = await policyEvaluator.evaluateAccess({
    pageName: '*',
    action: action,
    userContext: userContext
  });

  return result.allowed;
}

ACLManager.checkPagePermission()

Used for page-specific access control:

async checkPagePermission(pageName, action, userContext, pageContent) {
  // 1. Map legacy action names to policy actions
  const policyAction = actionMap[action.toLowerCase()] || action;

  // 2. Evaluate global policies first
  const policyResult = await policyEvaluator.evaluateAccess({
    pageName,
    action: policyAction,
    userContext
  });

  if (policyResult.hasDecision) {
    return policyResult.allowed;
  }

  // 3. Check page-level ACLs if no policy matched
  // 4. Default deny if nothing matched
}

User Context Construction

User contexts are built consistently across the system:

Anonymous Users:

{
  username: 'Anonymous',
  roles: ['anonymous', 'All'],
  isAuthenticated: false
}

Authenticated Users:

{
  username: 'jim',
  roles: ['reader', 'editor', 'admin', 'Authenticated', 'All'],
  isAuthenticated: true
}

Note: Authenticated and All roles are automatically added.

Example Access Checks

Example 1: Anonymous User Views Welcome Page

text Request: GET /view/Welcome User: Anonymous Action: view → page:read Policy Evaluation: 1. admin-full-access: NO (not admin role) 2. deny-anonymous-system-pages: NO (Welcome doesn't match *Admin*) 3. editor-permissions: NO (not editor role) 4. contributor-permissions: NO (not contributor role) 5. reader-permissions: NO (not reader role) 6. anonymous-read-only: NO (anonymous role but needs resource match) 7. default-view-for-all: YES (All role, page:read, * pattern) Result: ALLOWED (policy: default-view-for-all) <span data-jspwiki-placeholder="d3fdc9d9-7"></span> ### Example 3: Anonymous User Tries Admin Page text
Request: GET /admin/users
User: Anonymous
Action: admin:users

Policy Evaluation:

  1. admin-full-access: NO (not admin role)
  2. deny-anonymous-system-pages: YES (anonymous, page matches Admin, deny)

Result: DENIED (policy: deny-anonymous-system-pages)


### Example 4: Editor Creates Page

``` text
Request: POST /create
User: editor_user (roles: editor)
Action: page:create

Policy Evaluation:
1. admin-full-access: NO (not admin role)
2. deny-anonymous-system-pages: N/A (not anonymous)
3. editor-permissions: YES (editor role, page:create in actions, * pattern)

Result: ALLOWED (policy: editor-permissions)

Logging and Debugging

The system provides extensive logging for troubleshooting:

``` text
POLICY Evaluate page=Welcome action=page:read user=Anonymous roles=Anonymous|All
POLICY Check policy=admin-full-access effect=allow match=false
POLICY Check policy=default-view-for-all effect=allow match=true
ACL PolicyEvaluator decision hasDecision=true allowed=true policy=default-view-for-all


## Adding Custom Policies

To add a new policy, edit `config/app-default-config.json`:

```json
{
  "id": "moderator-access",
  "name": "Moderator Permissions",
  "description": "Moderators can edit and delete but not create",
  "priority": 75,
  "effect": "allow",
  "subjects": [
    {"type": "role", "value": "moderator"}
  ],
  "resources": [
    {"type": "page", "pattern": "*"}
  ],
  "actions": [
    "page:read",
    "page:edit",
    "page:delete"
  ]
}

Security Considerations

  1. Priority Matters: Higher priority policies override lower ones. Place deny policies before allow policies.
  2. Default Deny: If no policy matches, access is denied by default.
  3. Role Accumulation: Users accumulate roles (Authenticated, All) automatically. Be careful with All role policies.
  4. Resource Patterns: Use specific patterns for sensitive resources to avoid unintended access.
  5. Action Granularity: Separate admin actions (admin:*) from page actions (page:*) to prevent privilege escalation.

Files Typically Affected

Developer Information

For implementation details, source file locations, and integration guidance, see Documentation for Developers.