npm package: @rubiklabs/nestjs-jacpol
Description: JACPoL-based access control for NestJS — Redis-first, plugin adapters
Author: RubikLabs
Latest version: 1.0.1

# @rubiklabs/nestjs-jacpol

JACPoL-based access control for NestJS — policy-driven, ABAC-first, RBAC-friendly. Implements the JSON-based Access Control Policy Language (JACPoL) from _Hao Jiang & Ahmed Bouabdallah, WISTP 2017_.

## Installation

```bash
npm install @rubiklabs/nestjs-jacpol
```

For the default Redis adapter you need **Redis** and **ioredis** (included as a dependency). For the optional Kysely (database) adapter, see [Plugin: Kysely](#plugin-rubiklabsnestjs-jacpol-kysely).

## Quick Start (Redis, minimal)

```typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { JacpolModule } from '@rubiklabs/nestjs-jacpol';

@Module({
  imports: [
    JacpolModule.forRoot({
      redis: {
        host: 'localhost',
        port: 6379,
      },
    }),
  ],
})
export class AppModule {}
```

Then protect a route with the guard and `@RequirePolicy`:

```typescript
// document.controller.ts
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { JacpolGuard, RequirePolicy } from '@rubiklabs/nestjs-jacpol';

@Controller('documents')
@UseGuards(JacpolGuard)
export class DocumentController {
  @Get(':id')
  @RequirePolicy('read', { type: 'document' })
  findOne(@Param('id') id: string) {
    return { id };
  }
}
```

Ensure the request has a **subject** (e.g. `req.user`) set by your auth layer (JWT guard, session, etc.). The guard builds an access request from `subject`, `resource` (from decorator + `req.params`), `action`, and `environment` (ip, time, weekday, date, userAgent), then asks the PDP. On **deny** it throws `ForbiddenException`; on **permit** it attaches any obligations to `req.jacpolObligations`.

## Module Configuration

### `forRoot(options)`

Synchronous configuration:

```typescript
JacpolModule.forRoot({
  adapter: 'redis',  // optional: 'memory' | 'file' | 'redis'; default is redis
  redis: {
    host: 'localhost',
    port: 6379,
    password: process.env.REDIS_PASSWORD,
    db: 0,
    url: 'redis://...',  // overrides host/port
    keyPrefix: 'jacpol:',
    ttl: 3600,
    client: myExistingRedisClient,
  },
  file: { basePath: './policies' },
  cache: { enabled: true, ttl: 30000 },
  defaultDecision: 'deny',
  debug: false,
});
```

### `forRootAsync(options)`

Async configuration (e.g. with `ConfigService`):

```typescript
import { ConfigModule, ConfigService } from '@nestjs/config';

JacpolModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    redis: {
      host: config.get('REDIS_HOST'),
      port: config.get<number>('REDIS_PORT'),
      password: config.get('REDIS_PASSWORD'),
      keyPrefix: 'myapp:jacpol:',
      ttl: 3600,
    },
    cache: { ttl: 60000 },
  }),
});
```

## Adapter Reference

| Adapter   | When to use                         | Config        |
|----------|--------------------------------------|----------------|
| **Redis** | Default; production PRP + optional TTL | `redis`        |
| **Memory** | Dev/test; no persistence             | none           |
| **File** | Simple file-based policies           | `file.basePath`|
| **Custom** | Your own storage                     | `adapter: myAdapter` |

### Redis (default)

- **host** — default `'localhost'`
- **port** — default `6379`
- **password**, **db**, **url** — optional
- **keyPrefix** — default `'jacpol:'`
- **ttl** — seconds; no expire if omitted
- **client** — inject existing `ioredis` client; other connection options ignored

### Memory

Use for tests or single-process dev. No config. Set `adapter: 'memory'`.

### File

Each PolicySet is stored as `{id}.json` under `file.basePath`. Set `adapter: 'file'` and `file: { basePath: './policies' }`.

### Custom adapter

Implement `IPolicyAdapter` and pass the instance:

```typescript
JacpolModule.forRoot({
  adapter: new MyDatabaseAdapter(),
});
```

### Plugin: @rubiklabs/nestjs-jacpol-kysely

For database-backed storage (PostgreSQL, MySQL, SQLite) with Kysely:

```bash
npm install @rubiklabs/nestjs-jacpol-kysely kysely pg
```

```typescript
import { KyselyAdapter, JacpolKyselyModule } from '@rubiklabs/nestjs-jacpol-kysely';
import { PostgresDialect } from 'kysely';
import * as pg from 'pg';

JacpolModule.forRoot({ redis: { host: 'localhost', port: 6379 } }),
JacpolKyselyModule.forRoot({
  dialect: new PostgresDialect({
    pool: new pg.Pool({ connectionString: process.env.DATABASE_URL }),
  }),
}),
```

Redis can act as PRP cache while the DB is the primary store (adapter override).

## Policy Structure

- **PolicySet** — top-level container; has `target`, `policies[]`, `algorithm`, optional `obligation`, `priority`.
- **Policy** — has `target`, `rules[]`, `algorithm`, optional `obligation`, `priority`.
- **Rule** — has optional `target`, `condition`, `effect` (`permit` | `deny`), optional `obligation`, `priority`.

Targets and conditions use the same structure: attribute paths (e.g. `subject.role`, `resource.type`, `action`, `environment.time`) and operator specs (e.g. `{ equalsTo: 'admin' }`, `{ in: ['read','write'] }`).

## Writing Policies

Example PolicySet (document access):

```json
{
  "id": "document-access-policy-set",
  "target": { "resource.type": { "equalsTo": "document" } },
  "algorithm": "denyOverrides",
  "policies": [
    {
      "id": "admin-policy",
      "target": { "subject.role": { "equalsTo": "admin" } },
      "algorithm": "permitOverrides",
      "rules": [{ "id": "admin-full-access", "effect": "permit" }]
    },
    {
      "id": "user-read-policy",
      "target": { "action": { "in": ["read", "list"] } },
      "algorithm": "firstApplicable",
      "rules": [
        {
          "id": "block-weekend",
          "condition": { "environment.weekday": { "in": ["saturday", "sunday"] } },
          "effect": "deny",
          "priority": 10
        },
        {
          "id": "allow-business-hours",
          "condition": { "environment.time": { "between": "09:00 18:00" } },
          "effect": "permit"
        }
      ]
    }
  ]
}
```

## Implicit Logic Operators (ILO)

- **JSON object** (e.g. `{ "subject.role": { "equalsTo": "admin" } }`) → **allOf**: every key’s condition must be true.
- **JSON array** (e.g. `[ { "subject.role": { "equalsTo": "admin" } }, { "subject.role": { "equalsTo": "user" } } ]`) → **anyOf**: at least one element must be true.

Explicit operators: **allOf**, **anyOf**, **not** (as keys in the condition object).

## Combining Algorithms

- **permitOverrides** — any permit → permit; all notApplicable → notApplicable; else deny.
- **denyOverrides** — any deny → deny; all notApplicable → notApplicable; else permit.
- **firstApplicable** — first non–notApplicable decision.
- **highestPriority** — highest priority among applicable; ties resolved with denyOverrides.

## Guard & Decorators

- **`@UseGuards(JacpolGuard)`** — Enforces policy for the handler. If no policy metadata is set, the guard allows access.
- **`@RequirePolicy(action, resource?, environment?)`** — Attaches policy metadata (action and optional resource/environment).
- **`@PolicySubject(path?)`** — Parameter decorator to get the subject from the request (default `req.user`; `path` e.g. `'auth'` for `req.auth`).

Example:

```typescript
@Get(':id')
@UseGuards(JacpolGuard)
@RequirePolicy('read', { type: 'document' })
findOne(@Param('id') id: string, @PolicySubject() subject: Record<string, any>) {
  return this.service.findOne(id);
}
```

## Obligation Handling

When the PDP returns obligations (e.g. from a rule or policy), they are attached to the request as **`req.jacpolObligations`** (shape: `{ permit?: Record<string, any[]>, deny?: Record<string, any[]> }`). Your interceptors or controllers can read them and enforce (e.g. audit, headers).

## Debug Mode

Set `debug: true` in module options for more verbose logging (implementation may log policy ids, decisions, etc.).

## TypeScript Types

All public types are exported: `PolicySet`, `Policy`, `Rule`, `AccessRequest`, `AccessDecision`, `Effect`, `Algorithm`, `Decision`, `Obligation`, `IPolicyAdapter`, `RedisAdapterConfig`, `PolicyMeta`, `IAttributeResolver`, etc. Use them for typing policies, requests, and custom adapters.