ldap-rest 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,144 @@
1
+ # Documenting plugin endpoints (OpenAPI)
2
+
3
+ The published API reference at <https://linagora.github.io/ldap-rest/api/> is
4
+ generated from `openapi.json`, which itself is built statically by
5
+ [`scripts/generate-openapi.ts`](../../scripts/generate-openapi.ts) — it walks
6
+ every plugin's `api()` method and reads the JSDoc comments above each route.
7
+
8
+ You don't need to touch the generator to enrich your plugin's documentation.
9
+ Two JSDoc directives drive the entire pipeline:
10
+
11
+ - `@openapi` — placed **just above an `app.method(path, handler)` call**,
12
+ describes that single route.
13
+ - `@openapi-component` — placed anywhere in the file, registers reusable
14
+ schemas under `components.schemas` so multiple routes can `$ref` them.
15
+
16
+ Both directives carry inline **YAML** that follows the
17
+ [OpenAPI 3.0 Operation Object](https://spec.openapis.org/oas/v3.0.3#operation-object)
18
+ (or [Schema Object](https://spec.openapis.org/oas/v3.0.3#schema-object) for
19
+ components). The generator parses the YAML and merges it on top of the
20
+ defaults it would have emitted, so you only describe what you want to enrich.
21
+
22
+ ## Per-route metadata: `@openapi`
23
+
24
+ ```ts
25
+ /**
26
+ * @openapi
27
+ * summary: Get group by CN
28
+ * description: |
29
+ * The `:cn` segment may be either the group's RDN value or its full DN.
30
+ * Returns the raw LDAP entry.
31
+ * responses:
32
+ * '200':
33
+ * description: Group entry.
34
+ * content:
35
+ * application/json:
36
+ * schema: { $ref: '#/components/schemas/Group' }
37
+ * example:
38
+ * dn: cn=admins,ou=groups,dc=example,dc=com
39
+ * cn: admins
40
+ * description: Server administrators
41
+ * '404':
42
+ * description: Group not found.
43
+ */
44
+ app.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`, asyncHandler(...));
45
+ ```
46
+
47
+ Anything below the `@openapi` line (up to the next `@directive` or the end of
48
+ the JSDoc block) is YAML. The keys you provide override the auto-generated
49
+ ones; everything you omit keeps the generator's defaults.
50
+
51
+ ### What you can override
52
+
53
+ Any field of an OpenAPI Operation Object, including:
54
+
55
+ | Field | Notes |
56
+ | ------------- | -------------------------------------------------------------------------------- |
57
+ | `summary` | Short, one-line title. |
58
+ | `description` | Long description (markdown). |
59
+ | `tags` | Array of strings; defaults to the plugin's tag. |
60
+ | `parameters` | Concatenated with auto-generated path params (your entries win on duplicates). |
61
+ | `requestBody` | Replace the generic `application/json` placeholder with a real schema + example. |
62
+ | `responses` | Replace per status code. |
63
+ | `security` | List of security requirement objects (works as-is). |
64
+ | `deprecated` | `true` to mark the operation as deprecated. |
65
+ | `operationId` | Stable identifier for code generators. |
66
+
67
+ ### What is autodetected
68
+
69
+ The generator already extracts:
70
+
71
+ - the **HTTP method** (`get`, `post`, …) from the call expression;
72
+ - the **path** (substituting local `const prefix = ...` variables, the
73
+ `${this.config.api_prefix}` template, and a few well-known config knobs);
74
+ - **path parameters** from `:param` segments;
75
+ - a default **summary** built from the verb and the last path segment;
76
+ - the plugin's **tag** (configured in `pluginTags` in the generator).
77
+
78
+ So a minimal, correct `@openapi` block is often only two or three keys.
79
+
80
+ ## Reusable schemas: `@openapi-component`
81
+
82
+ Place this anywhere in your plugin file (the convention is right above the
83
+ class). The body is a YAML map of `<SchemaName>: <SchemaDefinition>` pairs:
84
+
85
+ ```ts
86
+ /**
87
+ * @openapi-component
88
+ * Group:
89
+ * type: object
90
+ * required: [dn, cn]
91
+ * properties:
92
+ * dn: { type: string, example: cn=admins,ou=groups,dc=example,dc=com }
93
+ * cn: { type: string, example: admins }
94
+ * member:
95
+ * type: array
96
+ * items: { type: string }
97
+ * Error:
98
+ * type: object
99
+ * properties:
100
+ * error: { type: string }
101
+ * code: { type: integer }
102
+ */
103
+ export default class LdapGroups extends DmPlugin {
104
+ // ...
105
+ }
106
+ ```
107
+
108
+ Refer to them from any route using `$ref`:
109
+
110
+ ```yaml
111
+ schema: { $ref: '#/components/schemas/Group' }
112
+ ```
113
+
114
+ Component names are global to the spec, so prefix them with the plugin's
115
+ domain (`Group`, `ScimUser`, `PasswordPolicy`) to avoid collisions.
116
+
117
+ ## Tips
118
+
119
+ - **Examples beat specs.** A single concrete `example:` block is worth a
120
+ dozen `description:` lines for someone trying the API for the first time.
121
+ - **YAML alignment.** The generator strips the leading `*` from each line
122
+ before parsing, then dedents the block, so you can use any consistent
123
+ indentation as long as it's even within the block.
124
+ - **Multiline strings.** Use YAML block scalars (`description: |`) for
125
+ paragraphs and code samples — they round-trip cleanly through Redoc.
126
+ - **Annotation is opt-in.** Routes without an `@openapi` block are
127
+ **excluded** from the published spec — the generator prints a
128
+ `⚠️ Skipping undocumented route` warning so the author notices, but
129
+ the route does not surface in Redoc. The published doc therefore
130
+ reflects intentionally-documented API surface only, not every Express
131
+ call we happened to find.
132
+ - **A plugin with zero annotated routes disappears entirely** from the
133
+ spec, by the same rule.
134
+
135
+ ## Regenerate locally
136
+
137
+ ```sh
138
+ npm run generate:openapi # writes openapi.json
139
+ npm run build:pages # rebuilds the static site under _site/
140
+ ```
141
+
142
+ Open `_site/api/index.html` to preview the Redoc rendering. The published
143
+ site at <https://linagora.github.io/ldap-rest/api/> is rebuilt automatically
144
+ on every `v*` tag push.
@@ -27,6 +27,7 @@ All LDAP-Rest configuration options.
27
27
  - [core/auth/openidconnect](#coreauthopenidconnect)
28
28
  - [Authorization Plugins](#authorization-plugins)
29
29
  - [core/auth/authzPerBranch](#coreauthauthzperbranch)
30
+ - [core/auth/authzPerRoute](#coreauthauthzperroute)
30
31
  - [core/auth/authzLinid1](#coreauthauthzlinid1)
31
32
  - [Security Plugins](#security-plugins)
32
33
  - [core/auth/rateLimit](#coreauthratelimit)
@@ -236,6 +237,12 @@ Maintains consistency of department links when organizations are renamed/moved.
236
237
  | `--authz-per-branch-config` | `DM_AUTHZ_PER_BRANCH_CONFIG` | `{default:{read:true,write:false,delete:false}}` | Authorization config (JSON) |
237
238
  | `--authz-per-branch-cache-ttl` | `DM_AUTHZ_PER_BRANCH_CACHE_TTL` | `60` | Cache TTL (seconds) |
238
239
 
240
+ #### `core/auth/authzPerRoute`
241
+
242
+ | CLI | Env | Default | Description |
243
+ | ------------------- | --------------------- | ------- | ---------------------------------------------------------------------- |
244
+ | `--authz-per-route` | `DM_AUTHZ_PER_ROUTES` | `[]` | Per-user route ACL rules (see [docs](plugins/auth/authz-per-route.md)) |
245
+
239
246
  #### `core/auth/authzLinid1`
240
247
 
241
248
  | CLI | Env | Default | Description |
@@ -17,6 +17,7 @@ LDAP-Rest provides multiple authentication plugins to secure API access. These p
17
17
  | Method | Plugin | Description |
18
18
  | ----------------------------------------------- | -------------------------- | --------------------------- |
19
19
  | [Authorization Per Branch](authz-per-branch.md) | `core/auth/authzPerBranch` | Branch-level access control |
20
+ | [Authorization Per Route](authz-per-route.md) | `core/auth/authzPerRoute` | HTTP route-level ACL |
20
21
  | [Authorization LinID 1.x](authz-linid1.md) | `core/auth/authzLinid1` | LinID 1.x integration |
21
22
 
22
23
  ## Security Plugins
@@ -0,0 +1,144 @@
1
+ # Route-Level Authorization (`authzPerRoute`)
2
+
3
+ Restricts access to specific HTTP routes based on the authenticated user's name
4
+ (`req.user`), which is set by an auth plugin such as `core/auth/token`.
5
+
6
+ This plugin complements `authzPerBranch` (LDAP-branch ACL) and `authzDynamic`
7
+ (LDAP-backed tokens): whereas those operate at the LDAP-branch level,
8
+ `authzPerRoute` works at the HTTP route level and needs no LDAP connection.
9
+
10
+ ## Plugin loading order
11
+
12
+ `authzPerRoute` MUST run **after** the authentication plugin (so `req.user` is set)
13
+ AND **before** any API route plugin (so its Express middleware is registered before
14
+ the routes it protects).
15
+
16
+ When loaded via the CLI plugin loader, plugins not listed in
17
+ `src/plugins/priority.json` are loaded in parallel — this can race with API route
18
+ registration and result in an authorization bypass. To guarantee correct ordering,
19
+ `core/auth/authzPerRoute` is included in the priority list and loads sequentially
20
+ after the other auth plugins. If you build a custom loader, ensure equivalent
21
+ sequencing.
22
+
23
+ ```bash
24
+ --plugin core/auth/token \
25
+ --plugin core/auth/authzPerRoute
26
+ ```
27
+
28
+ ## Configuration
29
+
30
+ ### CLI
31
+
32
+ ```bash
33
+ --authz-per-route "full-access:*" \
34
+ --authz-per-route "updt-only:POST:/api/v1/ldap/updt" \
35
+ --authz-per-route "updt-only:GET:/api/v1/ldap/updt/**"
36
+ ```
37
+
38
+ ### Environment Variable
39
+
40
+ ```bash
41
+ DM_AUTHZ_PER_ROUTES="full-access:*,updt-only:POST:/api/v1/ldap/updt,updt-only:GET:/api/v1/ldap/updt/**"
42
+ ```
43
+
44
+ Multiple values are comma-separated. Repeating `--authz-per-route` on the CLI
45
+ is equivalent.
46
+
47
+ ## Rule Syntax
48
+
49
+ Each entry has one of two forms:
50
+
51
+ | Form | Meaning |
52
+ | ---------------------------- | ----------------------------------------------------------- |
53
+ | `<user>:*` | Full wildcard — allow any method on any path |
54
+ | `<user>:<METHOD>:<pathGlob>` | Allow only the given HTTP method on paths matching the glob |
55
+
56
+ - `<user>` — the value of `req.user` as set by the auth plugin.
57
+ - `<METHOD>` — an HTTP verb (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`,
58
+ `OPTIONS`) or `*` (any verb).
59
+ - `<pathGlob>` — a glob pattern (not a regex). Patterns are implicitly
60
+ anchored start-to-end. Rules:
61
+ - `*` matches any sequence of characters **except** `/` (one path segment).
62
+ - `**` matches any sequence of characters **including** `/` (multiple segments).
63
+ - All other characters are matched literally — regex special characters (`.`, `+`,
64
+ `?`, etc.) have no special meaning.
65
+ - **Allowed characters:** alphanumerics (`a-z`, `A-Z`, `0-9`), `/`, `_`, `-`,
66
+ `.`, `+`, and `*`. Globs containing any other character (e.g. `;`, space,
67
+ `?`) are rejected at startup with a warning and the rule is skipped.
68
+
69
+ A user may have multiple entries; access is granted if **any** rule matches
70
+ (logical OR).
71
+
72
+ ### Examples
73
+
74
+ ```bash
75
+ # admin token: full access
76
+ full-access:*
77
+
78
+ # read-only token: GET only on exactly /api/hello
79
+ reader:GET:/api/hello
80
+
81
+ # update token: POST on exact path, GET on that path and all sub-paths
82
+ updt:POST:/api/v1/ldap/updt
83
+ updt:GET:/api/v1/ldap/updt**
84
+
85
+ # any method on a path and all sub-paths
86
+ any-method:*:/api/v1/ldap/users**
87
+
88
+ # match one level deep only (not /api/v1/ldap/users/uid=bob/sub)
89
+ one-level:GET:/api/v1/ldap/users/*
90
+ ```
91
+
92
+ ### Glob pattern quick reference
93
+
94
+ | Pattern | Matches | Does NOT match |
95
+ | ---------------- | ----------------------------------------------------- | ----------------------------------- |
96
+ | `/api/hello` | `/api/hello` | `/api/hello/sub` |
97
+ | `/api/hello**` | `/api/hello`, `/api/hello/sub`, `/api/hello/sub/deep` | `/api/hell` |
98
+ | `/api/hello/**` | `/api/hello/sub`, `/api/hello/sub/deep` | `/api/hello` |
99
+ | `/api/hello/*` | `/api/hello/sub` | `/api/hello`, `/api/hello/sub/deep` |
100
+ | `/api/hello.bak` | `/api/hello.bak` | `/api/helloXbak` (`.` is literal) |
101
+
102
+ ## Behavior
103
+
104
+ | Situation | Result |
105
+ | ----------------------------------------------------- | ------------------------------------------------------ |
106
+ | `req.user` is unset (no auth plugin ran) | Pass through (`next()`) — let upstream auth return 401 |
107
+ | User authenticated but has no rules configured | 403 Forbidden |
108
+ | User has at least one matching rule | 200 / pass through |
109
+ | User has rules but none match the current method+path | 403 Forbidden |
110
+
111
+ ## Full Example
112
+
113
+ ```bash
114
+ DM_AUTH_TOKENS="tok-admin:admin,tok-ro:reader"
115
+ DM_AUTHZ_PER_ROUTES="admin:*,reader:GET:/api/v1/ldap/users**,reader:GET:/api/v1/ldap/groups**"
116
+
117
+ npx ldap-rest \
118
+ --plugin core/auth/token \
119
+ --plugin core/auth/authzPerRoute \
120
+ --plugin core/ldap/users \
121
+ --plugin core/ldap/groups \
122
+ --ldap-url ldap://localhost:389 \
123
+ ...
124
+ ```
125
+
126
+ ```bash
127
+ # admin: any request allowed
128
+ curl -H "Authorization: Bearer tok-admin" http://api/v1/ldap/users
129
+ curl -H "Authorization: Bearer tok-admin" -X DELETE http://api/v1/ldap/users/uid=bob,ou=users,dc=example,dc=org
130
+
131
+ # reader: GET allowed
132
+ curl -H "Authorization: Bearer tok-ro" http://api/v1/ldap/users # 200
133
+
134
+ # reader: write denied
135
+ curl -H "Authorization: Bearer tok-ro" -X POST http://api/v1/ldap/users # 403
136
+ ```
137
+
138
+ ## Difference vs. Other Authz Plugins
139
+
140
+ | Plugin | Scope | Requires LDAP |
141
+ | ---------------- | ------------------------------ | ------------------ |
142
+ | `authzPerRoute` | HTTP method + path | No |
143
+ | `authzPerBranch` | LDAP branch read/write/delete | No (static config) |
144
+ | `authzDynamic` | LDAP branch ACL stored in LDAP | Yes |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-rest",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Light directory manager",
5
5
  "keywords": [
6
6
  "Identity-Manager"
@@ -52,6 +52,10 @@
52
52
  "types": "./dist/src/plugins/auth/authzPerBranch.d.ts",
53
53
  "import": "./dist/plugins/auth/authzPerBranch.js"
54
54
  },
