@lamalibre/create-lamaste 4.5.0 → 4.7.0
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.
- package/package.json +2 -2
- package/scripts/bundle-vendor.js +20 -3
- package/scripts/lamaste-priv +56 -5
- package/scripts/pin-release.js +98 -0
- package/src/index.js +8 -1
- package/src/lib/env.js +19 -0
- package/src/lib/service-config.js +7 -0
- package/src/tasks/redeploy.js +8 -0
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/authentication.md +6 -2
- package/vendor/docs/01-concepts/authorization.md +4 -0
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/dns-and-domains.md +9 -0
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +28 -3
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +5 -5
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
- package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/managing-users.md +5 -11
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/second-domain.md +82 -0
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/02-guides/static-sites.md +4 -0
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +80 -12
- package/vendor/docs/03-architecture/overview.md +5 -5
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +2 -2
- package/vendor/docs/03-architecture/state-management.md +3 -3
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +116 -37
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/domains.md +192 -0
- package/vendor/docs/04-api-reference/gatekeeper.md +54 -8
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +4 -4
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +9 -0
- package/vendor/docs/04-api-reference/registry-and-oidc.md +26 -13
- package/vendor/docs/04-api-reference/sites.md +9 -0
- package/vendor/docs/04-api-reference/tunnels.md +35 -28
- package/vendor/docs/04-api-reference/users.md +41 -29
- package/vendor/docs/05-operations/backup-and-restore.md +1 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +15 -6
- package/vendor/docs/06-reference/config-files.md +19 -15
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +1 -1
- package/vendor/docs/06-reference/troubleshooting.md +8 -8
- package/vendor/docs/_index.json +105 -19
- package/vendor/docs/external-members-design.md +816 -0
- package/vendor/docs/gatekeeper-design.md +8 -8
- package/vendor/docs/identity-directory-design.md +3 -3
- package/vendor/docs/multi-domain-relay-design.md +836 -0
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/index.d.ts +3 -3
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/index.js +2 -2
- package/vendor/gatekeeper/dist/index.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/authz.d.ts +9 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/authz.js +31 -0
- package/vendor/gatekeeper/dist/lib/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/constants.d.ts +5 -2
- package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/constants.js +5 -2
- package/vendor/gatekeeper/dist/lib/constants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.js +20 -13
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0004_unconsume_access_grants.sql +8 -0
- package/vendor/gatekeeper/dist/lib/secret.d.ts +6 -0
- package/vendor/gatekeeper/dist/lib/secret.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/secret.js +12 -0
- package/vendor/gatekeeper/dist/lib/secret.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts +20 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/templates.js +80 -14
- package/vendor/gatekeeper/dist/lib/templates.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts +3 -6
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/tunnels.js +4 -16
- package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/types.d.ts +0 -19
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +6 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +56 -11
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +325 -55
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +5 -2
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/bin/lamaste-server.js +7 -0
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/domains.js +214 -0
- package/vendor/server/src/commands/plugin-identity.js +2 -16
- package/vendor/server/src/commands/plugin-web-surface.js +269 -0
- package/vendor/server/src/commands/plugins.js +175 -14
- package/vendor/server/src/commands/registry-credential.js +10 -3
- package/vendor/server/src/commands/sites.js +18 -1
- package/vendor/server/src/commands/status.js +19 -0
- package/vendor/server/src/commands/tunnels.js +4 -1
- package/vendor/server/src/commands/users.js +8 -19
- package/vendor/server/src/domains.js +92 -0
- package/vendor/server/src/fail.js +21 -0
- package/vendor/server/src/oidc-client-grants.js +63 -0
- package/vendor/server/src/panel-api.js +15 -0
- package/vendor/server/src/plugin-approval.js +80 -1
- package/vendor/server/src/plugin-web-surface.js +307 -0
- package/vendor/server-ui/dist/assets/index-B8Tv_ocF.js +84 -0
- package/vendor/server-ui/dist/assets/index-Cb-ADsbB.css +1 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +50 -19
- package/vendor/serverd/src/lib/app-error.js +24 -0
- package/vendor/serverd/src/lib/audience.js +12 -3
- package/vendor/serverd/src/lib/authelia.js +341 -35
- package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
- package/vendor/serverd/src/lib/chisel.js +10 -2
- package/vendor/serverd/src/lib/directory.js +67 -52
- package/vendor/serverd/src/lib/domains.js +1792 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +34 -7
- package/vendor/serverd/src/lib/hook-pipelines.js +8 -3
- package/vendor/serverd/src/lib/identity-sync.js +35 -0
- package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
- package/vendor/serverd/src/lib/migrations/state/0016_domains.sql +53 -0
- package/vendor/serverd/src/lib/migrations/state/0017_membership_kind.sql +15 -0
- package/vendor/serverd/src/lib/migrations/state/0018_domain_group_keys.sql +23 -0
- package/vendor/serverd/src/lib/nginx.js +576 -131
- package/vendor/serverd/src/lib/owned-groups.js +326 -0
- package/vendor/serverd/src/lib/plugin-domain.js +86 -0
- package/vendor/serverd/src/lib/plugin-hooks.js +40 -11
- package/vendor/serverd/src/lib/plugin-lifecycle.js +129 -5
- package/vendor/serverd/src/lib/plugin-oidc.js +144 -42
- package/vendor/serverd/src/lib/plugin-packages.js +74 -2
- package/vendor/serverd/src/lib/plugins.js +82 -9
- package/vendor/serverd/src/lib/registry-credential.js +39 -6
- package/vendor/serverd/src/lib/services.js +9 -1
- package/vendor/serverd/src/lib/state.js +29 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +2 -4
- package/vendor/serverd/src/lib/user-surface.js +77 -31
- package/vendor/serverd/src/lib/users-lock.js +23 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +151 -41
- package/vendor/serverd/src/lib/web-surface.js +783 -0
- package/vendor/serverd/src/middleware/user-access-session.js +11 -4
- package/vendor/serverd/src/routes/invite.js +15 -5
- package/vendor/serverd/src/routes/management/domains.js +154 -0
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +44 -8
- package/vendor/serverd/src/routes/management/identity.js +21 -7
- package/vendor/serverd/src/routes/management/invitations.js +33 -4
- package/vendor/serverd/src/routes/management/plugin-web-surface.js +151 -0
- package/vendor/serverd/src/routes/management/plugins.js +6 -2
- package/vendor/serverd/src/routes/management/registry-credential.js +86 -57
- package/vendor/serverd/src/routes/management/sites.js +188 -71
- package/vendor/serverd/src/routes/management/tunnels.js +37 -22
- package/vendor/serverd/src/routes/management/users.js +184 -64
- package/vendor/serverd/src/routes/management.js +4 -0
- package/vendor/serverd/src/routes/onboarding/dns.js +3 -17
- package/vendor/serverd/src/routes/onboarding/provision.js +22 -13
- package/vendor/serverd/src/routes/user-access.js +13 -1
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +0 -7
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +0 -14
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +0 -9
- package/vendor/server-ui/dist/assets/index-D-fer5ef.css +0 -1
- package/vendor/server-ui/dist/assets/index-DSaDPJaw.js +0 -84
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Domains API
|
|
2
|
+
|
|
3
|
+
> The relay's base domains and who is a member of each — one relay, one user directory, a sign-in and a member list per domain.
|
|
4
|
+
|
|
5
|
+
## In Plain English
|
|
6
|
+
|
|
7
|
+
A relay serves one **primary** domain (the panel's, set at onboarding) and any number of **added** domains. Every domain has its own sign-in page, `auth.<domain>`, and its own member list. A person has one account — one password, one TOTP — and reaches a domain's protected hosts (`authenticated` and `restricted` tunnels, protected sites, plugin user and web surfaces, OIDC sign-ins of the domain's plugins) only as a member of it. Admin stays relay-wide: the admin certificate manages every domain. The panel (`panel.`) and the tunnel endpoint (`tunnel.`) stay on the primary.
|
|
8
|
+
|
|
9
|
+
A membership has a **kind**. A `member` reaches what the domain's members reach. An `external` member — a client's people, say — reaches only what names them: `restricted` tunnels, user surfaces and web surfaces that grant them (Gatekeeper grants, to them or a Lamaste group they belong to), static sites whose `allowedUsers` lists them, and OIDC clients granted to them. An external member holds no Authelia groups (`admins`, `feria-*`).
|
|
10
|
+
|
|
11
|
+
While the relay has one domain, every user is its member and nothing looks different — unless a user is made external. See [DNS & Domains](../01-concepts/dns-and-domains.md#multiple-domains) and the guide [Adding a Second Domain](../02-guides/second-domain.md).
|
|
12
|
+
|
|
13
|
+
## Authentication
|
|
14
|
+
|
|
15
|
+
Every endpoint requires an admin certificate (mTLS) — agents never manage domains.
|
|
16
|
+
|
|
17
|
+
## Domain Object
|
|
18
|
+
|
|
19
|
+
| Field | Type | Description |
|
|
20
|
+
| ----------------- | -------------- | --------------------------------------------------------------------------------------------------------- |
|
|
21
|
+
| `name` | string | Lower case, two labels or more (`codelama.com.tr`) |
|
|
22
|
+
| `primary` | boolean | The panel's domain; exactly one; never removed |
|
|
23
|
+
| `status` | string | `pending` (added, sign-in not served) or `active` (`auth.<name>` issued, members admitted) |
|
|
24
|
+
| `createdAt` | string | ISO 8601 |
|
|
25
|
+
| `activatedAt` | string \| null | ISO 8601, once active |
|
|
26
|
+
| `members` | number | Members of kind `member`, disabled users included (`GET /api/domains` only) |
|
|
27
|
+
| `externalMembers` | number | External members, disabled users included (`GET /api/domains` only) |
|
|
28
|
+
| `usedBy` | object | `{ tunnels, sites, plugins, registry }` — hostnames, plugin names, whether the registry trusts its issuer |
|
|
29
|
+
|
|
30
|
+
Domains never overlap: `a.samplepark.tr` cannot be added beside `samplepark.tr` (a browser sends a domain's cookies to every host under it).
|
|
31
|
+
|
|
32
|
+
## Endpoints
|
|
33
|
+
|
|
34
|
+
### `GET /api/domains`
|
|
35
|
+
|
|
36
|
+
Every domain, the primary first, with its member count and what uses it, and `newUsersNeedDomains` — whether users and invitations must name their domains (the relay has two or more; clients ask for domains only then).
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"domains": [
|
|
41
|
+
{
|
|
42
|
+
"name": "samplepark.tr",
|
|
43
|
+
"primary": true,
|
|
44
|
+
"status": "active",
|
|
45
|
+
"createdAt": "2026-10-25T10:00:00.000Z",
|
|
46
|
+
"activatedAt": "2026-10-25T10:00:00.000Z",
|
|
47
|
+
"members": 5,
|
|
48
|
+
"externalMembers": 2,
|
|
49
|
+
"usedBy": {
|
|
50
|
+
"tunnels": ["gitlab.samplepark.tr"],
|
|
51
|
+
"sites": [],
|
|
52
|
+
"plugins": [],
|
|
53
|
+
"registry": false
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"newUsersNeedDomains": false
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `POST /api/domains`
|
|
62
|
+
|
|
63
|
+
Add a domain, `pending`. Body `{ "name": "codelama.com.tr" }`. **201** `{ domain }`.
|
|
64
|
+
|
|
65
|
+
| Status | `code` | When |
|
|
66
|
+
| ------ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
67
|
+
| 400 | `invalid_domain` | Not a domain name |
|
|
68
|
+
| 400 | `public_suffix` | A public suffix (`com.tr`, `github.io`), not a domain |
|
|
69
|
+
| 409 | `domain_overlap` | The same as, or under or above, a domain |
|
|
70
|
+
| 409 | `same_registrable_domain` | Shares its registrable domain (eTLD+1, Public Suffix List) with a domain — an application on one could set cookies for the other |
|
|
71
|
+
| 409 | `relay_host_taken` | A tunnel, site or alias already holds one of its relay hostnames (`auth.`, `panel.`, `tunnel.`, `agent-*`); also refused at activation |
|
|
72
|
+
|
|
73
|
+
### `POST /api/domains/:name/verify-dns`
|
|
74
|
+
|
|
75
|
+
The DNS records the domain needs at its DNS host, each with what it resolves to now — `auth.<name>` (required) and the wildcard `*.<name>` (optional: one A record per hostname does as well; checked through a probe name). `ok` once every required record points at the relay. Clients show these records as they are; they derive none. Rate limited (30 a minute).
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"ok": true,
|
|
80
|
+
"expectedIp": "138.68.65.228",
|
|
81
|
+
"records": [
|
|
82
|
+
{
|
|
83
|
+
"type": "A",
|
|
84
|
+
"name": "auth.codelama.com.tr",
|
|
85
|
+
"value": "138.68.65.228",
|
|
86
|
+
"required": true,
|
|
87
|
+
"purpose": "the domain’s sign-in page",
|
|
88
|
+
"resolvedIps": ["138.68.65.228"],
|
|
89
|
+
"ok": true
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"type": "A",
|
|
93
|
+
"name": "*.codelama.com.tr",
|
|
94
|
+
"value": "138.68.65.228",
|
|
95
|
+
"required": false,
|
|
96
|
+
"purpose": "every tunnel and site hostname — or one A record per hostname instead",
|
|
97
|
+
"resolvedIps": [],
|
|
98
|
+
"ok": false
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `POST /api/domains/:name/activate`
|
|
105
|
+
|
|
106
|
+
Issue the certificate of `auth.<name>` (HTTP-01), write its vhost, add its Authelia session cookie and its rules to `access_control`, give its members and external members their groups in `users.yml` and write Gatekeeper's projection, restart Authelia — one operation, rolled back as a whole on failure. Rate limited (10 a minute). **200** `{ domain }`.
|
|
107
|
+
|
|
108
|
+
| Status | `code` | When |
|
|
109
|
+
| ------ | ------------------------------------------------ | -------------------------------------- |
|
|
110
|
+
| 404 | `domain_not_found` | |
|
|
111
|
+
| 502 | `cert_failed`, `nginx_failed`, `authelia_failed` | The step named failed; nothing changed |
|
|
112
|
+
|
|
113
|
+
### `DELETE /api/domains/:name`
|
|
114
|
+
|
|
115
|
+
Remove a domain: its memberships end (audited, reason `domain-removed`), its sign-in and session cookie go. **200** `{ removed: true }`.
|
|
116
|
+
|
|
117
|
+
| Status | `code` | When |
|
|
118
|
+
| ------ | ---------------- | ------------------------------------------------------------------------------- |
|
|
119
|
+
| 400 | `primary_domain` | The primary is never removed |
|
|
120
|
+
| 409 | `domain_in_use` | A tunnel, site, plugin or the registry credential uses it — `usedBy` lists them |
|
|
121
|
+
|
|
122
|
+
### `GET /api/domains/:name/members`
|
|
123
|
+
|
|
124
|
+
`{ members: [{ userId, username, status, kind, addedAt, addedBy }] }` — `status` is the directory's (`active`, `disabled`, `removed`); `kind` is `member` or `external`.
|
|
125
|
+
|
|
126
|
+
### `PUT /api/domains/:name/members/:user`
|
|
127
|
+
|
|
128
|
+
Make a user a member — `:user` is a directory `userId` or the username of a user the directory holds. Body `{ "kind": "member" | "external" }` (optional, `member`): a user who is a member already gets the kind named (audited `directory.membership.changed`). **200** `{ added, changed }` (both `false` when nothing changed), with `warnings` when Gatekeeper or the audiences could not be brought along. A pending domain takes members too: they are let in when it is activated.
|
|
129
|
+
|
|
130
|
+
A change applies at the user's next request — Authelia is not restarted (see [Where membership is enforced](#where-membership-is-enforced)).
|
|
131
|
+
|
|
132
|
+
| Status | `code` | When |
|
|
133
|
+
| ------ | ------------------------ | -------------------------------------------------------------------------- |
|
|
134
|
+
| 404 | `user_not_found` | |
|
|
135
|
+
| 409 | `user_removed` | The user is deleted |
|
|
136
|
+
| 409 | `external_member_groups` | Making a user external who holds Authelia groups — remove the groups first |
|
|
137
|
+
|
|
138
|
+
### `DELETE /api/domains/:name/members/:user`
|
|
139
|
+
|
|
140
|
+
End a membership. **200** `{ removed }`. When the user no longer reaches the registry's sign-in (a member of none of the registry's domains, nor an external member of one granted its client), their registry tokens are revoked too.
|
|
141
|
+
|
|
142
|
+
### `PUT /api/plugins/:name/domain`
|
|
143
|
+
|
|
144
|
+
Move a plugin to another active domain: `{ "domain": "codelama.com.tr" }`. A plugin belongs to one domain — its OIDC clients' `{domain}` and sign-in policy, which tunnels may be bound as its user surface, its web surface, its audience (entitled users who are members). **200** `{ name, domain, changed }`. **409** `surface_bound` while a tunnel is bound as its user surface or it owns a web surface; **400** `unknown_domain` for a domain that is not active.
|
|
145
|
+
|
|
146
|
+
## Memberships
|
|
147
|
+
|
|
148
|
+
- **Explicit, by `userId`.** A re-created username is a new user with no membership.
|
|
149
|
+
- **Kinds.** `member` or `external`, per membership: a user may be a member of one domain and an external member of another. Where a request names memberships (`domains` on users and invitations) each is a domain name — a `member` — or `{ "name": "codelama.com.tr", "kind": "external" }`; one domain named with two kinds is refused (400 `conflicting_membership`). Answers always carry `{ name, kind }`.
|
|
150
|
+
- **New users.** While the relay has one domain, every new user — created, invited, adopted from `users.yml` — becomes its member unless the request names it as `external`. With two or more, `POST /api/users` and `POST /api/invitations` take `domains` (required, and not empty: 400 `domains_required` — a user who is a member of nothing reaches nothing); a user adopted from `users.yml` gets none until an admin adds one. `PUT /api/users/:username` with `domains` sets a user's memberships exactly: the difference applied in one change, each membership audited.
|
|
151
|
+
- **External members hold no Authelia groups.** Giving one a group, making a user with groups external, or inviting with both is refused (`external_member_groups`). Lamaste groups (Gatekeeper's) are not Authelia groups: grants to them reach external members too.
|
|
152
|
+
- **Disable** keeps memberships; a disabled user is let in nowhere, and enabling restores them. **Delete** ends them.
|
|
153
|
+
- **Audit:** `domain.added`, `domain.activated`, `domain.removed`, `directory.membership.added` (with `kind`), `directory.membership.changed` (`from`, `to`), `directory.membership.removed` — by `userId` and domain name, never username.
|
|
154
|
+
|
|
155
|
+
## Where membership is enforced
|
|
156
|
+
|
|
157
|
+
1. **Authelia.** Who is let in lives in `users.yml` as groups Lamaste owns, which Authelia watches (`authentication_backend.file.watch: true`, `refresh_interval: always`, its own password change and reset disabled) and re-reads at every request — so a membership, a kind, a grant or a site's `allowedUsers` changes without restarting Authelia or ending anyone's session:
|
|
158
|
+
- `lamaste:m:<key>`, `lamaste:x:<key>` — the domain's members and external members, `<key>` a random key of the domain record (a group names no domain);
|
|
159
|
+
- `lamaste:s:<site id>` — a protected static site's listed users;
|
|
160
|
+
- `<hostname>` — the external members granted a `restricted` tunnel, user surface or web surface (enabled; a tunnel without an access mode is restricted);
|
|
161
|
+
- `lamaste:oidc@<client id>` — the external members granted a user-facing OIDC client.
|
|
162
|
+
|
|
163
|
+
`configuration.yml` names only those groups and changes only with the domains and protected sites (Authelia restarts then): one `session.cookies` entry per active domain; `access_control` per protected site (its site group; admins who are members), then per domain — its sign-in portal `auth.<domain>` for members and external members, members on every host, external members only on a host whose name is one of their groups (`domain_regex` with a `(?P<Group>…)` capture), deny — and `default_policy: deny`; one OIDC authorization policy per user-facing client (`lamaste_client_<id>`: members of its domain, external members with its group). The panel never shows these groups as groups, refuses assigning a name with `.`, `:` or `@` (400 `reserved_group`), and Gatekeeper strips them from `Remote-Groups`. Authelia's device-code flow checks a client's policy against the groups a session last refreshed, so a change that takes a device-code client away from someone restarts Authelia (every session ends) — the one change to a person that does.
|
|
164
|
+
|
|
165
|
+
2. **Gatekeeper** (`/etc/lamalibre/lamaste/domains.json`, version 2, 0600, written by the panel): checked again after Authelia on every tunnel, user-surface and shell request; a missing, unreadable or older projection refuses. A signed-in non-member gets a refusal page (JSON `{ "error": "not_a_member" }` on a user surface) — never the sign-in page again. An external member on an `authenticated` tunnel or web-surface shell gets the access-request page; on an `authenticated` user surface a 403 (`forbidden`); on a `restricted` host their grant decides, as for members. When Authelia itself refused an external member (no group names the host), Gatekeeper asks Authelia about the same session for `https://auth.<domain>/` to learn who it was, and answers them the same way; the request stays refused.
|
|
166
|
+
|
|
167
|
+
3. **Desktop user access** (the primary domain's): `authorize`, `exchange` and every session request require a `member` — an external member gets no desktop session.
|
|
168
|
+
|
|
169
|
+
## CLI
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
lamaste-server domains list
|
|
173
|
+
lamaste-server domains add codelama.com.tr
|
|
174
|
+
lamaste-server domains verify-dns codelama.com.tr
|
|
175
|
+
lamaste-server domains activate codelama.com.tr
|
|
176
|
+
lamaste-server domains members codelama.com.tr add onur
|
|
177
|
+
lamaste-server domains members codelama.com.tr add client-ali --external
|
|
178
|
+
lamaste-server domains members codelama.com.tr kind client-ali member
|
|
179
|
+
lamaste-server domains members codelama.com.tr remove onur
|
|
180
|
+
lamaste-server plugins oidc-clients corral grant corral-web --user client-ali
|
|
181
|
+
lamaste-server domains remove codelama.com.tr
|
|
182
|
+
lamaste-server plugins domain feria codelama.com.tr
|
|
183
|
+
lamaste-server tunnels create --subdomain feria --port 20060 --agent feria --domain codelama.com.tr
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Related
|
|
187
|
+
|
|
188
|
+
- [Users API](users.md) — `domains` on users and invitations
|
|
189
|
+
- [Tunnels API](tunnels.md), [Sites API](sites.md) — `domain` on creation
|
|
190
|
+
- [Registry and OIDC](registry-and-oidc.md) — one issuer per domain, `issuerDomain`
|
|
191
|
+
- [Gatekeeper](gatekeeper.md) — the membership check and refusal page
|
|
192
|
+
- Design: `packages/core/docs/multi-domain-relay-design.md`, `packages/core/docs/external-members-design.md`
|
|
@@ -272,7 +272,13 @@ Create a new authorization grant linking a principal (user or group) to a resour
|
|
|
272
272
|
|
|
273
273
|
**Plugin role grants.** For `resourceType: "plugin"` with `context.roles`, the panel checks the grant before Gatekeeper sees it: `resourceId` must be the route of an installed plugin that declares `identity` (`400`, `code: "no_identity"`) and `roles` a non-empty list of its role ids (`400`, `code: "invalid_roles"`, or `"unknown_roles"` with the `unknown` ones); they go on sorted and without repeats. The plugin's **People** panel and `lamaste-server plugins entitlements` manage these grants one principal at a time (`PUT /api/plugins/:name/entitlements` replaces a principal's role grants) — prefer them to creating one here. A plugin grant without `roles` (the desktop's local-install grants) never makes anyone a plugin user.
|
|
274
274
|
|
|
275
|
-
**
|
|
275
|
+
**OIDC client grants.** For `resourceType: "oidc-client"` the panel checks that `resourceId` is a client an installed plugin declares (`400`, `code: "unknown_oidc_client"`) that signs users in (`400`, `code: "service_oidc_client"`). Such a grant admits an **external member** of the plugin's domain to the client's sign-in ([Registry and OIDC](registry-and-oidc.md#several-domains-one-issuer-each)); members need none. Gatekeeper is not in the OIDC path — it only keeps the grant; the panel renders it into Authelia.
|
|
276
|
+
|
|
277
|
+
**Every grant or group change through the proxy re-renders** who may reach what: the groups Lamaste owns in `users.yml` (external members' host groups follow their grants on `restricted` hosts) — applied by Authelia without a restart, except a revocation that takes a device-code OIDC client away from someone (Authelia restarts: its device flow reads a session's groups as they were). A change Gatekeeper took whose rendering failed is answered with `warnings` and rendered again within seconds; a change made with Gatekeeper's own CLI is rendered by the identity tick within a minute.
|
|
278
|
+
|
|
279
|
+
**Grants are never consumed** but a local plugin grant (by its desktop enrolment); the 90-day sweep deletes only consumed local plugin grants (Gatekeeper migration `0004` un-consumed the tunnel, site, agent-side plugin and OIDC-client grants older versions created consumed).
|
|
280
|
+
|
|
281
|
+
**Errors:** 400 (invalid body; a role grant or an OIDC client grant as above), 409 (duplicate grant), 503 (Gatekeeper unavailable, or a removed user's principal under `principalId` cannot be cleared — `code: "gatekeeper_unavailable"`)
|
|
276
282
|
|
|
277
283
|
---
|
|
278
284
|
|
|
@@ -626,14 +632,54 @@ GET /authz/user-surface/:fqdn
|
|
|
626
632
|
|
|
627
633
|
**Host:** `127.0.0.1:9294`
|
|
628
634
|
|
|
629
|
-
The `auth_request` target of a plugin's user surface — `/api/<route>/app` and everything under it on the host of a tunnel bound to the plugin ([Tunnels API](tunnels.md#user-surface-binding)). The vhost names its tunnel in the path, `X-Original-URL` and `Host`, as for `/authz/tunnel/:fqdn`, and sends `X-Original-Method`. Gatekeeper checks, in order: the hostnames agree; the tunnel is known, enabled, not public and has a user surface; the raw request target — what follows `https://<fqdn>` in `X-Original-URL`, never re-encoded by a URL parser — lies on the user surface of a route bound to the tunnel, with no `.` or `..` segment, backslash, percent-encoded `.`, `/`, `\` or `%` (`%25`), or escape that does not decode; the tunnel's agent hosts the plugin — it carries the plugin's internal tunnel, the one `tunnels.json` records for the route (core `tunnelServedUserSurfaceRoutes`; refused `not_hosting_agent` otherwise: the tunnel moved or its agent revoked, the plugin hosted elsewhere or not at all); the Authelia session, read as for `/authz/tunnel/:fqdn` (one `authelia_session` only); on a `restricted` tunnel, the user's grant for the tunnel. Then it signs a **relay assertion for the panel**: Ed25519 with the relay signing key, audience `@panel`, port 3100, caller `{ type: "user", username }` — the username alone; the panel takes everything else about the user from its directory — bound to the method and the raw target (`{ route, path }`).
|
|
635
|
+
The `auth_request` target of a plugin's user surface — `/api/<route>/app` and everything under it on the host of a tunnel bound to the plugin ([Tunnels API](tunnels.md#user-surface-binding)) or of the site the plugin owns ([Web surface](agent-hosted-plugins.md#web-surface)). Gatekeeper reads both kinds of host from `tunnels.json` and `sites.json`, re-read when either changes, the way the panel reads them (core `userSurfaceHosts`): a route two hosts would serve is served by neither, and a host whose name another host shares is no host. Where this section says tunnel, read the host; a site's grant is a `site` grant. The vhost names its tunnel in the path, `X-Original-URL` and `Host`, as for `/authz/tunnel/:fqdn`, and sends `X-Original-Method`. Gatekeeper checks, in order: the hostnames agree; the tunnel is known, enabled, not public and has a user surface; the raw request target — what follows `https://<fqdn>` in `X-Original-URL`, never re-encoded by a URL parser — lies on the user surface of a route bound to the tunnel, with no `.` or `..` segment, backslash, percent-encoded `.`, `/`, `\` or `%` (`%25`), or escape that does not decode; the tunnel's agent hosts the plugin — it carries the plugin's internal tunnel, the one `tunnels.json` records for the route (core `tunnelServedUserSurfaceRoutes`; refused `not_hosting_agent` otherwise: the tunnel moved or its agent revoked, the plugin hosted elsewhere or not at all); the Authelia session, read as for `/authz/tunnel/:fqdn` (one `authelia_session` only); on a `restricted` tunnel, the user's grant for the tunnel. Then it signs a **relay assertion for the panel**: Ed25519 with the relay signing key, audience `@panel`, port 3100, caller `{ type: "user", username }` — the username alone; the panel takes everything else about the user from its directory — bound to the method and the raw target (`{ route, path }`).
|
|
636
|
+
|
|
637
|
+
| Status | Meaning |
|
|
638
|
+
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
639
|
+
| 200 | `X-Lamalibre-Relay-Assertion` for this one request; `Cache-Control: no-store`; no `Remote-*` headers |
|
|
640
|
+
| 401 | No valid session (or more than one session cookie) — nginx answers with `GET /authz/user-surface-unauthenticated`: a JSON `401 { "error": "unauthenticated" }`, never a redirect (the caller is an application's request) |
|
|
641
|
+
| 403 | Anything else refused — never the access-request page; the reason is logged (`user-surface authz denied`) and named in `X-Lamalibre-Refusal`: `not_a_member` for a user who is not a member of the host's domain, `forbidden` otherwise. nginx keeps only the status and headers (`auth_request`) and answers the application itself: `403 { "error": "not_a_member" }` for that value, `403 { "error": "forbidden" }` for any other |
|
|
642
|
+
| 503 | The relay signing key is unusable |
|
|
643
|
+
|
|
644
|
+
### Web-Surface Shell Authorization (nginx auth_request)
|
|
645
|
+
|
|
646
|
+
```
|
|
647
|
+
GET /authz/user-surface-shell/:fqdn
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
**Host:** `127.0.0.1:9294`
|
|
651
|
+
|
|
652
|
+
The `auth_request` target of every path of a plugin-owned site but its user surface's API: the shell of the plugin's [web surface](agent-hosted-plugins.md#web-surface). The vhost names the site in the path, `X-Original-URL` and `Host`. Gatekeeper checks, in order: the hostnames agree; the host is a site a plugin owns; its route is served (the plugin hosted, no other host serving the route); the Authelia session (one `authelia_session` only); the user is in the plugin's **audience** — an entitlement on the plugin (`resourceType: "plugin"`, `resourceId: <route>`) with a non-empty `context.roles`, to the user or to one of their Lamaste groups (a grant without roles never counts); on a `restricted` site, a grant on the site (`resourceType: "site"`, `resourceId: <site id>`). The panel decides again, from its directory, on every request to the plugin's API — a disabled or removed user, a plugin not ready.
|
|
653
|
+
|
|
654
|
+
| Status | Meaning |
|
|
655
|
+
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
656
|
+
| 200 | Allowed; nothing else is passed on — nginx serves the shell from disk, no backend receives a user |
|
|
657
|
+
| 401 | No valid session (or more than one session cookie) — nginx redirects a page to the login |
|
|
658
|
+
| 403 | No entitlement or no site grant: the access-request page, served inline; anything else refused (not a web surface, not served, names disagree): an empty body |
|
|
659
|
+
|
|
660
|
+
Every answer is `Cache-Control: no-store`. A refusal for want of an entitlement or a grant is logged with the resource and the reason only, and recorded in the access-request log when it is on.
|
|
661
|
+
|
|
662
|
+
### Domain membership
|
|
663
|
+
|
|
664
|
+
Every authorization route (tunnel, user surface, shell) checks the user's membership of the host's domain after Authelia, from `/etc/lamalibre/lamaste/domains.json` (written by the panel, watched like `tunnels.json`): `{ "v": 2, "domains": [{ "name", "members": [username…], "externals": [username…] }] }`, active domains and active people only. A missing or unreadable projection, one of another version, or a host under no domain, refuses (fail closed). Authelia's own 403 — it renders the same membership — is no longer taken for "not signed in": a signed-in non-member gets **403** with the domain refusal page (the domain, the admin contact, a sign-out link to `https://auth.<domain>/logout`; no username), or JSON `{ "error": "not_a_member" }` on a user surface, instead of a 401 that sent the browser back to the sign-in page in a loop. Refusals are never cached; a membership change busts the session cache.
|
|
665
|
+
|
|
666
|
+
**External members** reach only what names them. After the membership check:
|
|
667
|
+
|
|
668
|
+
| Route | `authenticated` host | `restricted` host |
|
|
669
|
+
| ----------------- | -------------------------------------------------------- | ---------------------------------------- |
|
|
670
|
+
| Tunnel | access-request page (403), logged `external_not_granted` | their grant (to them or a Lamaste group) |
|
|
671
|
+
| User surface | 403, `X-Lamalibre-Refusal: forbidden` | the host's grant |
|
|
672
|
+
| Web-surface shell | access-request page (403) | the plugin's audience and the site grant |
|
|
673
|
+
|
|
674
|
+
Members are unchanged. **Groups Lamaste owns** (`lamaste:…`, and hostnames under the relay's domains — [Domains API](domains.md#where-membership-is-enforced)) are stripped from `Remote-Groups` and from a plugin tunnel's relay assertion: a backend sees only the groups an administrator assigned.
|
|
675
|
+
|
|
676
|
+
### Refusal Page (nginx `error_page 403`)
|
|
677
|
+
|
|
678
|
+
```
|
|
679
|
+
GET /authz/refusal/:fqdn
|
|
680
|
+
```
|
|
630
681
|
|
|
631
|
-
|
|
632
|
-
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
633
|
-
| 200 | `X-Lamalibre-Relay-Assertion` for this one request; `Cache-Control: no-store`; no `Remote-*` headers |
|
|
634
|
-
| 401 | No valid session (or more than one session cookie) — nginx answers with `GET /authz/user-surface-unauthenticated`: a JSON `401 { "error": "unauthenticated" }`, never a redirect (the caller is an application's request) |
|
|
635
|
-
| 403 | Anything else refused, with an empty body — never the access-request page; the reason is logged (`user-surface authz denied`) |
|
|
636
|
-
| 503 | The relay signing key is unusable |
|
|
682
|
+
What an Authelia-protected static site answers a signed-in user Authelia refused with (the site asks Authelia directly, not Gatekeeper). Always 403 with the refusal page; reads no session and decides nothing.
|
|
637
683
|
|
|
638
684
|
### Principals (Gatekeeper's own API)
|
|
639
685
|
|
|
@@ -252,13 +252,13 @@ Relay hooks run in `lamalibre-lamaste-hooks@<route>.service`, started by its soc
|
|
|
252
252
|
|
|
253
253
|
`lamaste-priv hooks install <route> <sha256|none>` (module on stdin, checked against the approved digest) writes the module root-owned to `/opt/lamalibre/lamaste/hooks/<route>/<sha256>.mjs` and enables the socket; `hooks activate` points back at the previous module (an update's revert); `hooks restart` stops a stuck runner; `hooks remove` disables it and deletes its modules. The panel reinstalls an approved runner at start-up if it is missing.
|
|
254
254
|
|
|
255
|
-
The protocol between the panel and the runner is one length-prefixed JSON frame each way per connection (`@lamalibre/lamaste@4.
|
|
255
|
+
The protocol between the panel and the runner is one length-prefixed JSON frame each way per connection (`@lamalibre/lamaste@4.7.0/hooks`, `protocol.ts`).
|
|
256
256
|
|
|
257
257
|
## Writing hooks
|
|
258
258
|
|
|
259
259
|
```js
|
|
260
260
|
// hooks/relay.mjs — bundled into one file (esbuild), Node built-ins only
|
|
261
|
-
import { defineRelayHooks } from '@lamalibre/lamaste@4.
|
|
261
|
+
import { defineRelayHooks } from '@lamalibre/lamaste@4.7.0/hooks';
|
|
262
262
|
|
|
263
263
|
export default defineRelayHooks({
|
|
264
264
|
'install:pre-tunnel': async (input) =>
|
|
@@ -287,10 +287,10 @@ Every decision may carry `effects` (at most 10): `{ type: 'audit', event, data?
|
|
|
287
287
|
|
|
288
288
|
### Agent hooks
|
|
289
289
|
|
|
290
|
-
The agent half answers `POST /admin/hooks/<stage>` (body `{ stage, input }`, answer `{ ok: true, output? }` or `{ ok: false, reason }`). `createAgentHookHandler` from `@lamalibre/lamaste@4.
|
|
290
|
+
The agent half answers `POST /admin/hooks/<stage>` (body `{ stage, input }`, answer `{ ok: true, output? }` or `{ ok: false, reason }`). `createAgentHookHandler` from `@lamalibre/lamaste@4.7.0/agent` does the checking — the caller assertion must name Lamaste itself (`{ type: 'lamaste' }`), which no forwarded caller can, and bind the SHA-256 of the exact body (`wrong_body` otherwise), so it needs the body **as received** (a `Buffer` or a string; a parsed object is refused `400` "The body must be passed as received"); the forwarder never reaches `/admin/hooks*`. Send the `status`, `headers` and `bodyText` it returns, `bodyText` byte for byte: its `X-Lamalibre-Responder` header signs those bytes, and the relay counts a hook answer without a valid one as a failure (`unauthenticated`) — see [Authentication between the relay and the agent half](agent-hosted-plugins.md#authentication-between-the-relay-and-the-agent-half):
|
|
291
291
|
|
|
292
292
|
```js
|
|
293
|
-
import { createAgentHookHandler, createCallerAssertionVerifier } from '@lamalibre/lamaste@4.
|
|
293
|
+
import { createAgentHookHandler, createCallerAssertionVerifier } from '@lamalibre/lamaste@4.7.0/agent';
|
|
294
294
|
|
|
295
295
|
const hooks = createAgentHookHandler({
|
|
296
296
|
verifier: createCallerAssertionVerifier({ configPath, plugin: 'fx' }),
|
|
@@ -255,7 +255,7 @@ The wildcard check probes `test-lamaste-check.<domain>`. Wildcard DNS is optiona
|
|
|
255
255
|
|
|
256
256
|
### `POST /api/onboarding/provision`
|
|
257
257
|
|
|
258
|
-
Starts the full stack provisioning process in the background. This configures and starts Chisel and Authelia, issues certbot certificates, and writes nginx vhosts. The Chisel and Authelia binaries and units are installed by `create-lamaste`, not here: if one is missing, the task fails with the error "Chisel is not installed. Run `npx @lamalibre/create-lamaste@4.
|
|
258
|
+
Starts the full stack provisioning process in the background. This configures and starts Chisel and Authelia, issues certbot certificates, and writes nginx vhosts. The Chisel and Authelia binaries and units are installed by `create-lamaste`, not here: if one is missing, the task fails with the error "Chisel is not installed. Run `npx @lamalibre/create-lamaste@4.7.0` on the server to repair the installation." (likewise for Authelia). Task IDs are unchanged from earlier versions; only the titles changed.
|
|
259
259
|
|
|
260
260
|
Provisioning runs asynchronously. This endpoint returns immediately with a 202 status. Use the WebSocket stream endpoint to follow progress in real time.
|
|
261
261
|
|
|
@@ -319,6 +319,15 @@ There are two exceptions:
|
|
|
319
319
|
| PUT | `/api/users/:username` | Management | Update user |
|
|
320
320
|
| DELETE | `/api/users/:username` | Management | Delete user |
|
|
321
321
|
| POST | `/api/users/:username/reset-totp` | Management | Reset TOTP secret |
|
|
322
|
+
| GET | `/api/domains` | Management | List domains |
|
|
323
|
+
| POST | `/api/domains` | Management | Add a domain |
|
|
324
|
+
| POST | `/api/domains/:name/verify-dns` | Management | Check a domain's DNS |
|
|
325
|
+
| POST | `/api/domains/:name/activate` | Management | Activate a domain |
|
|
326
|
+
| DELETE | `/api/domains/:name` | Management | Remove a domain |
|
|
327
|
+
| GET | `/api/domains/:name/members` | Management | List a domain's members |
|
|
328
|
+
| PUT | `/api/domains/:name/members/:user` | Management | Add a member |
|
|
329
|
+
| DELETE | `/api/domains/:name/members/:user` | Management | End a membership |
|
|
330
|
+
| PUT | `/api/plugins/:name/domain` | Management | Move a plugin to a domain |
|
|
322
331
|
| GET | `/api/certs` | Management | List certificates |
|
|
323
332
|
| GET | `/api/certs/auto-renew-status` | Management | Auto-renew timer status |
|
|
324
333
|
| POST | `/api/certs/:domain/renew` | Management | Force-renew certificate |
|
|
@@ -37,19 +37,21 @@ Plugins use the same Authelia to let people and programs log in. A plugin's mani
|
|
|
37
37
|
}
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
| Field | Default | Meaning
|
|
41
|
-
| ------------------------------ | ----------------- |
|
|
42
|
-
| `id` | — | Client id. Must be the plugin's route (`feria` for `@lamalibre/feria-server`) or `<route>-<suffix>`; one plugin cannot claim another's client
|
|
43
|
-
| `name` | `<plugin> (<id>)` | Shown on Authelia's consent screen
|
|
44
|
-
| `grantTypes` | — | Any of `urn:ietf:params:oauth:grant-type:device_code`, `authorization_code`, `
|
|
45
|
-
| `scopes` | `["openid"]` | Scopes the client may request (lowercase letters and underscores)
|
|
46
|
-
| `audience` | `[]` | Audiences the client may request; `{domain}` becomes the relay domain. Clients must request an audience explicitly (`requested_audience_mode: explicit`)
|
|
47
|
-
| `redirectUris` | `[]` | For `authorization_code`; `{domain}` is expanded
|
|
48
|
-
| `accessTokenSignedResponseAlg` | `RS256` | `RS256` makes access tokens JWTs (RFC 9068) a resource server verifies against Authelia's JWKS; `none` keeps them opaque
|
|
49
|
-
| `accessTokenClaims` | `[]` | Claims copied into the JWT access token through a per-client claims policy: `groups` (needs the `groups` scope), `preferred_username` and `name` (`profile`), `email` (`email`). Without a policy Authelia puts these claims only in the UserInfo response
|
|
50
|
-
| `authorizationPolicy` | `two_factor` | `one_factor` or `two_factor` for the user-facing grants. A client with the `client_credentials` grant has no end user: Authelia requires `one_factor` there, so it defaults to `one_factor` and `two_factor` is rejected
|
|
51
|
-
| `requirePkce` | `false` | Require PKCE on authorization-code requests
|
|
52
|
-
| `confidential` | `false` | A confidential client authenticates to the token endpoint with a secret (`client_secret_basic`). Lamaste generates the secret
|
|
40
|
+
| Field | Default | Meaning |
|
|
41
|
+
| ------------------------------ | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
42
|
+
| `id` | — | Client id. Must be the plugin's route (`feria` for `@lamalibre/feria-server`) or `<route>-<suffix>`; one plugin cannot claim another's client |
|
|
43
|
+
| `name` | `<plugin> (<id>)` | Shown on Authelia's consent screen |
|
|
44
|
+
| `grantTypes` | — | Any of `urn:ietf:params:oauth:grant-type:device_code`, `authorization_code`, `client_credentials` — never `refresh_token` (below). `client_credentials` requires `confidential: true`; `authorization_code` requires `redirectUris`. A client whose only grant is `client_credentials` may not have `openid` or `offline_access` |
|
|
45
|
+
| `scopes` | `["openid"]` | Scopes the client may request (lowercase letters and underscores) |
|
|
46
|
+
| `audience` | `[]` | Audiences the client may request; `{domain}` becomes the relay domain. Clients must request an audience explicitly (`requested_audience_mode: explicit`) |
|
|
47
|
+
| `redirectUris` | `[]` | For `authorization_code`; `{domain}` is expanded |
|
|
48
|
+
| `accessTokenSignedResponseAlg` | `RS256` | `RS256` makes access tokens JWTs (RFC 9068) a resource server verifies against Authelia's JWKS; `none` keeps them opaque |
|
|
49
|
+
| `accessTokenClaims` | `[]` | Claims copied into the JWT access token through a per-client claims policy: `groups` (needs the `groups` scope), `preferred_username` and `name` (`profile`), `email` (`email`). Without a policy Authelia puts these claims only in the UserInfo response |
|
|
50
|
+
| `authorizationPolicy` | `two_factor` | `one_factor` or `two_factor` for the user-facing grants. A client with the `client_credentials` grant has no end user: Authelia requires `one_factor` there, so it defaults to `one_factor` and `two_factor` is rejected |
|
|
51
|
+
| `requirePkce` | `false` | Require PKCE on authorization-code requests |
|
|
52
|
+
| `confidential` | `false` | A confidential client authenticates to the token endpoint with a secret (`client_secret_basic`). Lamaste generates the secret |
|
|
53
|
+
|
|
54
|
+
**No refresh tokens.** A manifest declaring the `refresh_token` grant, or the `offline` / `offline_access` scope, is refused at install and update: Authelia checks membership of the client's domain only when a user authorizes a client, so a removed member could keep refreshing. A plugin installed before is served without them (`validateInstalledManifest`).
|
|
53
55
|
|
|
54
56
|
What the panel writes for a public client: `public: true`, `token_endpoint_auth_method: none`, `consent_mode: explicit`. For a confidential client: `client_secret` as a pbkdf2-sha512 digest (`$pbkdf2-sha512$310000$<salt>$<key>`), `token_endpoint_auth_method: client_secret_basic`, and no consent mode when the only grant is `client_credentials`. Any client with the `client_credentials` grant is rendered with `authorization_policy: one_factor` and may not carry the `openid`, `offline` or `offline_access` scopes — Authelia 4.39 refuses the configuration otherwise (`validator/identity_providers.go`). A client with `accessTokenClaims` gets `claims_policy: <id>` and a policy of the same name under `identity_providers.oidc.claims_policies` with `access_token: [...]`.
|
|
55
57
|
|
|
@@ -162,6 +164,7 @@ Admin only, 10 per minute. Verifies the whole chain against the registry, then s
|
|
|
162
164
|
|
|
163
165
|
```json
|
|
164
166
|
{ "registryUrl": "https://feria.example.com", "auth": { "kind": "oidc" } }
|
|
167
|
+
{ "registryUrl": "https://feria.codelama.com.tr", "auth": { "kind": "oidc", "issuerDomain": "codelama.com.tr" } }
|
|
165
168
|
{ "registryUrl": "https://feria.example.com", "auth": { "kind": "token", "token": "feria_…" }, "scope": "@lamalibre" }
|
|
166
169
|
```
|
|
167
170
|
|
|
@@ -206,6 +209,16 @@ CLI: `lamaste-server registry-credential show | set --url <registry> (--oidc | -
|
|
|
206
209
|
|
|
207
210
|
When a user loses a `feria-*` group, or is deleted, the panel asks the registry to revoke that user's tokens (`DELETE /-/feria/tokens?owner=user:<name>`) with the same credential. This is best effort — the user operation reports `warnings` rather than failing. The identity change feed planned for the identity directory (see [Identity directory design](../identity-directory-design.md)) will supersede this hook: plugins will subscribe to membership changes instead of the panel calling each registry.
|
|
208
211
|
|
|
212
|
+
## Several domains: one issuer each
|
|
213
|
+
|
|
214
|
+
Authelia takes the issuer from the request's host, so every active domain is an issuer of its own: `https://auth.<domain>`. Every client is known to every issuer — Authelia has no per-host client list — but a token carries the issuer it was obtained at, and a relying party that trusts one issuer (Feria) refuses the others.
|
|
215
|
+
|
|
216
|
+
- **A plugin's clients belong to its domain** ([`PUT /api/plugins/:name/domain`](domains.md#put-apipluginsnamedomain); the primary until moved): `{domain}` in `audience` and `redirectUris` expands to it, and each user-facing client has an authorization policy of its own, `lamaste_client_<id>` under `identity_providers.oidc.authorization_policies` (`default_policy: deny`): the domain's members (group `lamaste:m:<key>`, the domain's random group key) with the client's factor, and its external members only with a grant on the client (groups `lamaste:x:<key>` and `lamaste:oidc@<id>`). Authelia reads a user's groups afresh on the authorization-code path but not on the device-code path, so a change that takes a device-code client away from someone restarts Authelia. Uninstalling a plugin revokes the grants on its clients. A user's registry tokens are revoked when none of the registry's own sign-in clients (user-facing clients whose audience is the registry) admits them any more. Authelia's `access_control` does not apply to OIDC authorization; these policies are how `feria login` admits only members — and the external members granted it. A pure `client_credentials` client (no user) stays `one_factor`.
|
|
217
|
+
- **Granting a client to external members**: a Gatekeeper grant with `resourceType: "oidc-client"` and the client id as `resourceId`, to a user or a Lamaste group (`POST /api/gatekeeper/grants`; the panel refuses a client no installed plugin declares, `unknown_oidc_client`, and a service client, `service_oidc_client`). `GET /api/plugins/:name/oidc-clients` lists each user-facing client's `grants` (`userFacing: true`; `grants: null` when Gatekeeper could not be read); the plugin card grants and revokes; CLI `lamaste-server plugins oidc-clients <name> grant|revoke <clientId> --user <u>|--group <g>`. A grant or a revocation is a `users.yml` change: it applies at the user's next authorization, without restarting Authelia. Members need no grant.
|
|
218
|
+
- **Token lifetime.** Access and ID tokens of user-facing clients live 15 minutes (a custom lifespan the clients name): what an authorization already issued survives a revocation at most that long. Registry tokens Feria minted from a login are revoked by the panel when the user no longer reaches the registry's sign-in.
|
|
219
|
+
- **The registry credential names its issuer**: `auth.issuerDomain` (an active domain; absent = the primary) is the domain whose issuer the panel's tokens carry (`X-Forwarded-Host: auth.<issuerDomain>`); the owner Feria records is then `ci:auth.<issuerDomain>/lamaste-server`. CLI: `lamaste-server registry-credential set --url <registry> --oidc --issuer-domain <domain>`. Losing what reaches the registry's sign-in — membership of one of the registry's domains, or, for an external member, the grant on its client — revokes a user's registry tokens.
|
|
220
|
+
- **Moving Feria to another domain**: move its plugin's domain, re-create its tunnel there, point Feria's `auth.json` at the new issuer and audience, and set the credential again with the new `issuerDomain`.
|
|
221
|
+
|
|
209
222
|
## Related
|
|
210
223
|
|
|
211
224
|
- [Authentication (Authelia)](../01-concepts/authentication.md) — the OpenID Connect provider configuration
|
|
@@ -150,6 +150,7 @@ For a custom domain:
|
|
|
150
150
|
| `aliases` | `string[]` | Custom sites only, optional, at most 10 hostnames | Hostnames that 301 to the site, e.g. `["www.myblog.net"]`. Each needs its own A record; one certificate covers them all |
|
|
151
151
|
| `spaMode` | `boolean` | Optional, defaults to `false` | Serve `index.html` for all routes |
|
|
152
152
|
| `autheliaProtected` | `boolean` | Optional, defaults to `false` | Require Authelia login |
|
|
153
|
+
| `domain` | `string` | Optional, managed sites only; an active domain (defaults to the primary) | A managed site's base domain: `<name>.<domain>` ([Domains](domains.md)) |
|
|
153
154
|
|
|
154
155
|
**Name regex:**
|
|
155
156
|
|
|
@@ -620,6 +621,10 @@ curl -s --cert client.p12:password \
|
|
|
620
621
|
| 400 | `{"error":"..."}` | Path traversal attempt or file not found |
|
|
621
622
|
| 404 | `{"error":"Site not found"}` | No site with the given UUID |
|
|
622
623
|
|
|
624
|
+
## Sites and domains
|
|
625
|
+
|
|
626
|
+
A managed site is `<name>.<domain>`; site **names** stay unique across the relay (agents' `allowedSites` and the CLI name sites by them), so a second `blog.` on another domain is a custom site there. A custom site's hostname names its own domain. An Authelia-protected site admits members of its domain only (its `allowedUsers` narrowed to them); a site whose hostname is under none of the relay's domains cannot be protected (400 `NOT_UNDER_DOMAIN` — no session cookie reaches it). A signed-in user Authelia refuses gets Gatekeeper's refusal page (`error_page 403` → `/authz/refusal/<fqdn>`), which names both possible reasons: not a member of the domain, or not one of the site's users. The relay's own hosts (`panel.`, `auth.`, `tunnel.`, `agent-*` under any domain) are never a custom site or an alias; a domain's apex and `www.` may be.
|
|
627
|
+
|
|
623
628
|
## Quick Reference
|
|
624
629
|
|
|
625
630
|
| Method | Path | Description |
|
|
@@ -652,6 +657,10 @@ curl -s --cert client.p12:password \
|
|
|
652
657
|
}
|
|
653
658
|
```
|
|
654
659
|
|
|
660
|
+
A site a plugin owns for its [web surface](agent-hosted-plugins.md#web-surface) is listed with two more fields — `"owner": { "plugin": "<route>", "package": "<server package>" }` and `"accessMode": "authenticated" | "restricted"` — and its `rootPath` is the plugin's `current` tree under `/var/www/lamaste/apps/<route>/`; `GET /api/sites` adds `webSurface: { served, reason }` — whether it serves its plugin now, or answers `503` and why (the reasons of [`GET /api/plugins/:name/web-surface`](agent-hosted-plugins.md#get-apipluginsnameweb-surface)). The routes below do not manage it: `PATCH`, `DELETE`, `verify-dns` and the file routes answer `409` with `details.code: "plugin_owned_site"` (and so `lamaste-agent deploy` is refused); it is created, moved to and deleted through `/api/plugins/:name/web-surface`.
|
|
661
|
+
|
|
662
|
+
**Locks.** Every change of `sites.json` runs under one lock; one that gives a site a hostname (creating a site, changing its aliases, verifying its DNS) takes the tunnel lock first, as creating or enabling a tunnel does — a site and a tunnel never take one name at once. The size recorded after an upload or a deletion is written from `sites.json` as it is then, never from a copy read before.
|
|
663
|
+
|
|
655
664
|
### Managed vs Custom Site Flow
|
|
656
665
|
|
|
657
666
|
```
|
|
@@ -98,15 +98,16 @@ The carrying agent's sync timer picks the tunnel up within about 30 seconds and
|
|
|
98
98
|
}
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
| Field | Type | Validation
|
|
102
|
-
| --------------- | --------- |
|
|
103
|
-
| `subdomain` | `string` | Lowercase alphanumeric + hyphens, max 63 chars, cannot start/end with hyphen
|
|
104
|
-
| `port` | `integer` | 1024 - 65535, not a [reserved port](#port)
|
|
105
|
-
| `agentLabel` | `string` | An enrolled, non-revoked agent's label
|
|
106
|
-
| `description` | `string` | Max 200 chars, optional (defaults to `""`)
|
|
107
|
-
| `type` | `string` | `'app'` (default), `'panel'`, or `'plugin'`
|
|
108
|
-
| `accessMode` | `string` | `'restricted'` (default), `'authenticated'`, `'public'`
|
|
109
|
-
| `maxBodySizeMb` | `integer` | 1 - 10240 for admins, 1 - 100 for agents; optional (defaults to 10)
|
|
101
|
+
| Field | Type | Validation | Description |
|
|
102
|
+
| --------------- | --------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
103
|
+
| `subdomain` | `string` | Lowercase alphanumeric + hyphens, max 63 chars, cannot start/end with hyphen | The subdomain to create |
|
|
104
|
+
| `port` | `integer` | 1024 - 65535, not a [reserved port](#port) | Port the agent forwards |
|
|
105
|
+
| `agentLabel` | `string` | An enrolled, non-revoked agent's label | **Required for admin certificates.** An agent certificate may omit it (defaults to itself) but may not name another agent |
|
|
106
|
+
| `description` | `string` | Max 200 chars, optional (defaults to `""`) | Human-readable description |
|
|
107
|
+
| `type` | `string` | `'app'` (default), `'panel'`, or `'plugin'` | `panel` requires the `panel:expose` capability; `plugin` is admin-only |
|
|
108
|
+
| `accessMode` | `string` | `'restricted'` (default), `'authenticated'`, `'public'` | `authenticated` and `public` are admin-only |
|
|
109
|
+
| `maxBodySizeMb` | `integer` | 1 - 10240 for admins, 1 - 100 for agents; optional (defaults to 10) | Written into the vhost as `client_max_body_size`; larger requests get HTTP 413. Raise it for apps that take big uploads |
|
|
110
|
+
| `domain` | `string` | An active domain of the relay; optional (defaults to the primary); panel tunnels only on the primary | The base domain of the hostname: `<subdomain>.<domain>` ([Domains](domains.md)) |
|
|
110
111
|
|
|
111
112
|
**Subdomain regex:**
|
|
112
113
|
|
|
@@ -153,24 +154,24 @@ curl -s --cert client.p12:password \
|
|
|
153
154
|
|
|
154
155
|
**Errors:**
|
|
155
156
|
|
|
156
|
-
| Status | Body
|
|
157
|
-
| ------ |
|
|
158
|
-
| 400 | `{"error":"Validation failed","details":{"issues":[...]}}`
|
|
159
|
-
| 400 | `{"error":"agentLabel is required: name the agent that will carry the tunnel"}`
|
|
160
|
-
| 400 | `{"error":"Failed to create tunnel","details":"Agent 'x' is not enrolled or has been revoked"}`
|
|
161
|
-
| 400 | `{"error":"Failed to create tunnel","details":"Subdomain 'panel' is reserved"}`
|
|
162
|
-
| 400 | `{"error":"Failed to create tunnel","details":"Subdomain 'app' is already in use"}`
|
|
163
|
-
| 400 | `{"error":"Failed to create tunnel","details":"Port 3000 is already in use by another tunnel"}`
|
|
164
|
-
| 400 | `{"error":"Failed to create tunnel","details":"Domain 'app.example.com' is already served by a static site"}` | A static site or site alias already answers on this hostname |
|
|
165
|
-
| 400 | `{"error":"Domain and email must be configured before creating tunnels"}`
|
|
166
|
-
| 403 | `{"error":"Agents can only create tunnels they carry themselves"}`
|
|
167
|
-
| 403 | `{"error":"Plugin-agent certificates cannot create tunnels — use the agent certificate of ..."}`
|
|
168
|
-
| 403 | `{"error":"Only administrators can set a body limit above 100 MiB"}`
|
|
169
|
-
| 403 | `{"error":"Only administrators can set tunnel access mode to public or authenticated"}`
|
|
170
|
-
| 500 | `{"error":"Failed to create tunnel","details":"Certificate issuance failed: ..."}`
|
|
171
|
-
| 500 | `{"error":"Failed to create tunnel","details":"Nginx configuration failed: ..."}`
|
|
172
|
-
| 500 | `{"error":"Failed to create tunnel","details":"State persistence failed: ..."}`
|
|
173
|
-
| 500 | `{"error":"Failed to create tunnel","details":"Chisel reconfiguration failed: ..."}`
|
|
157
|
+
| Status | Body | When |
|
|
158
|
+
| ------ | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
159
|
+
| 400 | `{"error":"Validation failed","details":{"issues":[...]}}` | Invalid subdomain format, port out of range or reserved |
|
|
160
|
+
| 400 | `{"error":"agentLabel is required: name the agent that will carry the tunnel"}` | Admin certificate did not name the carrying agent |
|
|
161
|
+
| 400 | `{"error":"Failed to create tunnel","details":"Agent 'x' is not enrolled or has been revoked"}` | `agentLabel` does not name an active agent |
|
|
162
|
+
| 400 | `{"error":"Failed to create tunnel","details":"Subdomain 'panel' is reserved"}` | Subdomain is in the reserved list |
|
|
163
|
+
| 400 | `{"error":"Failed to create tunnel","details":"Subdomain 'app' is already in use"}` | Another tunnel uses this subdomain |
|
|
164
|
+
| 400 | `{"error":"Failed to create tunnel","details":"Port 3000 is already in use by another tunnel"}` | Another tunnel uses this port |
|
|
165
|
+
| 400 | `{"error":"Failed to create tunnel","details":"Domain 'app.example.com' is already served by a static site ('app')"}` | A static site or site alias already answers on this hostname (checked under the tunnel lock; enabling a tunnel is refused the same way) |
|
|
166
|
+
| 400 | `{"error":"Domain and email must be configured before creating tunnels"}` | Domain not set in config |
|
|
167
|
+
| 403 | `{"error":"Agents can only create tunnels they carry themselves"}` | Agent certificate named another agent |
|
|
168
|
+
| 403 | `{"error":"Plugin-agent certificates cannot create tunnels — use the agent certificate of ..."}` | Plugin-agent certificate |
|
|
169
|
+
| 403 | `{"error":"Only administrators can set a body limit above 100 MiB"}` | Agent certificate asked for `maxBodySizeMb` over 100 |
|
|
170
|
+
| 403 | `{"error":"Only administrators can set tunnel access mode to public or authenticated"}` | Agent certificate asked for a non-restricted mode |
|
|
171
|
+
| 500 | `{"error":"Failed to create tunnel","details":"Certificate issuance failed: ..."}` | certbot failed |
|
|
172
|
+
| 500 | `{"error":"Failed to create tunnel","details":"Nginx configuration failed: ..."}` | nginx vhost write or test failed |
|
|
173
|
+
| 500 | `{"error":"Failed to create tunnel","details":"State persistence failed: ..."}` | Failed to write tunnels.json |
|
|
174
|
+
| 500 | `{"error":"Failed to create tunnel","details":"Chisel reconfiguration failed: ..."}` | Chisel authfile sync failed |
|
|
174
175
|
|
|
175
176
|
### Creation Flow
|
|
176
177
|
|
|
@@ -318,7 +319,7 @@ curl -s --cert client.p12:password \
|
|
|
318
319
|
| `chiselArgs` | `string[]` | Chisel client arguments: `client`, the server URL, and one `R:127.0.0.1:<port>:127.0.0.1:<port>` remote per tunnel. No `--tls-skip-verify` (the agent verifies the relay's certificate) and no credential — the agent supplies its own through the service environment, never argv |
|
|
319
320
|
| `chiselCredentialIssuedAt` | `string \| null` | When the agent's current Chisel credential was issued — a timestamp, never the credential. An agent holding a credential with a different issue time (an administrator rotated it) fetches the current one from `GET /api/agents/me/chisel-credential` |
|
|
320
321
|
| `tunnels` | `array` | The agent's enabled tunnels with port and subdomain |
|
|
321
|
-
| `hostFactsHash` | `string \| null` | Agent certificates only: the SHA-256 of the [host facts](agent-hosted-plugins.md#host-facts) the relay stores for the agent, computed as the agent computes it over its own (`hostFactsHash` in `@lamalibre/lamaste@4.
|
|
322
|
+
| `hostFactsHash` | `string \| null` | Agent certificates only: the SHA-256 of the [host facts](agent-hosted-plugins.md#host-facts) the relay stores for the agent, computed as the agent computes it over its own (`hostFactsHash` in `@lamalibre/lamaste@4.7.0`); `null` when it stores none. The agent's sync reports its facts again when this is not the hash of its own, so a record replaced since its last report — the agent enrolled again on its label, the relay restored from a backup — has them again after one sync, not a day. An agent behind a relay without the field reports as before |
|
|
322
323
|
|
|
323
324
|
**Errors:**
|
|
324
325
|
|
|
@@ -329,6 +330,8 @@ curl -s --cert client.p12:password \
|
|
|
329
330
|
|
|
330
331
|
## User Surface Binding
|
|
331
332
|
|
|
333
|
+
A plugin that owns a site for its [web surface](agent-hosted-plugins.md#web-surface) serves its user surface there; binding it to a tunnel is refused (`400`). A tunnel bound to one plugin alone can hand its host over to that plugin's web surface in one reload ([Moving from a bound tunnel](agent-hosted-plugins.md#moving-from-a-bound-tunnel)); it stays in the state, disabled and unbound, and cannot be enabled again while the site holds its hostname.
|
|
334
|
+
|
|
332
335
|
An app tunnel can be **bound** as the user surface of an agent-hosted plugin: its host then serves the plugin's users at `/api/<route>/app` and everything under it — Gatekeeper authenticates the Authelia session and vouches for the user to the panel, and the panel forwards the request to the plugin's agent half with the directory's `userId`, username and the user's roles ([Agent-Hosted Plugins](agent-hosted-plugins.md#the-user-surface)). Every other path of the host goes down the tunnel as before. Admin only, 30 a minute.
|
|
333
336
|
|
|
334
337
|
**What can be bound.** An enabled app tunnel (`type` `app`, not internal), with access mode `authenticated` or `restricted` — a public host has no signed-in user to vouch for — **carried by the agent that hosts the plugin** (`400` otherwise: "Plugin '…' is hosted on agent '…', but this tunnel is carried by '…'", or "… is not hosted on an agent" before it is hosted). The host whose sessions reach the plugin's users is never another agent's: whatever answers on its other paths — another agent's application — would share the user surface's origin. On a `restricted` tunnel the user also needs a grant for the tunnel. The route must be held by an installed agent-hosted plugin; a plugin's user surface is on one host at most, and one host serves at most 16 plugins' user surfaces. A bound tunnel cannot be made public (`PATCH` answers `409` "… cannot be made public — unbind it first").
|
|
@@ -405,6 +408,10 @@ Revoking an agent's certificate releases its tunnels: its app and plugin tunnels
|
|
|
405
408
|
- Must be unique across all tunnels
|
|
406
409
|
- Must not be a hostname a static site or site alias already serves (checked on tunnel creation; site creation and alias changes check the reverse)
|
|
407
410
|
|
|
411
|
+
### Hostname
|
|
412
|
+
|
|
413
|
+
A tunnel's hostname is `<subdomain>.<domain>` and is unique as a whole: `feria.samplepark.tr` and `feria.codelama.com.tr` may both exist. Reserved labels (`panel`, `auth`, `tunnel`, `www`, `mail`, `ftp`, `api`, and `agent-*` except an agent's own panel tunnel) are reserved on every domain. `agent-config` lists each carried tunnel with its `fqdn`; agents' chisel needs only the port and the primary's `tunnel.` endpoint, so a tunnel on another domain changes nothing on the agent. The vhost is `lamalibre-lamaste-app-<fqdn>` (`lamalibre-lamaste-agent-panel-<fqdn>` for a panel tunnel).
|
|
414
|
+
|
|
408
415
|
### Port
|
|
409
416
|
|
|
410
417
|
- Must be an integer
|