Add members to a group. With `req`, the change is judged as a modification of the group, and each member must be an entry the caller can read: the routes used to call this without it, which every authorization plugin reads as an internal call and lets through — anyone authenticated could join any group, a team mailbox included (#325). @param cn group cn or DN @param member member DN(s) @param req the request, to authorize the change against its caller
Get password status for a user Every account lookup and write carries the request, so an authorization plugin answers for the caller: without it, a route read as an internal call, and a delegated administrator could read or unlock any account of the directory (#325).
Get user DN from uid An identity naming several entries is refused (`AmbiguousIdentityError`) rather than answered with `null`: under `--authz-unresolved-user allow`, `null` means "skip authorization", so an ambiguous directory would open every branch. A failed search propagates for the same reason — it used to read as `null` too, and `resolveCaller` then cached it.
One value per occurrence, and a second word after it used to be
Only the search is inside the `try`. The refusal used to be too, so
Recompute the path of a renamed or moved organization and of every organization below it. A path names an organization's ancestors, root first, its own name last, the top organization left out. Moving or renaming one therefore changes the path of its whole subtree. Only the entries *linked* to the tree used to be updated, from paths the tree itself still held: the organizations kept the path of their former parent, the linked users were rewritten to it, and nothing could correct it since the path is computed by the server. Only organizations already holding a path are written. @param orgDn DN of the organization after the rename
Refuse a delegated administrator a mail domain their branch was not given. The domains an organization's entries may use are those it links and those every organization above it links (`enterpriseRules`). Writing an organization only asks for write on its parent, so an administrator could create a sub-organization of their branch linked to any domain of the directory — another customer's — and create addresses in it (#325). A domain newly linked to an organization must therefore be one an organization above it already links. Only a caller accepted by {@link mayLinkAnyDomain} may link another. Removing a link narrows nothing above the caller's own branch and is judged as any other write. @param user the resolved caller @param req the request, for the refusal's wording @param dn the organization being written @param submitted the domain DNs the write gives it @param attr the domain link attribute @param creating whether the organization is being created, so holds none
Register with the dispatcher, like every other authentication plugin. This plugin used to mount three layers of its own — the `beforeAuth` hooks, the library's router, then the identity — which kept their registration order. An instance carrying a name, the only form that can hold `auth_path_prefix`, landed in the parallel batch and mounted after the plugins that were supposed to read what it publishes: with `core/auth/authzPerRoute` loaded, every rule was judged before `req.user` existed, and `authzPerRoute` passes a request it cannot identify. The rules read as enforced and were inert. The dispatcher is mounted before any plugin can register a route, runs only the plugin whose claim is most specific, and hands `authMethod` the `beforeAuth`/`afterAuth` hooks — the three layers, in a place where order is not a configuration accident. (`afterAuth` used to be gated on a flag nothing sets, so this plugin's own copy of it never ran either.) @param app the express application
Resolve the caller, or say what an identity that does not resolve means. `shouldSkipAuthorization` covers the request that carries no identity at all — anonymous, and skipped by design. This covers the other case: a request that *was* authenticated, whose identity this plugin's model cannot place. That used to return the operation untouched behind a `warn`, which made a configuration mismatch an open door rather than a refusal. `authzLinid1` resolves `req.user` by searching `(<ldap_user_main_attribute>=<identity>)`, so an authenticator publishing anything else — `core/auth/openidconnect` publishes the OIDC `sub`, `core/auth/llng` whatever `whatToTrace` traces — resolved to nothing on every request, and read, write and delete went through across the whole tree, with a line a `notice` production log does not show. (`authzPerBranch` fails the other way: the identity is its own key, so a mismatch yields all-false permissions and a refusal.) An authenticated identity that does not resolve is a failure of the configuration, not an absence of one, so it is refused. `--authz-unresolved-user allow` restores the old behaviour for a deployment that turns out to rely on it — a security fix should not rewrite every configuration by itself. Both answers are cached for `cacheTTL`, negatives included: a mismatch would otherwise turn every request into a directory search, so a configuration mistake would also become a load problem. The TTL cuts both ways, and neither way is free: - an identity that appears in the directory after being refused waits out the TTL before it is looked up again; - a *positive* answer can go stale. `authzLinid1` resolves an identity to a DN, so an administrator whose own entry is renamed or moved is still resolved to the former DN, where no organization names them: every operation is refused until the TTL runs out. A rename or a delete therefore drops what was resolved, which closes the window for the case that produces it; a change made directly in the directory still waits. `authzScope` resolves the identity itself (`authzScope.ts`), outside this cache, so during such a window it can describe a scope the hooks no longer grant. @param req the request being authorized, which carries an identity @returns the resolved user, or null when the policy is to allow it @throws AuthzRefusedError when the policy is to deny it
Resolve the principal user entry from the `:user` path param. `:user` is matched against the configured resolution attribute (`app_accounts_user_attribute`, default the mail attribute). Mail is globally unique; resolving by the LDAP `uid` instead is only safe when uid is unique directory-wide: a `uid` repeated under different subtrees is answered 409 when the caller reads several of its holders, and resolves to the one it reads otherwise (see #88). On failure (no match, or an ambiguous one) this writes the HTTP error response and returns null, so callers can simply `return` when it does. @param principal - The `:user` path param value @param res - The Express response, used to emit error statuses @param attributes - Optional attribute projection for the search @returns The single matching LDAP entry, or null when none/ambiguous
Say, once every plugin is loaded, when no rule can ever match. The rules are keyed on identities, and which values an authenticator publishes is the whole subject of #187: a rule written for a token name is inert behind OpenID Connect, where the identity is a `sub`. Inert is the part worth a line — nothing is refused, so nothing looks wrong.
See issue #175, which tracks both.
That the entry a route writes is a group, once the write is judged: another entry named alike is not one, and is not written as one (#395). Judged first, a caller who may not make the write gets the refusal whether the entry is there, a group or not; only one who may learns that it is none. @param dn the entry, from {@link routeGroupDn} @param judge the authorization of the write, as `ldap.authorize`
The authenticated request being served. The authentication dispatcher runs everything downstream of a request it let through inside it, so an operation made while serving it, by whatever plugin, is judged as its caller even when the request was not handed down (#385). A call that forgets the request then fails closed instead of being let through unjudged.
The child organizations of a node, and what to do when the directory will not list them all. Every failure used to be read as "no children" (#179): the node's own children were searched without a limit, and a directory answering `sizeLimitExceeded` rather than a long list left the endpoint returning `200 []` with nothing above `debug` to say so. An empty tree is what a console then draws for a branch holding a thousand organizations. Three outcomes, and only one of them is emptiness: - `noSuchObject` (32): nothing is there. An empty answer is the truth. - `sizeLimitExceeded` (4): the answer was longer than the server's limit. Measured against OpenLDAP 2.5, the paged control does not lift it — `size.prtotal` bounds the whole search, so a paged walk ends on the same refusal. What does change the answer is asking for a bounded number: ldapts raises the refusal only when the request carried no `sizeLimit` of its own, so a second, bounded search comes back with entries where the first came back with an error. The answer then says it is partial, through the `moreIndicator` row the attached entries already use. - anything else is a failure, and answering `[]` to a failure is how a broken directory comes to look exactly like an empty one. The first search is paged for the directories where paging *is* what lifts the limit: Active Directory answers an unpaged search with at most `MaxPageSize` entries and expects the control for the rest. @param dn organization whose children are wanted @param filter what those children have to match @param cap how many to keep when the directory will not list them all @param req incoming request, forwarded to the authorization hooks @returns the children, followed by a `moreIndicator` row when the directory refused to list them all
The directory as seen by one HTTP request. `ldapActions`'s own methods take the request as a trailing optional argument, which the authorization plugins judge by. One naming none is made for the request being served, if any (#385); this says which request is meant rather than leaving it to the context. These methods take no request parameter: it is captured once by `ldapActions.forRequest()`. Request handlers should bind once and use this; the server's own work goes through {@link ldapActions.system}.
The same two searches as `getOrganisationSubnodes`, narrowed by a query. Both go through the helpers that classify a failure rather than reading every one of them as emptiness (#179), and both carry the request the authorization plugins judge them by. The attached entries are capped like the other endpoint's, since an answer the directory refuses to finish is what this is about. @param dn organization to search under @param query what to look for in a name, a description or an identity @param req incoming request, forwarded to the authorization hooks @returns matching organizations then matching attached entries
The server's logger, once the `DM` constructor has installed it. Undefined until then, and said so in the type: this module is loaded before any `DM` exists, and a caller reading it as always there throws from inside the error path it was reporting on (#182, #199). The compiler now holds every reader to a guard.
They used to be recreated at the same DN carrying the new mail but
Thread a value through every subscriber of a chained hook, each getting what the one before answered. The first failure stops the chain and travels up: a refusal is a refusal, and the caller must not go on as if the chain had said yes. Hooks run outside the request being served: what a hook reads or writes to validate, complete or keep track of an operation is the server's (#385). The request is in the arguments of the hooks that judge, which judge by it; a hook acting as the caller names it. @param name the hook @param value the value to thread @returns the last value in the chain
Unconditionally, as `beforeAuth` above. It used to run only when
What the loaded plugins can enforce, decided once the configuration is loaded (#385). - No authentication nor authorization plugin: every request is served with every right. An open mode on purpose — local development, a trusted network — said once. - Authorization without authentication: no request carries an identity, so every authorization plugin steps aside and everything passes while the operator believes authorization is on. The server does not start. - Authentication without authorization: every authenticated caller has every right, which a single administration token legitimately wants. Said once. A plugin carrying both roles (`authzDynamic`) authenticates the requests it judges. @param server the server, its configuration loaded @throws Error naming the authorization plugins nothing authenticates for
Whether this request is allowed past the token ACLs without a token. The plugin used to step aside as soon as anything else had set `req.user`, which is not a decision anyone wrote down: with `core/auth/token` and this plugin both unscoped, the dispatcher runs the token plugin first, it sets `req.user`, and every LDAP operation of that request was then checked against no ACL at all — a branch-unrestricted administrator. Registered the other way round, the same configuration demanded both credentials. An ordering accident decided which. The comment that justified it named `trustedProxy + authzDynamic`, a composition that never worked: `trustedProxy` sets `req.trustedProxy` and `req.proxyAuthUser`, never `req.user`, so that pair never took this path — while every authenticator that does set it, did. So the bypass is named now. `--authz-dynamic-bypass` lists the identities that may skip the ACLs, plus `trusted-proxy` for the composition the comment meant and `any-authenticated` for the old behaviour, which a deployment relying on it can ask for in one line. @param req request being authenticated @returns what allowed it past, or undefined when nothing does
Wrap a driver error without losing its LDAP result code. These methods re-throw their own `Error` so the message says which operation failed. That used to leave the numeric code — the only dependable way for a caller to tell noSuchObject from entryAlreadyExists, or a schema refusal from a server fault — recoverable only by matching the driver's wording, which no driver promises to keep. `Error.cause` would be the idiomatic home for it, but it is not in this project's `lib` target; the code itself is what callers read, as `extractLdapCode` finds it, message patterns included.
`branches` widens the search, it does not move it: it used to *replace*
`res instanceof Promise` guard that used to stand here could not be
cache stored nothing at all until #166, so no deployment has ever run
checks — administering a branch read-only used to report `create: true`
every request, so the plugin used to answer 500 to all of them while
globally unique (see #88). Set to `uid` to restore the pre-#89 contract
module name, so every named instance used to leave this pass for the
once per level (#350)
ones (#103).
otherwise keep answering the entry that used to carry this DN.
pre-#89 contract where `:user` is the LDAP uid. The app-account uid is
read cached under the new DN — of whatever used to carry it — would
so it used to mean "generate one": the answer said `success` after
the request is handed down (#385)
throwing (it used to be an opaque TypeError); a BadRequestError just
true, and nothing was ever cached (#166). What the hooks see is
{@link toGroupDn} for a route serving the groups branch: what is not below it, nor named as these groups are (`cn=`), is no group of these routes, whatever entry it is (#395). The branch may be the whole directory, which is the default: the naming is what tells an account from a group there. A multi-valued RDN holding the main attribute passes here; the group class, checked next, is what settles it. @param cn the group's name, or its DN @returns its DN