55
+ "./plugin-auth-authzperroute": {
56
+ "types": "./dist/src/plugins/auth/authzPerRoute.d.ts",
57
+ "import": "./dist/plugins/auth/authzPerRoute.js"
58
+ },
55
59
  "./plugin-auth-crowdsec": {
56
60
  "types": "./dist/src/plugins/auth/crowdsec.d.ts",
57
61
  "import": "./dist/plugins/auth/crowdsec.js"
@@ -120,6 +124,10 @@
120
124
  "types": "./dist/src/plugins/ldap/organization.d.ts",
121
125
  "import": "./dist/plugins/ldap/organization.js"
122
126
  },
127
+ "./plugin-ldap-organizations": {
128
+ "types": "./dist/src/plugins/ldap/organizations.d.ts",
129
+ "import": "./dist/plugins/ldap/organizations.js"
130
+ },
123
131
  "./plugin-ldap-passwordpolicy": {
124
132
  "types": "./dist/src/plugins/ldap/passwordPolicy.d.ts",
125
133
  "import": "./dist/plugins/ldap/passwordPolicy.js"
@@ -188,6 +196,10 @@
188
196
  "types": "./dist/src/plugins/twake/calendarResources.d.ts",
189
197
  "import": "./dist/plugins/twake/calendarResources.js"
190
198
  },
199
+ "./plugin-twake-cozyprovision": {
200
+ "types": "./dist/src/plugins/twake/cozyProvision.d.ts",
201
+ "import": "./dist/plugins/twake/cozyProvision.js"
202
+ },
191
203
  "./plugin-twake-drive": {
192
204
  "types": "./dist/src/plugins/twake/drive.d.ts",
193
205
  "import": "./dist/plugins/twake/drive.js"
@@ -552,6 +564,7 @@
552
564
  "build:dev": "./.dev.mk builddev",
553
565
  "build:docker": "./.dev.mk builddocker",
554
566
  "build:dockerfile": "./.dev.mk Dockerfile",
567
+ "build:pages": "tsx scripts/build-pages.ts",
555
568
  "build:prod": "npm run clean && NODE_ENV=production rollup -c && npm run build:browser && node scripts/moveBrowserLibs.mjs && npm run build:dockerfile",
556
569
  "build:watch": "rollup -c --watch",
557
570
  "check": "npm run check:ts && npm run lint && npm run format:check",
@@ -598,6 +611,7 @@
598
611
  "@rollup/plugin-typescript": "^12.1.4",
599
612
  "@types/chai": "^5.2.2",
600
613
  "@types/express": "^5.0.3",
614
+ "@types/js-yaml": "^4.0.9",
601
615
  "@types/mocha": "^10.0.10",
602
616
  "@types/multer": "^2.0.0",
603
617
  "@types/node": "^24.5.1",
@@ -611,6 +625,7 @@
611
625
  "eslint-plugin-node": "^11.1.0",
612
626
  "eslint-plugin-prettier": "^5.5.4",
613
627
  "fast-glob": "^3.3.3",
628
+ "js-yaml": "^4.1.1",
614
629
  "mocha": "^11.7.2",
615
630
  "nock": "^14.0.10",
616
631
  "prettier": "^3.6.2",
@@ -624,6 +639,7 @@
624
639
  "typescript": "^5.9.2"
625
640
  },
626
641
  "optionalDependencies": {
642
+ "@linagora/rabbitmq-client": "^0.1.2",
627
643
  "express-openid-connect": "^2.19.2",
628
644
  "lemonldap-ng-handler": "^0.7.3",
629
645
  "re2": "^1.22.1"