ldap-rest 1.2.1 → 1.3.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/bin/sync-james.mjs +13 -2
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/abstract/plugin.js +1 -1
- package/dist/abstract/twakePlugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/config/options.js +1 -0
- package/dist/config/schema.js +1 -1
- package/dist/lib/auth/base.js +1 -1
- package/dist/lib/authz/base.js +1 -1
- package/dist/lib/entitySchema/validator.js +1 -1
- package/dist/lib/errors.js +1 -1
- package/dist/lib/expressFormatedResponses.js +1 -1
- package/dist/lib/health.js +1 -1
- package/dist/lib/hookBus.js +1 -0
- package/dist/lib/instanceProviders.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/ldapSchema.js +1 -1
- package/dist/lib/parseConfig.js +1 -1
- package/dist/lib/route.js +1 -0
- package/dist/lib/schemaFile.js +1 -1
- package/dist/lib/storage/store.js +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/lib/version.js +1 -0
- package/dist/logger/winston.js +1 -1
- package/dist/plugins/auth/auth.options.js +1 -0
- package/dist/plugins/auth/authzDynamic.js +1 -1
- package/dist/plugins/auth/authzLinid1.js +1 -1
- package/dist/plugins/auth/authzPerBranch.js +1 -1
- package/dist/plugins/auth/authzPerRoute.js +1 -1
- package/dist/plugins/auth/authzScope.js +1 -1
- package/dist/plugins/auth/crowdsec.js +1 -1
- package/dist/plugins/auth/fake.js +1 -1
- package/dist/plugins/auth/hmac.js +1 -1
- package/dist/plugins/auth/llng.js +1 -1
- package/dist/plugins/auth/openidconnect.js +1 -1
- package/dist/plugins/auth/rateLimit.js +1 -1
- package/dist/plugins/auth/token.js +1 -1
- package/dist/plugins/auth/totp.js +1 -1
- package/dist/plugins/auth/trustedProxy.js +1 -1
- package/dist/plugins/bcl.js +1 -1
- package/dist/plugins/bcl.options.js +1 -0
- package/dist/plugins/configApi.js +1 -1
- package/dist/plugins/demo/helloworld.js +1 -1
- package/dist/plugins/ldap/accountLifecycle.js +1 -1
- package/dist/plugins/ldap/bulkImport.js +1 -1
- package/dist/plugins/ldap/departmentSync.js +1 -1
- package/dist/plugins/ldap/enterpriseRules.js +1 -1
- package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
- package/dist/plugins/ldap/flatGeneric.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/ldap.options.js +1 -0
- package/dist/plugins/ldap/onChange.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/ldap/passwordPolicy.js +1 -1
- package/dist/plugins/ldap/raw.js +1 -1
- package/dist/plugins/ldap/trash.js +1 -1
- package/dist/plugins/metrics.js +1 -1
- package/dist/plugins/metrics.options.js +1 -0
- package/dist/plugins/rabbitmq.js +1 -1
- package/dist/plugins/rabbitmq.options.js +1 -0
- package/dist/plugins/scim/baseResolver.js +1 -1
- package/dist/plugins/scim/bulk.js +1 -1
- package/dist/plugins/scim/discovery.js +1 -1
- package/dist/plugins/scim/errors.js +1 -1
- package/dist/plugins/scim/filter.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/scim/mapping.js +1 -1
- package/dist/plugins/scim/patch.js +1 -1
- package/dist/plugins/scim/scim.js +1 -1
- package/dist/plugins/scim/scim.options.js +1 -0
- package/dist/plugins/scim/users.js +1 -1
- package/dist/plugins/static.js +1 -1
- package/dist/plugins/static.options.js +1 -0
- package/dist/plugins/storage/file.js +1 -1
- package/dist/plugins/storage/ldap.js +1 -1
- package/dist/plugins/storage/postgres.js +1 -1
- package/dist/plugins/storage/valkey.js +1 -1
- package/dist/plugins/storage.js +1 -1
- package/dist/plugins/storage.options.js +1 -0
- package/dist/plugins/twake/appAccounts.options.js +1 -0
- package/dist/plugins/twake/appAccountsApi.js +1 -1
- package/dist/plugins/twake/appAccountsConsistency.js +1 -1
- package/dist/plugins/twake/calendar.js +1 -1
- package/dist/plugins/twake/calendar.options.js +1 -0
- package/dist/plugins/twake/calendarResources.js +1 -1
- package/dist/plugins/twake/cloudery.options.js +1 -0
- package/dist/plugins/twake/clouderyProvision.js +1 -1
- package/dist/plugins/twake/cozy.options.js +1 -0
- package/dist/plugins/twake/cozyProvision.js +1 -1
- package/dist/plugins/twake/drive.js +1 -1
- package/dist/plugins/twake/drive.options.js +1 -0
- package/dist/plugins/twake/groups.js +1 -1
- package/dist/plugins/twake/instances.js +1 -1
- package/dist/plugins/twake/james.js +1 -1
- package/dist/plugins/twake/james.options.js +1 -0
- package/dist/plugins/twake/lifecycleAttributes.js +1 -1
- package/dist/plugins/twake/lifecycleEvents.js +1 -1
- package/dist/plugins/twake/migrationProxy.js +1 -1
- package/dist/plugins/twake/migrationProxy.options.js +1 -0
- package/dist/plugins/twake/spaces.js +1 -1
- package/dist/plugins/twake/tombstone.js +1 -1
- package/dist/plugins/twake/twakeGroups.options.js +1 -0
- package/dist/plugins/twake/twakeInstances.options.js +1 -0
- package/dist/plugins/twake/twakeLifecycle.options.js +1 -0
- package/dist/plugins/twake/twakeSpaces.options.js +1 -0
- package/dist/plugins/twake/twakeTombstone.options.js +1 -0
- package/dist/plugins/weblogs.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +28 -10
- package/dist/src/abstract/plugin.d.ts +10 -5
- package/dist/src/abstract/twakePlugin.d.ts +14 -4
- package/dist/src/bin/index.d.ts +17 -4
- package/dist/src/config/args.d.ts +2 -4
- package/dist/src/config/options.d.ts +89 -0
- package/dist/src/config/schema.d.ts +50 -1
- package/dist/src/hooks.d.ts +60 -22
- package/dist/src/lib/authz/base.d.ts +4 -51
- package/dist/src/lib/authz/composition.d.ts +2 -2
- package/dist/src/lib/entitySchema/validator.d.ts +63 -6
- package/dist/src/lib/errors.d.ts +56 -1
- package/dist/src/lib/expressFormatedResponses.d.ts +49 -7
- package/dist/src/lib/hookBus.d.ts +226 -0
- package/dist/src/lib/instanceProviders.d.ts +0 -8
- package/dist/src/lib/ldapActions.d.ts +7 -0
- package/dist/src/lib/parseConfig.d.ts +5 -0
- package/dist/src/lib/route.d.ts +99 -0
- package/dist/src/lib/utils.d.ts +31 -36
- package/dist/src/lib/version.d.ts +12 -0
- package/dist/src/plugins/auth/auth.options.d.ts +88 -0
- package/dist/src/plugins/auth/authzDynamic.d.ts +10 -1
- package/dist/src/plugins/auth/authzLinid1.d.ts +6 -0
- package/dist/src/plugins/auth/authzPerBranch.d.ts +9 -0
- package/dist/src/plugins/auth/authzPerRoute.d.ts +3 -0
- package/dist/src/plugins/auth/authzScope.d.ts +23 -14
- package/dist/src/plugins/auth/crowdsec.d.ts +5 -0
- package/dist/src/plugins/auth/fake.d.ts +3 -0
- package/dist/src/plugins/auth/hmac.d.ts +4 -0
- package/dist/src/plugins/auth/llng.d.ts +4 -0
- package/dist/src/plugins/auth/openidconnect.d.ts +7 -0
- package/dist/src/plugins/auth/rateLimit.d.ts +4 -0
- package/dist/src/plugins/auth/token.d.ts +3 -0
- package/dist/src/plugins/auth/totp.d.ts +5 -0
- package/dist/src/plugins/auth/trustedProxy.d.ts +4 -0
- package/dist/src/plugins/bcl.d.ts +3 -0
- package/dist/src/plugins/bcl.options.d.ts +8 -0
- package/dist/src/plugins/configApi.d.ts +24 -5
- package/dist/src/plugins/demo/helloworld.d.ts +8 -19
- package/dist/src/plugins/ldap/accountLifecycle.d.ts +17 -0
- package/dist/src/plugins/ldap/bulkImport.d.ts +6 -76
- package/dist/src/plugins/ldap/departmentSync.d.ts +1 -0
- package/dist/src/plugins/ldap/enterpriseRules.d.ts +8 -1
- package/dist/src/plugins/ldap/externalUsersInGroups.d.ts +4 -0
- package/dist/src/plugins/ldap/flatGeneric.d.ts +4 -0
- package/dist/src/plugins/ldap/groups.d.ts +27 -11
- package/dist/src/plugins/ldap/ldap.options.d.ts +75 -0
- package/dist/src/plugins/ldap/onChange.d.ts +10 -2
- package/dist/src/plugins/ldap/organizations.d.ts +16 -0
- package/dist/src/plugins/ldap/passwordPolicy.d.ts +32 -1
- package/dist/src/plugins/ldap/raw.d.ts +50 -19
- package/dist/src/plugins/ldap/trash.d.ts +6 -0
- package/dist/src/plugins/metrics.d.ts +3 -0
- package/dist/src/plugins/metrics.options.d.ts +8 -0
- package/dist/src/plugins/rabbitmq.d.ts +3 -0
- package/dist/src/plugins/rabbitmq.options.d.ts +8 -0
- package/dist/src/plugins/scim/baseResolver.d.ts +2 -1
- package/dist/src/plugins/scim/bulk.d.ts +6 -7
- package/dist/src/plugins/scim/discovery.d.ts +3 -0
- package/dist/src/plugins/scim/errors.d.ts +2 -1
- package/dist/src/plugins/scim/groups.d.ts +6 -4
- package/dist/src/plugins/scim/scim.d.ts +37 -3
- package/dist/src/plugins/scim/scim.options.d.ts +35 -0
- package/dist/src/plugins/scim/users.d.ts +6 -7
- package/dist/src/plugins/static.d.ts +9 -0
- package/dist/src/plugins/static.options.d.ts +13 -0
- package/dist/src/plugins/storage.d.ts +12 -1
- package/dist/src/plugins/storage.options.d.ts +16 -0
- package/dist/src/plugins/twake/appAccounts.options.d.ts +13 -0
- package/dist/src/plugins/twake/appAccountsApi.d.ts +18 -0
- package/dist/src/plugins/twake/appAccountsConsistency.d.ts +8 -0
- package/dist/src/plugins/twake/calendar.d.ts +25 -2
- package/dist/src/plugins/twake/calendar.options.d.ts +19 -0
- package/dist/src/plugins/twake/cloudery.options.d.ts +26 -0
- package/dist/src/plugins/twake/clouderyProvision.d.ts +18 -0
- package/dist/src/plugins/twake/cozy.options.d.ts +23 -0
- package/dist/src/plugins/twake/cozyProvision.d.ts +14 -0
- package/dist/src/plugins/twake/drive.d.ts +7 -0
- package/dist/src/plugins/twake/drive.options.d.ts +15 -0
- package/dist/src/plugins/twake/groups.d.ts +29 -1
- package/dist/src/plugins/twake/instances.d.ts +43 -10
- package/dist/src/plugins/twake/james.d.ts +17 -0
- package/dist/src/plugins/twake/james.options.d.ts +21 -0
- package/dist/src/plugins/twake/lifecycleEvents.d.ts +12 -0
- package/dist/src/plugins/twake/migrationProxy.d.ts +11 -11
- package/dist/src/plugins/twake/migrationProxy.options.d.ts +16 -0
- package/dist/src/plugins/twake/spaces.d.ts +30 -0
- package/dist/src/plugins/twake/tombstone.d.ts +16 -14
- package/dist/src/plugins/twake/twakeGroups.options.d.ts +20 -0
- package/dist/src/plugins/twake/twakeInstances.options.d.ts +43 -0
- package/dist/src/plugins/twake/twakeLifecycle.options.d.ts +21 -0
- package/dist/src/plugins/twake/twakeSpaces.options.d.ts +19 -0
- package/dist/src/plugins/twake/twakeTombstone.options.d.ts +17 -0
- package/docs/README.md +1 -0
- package/docs/client-development/api/rest-api.md +9 -3
- package/docs/client-development/schemas/README.md +96 -32
- package/docs/plugin-development/README.md +45 -94
- package/docs/plugin-development/dependencies.md +11 -6
- package/docs/plugin-development/hooks.md +142 -23
- package/docs/plugin-development/openapi.md +14 -1
- package/docs/plugin-development/options.md +109 -0
- package/docs/plugin-development/routes.md +152 -0
- package/docs/plugin-development/testing.md +3 -3
- package/docs/usage/configuration.md +18 -11
- package/docs/usage/operations.md +19 -4
- package/docs/usage/plugins/ldap/enterprise-rules.md +10 -1
- package/docs/usage/plugins/ldap/flat-generic.md +4 -0
- package/docs/usage/plugins/ldap/raw.md +17 -1
- package/docs/usage/upgrading.md +194 -0
- package/package.json +358 -277
- package/static/browser/ldap-browser/LdapBrowser.js +8 -6
- package/static/browser/ldap-browser/LdapBrowser.js.map +1 -1
- package/static/browser/ldap-browser/api/RawApiClient.js +2 -0
- package/static/browser/ldap-browser/api/RawApiClient.js.map +1 -1
- package/static/browser/ldap-browser/components/EntryTree.js +9 -5
- package/static/browser/ldap-browser/components/EntryTree.js.map +1 -1
- package/static/browser/ldap-browser/components/EntryView.js +2 -0
- package/static/browser/ldap-browser/components/EntryView.js.map +1 -1
- package/static/browser/ldap-browser/schema.js +3 -2
- package/static/browser/ldap-browser/schema.js.map +1 -1
- package/static/browser/ldap-browser.esm.js +27 -18
- package/static/browser/ldap-browser.esm.js.map +1 -1
- package/static/browser/ldap-browser.js +27 -18
- package/static/browser/ldap-browser.js.map +1 -1
- package/static/browser/ldap-browser.min.js +1 -1
- package/static/browser/ldap-browser.min.js.map +1 -1
- package/static/browser/ldap-group-editor/LdapGroupEditor.js +8 -6
- package/static/browser/ldap-group-editor/LdapGroupEditor.js.map +1 -1
- package/static/browser/ldap-group-editor/api/GroupApiClient.js +1 -0
- package/static/browser/ldap-group-editor/api/GroupApiClient.js.map +1 -1
- package/static/browser/ldap-group-editor/components/CreateGroupModal.js +5 -1
- package/static/browser/ldap-group-editor/components/CreateGroupModal.js.map +1 -1
- package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js +6 -3
- package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js.map +1 -1
- package/static/browser/ldap-group-editor/components/GroupTree.js +6 -3
- package/static/browser/ldap-group-editor/components/GroupTree.js.map +1 -1
- package/static/browser/ldap-group-editor/components/MoveGroupModal.js +6 -3
- package/static/browser/ldap-group-editor/components/MoveGroupModal.js.map +1 -1
- package/static/browser/ldap-group-editor.esm.js +32 -16
- package/static/browser/ldap-group-editor.esm.js.map +1 -1
- package/static/browser/ldap-group-editor.js +32 -16
- package/static/browser/ldap-group-editor.js.map +1 -1
- package/static/browser/ldap-group-editor.min.js +1 -1
- package/static/browser/ldap-group-editor.min.js.map +1 -1
- package/static/browser/ldap-resource-editor/api/ResourceApiClient.js +3 -0
- package/static/browser/ldap-resource-editor/api/ResourceApiClient.js.map +1 -1
- package/static/browser/ldap-resource-editor/cache/CacheManager.js +3 -1
- package/static/browser/ldap-resource-editor/cache/CacheManager.js.map +1 -1
- package/static/browser/ldap-tree-viewer/LdapTreeViewer.js +6 -1
- package/static/browser/ldap-tree-viewer/LdapTreeViewer.js.map +1 -1
- package/static/browser/ldap-tree-viewer/api/LdapApiClient.js +2 -0
- package/static/browser/ldap-tree-viewer/api/LdapApiClient.js.map +1 -1
- package/static/browser/ldap-tree-viewer/components/TreeNode.js +4 -0
- package/static/browser/ldap-tree-viewer/components/TreeNode.js.map +1 -1
- package/static/browser/ldap-tree-viewer/components/TreeRoot.js +3 -0
- package/static/browser/ldap-tree-viewer/components/TreeRoot.js.map +1 -1
- package/static/browser/ldap-tree-viewer/store/Store.js +3 -1
- package/static/browser/ldap-tree-viewer/store/Store.js.map +1 -1
- package/static/browser/ldap-tree-viewer.esm.js +18 -2
- package/static/browser/ldap-tree-viewer.esm.js.map +1 -1
- package/static/browser/ldap-tree-viewer.js +18 -2
- package/static/browser/ldap-tree-viewer.js.map +1 -1
- package/static/browser/ldap-tree-viewer.min.js +1 -1
- package/static/browser/ldap-tree-viewer.min.js.map +1 -1
- package/static/browser/ldap-unit-editor/LdapUnitEditor.js +7 -5
- package/static/browser/ldap-unit-editor/LdapUnitEditor.js.map +1 -1
- package/static/browser/ldap-unit-editor/api/UnitApiClient.js +1 -0
- package/static/browser/ldap-unit-editor/api/UnitApiClient.js.map +1 -1
- package/static/browser/ldap-unit-editor/components/MoveUnitModal.js +7 -4
- package/static/browser/ldap-unit-editor/components/MoveUnitModal.js.map +1 -1
- package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js +7 -4
- package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js.map +1 -1
- package/static/browser/ldap-unit-editor/components/UnitTree.js +6 -3
- package/static/browser/ldap-unit-editor/components/UnitTree.js.map +1 -1
- package/static/browser/ldap-unit-editor.esm.js +28 -16
- package/static/browser/ldap-unit-editor.esm.js.map +1 -1
- package/static/browser/ldap-unit-editor.js +28 -16
- package/static/browser/ldap-unit-editor.js.map +1 -1
- package/static/browser/ldap-unit-editor.min.js +1 -1
- package/static/browser/ldap-unit-editor.min.js.map +1 -1
- package/static/browser/ldap-user-editor/LdapUserEditor.js +9 -7
- package/static/browser/ldap-user-editor/LdapUserEditor.js.map +1 -1
- package/static/browser/ldap-user-editor/api/UserApiClient.js +2 -0
- package/static/browser/ldap-user-editor/api/UserApiClient.js.map +1 -1
- package/static/browser/ldap-user-editor/cache/CacheManager.js +3 -1
- package/static/browser/ldap-user-editor/cache/CacheManager.js.map +1 -1
- package/static/browser/ldap-user-editor/components/MoveUserModal.js +7 -4
- package/static/browser/ldap-user-editor/components/MoveUserModal.js.map +1 -1
- package/static/browser/ldap-user-editor/components/PointerField.js +6 -1
- package/static/browser/ldap-user-editor/components/PointerField.js.map +1 -1
- package/static/browser/ldap-user-editor/components/UserEditor.js +10 -5
- package/static/browser/ldap-user-editor/components/UserEditor.js.map +1 -1
- package/static/browser/ldap-user-editor/components/UserList.js +8 -4
- package/static/browser/ldap-user-editor/components/UserList.js.map +1 -1
- package/static/browser/ldap-user-editor/components/UserTree.js +7 -3
- package/static/browser/ldap-user-editor/components/UserTree.js.map +1 -1
- package/static/browser/ldap-user-editor.esm.js +55 -30
- package/static/browser/ldap-user-editor.esm.js.map +1 -1
- package/static/browser/ldap-user-editor.js +55 -30
- package/static/browser/ldap-user-editor.js.map +1 -1
- package/static/browser/ldap-user-editor.min.js +1 -1
- package/static/browser/ldap-user-editor.min.js.map +1 -1
- package/static/browser/shared/components/DisposableComponent.js +3 -5
- package/static/browser/shared/components/DisposableComponent.js.map +1 -1
- package/static/browser/shared/components/Modal.js +2 -1
- package/static/browser/shared/components/Modal.js.map +1 -1
- package/static/browser/shared/components/StatusMessage.js +3 -2
- package/static/browser/shared/components/StatusMessage.js.map +1 -1
- package/static/browser/shared/utils/form.js.map +1 -1
- package/static/browser/shared/utils/hmac.js +1 -0
- package/static/browser/shared/utils/hmac.js.map +1 -1
- package/static/browser/shared/utils/totp.js +1 -0
- package/static/browser/shared/utils/totp.js.map +1 -1
- package/static/browser/shared.esm.js +9 -8
- package/static/browser/shared.esm.js.map +1 -1
- package/static/browser/shared.js +9 -8
- package/static/browser/shared.js.map +1 -1
- package/static/browser/shared.min.js +1 -1
- package/static/browser/shared.min.js.map +1 -1
- package/static/schemas/ad/groups.json +1 -1
- package/static/schemas/ad/users.json +3 -3
- package/static/schemas/example/organizations.json +2 -2
- package/static/schemas/example/users.json +2 -2
- package/static/schemas/obm/users.json +2 -2
- package/static/schemas/standard/groups.json +1 -1
- package/static/schemas/standard/users.json +2 -2
- package/static/schemas/twake/groups.json +4 -4
- package/static/schemas/twake/teamMailboxes.json +3 -3
- package/static/schemas/twake/users.json +7 -7
package/bin/sync-james.mjs
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* @author Generated with Claude Code
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import fetch from 'node-fetch';
|
|
10
9
|
import { DM } from '../dist/bin/index.js';
|
|
11
10
|
|
|
12
11
|
// Parse command line arguments
|
|
@@ -59,6 +58,18 @@ function normalizeAlias(alias) {
|
|
|
59
58
|
return alias;
|
|
60
59
|
}
|
|
61
60
|
|
|
61
|
+
/**
|
|
62
|
+
* What to log about a failure: a refused connection says `fetch failed`,
|
|
63
|
+
* and the host and port are in its cause.
|
|
64
|
+
* @param {unknown} err
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
function errorMessage(err) {
|
|
68
|
+
if (!(err instanceof Error)) return String(err);
|
|
69
|
+
const cause = /** @type {{ message?: string }} */ (err.cause);
|
|
70
|
+
return cause?.message ? `${err.message}: ${cause.message}` : err.message;
|
|
71
|
+
}
|
|
72
|
+
|
|
62
73
|
/**
|
|
63
74
|
* Extract normalized aliases from an LDAP attribute value.
|
|
64
75
|
* @param {string|string[]|Buffer|Buffer[]|undefined} value
|
|
@@ -335,7 +346,7 @@ async function syncJames() {
|
|
|
335
346
|
// are removed even when the LDAP attribute has been cleared.
|
|
336
347
|
await syncAliases(dn, mail, entry[aliasAttr]);
|
|
337
348
|
} catch (err) {
|
|
338
|
-
dm.logger.error(`Error processing ${mail}: ${err
|
|
349
|
+
dm.logger.error(`Error processing ${mail}: ${errorMessage(err)}`);
|
|
339
350
|
stats.errors++;
|
|
340
351
|
}
|
|
341
352
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{assertModifyRequest as t}from"../lib/ldapActions.js";import{wantJson as e,jsonBody as i,created as s,tryMethod as r,multiStatus as a}from"../lib/expressFormatedResponses.js";import{getCompiledRegex as n,escapeDnValue as o,asyncHandler as h,escapeRegex as d,validateDnValue as c,isDummyMemberDn as u,normalizeDn as l,getParentDn as m,launchHooksChained as f,launchHooks as p,escapeLdapFilter as b,substringSearchFilter as g}from"../lib/utils.js";import{neverReturnAttributes as w,assertClientMaySet as y,modifiedAttributeNames as $,toDirectoryEntry as A}from"../config/schema.js";import{applyFixedDefaults as v,EntityValidator as E}from"../lib/entitySchema/validator.js";import{ConflictError as N,BadRequestError as j,NotFoundError as x,HttpError as k,UnprocessableEntityError as C}from"../lib/errors.js";import{extractLdapCode as D}from"../lib/ldapCodes.js";import{loadSchemaFile as P}from"../lib/schemaFile.js";import q from"./plugin.js";class LdapFlat extends q{constructor(t,e){if(super(t),this.ldap=t.ldap,this.base=e.base,this.mainAttribute=e.mainAttribute,this.objectClass=e.objectClass,this.defaultAttributes=e.defaultAttributes||{},this.singularName=e.singularName,this.pluralName=e.pluralName,this.hookPrefix=e.hookPrefix,!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(this.ldap.declareObjectClasses(this.base,this.objectClass),e.schemaPath)try{this.schema=P(e.schemaPath,{config:this.config}),this.ldap.knowAttributeNames(Object.keys(this.schema.attributes)),this.logger.info(`${this.singularName} schema loaded from ${e.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${e.schemaPath}: ${t}`)}}hiddenAttributes(){return w([this.schema])}entryAttributes(){return["*",...Object.keys(this.schema?.attributes??{})]}project(t){const e=this.hiddenAttributes();if(0===e.size)return t;const i={};for(const[s,r]of Object.entries(t))e.has(s.split(";")[0].toLowerCase())||(i[s]=r);return i}projectList(t){if(0===this.hiddenAttributes().size)return t;const e={};for(const[i,s]of Object.entries(t))e[i]=this.project(s);return e}rejectForbiddenInput(t){y(this.schema,t,this.mainAttribute)}async generateMainAttribute(t){const e=this.deriveMainAttribute(t),i=this.schema?.attributes[this.mainAttribute]?.generatedFrom;if(void 0!==e&&i){if(!await this.rdnExists(e))return e;if("suffix"!==i.onCollision)throw new N(`${this.singularName} "${e}" already exists (generated from ${i.attribute})`);for(let t=2;t<1e3;t++){const i=`${e}-${t}`;if(!await this.rdnExists(i))return i}throw new N(`Cannot generate a free "${this.mainAttribute}" from "${i.attribute}"`)}}deriveMainAttribute(t){const e=this.schema?.attributes[this.mainAttribute],i=e?.generatedFrom;if(!i||!e.generated)return;const s=t[i.attribute],r=Array.isArray(s)?s[0]:s;if(null==r||""===r)throw new j(`Attribute "${i.attribute}" is required to generate "${this.mainAttribute}"`);let a=String(r);if(i.extract){const t=n(i.extract).exec(a);t&&(a=void 0!==t[1]?t[1]:t[0])}if(i.lowercase&&(a=a.toLowerCase()),i.strip&&(a=a.replace(n(i.strip,"g"),"")),!a)throw new j(`Cannot generate "${this.mainAttribute}" from "${i.attribute}"`);return a}candidateEntry(t,e){const i={...this.defaultAttributes,[this.mainAttribute]:t,...e,objectClass:this.objectClass};return v(this.schema,i),i}async rdnExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base"},`${this.mainAttribute}=${o(t)},${this.base}`);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}validator(){return new E(this.schema,{entryExists:async t=>{try{return(await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}},loadedPlugins:this.server.loadedPlugins,singularName:this.singularName})}api(t){t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,h(async(t,i)=>{if(!e(t,i))return;const s={};let r,a;if(t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute&&(s.filter=g(t.query.match,t.query.attribute)),t.query.attributes&&"string"==typeof t.query.attributes&&(s.attributes=t.query.attributes.split(",")),void 0!==t.query.limit){const e=t.query.limit;if(r="string"==typeof e&&/^\d+$/.test(e)?+e:NaN,!Number.isSafeInteger(r)||r<1)throw new j('Query parameter "limit" must be a positive integer')}try{a=await this.listEntriesLimited({...s,limit:r,req:t})}catch(t){if(void 0!==r||4!==D(t))throw t;throw new C("Too many entries to list at once: the directory's size limit was exceeded. Narrow the search, or pass `limit`.")}a.truncated&&i.set("X-Result-Truncated","true"),i.json(this.projectList(a.entries))})),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,h(async(t,e)=>this.apiGet(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,h(async(t,e)=>this.apiAdd(t,e))),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,h(async(t,e)=>this.apiDelete(t,e))),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,h(async(t,e)=>this.apiModify(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,h(async(t,e)=>this.apiMove(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/rename`,h(async(t,e)=>this.apiRename(t,e)))}async apiGet(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);try{const e=this.resolveDn(s),r=await this.ldap.search({paged:!1,scope:"base",attributes:this.entryAttributes()},e,t);if(0===r.searchEntries.length)throw new x(`${this.singularName} not found`);i.json(this.project(r.searchEntries[0]))}catch(t){if(t.code&&32===t.code)throw new x(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const r=this.schema?.attributes[this.mainAttribute],a=Boolean(r?.generated&&r.generatedFrom),n=a?i(t,e):i(t,e,this.mainAttribute);if(!n)return;this.rejectForbiddenInput(Object.keys(n));const o={...n};delete o[this.mainAttribute],delete o.dn;const h=a?this.deriveMainAttribute(n):void 0;void 0!==h&&await this.ldap.authorize("ldapaddrequest",[this.resolveDn(h),this.candidateEntry(h,o),t]);const d=a?await this.generateMainAttribute(n):n[this.mainAttribute];await this.addEntry(d,o,t);const c=await this.ldap.system.search({paged:!1,scope:"base",attributes:[this.mainAttribute]},this.resolveDn(d));return s(e,this.project(c.searchEntries[0]))}async apiDelete(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);await r(i,this.deleteEntry.bind(this),s,t)}async apiModify(e,s){const a=i(e,s);if(!a)return;t(a),this.rejectForbiddenInput($(a));const n=decodeURIComponent(e.params.id);await r(s,this.modifyEntry.bind(this),n,a,e)}async apiMove(t,s){if(!e(t,s))return;const r=i(t,s,"targetOrgDn");if(!r)return;const a=decodeURIComponent(t.params.id),{targetOrgDn:n}=r;if(!n||"string"!=typeof n)throw new j("Missing or invalid targetOrgDn in request body");const o=await this.moveEntry(a,n,t);s.json({success:!0,...o})}async apiRename(t,s){if(!e(t,s))return;const r=i(t,s,"newId");if(!r)return;const n=r.newId;if("string"!=typeof n)throw new j("Missing or invalid newId in request body");if(this.rejectForbiddenInput(Object.keys(r).filter(t=>"newId"!==t)),new RegExp(`^${d(this.mainAttribute)}=`,"i").test(n))throw new j(`newId must be a ${this.mainAttribute} value, not a DN`);try{c(n,this.mainAttribute)}catch(t){throw new j(t instanceof Error?t.message:"Invalid newId")}await this.validator().validateValue(this.mainAttribute,n);const o=decodeURIComponent(t.params.id),h=this.resolveDn(o),m=this.resolveDn(n),f=this.config.group_dummy_user;if(f&&(u(h,f)||u(m,f)))throw new N(`${f} is the configured placeholder member and cannot be renamed`);await this.ldap.authorize("ldaprenamerequest",[h,m,t]);const p=l(h)===l(m),b=await this.entryExists(h),g=p?b:await this.entryExists(m),w=this.requirePlugin("ldapEnterpriseRules");let y=[];if(p){if(!b)throw new x(`${this.singularName} not found`)}else if(b){if(g)throw new N(`${this.singularName} "${n}" already exists`);y=await(w?.findReferences(h))||[],await this.renameEntry(h,m,t)}else{if(!g)throw new x(`${this.singularName} not found`);y=await(w?.findReferences(h))||[]}const $=w&&y.length>0?await w.rewriteReferences(y,h,m,t):{updated:0,failed:[]},A={success:0===$.failed.length,dn:m,referencesUpdated:$.updated};0!==$.failed.length?a(s,{...A,referencesFailed:$.failed}):s.json(A)}async entryExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}resolveDn(t){if(new RegExp(`^${d(this.mainAttribute)}=`,"i").test(t)){const e=m(t),i=e!==t;if(!i||e.toLowerCase()!==this.base.toLowerCase())throw new j(`DN must be a direct child of "${this.base}". Provided DN "${t}" has parent "${i?e:"<none>"}"`);return t}return`${this.mainAttribute}=${o(t)},${this.base}`}async addEntry(t,e={},i){let s;await this.ldap.canonicalAttributes(e),new RegExp(`^${this.mainAttribute}=`,"i").test(t)?(s=this.resolveDn(t),t=t.replace(new RegExp(`^${this.mainAttribute}=((?:\\\\.|[^,])+)(?:,.*)?$`,"i"),"$1")):(c(t,this.mainAttribute),s=this.resolveDn(t));let r=this.candidateEntry(t,e);await this.ldap.authorize("ldapaddrequest",[s,{...r},i]);const a={[this.mainAttribute]:t,...e,objectClass:this.objectClass};await this.validateNewEntry(s,a),Object.assign(r,a),A(this.schema,r),this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[s,r]=await f(this.registeredHooks[`${this.hookPrefix}add`],[s,r]),this.logger.debug("Adding LDAP entry:",{dn:s,entry:JSON.stringify(r,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(r))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);let n;try{n=await this.ldap.add(s,r,i)}catch(t){const e=t instanceof k&&t.statusCode<500;if(this.logger[e?"debug":"error"]("LDAP add failed:",{dn:s,entry:JSON.stringify(r,null,2),error:t}),t instanceof k)throw t;throw new Error(`Failed to add ${this.singularName} ${s}: ${t}`)}return p(this.registeredHooks[`${this.hookPrefix}adddone`],[s,r]),n}async modifyEntry(t,e,i){await this.ldap.canonicalChanges(e);let s=this.resolveDn(t);const r=this.opNumber();if([s,e]=await f(this.registeredHooks[`${this.hookPrefix}modify`],[s,e,r]),e.add&&e.add[this.mainAttribute])throw new N(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new j(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new j(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new j(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.ldap.authorize("ldapmodifyrequest",[s,e,r,i]),await this.validateChanges(s,e);const a=await this.ldap.modify(s,e,i);return p(this.registeredHooks[`${this.hookPrefix}modifydone`],[s,e,r]),a}async renameEntry(t,e,i){/,/.test(t)||c(t,this.mainAttribute),/,/.test(e)||c(e,this.mainAttribute);let s=this.resolveDn(t),r=this.resolveDn(e);[s,r]=await f(this.registeredHooks[`${this.hookPrefix}rename`],[s,r]);const a=await this.ldap.rename(s,r,i);return p(this.registeredHooks[`${this.hookPrefix}renamedone`],[s,r]),a}async deleteEntry(t,e){let i=this.resolveDn(t);i=await f(this.registeredHooks[`${this.hookPrefix}delete`],i);const s=await this.ldap.delete(i,e);return p(this.registeredHooks[`${this.hookPrefix}deletedone`],i),s}async moveEntry(t,e,i){const s=this.resolveDn(t),r=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",a=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[r]||!this.schema?.attributes[a])throw new j(`Schema for ${this.singularName} does not support move operation (missing ${r} or ${a})`);let n;try{n=await this.ldap.search({paged:!1,scope:"base",attributes:[r]},s,i)}catch(t){if(32===D(t))throw new x(`Entry ${s} not found`);throw t}if(!n.searchEntries||0===n.searchEntries.length)throw new x(`Entry ${s} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await f(this.registeredHooks[`${this.hookPrefix}move`],[s,e,i]);const h={replace:{[r]:e,[a]:o}};return await this.modifyEntry(t,h,i),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const s=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!s.searchEntries||0===s.searchEntries.length)throw new x(`Organization ${t} not found`);const r=s.searchEntries[0];if(r[i]){const t=r[i];return Array.isArray(t)?String(t[0]):String(t)}const a=r.ou||r.o;if(a){return`/${Array.isArray(a)?String(a[0]):String(a)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){if(e instanceof x)throw e;const i=D(e);if(32===i)throw new x(`Organization ${t} not found`);if(34===i)throw new j(`${t} is not a valid DN`);throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries(t){return(await this.listEntriesLimited(t)).entries}async listEntriesLimited({filter:t,attributes:e,req:i,limit:s}){const r={paged:void 0===s||{pageSize:Math.min(s+2,100)},filter:t||"(objectClass=*)"};if(e&&e.length>0){const t=this.mainAttribute.toLowerCase();r.attributes=e.some(e=>e.toLowerCase()===t)?e:[...e,this.mainAttribute]}const a=function(t,e){const i={},s=new Set;return{entries:i,add(r){for(const a of r){const r=a[t],n=Array.isArray(r)?r[0]:r;if(null==n||""===n)continue;const o=String(n);if(!s.has(o)){if(void 0!==e&&s.size>=e)return!1;s.add(o)}i[o]=a}return!0}}}(this.mainAttribute,s);try{const t=await this.ldap.search(r,this.base,i);for await(const e of t)if(!a.add(e.searchEntries))return{entries:a.entries,truncated:!0};return{entries:a.entries,truncated:!1}}catch(t){if(void 0===s||4!==D(t))throw t}try{const t=await this.ldap.search({...r,paged:!1,sizeLimit:Math.min(s+1,2147483647)},this.base,i);a.add(t.searchEntries)}catch(t){if(4!==D(t))throw t}return{entries:a.entries,truncated:!0}}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const s=b(t),r=e?`(${this.mainAttribute}=*${s}*)`:`(${this.mainAttribute}=${s})`;return await this.listEntries({filter:r,attributes:i})}async validateNewEntry(t,e){return await this.validator().validateNew(e),!0}async validateChanges(t,e){return await this.validator().validateChanges(e),!0}}export{LdapFlat as default};
|
|
1
|
+
import{Type as t}from"@sinclair/typebox";import{assertModifyRequest as e}from"../lib/ldapActions.js";import{jsonBody as i,created as a,tryMethod as s,multiStatus as r,WriteResponse as n}from"../lib/expressFormatedResponses.js";import{getCompiledRegex as o,escapeDnValue as h,escapeRegex as d,validateDnValue as c,isDummyMemberDn as l,normalizeDn as u,namesByDn as m,getParentDn as p,assertedRdnValue as b,escapeLdapFilter as f,substringSearchFilter as w}from"../lib/utils.js";import{neverReturnAttributes as g,assertClientMaySet as y,modifiedAttributeNames as A,toDirectoryEntry as $}from"../config/schema.js";import{applyFixedDefaults as v,EntityValidator as E}from"../lib/entitySchema/validator.js";import{ConflictError as j,BadRequestError as N,NotFoundError as x,HttpError as C,isAuthzRefusal as D,UnprocessableEntityError as k}from"../lib/errors.js";import{extractLdapCode as O}from"../lib/ldapCodes.js";import{loadSchemaFile as S}from"../lib/schemaFile.js";import L from"./plugin.js";import{defineRoute as P}from"../lib/route.js";const _=t.Object({match:t.Optional(t.String()),attribute:t.Optional(t.String()),attributes:t.Optional(t.String()),limit:t.Optional(t.Integer({minimum:1,maximum:Number.MAX_SAFE_INTEGER}))}),I=t.Object({id:t.String({minLength:1})}),q=t.Object({targetOrgDn:t.String({minLength:1})}),z=t.Object({success:t.Boolean(),departmentPath:t.String(),departmentLink:t.String()}),F=t.Object({newId:t.String({minLength:1})});class LdapFlat extends L{base;ldap;mainAttribute;objectClass;defaultAttributes;schema;singularName;pluralName;hookPrefix;ownHooks;constructor(t,e){if(super(t),this.ldap=t.ldap,this.base=e.base,this.mainAttribute=e.mainAttribute,this.objectClass=e.objectClass,this.defaultAttributes=e.defaultAttributes||{},this.singularName=e.singularName,this.pluralName=e.pluralName,this.hookPrefix=e.hookPrefix,this.ownHooks=this.hookBus.prefixed(this.hookPrefix),!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(this.ldap.declareObjectClasses(this.base,this.objectClass),e.schemaPath)try{this.schema=S(e.schemaPath,{config:this.config}),this.ldap.knowAttributeNames(Object.keys(this.schema.attributes)),this.logger.info(`${this.singularName} schema loaded from ${e.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${e.schemaPath}: ${t}`)}}hiddenAttributes(){return g([this.schema])}entryAttributes(){return["*",...Object.keys(this.schema?.attributes??{})]}project(t){const e=this.hiddenAttributes();if(0===e.size)return t;const i={};for(const[a,s]of Object.entries(t))e.has(a.split(";")[0].toLowerCase())||(i[a]=s);return i}projectList(t){if(0===this.hiddenAttributes().size)return t;const e={};for(const[i,a]of Object.entries(t))e[i]=this.project(a);return e}rejectForbiddenInput(t){y(this.schema,t,this.mainAttribute)}async generateMainAttribute(t){const e=this.deriveMainAttribute(t),i=this.schema?.attributes[this.mainAttribute]?.generatedFrom;if(void 0!==e&&i){if(!await this.rdnExists(e))return e;if("suffix"!==i.onCollision)throw new j(`${this.singularName} "${e}" already exists (generated from ${i.attribute})`);for(let t=2;t<1e3;t++){const i=`${e}-${t}`;if(!await this.rdnExists(i))return i}throw new j(`Cannot generate a free "${this.mainAttribute}" from "${i.attribute}"`)}}deriveMainAttribute(t){const e=this.schema?.attributes[this.mainAttribute],i=e?.generatedFrom;if(!i||!e.generated)return;const a=t[i.attribute],s=Array.isArray(a)?a[0]:a;if(null==s||""===s)throw new N(`Attribute "${i.attribute}" is required to generate "${this.mainAttribute}"`);let r=String(s);if(i.extract){const t=o(i.extract).exec(r);t&&(r=void 0!==t[1]?t[1]:t[0])}if(i.lowercase&&(r=r.toLowerCase()),i.strip&&(r=r.replace(o(i.strip,"g"),"")),!r)throw new N(`Cannot generate "${this.mainAttribute}" from "${i.attribute}"`);return r}candidateEntry(t,e){const i={...this.defaultAttributes,[this.mainAttribute]:t,...e,objectClass:this.objectClass};return v(this.schema,i),i}async rdnExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base"},`${this.mainAttribute}=${h(t)},${this.base}`);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}validator(){return new E(this.schema,{entryExists:async t=>{try{return(await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}},loadedPlugins:this.server.loadedPlugins,singularName:this.singularName})}api(t){P(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,query:_,handler:async({req:t,res:e,query:i})=>{const a={};i.match&&i.attribute&&(a.filter=w(i.match,i.attribute)),i.attributes&&(a.attributes=i.attributes.split(","));const s=i.limit;let r;try{r=await this.listEntriesLimited({...a,limit:s,req:t})}catch(t){if(void 0!==s||4!==O(t))throw t;throw new k("Too many entries to list at once: the directory's size limit was exceeded. Narrow the search, or pass `limit`.")}r.truncated&&e.set("X-Result-Truncated","true"),e.json(this.projectList(r.entries))}}),P(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,params:I,handler:async({req:t,res:e})=>this.apiGet(t,e)}),P(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,handler:async({req:t,res:e})=>this.apiAdd(t,e)}),P(t,{method:"delete",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,params:I,responses:{200:n},handler:async({req:t,res:e})=>this.apiDelete(t,e)}),P(t,{method:"put",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,params:I,responses:{200:n},handler:async({req:t,res:e})=>this.apiModify(t,e)}),P(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,params:I,body:q,responses:{200:z},handler:async({req:t,res:e})=>this.apiMove(t,e)}),P(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/rename`,params:I,body:F,handler:async({req:t,res:e})=>this.apiRename(t,e)})}async apiGet(t,e){const i=decodeURIComponent(t.params.id);try{const a=this.resolveDn(i),s=await this.ldap.search({paged:!1,scope:"base",attributes:this.entryAttributes()},a,t);if(0===s.searchEntries.length)throw new x(`${this.singularName} not found`);e.json(this.project(s.searchEntries[0]))}catch(t){if(t.code&&32===t.code)throw new x(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const s=this.schema?.attributes[this.mainAttribute],r=Boolean(s?.generated&&s.generatedFrom),n=r?i(t,e):i(t,e,this.mainAttribute);if(!n)return;this.rejectForbiddenInput(Object.keys(n));const o={...n};delete o[this.mainAttribute],delete o.dn;const h=r?this.deriveMainAttribute(n):void 0;void 0!==h&&await this.ldap.authorize("ldapaddrequest",[this.resolveDn(h),this.candidateEntry(h,o),t]);const d=r?await this.generateMainAttribute(n):n[this.mainAttribute];await this.addEntry(d,o,t);const c=await this.ldap.system.search({paged:!1,scope:"base",attributes:[this.mainAttribute]},this.resolveDn(d));return a(e,this.project(c.searchEntries[0]))}async apiDelete(t,e){const i=decodeURIComponent(t.params.id);await s(e,this.deleteEntry.bind(this),i,t)}async apiModify(t,a){const r=i(t,a);if(!r)return;e(r),this.rejectForbiddenInput(A(r));const n=decodeURIComponent(t.params.id);await s(a,this.modifyEntry.bind(this),n,r,t)}async apiMove(t,e){const i=t.body,a=decodeURIComponent(t.params.id),{targetOrgDn:s}=i,r=await this.moveEntry(a,s,t);e.json({success:!0,...r})}async apiRename(t,e){const i=t.body,a=i.newId;if(this.rejectForbiddenInput(Object.keys(i).filter(t=>"newId"!==t)),new RegExp(`^${d(this.mainAttribute)}=`,"i").test(a))throw new N(`newId must be a ${this.mainAttribute} value, not a DN`);try{c(a,this.mainAttribute)}catch(t){throw new N(t instanceof Error?t.message:"Invalid newId")}await this.validator().validateValue(this.mainAttribute,a);const s=decodeURIComponent(t.params.id),n=this.resolveDn(s),o=this.resolveDn(a),h=this.config.group_dummy_user;if(h&&(l(n,h)||l(o,h)))throw new j(`${h} is the configured placeholder member and cannot be renamed`);await this.ldap.authorize("ldaprenamerequest",[n,o,t]);const m=u(n)===u(o),p=await this.entryExists(n),b=m?p:await this.entryExists(o),f=this.requirePlugin("ldapEnterpriseRules");let w=[];if(m){if(!p)throw new x(`${this.singularName} not found`)}else if(p){if(b)throw new j(`${this.singularName} "${a}" already exists`);w=await(f?.findReferences(n))||[],await this.renameEntry(n,o,t)}else{if(!b)throw new x(`${this.singularName} not found`);w=await(f?.findReferences(n))||[]}const g=f&&w.length>0?await f.rewriteReferences(w,n,o,t):{updated:0,failed:[]},y={success:0===g.failed.length,dn:o,referencesUpdated:g.updated};0!==g.failed.length?r(e,{...y,referencesFailed:g.failed}):e.json(y)}async entryExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}resolveDn(t){if(m(t,this.mainAttribute)){const e=p(t),i=e!==t;if(!i||e.toLowerCase()!==this.base.toLowerCase())throw new N(`DN must be a direct child of "${this.base}". Provided DN "${t}" has parent "${i?e:"<none>"}"`);return t}return`${this.mainAttribute}=${h(t)},${this.base}`}async addEntry(t,e={},i){let a;if(await this.ldap.canonicalAttributes(e),m(t,this.mainAttribute)){a=this.resolveDn(t);const e=b(t,this.mainAttribute,this.base);if(null===e)throw new N(`${this.mainAttribute} must be a value, or a "${this.mainAttribute}=…,${this.base}" DN`);t=e}else a=this.resolveDn(t);try{c(t,this.mainAttribute)}catch(t){throw new N(t instanceof Error?t.message:"Invalid identifier")}let s=this.candidateEntry(t,e);await this.ldap.authorize("ldapaddrequest",[a,{...s},i]);const r=await this.validateNewEntry(a,{[this.mainAttribute]:t,...e,objectClass:this.objectClass});s={...this.candidateEntry(t,{}),...r},$(this.schema,s),this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[a,s]=await this.ownHooks.chain("add",[a,s]),this.logger.debug("Adding LDAP entry:",{dn:a,entry:JSON.stringify(s,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(s))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);let n;try{n=await this.ldap.add(a,s,i)}catch(t){const e=t instanceof C&&t.statusCode<500;if(this.logger[e?"debug":"error"]("LDAP add failed:",{dn:a,entry:JSON.stringify(s,null,2),error:t}),t instanceof C||D(t))throw t;throw new Error(`Failed to add ${this.singularName} ${a}: ${String(t)}`,{cause:t})}return this.ownHooks.notify("adddone",a,s),n}async modifyEntry(t,e,i){await this.ldap.canonicalChanges(e);let a=this.resolveDn(t);const s=this.opNumber();if([a,e]=await this.ownHooks.chain("modify",[a,e,s]),e.add&&e.add[this.mainAttribute])throw new j(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new N(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new N(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new N(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.ldap.authorize("ldapmodifyrequest",[a,e,s,i]),await this.validateChanges(a,e);const r=await this.ldap.modify(a,e,i);return this.ownHooks.notify("modifydone",a,e,s),r}async renameEntry(t,e,i){/,/.test(t)||c(t,this.mainAttribute),/,/.test(e)||c(e,this.mainAttribute);let a=this.resolveDn(t),s=this.resolveDn(e);[a,s]=await this.ownHooks.chain("rename",[a,s]);const r=await this.ldap.rename(a,s,i);return this.ownHooks.notify("renamedone",a,s),r}async deleteEntry(t,e){let i=this.resolveDn(t);i=await this.ownHooks.chain("delete",i);const a=await this.ldap.delete(i,e);return this.ownHooks.notify("deletedone",i),a}async moveEntry(t,e,i){const a=this.resolveDn(t),s=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",r=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[s]||!this.schema?.attributes[r])throw new N(`Schema for ${this.singularName} does not support move operation (missing ${s} or ${r})`);let n;try{n=await this.ldap.search({paged:!1,scope:"base",attributes:[s]},a,i)}catch(t){if(32===O(t))throw new x(`Entry ${a} not found`);throw t}if(!n.searchEntries||0===n.searchEntries.length)throw new x(`Entry ${a} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await this.ownHooks.chain("move",[a,e,i]);const h={replace:{[s]:e,[r]:o}};return await this.modifyEntry(t,h,i),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const a=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!a.searchEntries||0===a.searchEntries.length)throw new x(`Organization ${t} not found`);const s=a.searchEntries[0];if(s[i]){const t=s[i];return Array.isArray(t)?String(t[0]):String(t)}const r=s.ou||s.o;if(r){return`/${Array.isArray(r)?String(r[0]):String(r)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){if(e instanceof x)throw e;const i=O(e);if(32===i)throw new x(`Organization ${t} not found`);if(34===i)throw new N(`${t} is not a valid DN`);throw new Error(`Failed to fetch organization ${t}: ${String(e)}`,{cause:e})}}async listEntries(t){return(await this.listEntriesLimited(t)).entries}async listEntriesLimited({filter:t,attributes:e,req:i,limit:a}){const s={paged:void 0===a||{pageSize:Math.min(a+2,100)},filter:t||"(objectClass=*)"};if(e&&e.length>0){const t=this.mainAttribute.toLowerCase();s.attributes=e.some(e=>e.toLowerCase()===t)?e:[...e,this.mainAttribute]}const r=function(t,e){const i={},a=new Set;return{entries:i,add(s){for(const r of s){const s=r[t],n=Array.isArray(s)?s[0]:s;if(null==n||""===n)continue;const o=String(n);if(!a.has(o)){if(void 0!==e&&a.size>=e)return!1;a.add(o)}i[o]=r}return!0}}}(this.mainAttribute,a);try{const t=await this.ldap.search(s,this.base,i);for await(const e of t)if(!r.add(e.searchEntries))return{entries:r.entries,truncated:!0};return{entries:r.entries,truncated:!1}}catch(t){if(void 0===a||4!==O(t))throw t}try{const t=await this.ldap.search({...s,paged:!1,sizeLimit:Math.min(a+1,2147483647)},this.base,i);r.add(t.searchEntries)}catch(t){if(4!==O(t))throw t}return{entries:r.entries,truncated:!0}}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const a=f(t),s=e?`(${this.mainAttribute}=*${a}*)`:`(${this.mainAttribute}=${a})`;return await this.listEntries({filter:s,attributes:i})}async validateNewEntry(t,e){return await this.validator().validateNew(e)}async validateChanges(t,e){return await this.validator().validateChanges(e),!0}}export{I as FlatEntryParams,_ as FlatListQuery,q as FlatMoveBody,z as FlatMoveResponse,F as FlatRenameBody,LdapFlat as default};
|
package/dist/abstract/plugin.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import{timedOutbound as e}from"../lib/metrics.js";import{neverReturnAttributes as r,withoutAttributes as o}from"../config/schema.js";import{readOptions as s}from"../config/options.js";import i from"../config/args.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,unescapeDnValue,validateDnValue}from"../lib/utils.js";export{defineRoute,registeredRoutes}from"../lib/route.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";const t=new Set(i.map(e=>e[0].replace(/^--?/,"").replace(/-/g,"_")));class DmPlugin{static optionDefinitions={};server;config;hookBus;logger;registeredHooks={};hooks;dependencies;roles;followedOperationalAttributes;configApiKey;fetch(r,o){return e(this.name,()=>fetch(r,o))}constructor(e){this.server=e,this.config=e.config,this.hookBus=e.hookBus,this.logger=e.logger,this.registeredHooks=e.hooks,this.options(this.constructor.optionDefinitions)}options(e){const r=this.constructor.optionDefinitions;if(e!==r)throw new Error(`${this.constructor.name}: options() takes the definitions the plugin declares`);return s(r,this.config,t)}opNumber(){return this.server.operationSequence++}hideNeverReturn(e){const s=[],isSchema=e=>"object"==typeof e?.attributes;for(const e of Object.values(this.server.loadedPlugins)){const r=e;if(isSchema(r.schema)&&s.push(r.schema),Array.isArray(r.instances))for(const e of r.instances)isSchema(e.schema)&&s.push(e.schema)}const i=r(s);if(0===i.size||!e||"object"!=typeof e)return e;const project=e=>e&&"object"==typeof e&&!Array.isArray(e)?o(e,i):e;return Array.isArray(e)?e.map(project):"dn"in e?o(e,i):Object.fromEntries(Object.entries(e).map(([e,r])=>[e,project(r)]))}missingPluginsWarned;requirePlugin(e){const r=this.server.loadedPlugins[e];return r||(this.missingPluginsWarned||(this.missingPluginsWarned=new Set),this.missingPluginsWarned.has(e)||(this.missingPluginsWarned.add(e),this.logger.warn(`${this.name}: required plugin '${e}' is not loaded — its features will be skipped`)),null)}}export{DmPlugin as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"p-limit";import{PROBE_TIMEOUT_MS as e}from"../lib/health.js";import{escapeLdapFilter as r}from"../lib/utils.js";import s from"./plugin.js";class TwakePlugin extends s{constructor(e,r
|
|
1
|
+
import t from"p-limit";import{PROBE_TIMEOUT_MS as e}from"../lib/health.js";import{escapeLdapFilter as r}from"../lib/utils.js";import s from"./plugin.js";class TwakePlugin extends s{webadminUrl;webadminToken;requestLimit;retryAttempts=1;retryDelay=0;mailAttr;displayNameAttr;constructor(e,r){super(e);let s=r.url;for(;s.endsWith("/");)s=s.slice(0,-1);if(this.webadminUrl=s,this.webadminToken=r.token,!this.webadminUrl)throw new Error(`Twake plugin: ${r.urlOption} is required`);this.webadminToken||this.logger.warn(`Twake plugin: No authentication token configured (${r.tokenOption}) - requests will be unauthenticated`);const i=r.concurrency;if(!Number.isInteger(i)||i<1)throw new Error(`Twake plugin: ${r.concurrencyOption} must be a positive integer`);this.requestLimit=t(i),this.logger.info(`Twake plugin HTTP request concurrency limit set to ${i}`),this.mailAttr=this.config.mail_attribute||"mail",this.displayNameAttr=this.config.display_name_attribute||"displayName"}createHeaders(t){const e={};return this.webadminToken&&(e.Authorization=`Bearer ${this.webadminToken}`),t&&(e["Content-Type"]=t),e}async callWebAdminApi(t,e,r,s,i,a,n=[]){const o={plugin:this.name,event:t,result:"error",dn:s,...a},u={method:r,headers:this.createHeaders(i?"application/json":void 0)};i&&(u.body=i);for(let r=1;;r++){try{const s=await this.requestLimit(()=>this.fetch(e,u));if(s.ok)return this.logger.info({...o,result:"success",http_status:s.status,url:e}),!0;if(this.shouldRetry(s.status,n,r)){this.logger.warn({...o,result:"retry",attempt:r,http_status:s.status,http_status_text:s.statusText,url:e}),await this.waitBeforeRetry(r);continue}if(this.shouldIgnoreError(s.status,t))return this.logger.debug({...o,result:"ignored",http_status:s.status,http_status_text:s.statusText,url:e}),!0;this.logger.error({...o,...r>1?{attempts:r}:{},http_status:s.status,http_status_text:s.statusText,url:e})}catch(t){this.logger.error({...o,error:t,url:e})}return!1}}async webadminHealth(){try{return(await this.fetch(`${this.webadminUrl}/healthcheck`,{headers:this.createHeaders(),signal:AbortSignal.timeout(e-500)})).ok?"ok":"degraded"}catch{return"degraded"}}async findReadableUser(t,e,s){const i=await this.server.ldap.system.search({scope:"sub",filter:`(uid=${r(t)})`,paged:!1,attributes:s},this.config.ldap_base||"");for(const t of i.searchEntries??[])if(await this.server.ldap.readableBy(String(t.dn),e))return t;return null}shouldRetry(t,e,r){return r<this.retryAttempts&&e.includes(t)}waitBeforeRetry(t){return new Promise(e=>setTimeout(e,this.retryDelay*t))}shouldIgnoreError(t,e){return 409===t&&/alias/i.test(e)}attributeToString(t){return t?Array.isArray(t)?t.length>0?String(t[0]):null:String(t):null}async ldapGetAttributes(t,e){try{const r=e&&e.length>0?e:void 0,s=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:r},t);return s.searchEntries&&s.searchEntries.length>0?s.searchEntries[0]:null}catch(e){return this.logger.debug(`Could not fetch attributes from DN ${t}: ${e}`),null}}getAliases(t){if(!t)return[];return(Array.isArray(t)?t:[t]).map(t=>Buffer.isBuffer(t)?t.toString("utf-8"):String(t)).map(t=>this.normalizeAlias(t))}normalizeAlias(t){return t.toLowerCase().startsWith("smtp:")?t.substring(5):t}extractMailDomain(t){const e=t.split("@");return 2===e.length?e[1]:null}extractDomainFromDn(t){const e=t.match(/dc=([^,]+)/gi);return e?e.map(t=>t.substring(3)).join("."):"example.com"}}export{TwakePlugin,TwakePlugin as default};
|
package/dist/bin/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fileURLToPath as e}from"url";import{join as t,dirname as s}from"path";import i from"express";import r from"body-parser";import{parseConfig as o}from"../lib/parseConfig.js";import{keepRawBody as n}from"../lib/rawBody.js";import a from"../config/args.js";import h from"../lib/ldapActions.js";import{buildLogger as l}from"../logger/winston.js";import{setLogger as d,setDebug as u,serverErrorBody as c}from"../lib/expressFormatedResponses.js";import{prefixCoversPath as g}from"../lib/auth/base.js";import{assertAuthzComposition as p,assertEnforceable as f,LDAP_JUDGING_HOOKS as m}from"../lib/authz/composition.js";import{errorBody as w,AUTHZ_REFUSED as b}from"../lib/errors.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";import{registerAttributeTypes as y,recordHookOwner as $}from"../lib/utils.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,fillPattern,getCompiledRegex,getParentDn,getRdn,hookOwner,isAuthzRefusal,isChildOf,isDnInBranch,isDnSyntax,isDummyMemberDn,knownAttributeName,launchHooks,launchHooksChained,movedToTrash,normalizeDn,organizationLink,parseDn,rdnTypes,rdnValue,runAsHookOwner,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";import{runWithLogContext as v,runForRequest as P}from"../lib/requestContext.js";import{requestId as j}from"../lib/changeContext.js";import{mountHealth as S,HEALTH_PATHS as x}from"../lib/health.js";import{httpRequestDuration as k,routeTemplate as M,secondsSince as E,unhandledRejections as O}from"../lib/metrics.js";import{registerProcessShutdown as C,shutdownTimeout as D}from"../lib/shutdown.js";import A from"../plugins/priority.json.js";const _=m.filter(e=>"ldapsearchfilter"!==e);class DM{get server(){return this.life.server}set server(e){this.life.server=e}get composed(){return this._composed}get stopping(){return void 0!==this.life.shutdown}get unhandledRejections(){return this.life.unhandledRejections}constructor(){this.hooks={},this.loadedPlugins={},this.authenticators=[],this._authState={dispatcherMounted:!1},this._errorMiddlewareSetup=!1,this.bodyParsers=i.Router(),this._composed=!1,this.life={unhandledRejections:0},this.inFlight=new Set,this.pluginLoads=new Map,this.authzHooks=new WeakSet,this.config=o(a),this.app=i(),this.app.disable("x-powered-by"),this.app.use((e,t,s)=>{const i=process.hrtime.bigint();t.setHeader("X-Content-Type-Options","nosniff"),this.stopping&&t.setHeader("Connection","close"),this.inFlight.add(t),t.once("finish",()=>{k.observe({method:e.method,route:M(e),status:String(t.statusCode)},E(i)),this.stopping&&this.server?.closeIdleConnections()}),t.once("close",()=>this.inFlight.delete(t)),s()}),this.app.use((e,t,s)=>{const i=j(e);t.setHeader("X-Request-Id",i),v({requestId:i},s)}),this.app.use(this.bodyParsers),this.app.use(r.json({verify:n})),this.app.use(r.urlencoded({extended:!0,verify:n})),S(this.app,this),this.logger=l(this.config),this.ldap=new h(this),d(this.logger),u(this.config.debug),this.operationSequence=0;const e=[];if(this.config.plugin){const t=this.config.plugin.find(e=>e.includes("configApi"));let s=this.config.plugin.filter(e=>!e.includes("configApi"));const i=this.logger,r=(async()=>{for(const e of A){const t=this.resolveModule(DM.moduleOf(e)),r=s.filter(e=>this.resolveModule(DM.moduleOf(e))===t);if(0!==r.length){s=s.filter(e=>!r.includes(e));for(const e of r)i.debug(`Loading ${e} in the priority pass`),await this.loadPlugin(e)}}})();e.push(r),e.push(r.then(async()=>{const e=s.map(e=>(i.debug(`Loading ${e} in the parallel batch`),this.loadPlugin(e)));await Promise.all(e)})),t&&e.push(Promise.all(e).then(()=>this.loadPlugin(t)))}this.ready=new Promise((t,s)=>{e.length>0?Promise.all(e).then(async()=>{this.setupErrorMiddleware(),this.warnUnauthenticatedRoutes(),await p(this),f(this),await this.learnAttributeTypes(),this._composed=!0,this.afterLoad(),t()}).catch(e=>s(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),this._composed=!0,t())})}async learnAttributeTypes(){if(Object.values(this.loadedPlugins).some(e=>e.roles?.includes("authz")))try{y((await this.ldap.schemaIndex()).schema.attributeTypes)}catch(e){this.logger.warn(`Could not read the directory schema, DNs are compared with the usual attribute type names only: ${String(e)}`)}}routerStack(){const e=this.app;return(e.router??e._router)?.stack}withConfig(e){return Object.assign(Object.create(Object.getPrototypeOf(this)),this,{config:e})}mountAuthDispatcher(e){const t=e.roles||[];t.includes("protect")||t.includes("logging")||this.mountAuthDispatcherNow()}mountAuthDispatcherNow(){this._authState.dispatcherMounted||(this._authState.dispatcherMounted=!0,this.app.use((e,t,s)=>{this.dispatchAuth(e,t,s)}))}registerAuthenticator(e){this.mountAuthDispatcherNow(),this.authenticators.includes(e)||this.authenticators.push(e)}registerBodyParser(e,t){this.bodyParsers.use(e,t)}dispatchAuth(e,t,s){let i=-1,r=[];for(const t of this.authenticators)for(const s of t.pathPrefixes)g(s,e.path)&&(s.length>i?(i=s.length,r=[t]):s.length!==i||r.includes(t)||r.push(t));if(i<0&&(r=this.authenticators.filter(e=>0===e.pathPrefixes.length)),0===r.length)return s();const finish=i=>{if(i>=r.length)return P(e,s);const o=r[i];if(!o.afterChain)return finish(i+1);o.afterChain(e,t,()=>finish(i+1))},o=[];e.authenticators=o;const run=s=>{if(s>=r.length)return finish(0);const i=r[s];i.authenticate(e,t,()=>{i.vouchedFor(e)&&o.push(i.name),run(s+1)})};run(0)}afterLoad(){for(const e of Object.values(this.loadedPlugins))try{e.afterLoad?.()}catch(t){this.logger.error(`Plugin ${e.name}: afterLoad failed: ${String(t)}`)}}warnUnauthenticatedRoutes(){const e=Object.values(this.loadedPlugins).filter(e=>e.roles?.includes("auth"));if(0===e.length)return[];const t=[];for(const s of e){if(!s.pathPrefixes||0===s.pathPrefixes.length)return[];t.push(...s.pathPrefixes)}const s=this.routerStack(),i=new Set;for(const e of s||[]){const t=e.route?.path;"string"==typeof t?i.add(t):Array.isArray(t)&&t.forEach(e=>"string"==typeof e&&i.add(e))}const r=[...i].filter(e=>!x.includes(e)&&!t.some(t=>e===t||e.startsWith(`${t}/`))),o=Object.values(this.loadedPlugins).some(e=>e.roles?.includes("authz"));return r.length>0&&this.logger.warn(`Authentication is restricted to ${t.join(", ")}, so these routes are served without authentication`+(o?" nor authorization":"")+`: ${r.sort().join(", ")}`),r.sort()}setupErrorMiddleware(){if(this._errorMiddlewareSetup&&this._errorMiddleware){const e=this.routerStack();if(e){const t=e.findIndex(e=>e.handle===this._errorMiddleware);-1!==t&&e.splice(t,1)}}this._errorMiddleware=(e,t,s,i)=>{let r="statusCode"in e?e.statusCode:500,o=e.message;if(500===r&&/\[authz-forbidden\]/.test(e.message)?(r=403,o=b):403===r&&/\[authz-forbidden\]/.test(e.message)&&(o=b),r>=400&&r<500)return this.logger.warn(`Client error ${r} in request ${t.method} ${t.path}: ${o}`),void(s.headersSent||s.status(r).json(w(e,o)));this.logger.error(`Server error ${r} in request ${t.method} ${t.path}: ${e.message}`,{stack:e.stack,url:t.url,method:t.method}),s.headersSent||s.status(r).json(c(e,this.config.debug))},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}exitOnCrash(){process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack}),setTimeout(()=>process.exit(1),this.config.shutdown_timeout),this.shutdown().finally(()=>process.exit(1))}),process.on("unhandledRejection",e=>{this.life.unhandledRejections++,O.inc(),this.logger.error(`Unhandled promise rejection: ${e instanceof Error?e.message:String(e)}`,{stack:e instanceof Error?e.stack:void 0})})}run(){return new Promise((e,t)=>{this.server=this.app.listen(this.config.port,s=>{s?(this.logger.error(`Error starting server: ${s}`),t(s)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}shutdown(){var e;return(e=this.life).shutdown??(e.shutdown=this.letGo()),this.life.shutdown}async letGo(){const e=this.config.shutdown_timeout,t=Date.now(),s=Math.max(1e3,e-5e3);this.logger.notice(`Shutting down, ${s} ms for what is in flight`);const i=this.server;if(i){const e=new Promise(e=>i.close(()=>e()));i.closeIdleConnections();for(const e of this.inFlight)e.headersSent||e.setHeader("Connection","close");await DM.within(e,s),i.closeAllConnections()}const r=new Map;for(const[e,t]of Object.entries(this.loadedPlugins))r.set(e,Promise.resolve().then(()=>t.close?.()).catch(t=>{this.logger.warn(`${e}: close failed: ${t instanceof Error?t.message:String(t)}`)}).finally(()=>r.delete(e)));const o=Promise.all(r.values()).then(()=>this.ldap.close().catch(()=>{})),n=Math.max(1e3,e-(Date.now()-t)-1e3);await DM.within(o,n)||(this.logger.warn(`Shut down with a close still pending: ${[...r.keys()].join(", ")||"the directory connections"}`),this.ldap.close().catch(()=>{})),this.logger.notice("Shut down")}static async within(e,t){let s;const i=new Promise(e=>{s=setTimeout(()=>e(!1),t)});try{return await Promise.race([e.then(()=>!0),i])}finally{clearTimeout(s)}}shutdownOnSignals(){D.ms=this.config.shutdown_timeout,C(()=>this.shutdown())}static moduleOf(e){const t=e.indexOf(":");return-1===t?e:e.substring(0,t)}resolveModule(i){return i.startsWith("core/")?i.replace("core/",t(s(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js"):i.startsWith(".")?e(new URL(i,import.meta.url)):i}loadPlugin(e){let t,s;if(/:/.test(e)){let i=e.substring(e.indexOf(":")+1);if(e=e.substring(0,e.indexOf(":")),/:/.test(i)){t=i.substring(0,i.indexOf(":")),t||(t=void 0),i=i.substring(i.indexOf(":")+1);try{s=JSON.parse(i),"object"!=typeof s?(this.logger.error(`Overrides for plugin ${e} are not valid: ${i}`),s=void 0):this.logger.debug(`Overrides for plugin ${t||e}: ${i}`)}catch(t){this.logger.error(`Failed to parse overrides for plugin ${e}: ${t}, using ${i}`),s=void 0}}else t=i,t||(t=void 0)}else t=void 0;const i=s?void 0:`${this.resolveModule(e)}\0${t??""}`,r=void 0===i?void 0:this.pluginLoads.get(i);if(r)return this.logger.debug(`Plugin ${e} already loaded, skipping`),r;const o=this.importPlugin(e,t,s);if(void 0!==i){this.pluginLoads.set(i,o);const forget=()=>{this.pluginLoads.get(i)===o&&this.pluginLoads.delete(i)};o.then(e=>e||forget(),forget)}return o}importPlugin(e,t,s){return this.logger.debug(`Loading plugin ${e}`),e.startsWith("core/")&&(e=this.resolveModule(e)),new Promise((i,r)=>{import(e).then(async o=>{let n;if(o&&o.default&&(o=o.default),s){const e={...this.config,...s};n=new o(this.withConfig(e))}else n=new o(this);if(!n)return r(new Error(`Unable to load ${e}`));i(await this.registerPlugin(e,n,t)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(t=>r(new Error(`Failed to load plugin ${e}: ${t}`)))})}async admitToComposition(e){this.loadedPlugins[e.name]=e;try{await p(this,e)}finally{delete this.loadedPlugins[e.name]}}async registerPlugin(e,t,s){if(t.name||(t.name=e),s&&(t.name=s),this.loadedPlugins[t.name])return this.logger.warn(`Plugin ${e} not registered: the name "${t.name}" is already taken. This instance is dropped and its routes will not exist. To load the same plugin twice, give each one a name: --plugin '${e}:${t.name}2:{…}'`),!1;if(this.logger.debug(`Registering plugin ${e} as ${t.name}`),t.dependencies)for(const e in t.dependencies)this.loadedPlugins[e]||(this.logger.debug(`Plugin ${t.name} depends on ${e}, loading it first`),await this.loadPlugin(t.dependencies[e]));if(this._composed&&await this.admitToComposition(t),t.api&&(this.mountAuthDispatcher(t),this.logger.debug(`Plugin ${t.name} has API, registering it`),await t.api(this.app),this._errorMiddlewareSetup&&this.setupErrorMiddleware()),t.hooks)for(const e in t.hooks){this.logger.debug(`Plugin ${t.name} has hook ${e}, registering it`);const s=t.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!s||"function"!=typeof s)throw new Error(`Plugin ${t.name}: hook ${e} is invalid`);{const i=this.hooks[e];if(t.roles?.includes("authz")&&_.includes(e)){const e=i.findIndex(e=>!this.authzHooks.has(e));i.splice(-1===e?i.length:e,0,s),this.authzHooks.add(s)}else i.push(s);$(s,t.name,e)}}return this.loadedPlugins[t.name]=t,!0}}export{DM,$ as recordHookOwner,y as registerAttributeTypes};
|
|
1
|
+
import{fileURLToPath as e}from"url";import{join as t,dirname as i}from"path";import s from"express";import o from"body-parser";import{ConfigParser as r}from"../lib/parseConfig.js";import{unknownEnvironmentOptions as n}from"../config/options.js";import{keepRawBody as a}from"../lib/rawBody.js";import h from"../config/args.js";import l from"../lib/ldapActions.js";import{buildLogger as u}from"../logger/winston.js";import{setLogger as d,setDebug as c,errorResponse as g,messageOf as p}from"../lib/expressFormatedResponses.js";import{prefixCoversPath as f}from"../lib/auth/base.js";import{assertAuthzComposition as m,assertEnforceable as w,LDAP_JUDGING_HOOKS as b}from"../lib/authz/composition.js";import{registerAttributeTypes as y}from"../lib/utils.js";export{assertedRdnValue,asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,fillPattern,getCompiledRegex,getParentDn,getRdn,isChildOf,isDnInBranch,isDnSyntax,isDummyMemberDn,knownAttributeName,movedToTrash,namesByDn,normalizeDn,organizationLink,parseDn,rdnTypes,rdnValue,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";import{errorChain as v}from"../lib/errors.js";export{AuthzRefusedError,BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";import $,{recordHookOwner as P}from"../lib/hookBus.js";export{hookOwner,runAsHookOwner}from"../lib/hookBus.js";import{runWithLogContext as j,runForRequest as k}from"../lib/requestContext.js";import{requestId as S}from"../lib/changeContext.js";import{mountHealth as x,HEALTH_PATHS as M}from"../lib/health.js";import{httpRequestDuration as O,routeTemplate as E,secondsSince as C,unhandledRejections as D}from"../lib/metrics.js";import{registerProcessShutdown as _,shutdownTimeout as A}from"../lib/shutdown.js";import R from"../plugins/priority.json.js";const stackChain=e=>{const t=v(e).map(e=>e.stack).filter(e=>Boolean(e));return t.length?t.join("\nCaused by: "):void 0},L=b.filter(e=>"ldapsearchfilter"!==e);class DM{app;config;ready;get server(){return this.life.server}set server(e){this.life.server=e}hooks={};hookBus=new $(this.hooks);loadedPlugins={};ldap;operationSequence;logger;authenticators=[];_authState={dispatcherMounted:!1};_errorMiddlewareSetup=!1;bodyParsers=s.Router();_composed=!1;get composed(){return this._composed}life={unhandledRejections:0};inFlight=new Set;get stopping(){return void 0!==this.life.shutdown}get unhandledRejections(){return this.life.unhandledRejections}_errorMiddleware;pluginLoads=new Map;unknownCliOptions;authzHooks=new WeakSet;constructor(){const e=new r(h);this.config=e.parse(),this.unknownCliOptions=e.unknownOptions,this.app=s(),this.app.disable("x-powered-by"),this.app.use((e,t,i)=>{const s=process.hrtime.bigint();t.setHeader("X-Content-Type-Options","nosniff"),this.stopping&&t.setHeader("Connection","close"),this.inFlight.add(t),t.once("finish",()=>{O.observe({method:e.method,route:E(e),status:String(t.statusCode)},C(s)),this.stopping&&this.server?.closeIdleConnections()}),t.once("close",()=>this.inFlight.delete(t)),i()}),this.app.use((e,t,i)=>{const s=S(e);t.setHeader("X-Request-Id",s),j({requestId:s},i)}),this.app.use(this.bodyParsers),this.app.use(o.json({verify:a})),this.app.use(o.urlencoded({extended:!0,verify:a})),x(this.app,this),this.logger=u(this.config),this.ldap=new l(this),d(this.logger),c(this.config.debug),this.operationSequence=0;const t=[];if(this.config.plugin){const e=this.config.plugin.find(e=>e.includes("configApi"));let i=this.config.plugin.filter(e=>!e.includes("configApi"));const s=this.logger,o=(async()=>{for(const e of R){const t=this.resolveModule(DM.moduleOf(e)),o=i.filter(e=>this.resolveModule(DM.moduleOf(e))===t);if(0!==o.length){i=i.filter(e=>!o.includes(e));for(const e of o)s.debug(`Loading ${e} in the priority pass`),await this.loadPlugin(e)}}})();t.push(o),t.push(o.then(async()=>{const e=i.map(e=>(s.debug(`Loading ${e} in the parallel batch`),this.loadPlugin(e)));await Promise.all(e)})),e&&t.push(Promise.all(t).then(()=>this.loadPlugin(e)))}this.ready=new Promise((e,i)=>{t.length>0?Promise.all(t).then(async()=>{this.setupErrorMiddleware(),this.warnUnauthenticatedRoutes(),await m(this),w(this),await this.learnAttributeTypes(),this._composed=!0,this.reportUnknownOptions(),this.afterLoad(),e()}).catch(e=>i(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),this._composed=!0,e())})}async learnAttributeTypes(){if(Object.values(this.loadedPlugins).some(e=>e.roles?.includes("authz")))try{y((await this.ldap.schemaIndex()).schema.attributeTypes)}catch(e){this.logger.warn(`Could not read the directory schema, DNs are compared with the usual attribute type names only: ${String(e)}`)}}routerStack(){const e=this.app;return(e.router??e._router)?.stack}withConfig(e){return Object.assign(Object.create(Object.getPrototypeOf(this)),this,{config:e})}reportUnknownOptions(){const e=Object.values(this.loadedPlugins).map(e=>e.constructor.optionDefinitions??{}),t=new Set(e.flatMap(e=>Object.keys(e))),i=[...this.unknownCliOptions.filter(e=>!t.has(e.replace(/^--?/,"").replace(/-/g,"_"))).map(e=>`option ${e}`),...n(h,process.env,e).map(e=>`environment variable ${e}`)];if(0===i.length)return;const s=`Unknown configuration: ${i.join(", ")} — no declared option reads it`;if(this.config.strict_options)throw new Error(s);this.logger.warn(s)}mountAuthDispatcher(e){const t=e.roles||[];t.includes("protect")||t.includes("logging")||this.mountAuthDispatcherNow()}mountAuthDispatcherNow(){this._authState.dispatcherMounted||(this._authState.dispatcherMounted=!0,this.app.use((e,t,i)=>{this.dispatchAuth(e,t,i)}))}registerAuthenticator(e){this.mountAuthDispatcherNow(),this.authenticators.includes(e)||this.authenticators.push(e)}registerBodyParser(e,t){this.bodyParsers.use(e,t)}dispatchAuth(e,t,i){let s=-1,o=[];for(const t of this.authenticators)for(const i of t.pathPrefixes)f(i,e.path)&&(i.length>s?(s=i.length,o=[t]):i.length!==s||o.includes(t)||o.push(t));if(s<0&&(o=this.authenticators.filter(e=>0===e.pathPrefixes.length)),0===o.length)return i();const finish=s=>{if(s>=o.length)return k(e,i);const r=o[s];if(!r.afterChain)return finish(s+1);r.afterChain(e,t,()=>finish(s+1))},r=[];e.authenticators=r;const run=i=>{if(i>=o.length)return finish(0);const s=o[i];s.authenticate(e,t,()=>{s.vouchedFor(e)&&r.push(s.name),run(i+1)})};run(0)}afterLoad(){for(const e of Object.values(this.loadedPlugins))try{e.afterLoad?.()}catch(t){this.logger.error(`Plugin ${e.name}: afterLoad failed: ${String(t)}`)}}warnUnauthenticatedRoutes(){const e=Object.values(this.loadedPlugins).filter(e=>e.roles?.includes("auth"));if(0===e.length)return[];const t=[];for(const i of e){if(!i.pathPrefixes||0===i.pathPrefixes.length)return[];t.push(...i.pathPrefixes)}const i=this.routerStack(),s=new Set;for(const e of i||[]){const t=e.route?.path;"string"==typeof t?s.add(t):Array.isArray(t)&&t.forEach(e=>"string"==typeof e&&s.add(e))}const o=[...s].filter(e=>!M.includes(e)&&!t.some(t=>e===t||e.startsWith(`${t}/`))),r=Object.values(this.loadedPlugins).some(e=>e.roles?.includes("authz"));return o.length>0&&this.logger.warn(`Authentication is restricted to ${t.join(", ")}, so these routes are served without authentication`+(r?" nor authorization":"")+`: ${o.sort().join(", ")}`),o.sort()}setupErrorMiddleware(){if(this._errorMiddlewareSetup&&this._errorMiddleware){const e=this.routerStack();if(e){const t=e.findIndex(e=>e.handle===this._errorMiddleware);-1!==t&&e.splice(t,1)}}this._errorMiddleware=(e,t,i,s)=>{const{status:o,body:r}=g(e,this.config.debug);if(o<500)return this.logger.warn(`Client error ${o} in request ${t.method} ${t.path}: ${r.error}`),void(i.headersSent||i.status(o).json(r));this.logger.error(`Server error ${o} in request ${t.method} ${t.path}: ${p(e)}`,{stack:stackChain(e),url:t.url,method:t.method}),i.headersSent||i.status(o).json(r)},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}exitOnCrash(){process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:stackChain(e)}),setTimeout(()=>process.exit(1),this.config.shutdown_timeout),this.shutdown().finally(()=>process.exit(1))}),process.on("unhandledRejection",e=>{this.life.unhandledRejections++,D.inc(),this.logger.error(`Unhandled promise rejection: ${e instanceof Error?e.message:String(e)}`,{stack:stackChain(e)})})}run(){return new Promise((e,t)=>{this.server=this.app.listen(this.config.port,i=>{i?(this.logger.error(`Error starting server: ${i}`),t(i)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}shutdown(){return this.life.shutdown??=this.letGo(),this.life.shutdown}async letGo(){const e=this.config.shutdown_timeout,t=Date.now(),i=Math.max(1e3,e-5e3);this.logger.notice(`Shutting down, ${i} ms for what is in flight`);const s=this.server;if(s){const e=new Promise(e=>s.close(()=>e()));s.closeIdleConnections();for(const e of this.inFlight)e.headersSent||e.setHeader("Connection","close");await DM.within(e,i),s.closeAllConnections()}const o=new Map;for(const[e,t]of Object.entries(this.loadedPlugins))o.set(e,Promise.resolve().then(()=>t.close?.()).catch(t=>{this.logger.warn(`${e}: close failed: ${t instanceof Error?t.message:String(t)}`)}).finally(()=>o.delete(e)));const r=Promise.all(o.values()).then(()=>this.ldap.close().catch(()=>{})),n=Math.max(1e3,e-(Date.now()-t)-1e3);await DM.within(r,n)||(this.logger.warn(`Shut down with a close still pending: ${[...o.keys()].join(", ")||"the directory connections"}`),this.ldap.close().catch(()=>{})),this.logger.notice("Shut down")}static async within(e,t){let i;const s=new Promise(e=>{i=setTimeout(()=>e(!1),t)});try{return await Promise.race([e.then(()=>!0),s])}finally{clearTimeout(i)}}shutdownOnSignals(){A.ms=this.config.shutdown_timeout,_(()=>this.shutdown())}static moduleOf(e){const t=e.indexOf(":");return-1===t?e:e.substring(0,t)}resolveModule(s){return s.startsWith("core/")?s.replace("core/",t(i(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js"):s.startsWith(".")?e(new URL(s,import.meta.url)):s}loadPlugin(e){let t,i;if(/:/.test(e)){let s=e.substring(e.indexOf(":")+1);if(e=e.substring(0,e.indexOf(":")),/:/.test(s)){t=s.substring(0,s.indexOf(":")),t||(t=void 0),s=s.substring(s.indexOf(":")+1);try{i=JSON.parse(s),"object"!=typeof i?(this.logger.error(`Overrides for plugin ${e} are not valid: ${s}`),i=void 0):this.logger.debug(`Overrides for plugin ${t||e}: ${s}`)}catch(t){this.logger.error(`Failed to parse overrides for plugin ${e}: ${t}, using ${s}`),i=void 0}}else t=s,t||(t=void 0)}else t=void 0;const s=i?void 0:`${this.resolveModule(e)}\0${t??""}`,o=void 0===s?void 0:this.pluginLoads.get(s);if(o)return this.logger.debug(`Plugin ${e} already loaded, skipping`),o;const r=this.importPlugin(e,t,i);if(void 0!==s){this.pluginLoads.set(s,r);const forget=()=>{this.pluginLoads.get(s)===r&&this.pluginLoads.delete(s)};r.then(e=>e||forget(),forget)}return r}importPlugin(e,t,i){return this.logger.debug(`Loading plugin ${e}`),e.startsWith("core/")&&(e=this.resolveModule(e)),new Promise((s,o)=>{import(e).then(async r=>{let n;if(r&&r.default&&(r=r.default),i){const e={...this.config,...i};n=new r(this.withConfig(e))}else n=new r(this);if(!n)return o(new Error(`Unable to load ${e}`));s(await this.registerPlugin(e,n,t)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(t=>o(new Error(`Failed to load plugin ${e}: ${t}`)))})}async admitToComposition(e){this.loadedPlugins[e.name]=e;try{await m(this,e)}finally{delete this.loadedPlugins[e.name]}}async registerPlugin(e,t,i){if(t.name||(t.name=e),i&&(t.name=i),this.loadedPlugins[t.name])return this.logger.warn(`Plugin ${e} not registered: the name "${t.name}" is already taken. This instance is dropped and its routes will not exist. To load the same plugin twice, give each one a name: --plugin '${e}:${t.name}2:{…}'`),!1;if(this.logger.debug(`Registering plugin ${e} as ${t.name}`),t.dependencies)for(const e in t.dependencies)this.loadedPlugins[e]||(this.logger.debug(`Plugin ${t.name} depends on ${e}, loading it first`),await this.loadPlugin(t.dependencies[e]));if(this._composed&&await this.admitToComposition(t),t.api&&(this.mountAuthDispatcher(t),this.logger.debug(`Plugin ${t.name} has API, registering it`),await t.api(this.app),this._errorMiddlewareSetup&&this.setupErrorMiddleware()),t.hooks)for(const e in t.hooks){this.logger.debug(`Plugin ${t.name} has hook ${e}, registering it`);const i=t.hooks[e];if(void 0===i)continue;if("function"!=typeof i)throw new Error(`Plugin ${t.name}: hook ${e} is invalid`);const s=e;if(P(i,t.name,e),t.roles?.includes("authz")&&L.includes(s)){const e=this.hookBus.subscribers(s),t=e.findIndex(e=>!this.authzHooks.has(e));this.hookBus.insert(s,i,t<0?e.length:t),this.authzHooks.add(i)}else this.hookBus.subscribe(s,i)}return this.loadedPlugins[t.name]=t,!0}}export{DM,P as recordHookOwner,y as registerAttributeTypes};
|
package/dist/config/args.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fileURLToPath as _}from"url";import{join as e,dirname as a}from"path";const t="frame-ancestors 'self'",r="SAMEORIGIN",E=[["--port","DM_PORT",8081,"number"],["--shutdown-timeout","DM_SHUTDOWN_TIMEOUT",2e4,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins","identifiers"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--debug","DM_DEBUG",!1,"boolean"],["--log-file","DM_LOG_FILE","error.log"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains","identifiers"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN",""],["--ldap-pwd","DM_LDAP_PWD",""],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls","identifiers",!0],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",e(a(_(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuotaSize"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes","identifiers"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes","identifiers"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--organization-schema","DM_ORGANIZATION_SCHEMA",""],["--enterprise-domain-name-attribute","DM_ENTERPRISE_DOMAIN_NAME_ATTRIBUTE","associatedDomain"],["--enterprise-domain-link-attribute","DM_ENTERPRISE_DOMAIN_LINK_ATTRIBUTE",""],["--metrics-port","DM_METRICS_PORT",9464,"number"],["--storage-backend","DM_STORAGE_BACKEND",""],["--storage-sweep-interval","DM_STORAGE_SWEEP_INTERVAL",600,"number"],["--storage-ldap-base","DM_STORAGE_LDAP_BASE",""],["--storage-ldap-object-class","DM_STORAGE_LDAP_OBJECT_CLASS","applicationProcess"],["--storage-file-directory","DM_STORAGE_FILE_DIRECTORY",""],["--storage-postgres-url","DM_STORAGE_POSTGRES_URL",""],["--storage-postgres-table","DM_STORAGE_POSTGRES_TABLE","ldap_rest_storage"],["--storage-valkey-url","DM_STORAGE_VALKEY_URL",""],["--storage-valkey-prefix","DM_STORAGE_VALKEY_PREFIX","ldap-rest:"],["--bcl-retention","DM_BCL_RETENTION",604800,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes","identifiers"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",e(a(_(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes","identifiers"],["--static-path","DM_STATIC_PATH",e(a(_(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--static-csp","DM_STATIC_CSP",t],["--static-frame-options","DM_STATIC_FRAME_OPTIONS",r],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-flat-auto-repair","DM_LDAP_FLAT_AUTO_REPAIR",!0,"boolean"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases","dns"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes","identifiers"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-retry-attempts","DM_JAMES_RETRY_ATTEMPTS",5,"number"],["--james-retry-delay","DM_JAMES_RETRY_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches","dns"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--james-sync-forwards","DM_JAMES_SYNC_FORWARDS",!1,"boolean"],["--james-team-mailbox-any-domain","DM_JAMES_TEAM_MAILBOX_ANY_DOMAIN",!1,"boolean"],["--migration-proxy-url","DM_MIGRATION_PROXY_URL",""],["--migration-proxy-token","DM_MIGRATION_PROXY_TOKEN",""],["--migration-proxy-branch","DM_MIGRATION_PROXY_BRANCHES",[],"array","--migration-proxy-branches","dns"],["--migration-proxy-concurrency","DM_MIGRATION_PROXY_CONCURRENCY",10,"number"],["--migration-proxy-retry-attempts","DM_MIGRATION_PROXY_RETRY_ATTEMPTS",5,"number"],["--migration-proxy-retry-delay","DM_MIGRATION_PROXY_RETRY_DELAY",1e3,"number"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--twake-lifecycle-role-attribute","DM_TWAKE_LIFECYCLE_ROLE_ATTRIBUTE",""],["--twake-lifecycle-lock-attribute","DM_TWAKE_LIFECYCLE_LOCK_ATTRIBUTE",""],["--twake-lifecycle-lock-value","DM_TWAKE_LIFECYCLE_LOCK_VALUE",""],["--twake-lifecycle-deleted-attribute","DM_TWAKE_LIFECYCLE_DELETED_ATTRIBUTE",""],["--twake-lifecycle-deleted-value","DM_TWAKE_LIFECYCLE_DELETED_VALUE","TRUE"],["--twake-lifecycle-deleted-at-attribute","DM_TWAKE_LIFECYCLE_DELETED_AT_ATTRIBUTE",""],["--twake-lifecycle-deleted-at-format","DM_TWAKE_LIFECYCLE_DELETED_AT_FORMAT","iso8601"],["--twake-lifecycle-reason-attribute","DM_TWAKE_LIFECYCLE_REASON_ATTRIBUTE",""],["--twake-lifecycle-rules","DM_TWAKE_LIFECYCLE_RULES",""],["--twake-lifecycle-member-attribute","DM_TWAKE_LIFECYCLE_MEMBER_ATTRIBUTE","member"],["--twake-group-base","DM_TWAKE_GROUP_BASE",""],["--twake-group-user-base","DM_TWAKE_GROUP_USER_BASE",""],["--twake-group-organization-dn","DM_TWAKE_GROUP_ORGANIZATION_DN",""],["--twake-group-organization-status-attribute","DM_TWAKE_GROUP_ORGANIZATION_STATUS_ATTRIBUTE","twakeOrgStatus"],["--twake-group-organization-deleted-value","DM_TWAKE_GROUP_ORGANIZATION_DELETED_VALUE","deleted"],["--twake-group-display-name-attribute","DM_TWAKE_GROUP_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-group-color-attribute","DM_TWAKE_GROUP_COLOR_ATTRIBUTE","twakeGroupColor"],["--twake-group-created-at-attribute","DM_TWAKE_GROUP_CREATED_AT_ATTRIBUTE","twakeCreatedAt"],["--twake-group-max-page-limit","DM_TWAKE_GROUP_MAX_PAGE_LIMIT",1e3,"number"],["--twake-group-member-fields","DM_TWAKE_GROUP_MEMBER_FIELDS",{},"json"],["--twake-space-base","DM_TWAKE_SPACE_BASE",""],["--twake-space-class","DM_TWAKE_SPACE_CLASS",["top","twakeSpace"],"array",null,"identifiers"],["--twake-space-display-name-attribute","DM_TWAKE_SPACE_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-space-admin-attribute","DM_TWAKE_SPACE_ADMIN_ATTRIBUTE","twakeSpaceAdmin"],["--twake-space-editor-attribute","DM_TWAKE_SPACE_EDITOR_ATTRIBUTE","twakeSpaceEditor"],["--twake-space-viewer-attribute","DM_TWAKE_SPACE_VIEWER_ATTRIBUTE","twakeSpaceViewer"],["--twake-space-user-role-attribute","DM_TWAKE_SPACE_USER_ROLE_ATTRIBUTE",""],["--twake-space-exchange","DM_TWAKE_SPACE_EXCHANGE","space"],["--twake-space-sync-queue","DM_TWAKE_SPACE_SYNC_QUEUE","twake.space.sync.requested.ldap-rest"],["--twake-tombstone-dn","DM_TWAKE_TOMBSTONE_DN",[],"array",null,"dns"],["--twake-tombstone-default-reason","DM_TWAKE_TOMBSTONE_DEFAULT_REASON","deleted"],["--twake-tombstone-reason-header","DM_TWAKE_TOMBSTONE_REASON_HEADER","x-deletion-reason"],["--twake-tombstone-reasons","DM_TWAKE_TOMBSTONE_REASONS",[],"array",null,"identifiers"],["--twake-tombstone-clear-attributes","DM_TWAKE_TOMBSTONE_CLEAR_ATTRIBUTES",[],"array",null,"identifiers"],["--twake-tombstone-erase-min-age","DM_TWAKE_TOMBSTONE_ERASE_MIN_AGE",2592e3,"number"],["--twake-tombstone-group-bases","DM_TWAKE_TOMBSTONE_GROUP_BASES",[],"array",null,"dns"],["--twake-instance-dn","DM_TWAKE_INSTANCE_DN",[],"array",null,"dns"],["--twake-instance-skip-attribute","DM_TWAKE_INSTANCE_SKIP_ATTRIBUTE",""],["--twake-instance-skip-value","DM_TWAKE_INSTANCE_SKIP_VALUE",""],["--twake-instance-provider","DM_TWAKE_INSTANCE_PROVIDER","cloudery"],["--twake-instance-id","DM_TWAKE_INSTANCE_ID","{uid}"],["--twake-instance-fqdn-attribute","DM_TWAKE_INSTANCE_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--twake-instance-sent-attribute","DM_TWAKE_INSTANCE_SENT_ATTRIBUTE","twakeCreatedEventAt"],["--twake-instance-locale","DM_TWAKE_INSTANCE_LOCALE",""],["--twake-instance-timeout","DM_TWAKE_INSTANCE_TIMEOUT",3e4,"number"],["--twake-instance-cloudery-url","DM_TWAKE_INSTANCE_CLOUDERY_URL",""],["--twake-instance-cloudery-token","DM_TWAKE_INSTANCE_CLOUDERY_TOKEN",""],["--twake-instance-cloudery-offer","DM_TWAKE_INSTANCE_CLOUDERY_OFFER","b2b_twake_default"],["--twake-instance-cloudery-domain","DM_TWAKE_INSTANCE_CLOUDERY_DOMAIN",""],["--twake-instance-cloudery-organization-offer","DM_TWAKE_INSTANCE_CLOUDERY_ORGANIZATION_OFFER",""],["--twake-instance-cozy-url","DM_TWAKE_INSTANCE_COZY_URL",""],["--twake-instance-cozy-user","DM_TWAKE_INSTANCE_COZY_USER","admin"],["--twake-instance-cozy-passphrase","DM_TWAKE_INSTANCE_COZY_PASSPHRASE",""],["--twake-instance-cozy-domain","DM_TWAKE_INSTANCE_COZY_DOMAIN",""],["--twake-instance-cozy-context","DM_TWAKE_INSTANCE_COZY_CONTEXT","default"],["--twake-instance-cozy-apps","DM_TWAKE_INSTANCE_COZY_APPS","home,drive,settings,notes,dataproxy"],["--twake-instance-cozy-org-id","DM_TWAKE_INSTANCE_COZY_ORG_ID",""],["--twake-instance-cozy-org-domain","DM_TWAKE_INSTANCE_COZY_ORG_DOMAIN",""],["--twake-instance-auth-exchange","DM_TWAKE_INSTANCE_AUTH_EXCHANGE","auth"],["--twake-instance-b2b-exchange","DM_TWAKE_INSTANCE_B2B_EXCHANGE","b2b"],["--twake-instance-user-created-key","DM_TWAKE_INSTANCE_USER_CREATED_KEY","user.created"],["--twake-instance-organization-created-key","DM_TWAKE_INSTANCE_ORGANIZATION_CREATED_KEY","organization.created"],["--twake-instance-workplace-created-key","DM_TWAKE_INSTANCE_WORKPLACE_CREATED_KEY","workplace.created"],["--twake-instance-queue","DM_TWAKE_INSTANCE_QUEUE","workplace.created.ldap-rest"],["--twake-instance-organization-base","DM_TWAKE_INSTANCE_ORGANIZATION_BASE",""],["--twake-instance-organization-id-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_ID_ATTRIBUTE","ou"],["--twake-instance-organization-domain-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_DOMAIN_ATTRIBUTE","twakeDomain"],["--twake-instance-organization-name-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_NAME_ATTRIBUTE","description"],["--twake-instance-organization-fqdn-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_FQDN_ATTRIBUTE",""],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--app-accounts-user-attribute","DM_APP_ACCOUNTS_USER_ATTRIBUTE",""],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes","identifiers"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes","identifiers"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--llng-username-header","DM_LLNG_USERNAME_HEADER",""],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-fake-user","DM_AUTH_FAKE_USER",""],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps","phrases"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs","phrases"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array",null,"identifiers"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-transit-branch","DM_AUTHZ_TRANSIT_BRANCH",""],["--authz-transit-origin-attribute","DM_AUTHZ_TRANSIT_ORIGIN_ATTRIBUTE",""],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-bypass","DM_AUTHZ_DYNAMIC_BYPASS",[],"array",null,"identifiers"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--authz-global-branch","DM_AUTHZ_GLOBAL_BRANCHES",[],"array","--authz-global-branches","dns"],["--authz-global-write-branch","DM_AUTHZ_GLOBAL_WRITE_BRANCHES",[],"array","--authz-global-write-branches","dns"],["--authz-global-organization","DM_AUTHZ_GLOBAL_ORGANIZATION",""],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-username-claim","DM_OIDC_USERNAME_CLAIM","sub"],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies","identifiers"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-base","DM_PPOLICY_BASE",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes","identifiers"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes","identifiers"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as STATIC_CSP,r as STATIC_FRAME_OPTIONS,E as default};
|
|
1
|
+
import{fileURLToPath as t}from"url";import{join as o,dirname as i}from"path";import{bclOptions as a}from"../plugins/bcl.options.js";import{rabbitmqOptions as r}from"../plugins/rabbitmq.options.js";import{metricsOptions as s}from"../plugins/metrics.options.js";import{storageOptions as e}from"../plugins/storage.options.js";import{staticOptions as p}from"../plugins/static.options.js";export{STATIC_CSP,STATIC_FRAME_OPTIONS}from"../plugins/static.options.js";import{scimOptions as n}from"../plugins/scim/scim.options.js";import{llngOptions as _,tokenOptions as l,fakeOptions as m,totpOptions as u,hmacOptions as T,perRouteOptions as D,perBranchOptions as A,dynamicOptions as M,linid1Options as d,openidconnectOptions as E,rateLimitOptions as c,crowdsecOptions as I,trustedProxyOptions as f}from"../plugins/auth/auth.options.js";import{organizationOptions as g,enterpriseRulesOptions as R,groupsOptions as U,externalUsersOptions as L,flatGenericOptions as b,rawOptions as S,bulkImportOptions as O,trashOptions as P,passwordPolicyOptions as h}from"../plugins/ldap/ldap.options.js";import{appAccountsOptions as w}from"../plugins/twake/appAccounts.options.js";import{calendarOptions as j}from"../plugins/twake/calendar.options.js";import{clouderyOptions as N}from"../plugins/twake/cloudery.options.js";import{cozyOptions as k}from"../plugins/twake/cozy.options.js";import{driveOptions as C}from"../plugins/twake/drive.options.js";import{jamesOptions as H}from"../plugins/twake/james.options.js";import{migrationProxyOptions as y}from"../plugins/twake/migrationProxy.options.js";import{twakeGroupsOptions as B}from"../plugins/twake/twakeGroups.options.js";import{twakeInstancesOptions as z}from"../plugins/twake/twakeInstances.options.js";import{twakeLifecycleOptions as G}from"../plugins/twake/twakeLifecycle.options.js";import{twakeSpacesOptions as Z}from"../plugins/twake/twakeSpaces.options.js";import{twakeTombstoneOptions as F}from"../plugins/twake/twakeTombstone.options.js";import{toConfigEntries as x}from"./options.js";const v=[["--port","DM_PORT",8081,"number"],["--shutdown-timeout","DM_SHUTDOWN_TIMEOUT",2e4,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins","identifiers"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--debug","DM_DEBUG",!1,"boolean"],["--strict-options","DM_STRICT_OPTIONS",!1,"boolean"],["--log-file","DM_LOG_FILE","error.log"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains","identifiers"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN",""],["--ldap-pwd","DM_LDAP_PWD",""],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls","identifiers",!0],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",o(i(t(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuotaSize"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes","identifiers"],...x(g),...x(R),...x(s),...x(e),...x(a),...x(U),...x(L),...x(p),...x(b),...x(S),...x(O),["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],...x(H),...x(y),...x(C),...x(j),...x(k),...x(N),...x(G),...x(B),...x(Z),...x(F),...x(z),...x(r),...x(w),...x(P),...x(_),["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],...x(l),...x(m),...x(u),...x(T),...x(D),["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array",null,"identifiers"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-transit-branch","DM_AUTHZ_TRANSIT_BRANCH",""],["--authz-transit-origin-attribute","DM_AUTHZ_TRANSIT_ORIGIN_ATTRIBUTE",""],...x(A),...x(M),...x(d),...x(E),...x(c),...x(I),...x(f),...x(h),...x(n)];export{v as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{splitValue as e,parseNumber as r}from"../lib/parseConfig.js";function defineOptions(e){return e}const t={string:(e={})=>({type:"string",...e}),number:(e={})=>({type:"number",...e}),boolean:(e={})=>({type:"boolean",...e}),array:(e={})=>({type:"array",...e}),stringOrFalse:(e={})=>({type:"string",allowFalse:!0,...e}),json:(e={})=>({type:"json",...e})};function cliName(e){return`--${e.replace(/_/g,"-")}`}function emptyValue(e){switch(e){case"boolean":return!1;case"number":return 0;case"array":return[];case"json":return{};default:return""}}function toConfigEntries(e){return Object.entries(e).map(([e,r])=>{const t=void 0===r.default?emptyValue(r.type):r.default,n=r,o=n.plural?n.plural.startsWith("--")?n.plural:`--${n.plural}`:void 0;return[cliName(e),r.env??"",t,r.type,o,n.kind,Boolean(r.required)]})}function checkValue(e,r,t){const n=cliName(e);if(r.required&&function(e){return void 0===e||""===e||Array.isArray(e)&&0===e.length}(t))throw new Error(`Option ${n} is required`);switch(r.type){case"string":if("string"!=typeof t&&(!1!==t||!r.allowFalse))throw new Error(`Option ${n} must be a string`);if("string"==typeof t&&r.choices&&!r.choices?.includes(t))throw new Error(`Option ${n} must be one of `+r.choices?.join(", "));break;case"number":!function(e,r,t){if("number"!=typeof t||!Number.isInteger(t))throw new Error(`Option ${e} must be an integer, got "${String(t)}"`);if(void 0!==r.min&&t<r.min)throw new Error(`Option ${e} must be at least ${r.min}, got ${t}`);if(void 0!==r.max&&t>r.max)throw new Error(`Option ${e} must be at most ${r.max}, got ${t}`)}(n,r,t);break;case"boolean":if("boolean"!=typeof t)throw new Error(`Option ${n} must be true or false`);break;case"array":if(!Array.isArray(t)||t.some(e=>"string"!=typeof e))throw new Error(`Option ${n} must be a list of strings`);break;case"json":if(null===t||"object"!=typeof t||Array.isArray(t))throw new Error(`Option ${n} must be a JSON object`)}}function unknownEnvironmentOptions(e,r=process.env,t=[]){const n=new Set(e.map(e=>e[1]).filter(e=>Boolean(e)));for(const e of t)for(const r of Object.values(e))r.env&&n.add(r.env);return Object.keys(r).filter(e=>e.startsWith("DM_")&&!e.startsWith("DM_TEST_")&&!n.has(e))}function valueFromEnvironment(t,n,o){if("array"===n.type){const r=e(o,n.kind,"plural");return r.length>0?r:void 0}if("number"===n.type){if(!o.trim())return;const e=r(o);if(Number.isNaN(e))throw new Error(`Option ${t}: environment value "${o}" is not a number`);return e}if("boolean"===n.type)return"true"===o.toLowerCase();if("json"===n.type){if(!o.trim())return;try{return JSON.parse(o)}catch(e){throw new Error(`Option ${t}: environment value is not JSON: ${String(e)}`)}}return o}function readOptions(e,r,t=new Set){const n=r,o={};for(const[r,i]of Object.entries(e)){const e=n[r]??(t.has(r)||r in n||!i.env||void 0===process.env[i.env]?void 0:valueFromEnvironment(r,i,process.env[i.env])),a="string"===i.type&&!i.emptyIsValue&&""===e&&void 0!==i.default&&""!==i.default,s=void 0===e||a?void 0===i.default?emptyValue(i.type):Array.isArray(i.default)?i.default.slice():i.default:Array.isArray(e)?e.slice():e;checkValue(r,i,s),o[r]=s}return o}export{defineOptions,t as opt,readOptions,toConfigEntries,unknownEnvironmentOptions};
|
package/dist/config/schema.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BadRequestError as t}from"../lib/errors.js";import{isDnInBranch as e,getCompiledRegex as r}from"../lib/utils.js";function localizedText(t,e="en"){if(null==t)return;if("string"==typeof t)return t||void 0;const r=e.split("-")[0].toLowerCase();for(const n of[e,r,"en"])if(t[n])return t[n];return Object.values(t).find(Boolean)}function invalidValueMessage(t,e){const r=localizedText(e?.hint||e?.items?.hint);return`Invalid value for attribute "${t}"${r?`: ${r}`:""}`}function hasRole(t,e){return!!t?.role&&(Array.isArray(t.role)?t.role.includes(e):t.role===e)}function roleAttribute(t,e){if(t)for(const[r,n]of Object.entries(t.attributes))if(hasRole(n,e))return r}function schemaAttribute(t,e){if(!t)return;const r=e.split(";")[0],n=t.attributes[r];if(n)return[r,n];const i=r.toLowerCase();for(const[e,r]of Object.entries(t.attributes))if(e.toLowerCase()===i)return[e,r]}function modifiedAttributeNames(t){return[...Object.keys(t.add||{}),...Object.keys(t.replace||{}),...Array.isArray(t.delete)?t.delete.map(String):Object.keys(t.delete||{})]}function toDirectoryValue(e,r,n){if(null===n||"string"==typeof n||Buffer.isBuffer(n))return n;const i=e?.
|
|
1
|
+
import{BadRequestError as t}from"../lib/errors.js";import{isDnInBranch as e,getCompiledRegex as r}from"../lib/utils.js";function localizedText(t,e="en"){if(null==t)return;if("string"==typeof t)return t||void 0;const r=e.split("-")[0].toLowerCase();for(const n of[e,r,"en"])if(t[n])return t[n];return Object.values(t).find(Boolean)}function invalidValueMessage(t,e){const r=localizedText(e?.hint||e?.items?.hint);return`Invalid value for attribute "${t}"${r?`: ${r}`:""}`}function tooLongMessage(t,e){return`Attribute "${t}" accepts at most ${e} characters`}function hasRole(t,e){return!!t?.role&&(Array.isArray(t.role)?t.role.includes(e):t.role===e)}function roleAttribute(t,e){if(t)for(const[r,n]of Object.entries(t.attributes))if(hasRole(n,e))return r}function schemaAttribute(t,e){if(!t)return;const r=e.split(";")[0],n=Object.hasOwn(t.attributes,r)?t.attributes[r]:void 0;if(n)return[r,n];const i=r.toLowerCase();for(const[e,r]of Object.entries(t.attributes))if(e.toLowerCase()===i)return[e,r]}function modifiedAttributeNames(t){return[...Object.keys(t.add||{}),...Object.keys(t.replace||{}),...Array.isArray(t.delete)?t.delete.map(String):Object.keys(t.delete||{})]}function toDirectoryValue(e,r,n){if(null===n||"string"==typeof n||Buffer.isBuffer(n))return n;const i=schemaAttribute(e,r)?.[1],asText=(e,n)=>{if("string"==typeof e||Buffer.isBuffer(e))return e;if("number"==typeof e&&Number.isFinite(e)&&("number"===n||"integer"===n))return String(e);if("boolean"==typeof e&&"boolean"===n)return e?"TRUE":"FALSE";throw new t(`Attribute "${r}" must be a string or an array of strings`)};return Array.isArray(n)?n.map(t=>asText(t,i?.items?.type??i?.type)):asText(n,i?.type)}function toDirectoryEntry(t,e){for(const[r,n]of Object.entries(e))e[r]=toDirectoryValue(t,r,n)}function toDirectoryChanges(t,e){e.add&&toDirectoryEntry(t,e.add),e.replace&&toDirectoryEntry(t,e.replace),e.delete&&!Array.isArray(e.delete)&&toDirectoryEntry(t,e.delete)}function neverReturnAttributes(t){const e=new Set;for(const r of t)if(r)for(const[t,n]of Object.entries(r.attributes))n.neverReturn&&e.add(t.toLowerCase());return e}function withoutAttributes(t,e){if(0===e.size)return t;const r={};for(const[n,i]of Object.entries(t))e.has(n.split(";")[0].toLowerCase())||(r[n]=i);return r}async function checkDnValues(r,n,i,o){const s="pointer"===n.type,a=s?n.branch:n.items?.branch,u=s||"pointer"===n.items?.type;if(!u&&!a?.length)return;if(s&&"string"!=typeof i)throw new t(`Attribute "${r}" must be a string (DN pointer)`);const c=(Array.isArray(i)?i:[i]).map(t=>String(t));if(c.includes(""))throw new t(`Attribute "${r}" must point to a DN`);for(const n of c){if(a?.length&&!a.some(t=>e(n,t)))throw new t(`Attribute "${r}" must point to a DN within allowed branches: ${a.join(", ")}`);let i=!1;if(u){try{i=await o(n)}catch{i=!1}if(!i)throw new t(`Attribute "${r}" points to invalid or non-existent DN: ${n}`)}}}const n=4096;function maxLengthOf(t){return t?.maxLength??t?.items?.maxLength??n}function matchesPattern(t,e){const n=t.test??t.items?.test;if(!n)return!0;const i=maxLengthOf(t),o="string"==typeof n?r(n):n;return(Array.isArray(e)?e:[e]).every(t=>String(t).length<=i&&o.test(String(t)))}function tooLongValue(t,e){const r=t.maxLength??t.items?.maxLength;if(void 0===r&&!(t.test??t.items?.test))return;const i=r??n;return(Array.isArray(e)?e:[e]).find(t=>String(t).length>i)}function assertClientMaySet(e,r,n){if(e)for(const i of r){if("dn"===i)continue;const r=schemaAttribute(e,i);if(!r)continue;const[o,s]=r;if((!n||o.toLowerCase()!==n.toLowerCase()||s.generatedFrom)&&(s.generated||s.readOnly))throw new t(s.readOnly?`Attribute "${i}" is read-only and cannot be set`:`Attribute "${i}" is computed by the server and cannot be set`)}}function missingRequiredAttribute(t,e,r){if(!t)return;const filledByAPlugin=(e,n)=>Object.values(r).some(r=>{const i=r.fillsGeneratedAttribute;return"function"==typeof i&&i.call(r,e,n,t)});for(const[r,n]of Object.entries(t.attributes)){const t=e[r],i=null==t||""===t||Array.isArray(t)&&0===t.length;if(n.required&&i&&(!n.generated||!n.generatedFrom&&!filledByAPlugin(r,n)))return r}}export{n as DEFAULT_MAX_LENGTH,assertClientMaySet,checkDnValues,hasRole,invalidValueMessage,localizedText,matchesPattern,maxLengthOf,missingRequiredAttribute,modifiedAttributeNames,neverReturnAttributes,roleAttribute,schemaAttribute,toDirectoryChanges,toDirectoryEntry,toDirectoryValue,tooLongMessage,tooLongValue,withoutAttributes};
|
package/dist/lib/auth/base.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"../../abstract/plugin.js";import{serverError as t}from"../expressFormatedResponses.js";
|
|
1
|
+
import e from"../../abstract/plugin.js";import{serverError as t}from"../expressFormatedResponses.js";function stripTrailingSlashes(e){let t=e.length;for(;t>0&&"/"===e[t-1];)t--;return e.slice(0,t)}function prefixCoversPath(e,t){return t===e||t.startsWith(`${e}/`)}const r=["req.user","req.userName"];function assertIdentityMode(e,t){const i=e.authz_identity??"req.user";if(!r.includes(i))throw new Error(`${t}: unknown --authz-identity "${i}". Known: ${r.join(", ")}.`)}function identityFor(e,t){return e?"req.userName"!==t.authz_identity?{value:e.user,fellBack:!1}:e.userName?{value:e.userName,fellBack:!1}:{value:e.user,fellBack:Boolean(e.user)}:{fellBack:!1}}function identityCoverage(e){const t=new Set,r=[];for(const i of Object.values(e)){if(!i.roles?.includes("auth"))continue;const e=i.knownIdentities?.();if(e)for(const r of e)t.add(r);else r.push(i.name)}return{known:t,unverifiable:r}}function warnUnmatchedRuleKeys(e,t,r,i){if(0===e.length)return;const{known:n,unverifiable:s}=identityCoverage(t);if(e.filter(e=>n.has(e)).length>0)return;if(0===n.size&&0===s.length)return;const a=s.length?` ${s.join(", ")} cannot be checked before a login, so this may be right`:"";i.warn(`${r}: none of its configured identities (${e.join(", ")}) is one the loaded authenticators can publish${n.size?` (${[...n].join(", ")})`:""}.${a||" Every rule is inert."}`)}class AuthBase extends e{_pathPrefixes;get pathPrefixes(){if(this._pathPrefixes)return this._pathPrefixes;const e=this.config.auth_path_prefix,t=Array.isArray(e)?e:e?[e]:[],r=[];for(const e of t){if("string"!=typeof e)throw new Error(`${this.name}: auth_path_prefix must contain strings, got ${JSON.stringify(e)}`);const t=e.trim(),i=stripTrailingSlashes(t);if(0===i.length){if(0===t.length)throw new Error(`${this.name}: auth_path_prefix contains an empty entry`);return this.logger.info(`${this.name}: auth_path_prefix contains "${t}", which covers the whole server: this authentication guards every path`),this._pathPrefixes=[]}if(!i.startsWith("/"))throw new Error(`${this.name}: auth_path_prefix entry "${e}" must start with "/", otherwise it matches no request and guards nothing`);r.push(i)}return this._pathPrefixes=r}async authenticate(e,r,i){try{[e,r]=await this.server.hookBus.chain("beforeAuth",[e,r])}catch(e){return t(r,e)}this.authMethod(e,r,async()=>{try{[e,r]=await this.server.hookBus.chain("afterAuth",[e,r]),i()}catch(e){t(r,e)}})}publishIdentity(e,t,r){e.user=t,e.userName=r||t}knownIdentities(){}identitySource(){return"req.user and req.userName: the name this plugin knows the caller by"}afterLoad(){this.logger.info(`${this.name}: ${this.identitySource()}`)}passedThrough=new WeakSet;passThrough(e,t){this.passedThrough.add(e),t()}vouchedFor(e){return!this.passedThrough.has(e)}api(e){const t=this.pathPrefixes;this.server.registerAuthenticator(this),this.logger.info(0===t.length?`${this.name}: authentication guards every path not claimed by another plugin`:`${this.name}: authentication restricted to ${t.join(", ")}`)}}export{assertIdentityMode,AuthBase as default,identityCoverage,identityFor,prefixCoversPath,warnUnmatchedRuleKeys};
|
package/dist/lib/authz/base.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../../abstract/plugin.js";import{roleAttribute as e}from"../../config/schema.js";import{assertIdentityMode as r,identityFor as s}from"../auth/base.js";import{ServiceUnavailableError as i,AmbiguousIdentityError as n,ForbiddenError as a,BadRequestError as o}from"../errors.js";import{normalizeDn as h,getParentDn as c,isDnInBranch as u,organizationLink as l,getRdn as d,escapeLdapFilter as f,parseDn as g}from"../utils.js";import{authzFor as w,servesRequest as p}from"./composition.js";function mostSpecificBranchRule(t,e){let r=null,s=-1;for(const[i,n]of t){if(!u(e,i))continue;const t=g(i).length;t>s&&([r,s]=[n,t])}return r}class AuthzBase extends t{constructor(t){super(t),this.roles=["authz"],this.resolutionCache=new Map,this.resolutionCacheFullWarned=!1,this.fallbackWarned=!1,this.hooks={ldaprenamedone:()=>{this.resolutionCache.clear()},ldapdeletedone:()=>{this.resolutionCache.clear()},ldapmodifyrequest:async([t,e,r,s])=>{if(this.shouldSkipAuthorization(s))return[t,await this.recordTransitOrigin(t,e),r,s];const i=await this.resolveCaller(s);if(!i)return[t,await this.recordTransitOrigin(t,e),r,s];const n=await this.storedDn(t),a=this.config.ldap_organization_link_attribute,o=a?AuthzBase.organizationLinks(a,[...AuthzBase.valuesOf(e.replace,a),...AuthzBase.valuesOf(e.add,a)]):[];if(a&&o.length>0){let r,h=this.extractBranchDn(n),c=!1;try{const e=(await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[a]},t)).searchEntries[0];e&&(c=!0,r=l(e[a]),r&&(h=r))}catch{}const u=await this.storedDn(o[0]),d=this.filtersAttachedEntries(),f=d&&c&&this.inTransit(r),g=d&&this.inTransit(u);if(!f){const t=g?"write":this.sourcePermission(h);if(!(await this.getUserPermissions(i,h))[t])throw new Error(`[authz-forbidden] User ${s.user} does not have ${t} permission for source branch ${h}`)}if(g){if(f){if(0===(await this.getAuthorizedBranches(i)).length)throw new Error(`[authz-forbidden] User ${s.user} administers no branch`)}}else{if(!(await this.getUserPermissions(i,u)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for destination branch ${u}`)}if(!f&&!this.onlyRelinks(e)){const t=d?h:this.extractBranchDn(n);if(!(await this.getUserPermissions(i,t)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for branch ${t}`)}}else{const t=await this.effectiveBranch(n);if(!(await this.getUserPermissions(i,t)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for branch ${t}`)}const h=await this.refusingRule(i,n,["write"],!1);if(h)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for branch ${h}`);const c=this.domainLinkAttribute();return c&&await this.guardDomainLinks(i,s,t,[...AuthzBase.valuesOf(e.add,c),...AuthzBase.valuesOf(e.replace,c)],c,!1),[t,await this.recordTransitOrigin(t,e),r,s]},ldapaddrequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];let i;const n=this.config.ldap_organization_link_attribute,a=n?AuthzBase.organizationLinks(n,AuthzBase.valuesOf(e,n)):[],o=await this.storedParent(t);i=a.length>0?await this.storedDn(a[0]):this.extractBranchDn(o);if(!(await this.getUserPermissions(s,i)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for branch ${i}`);const h=await this.refusingRule(s,o,["write"],!1);if(h)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for branch ${h}`);const c=this.domainLinkAttribute();return c&&await this.guardDomainLinks(s,r,t,AuthzBase.valuesOf(e,c),c,!0),[t,e,r]},ldapsearchrequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];if(t===this.config.ldap_top_organization&&"base"===e.scope)return[t,{...e,[AuthzBase.topOrganizationRead]:!0},r];const i=await this.searchRefusal(s,t);if(i)throw new Error(`[authz-forbidden] User ${r.user} ${i}`);if(this.filtersAttachedEntries()){const s=this.config.ldap_organization_link_attribute;if(s&&!AuthzBase.wantsAttribute(e,s)){const t=e.attributes,r=Array.isArray(t)?t.map(String):[String(t)];e={...e,attributes:[...r,s],[AuthzBase.widenedForLink]:!0}}return[t,e,r]}return[t,e,r]},ldapsearchfilter:async([t,e,r])=>{const s=[t,e,r];if(this.shouldSkipAuthorization(e))return s;if(!t?.searchEntries?.length)return s;const i=await this.resolveCaller(e);if(!i)return s;if(!this.filtersAttachedEntries()){if(r?.[AuthzBase.topOrganizationRead])return s;const n=await this.ruleBranches(i),a=[],o=new Map,u=new Set(n.map(h));for(const r of t.searchEntries){const t=String(r.dn),s=h(c(t)),n=u.has(h(t));let l=n?void 0:o.get(s);void 0===l&&(l=!0===(await this.getUserPermissions(i,t)).read,n||o.set(s,l)),l?a.push(r):this.logger.debug(`${this.name}: hiding ${t} from ${e.user}, refused by a narrower branch rule`)}return t.searchEntries=a,s}const n=this.globalBranches();if(n.length){const r=new Map,s=[];for(const a of t.searchEntries){const t=n.find(t=>u(a.dn,t));void 0===t||(r.has(t)||r.set(t,!0===(await this.getUserPermissions(i,t)).read),r.get(t))?s.push(a):this.logger.debug(`${this.name}: hiding ${String(a.dn)} from ${e.user}, in the global branch ${t}`)}t.searchEntries=s}const a=this.config.ldap_organization_link_attribute;if(!a)return s;const o=await this.getAuthorizedBranches(i);if(0===o.length)return s;const l=this.transitKey(),d=new Map,reads=async t=>{if(e=t,!o.some(t=>u(e,t)))return!1;var e;const r=h(t);return d.has(r)||d.set(r,!0===(await this.getUserPermissions(i,t)).read),d.get(r)},f=[];for(const r of t.searchEntries){const t=r[a],s=null==t?[]:(Array.isArray(t)?t:[t]).map(String);if(0===s.length){f.push(r);continue}let i=!1;for(const t of s)if(await reads(t)){i=!0;break}i||s.some(t=>this.inTransit(t,l))?f.push(r):this.logger.debug(`${this.name}: hiding ${String(r.dn)} from ${e.user}, attached to ${s[0]}`)}if(t.searchEntries=f,r?.[AuthzBase.widenedForLink])for(const e of t.searchEntries)delete e[a];return[t,e,r]},getOrganisationTop:async([t,e])=>{if(this.shouldSkipAuthorization(t))return[t,e];const r=await this.resolveCaller(t);if(!r)return[t,e];const s=await this.getAuthorizedBranches(r);if(s.length>0){const e=[];for(const r of s)try{const s=await this.server.ldap.search({paged:!1,scope:"base"},r,t);1===s.searchEntries.length&&e.push(s.searchEntries[0])}catch(t){this.logger.warn(`Failed to fetch authorized branch ${r}: ${t}`)}if(1===e.length)return[t,e[0]];if(e.length>1)return[t,e[0]]}return[t,e]},ldaprenamerequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];const i=await this.storedDn(t),n=await this.storedParent(e),a=this.extractBranchDn(i),o=this.extractBranchDn(n),h=await this.getUserPermissions(s,a),c=this.sourcePermission(a);if(!h[c])throw new Error(`[authz-forbidden] User ${r.user} does not have ${c} permission for source branch ${a}`);const u=await this.refusingRule(s,i,["read","write"],!0);if(u)throw new Error(`[authz-forbidden] User ${r.user} does not have read and write permission for branch ${u}`);if(!(await this.getUserPermissions(s,o)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for destination branch ${o}`);const l=await this.refusingRule(s,n,["write"],!1);if(l)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for branch ${l}`);return[t,e,r]},ldapdeleterequest:async([t,e])=>{if(this.shouldSkipAuthorization(e))return[t,e];const r=await this.resolveCaller(e);if(!r)return[t,e];const s=Array.isArray(t)?t:[t];for(const t of s){const s=await this.storedDn(t),i=await this.effectiveBranch(s);if(!(await this.getUserPermissions(r,i)).delete)throw new Error(`[authz-forbidden] User ${e.user} does not have delete permission for branch ${i}`);const n=await this.refusingRule(r,s,["delete"],!0);if(n)throw new Error(`[authz-forbidden] User ${e.user} does not have delete permission for branch ${n}`)}return[t,e]}},r(this.config,this.constructor.name),w(this.config,this.constructor.name);const e=this.config.authz_unresolved_user??"deny";if(!AuthzBase.UNRESOLVED_POLICIES.includes(e))throw new Error(`${this.constructor.name}: unknown --authz-unresolved-user "${e}". Known: ${AuthzBase.UNRESOLVED_POLICIES.join(", ")}.`);this.config.authz_transit_branch&&!this.filtersAttachedEntries()&&this.logger.warn(`${this.constructor.name}: --authz-transit-branch is ignored, entries are not judged by their attachment here (--authz-filter-attached-entries)`)}static wantsAttribute(t,e){const r=t?.attributes;if(void 0===r)return!0;const s=(Array.isArray(r)?r:[r]).map(t=>String(t).toLowerCase());return 0===s.length||(!!s.includes("*")||s.includes(e.toLowerCase()))}extractBranchDn(t){return c(t)}filtersAttachedEntries(){return Boolean(this.config.authz_filter_attached_entries)}judgesByAttachment(){return this.filtersAttachedEntries()}globalBranches(){return[]}async searchRefusal(t,e){if(this.filtersAttachedEntries()){if(0===(await this.getAuthorizedBranches(t)).length)return"administers no branch";if(!this.globalBranches().some(t=>u(e,t)))return}return(await this.getUserPermissions(t,e)).read?void 0:`does not have read permission for branch ${e}`}sourcePermission(t){return"read"}async storedDn(t){try{const e=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:["1.1"]},t),r=e.searchEntries?.[0]?.dn;if(r)return String(r)}catch(e){if(32!==e?.code)throw this.logger.error(`${this.name}: ${t} could not be read to be authorized: ${String(e)}`),new i("Directory unavailable")}return t}async storedParent(t){const e=c(t);return e===t?t:`${d(t)},${await this.storedDn(e)}`}async effectiveBranch(t){const e=this.config.ldap_organization_link_attribute;if(!e||!this.filtersAttachedEntries())return this.extractBranchDn(t);try{const r=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[e]},t),s=l(r.searchEntries?.[0]?.[e]);if(s)return s}catch{}return this.extractBranchDn(t)}transitBranch(){if(!this.filtersAttachedEntries())return;const t=String(this.config.authz_transit_branch??"").trim();return t.length>0?t:void 0}inTransit(t,e=this.transitKey()){return void 0===t||void 0!==e&&h(t)===e}transitOriginAttribute(){if(void 0===this.transitBranch())return;const t=String(this.config.authz_transit_origin_attribute??"").trim();return t.length>0?t:void 0}async recordTransitOrigin(t,e){const r=this.transitOriginAttribute(),s=this.config.ldap_organization_link_attribute;if(!r||!s)return e;const same=(t,e)=>t.split(";")[0].toLowerCase()===e.toLowerCase(),named=(t,e)=>void 0!==t&&Object.keys(t).some(t=>same(t,e)),deleted=t=>Array.isArray(e.delete)?e.delete.some(e=>same(e,t)):named(e.delete,t);let i,n;if(named(e.replace,s))i=l(AuthzBase.valuesOf(e.replace,s));else if(named(e.add,s))i=l(AuthzBase.valuesOf(e.add,s));else if(!deleted(s))return e;if(named(e.replace,r)||named(e.add,r)||deleted(r))return e;try{n=(await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[s,r]},t)).searchEntries[0]}catch{}if(!n)return e;const a=l(AuthzBase.valuesOf(n,s)),o=this.inTransit(a),h=this.inTransit(i);return!o&&h&&a?{...e,replace:{...e.replace,[r]:a}}:o&&!h&&AuthzBase.valuesOf(n,r).length>0?{...e,delete:Array.isArray(e.delete)?[...e.delete,r]:{...e.delete,[r]:[]}}:e}transitKey(){const t=this.transitBranch();return void 0===t?void 0:h(t)}async findUserDn(t){const e=`(${this.config.ldap_user_main_attribute||"uid"}=${f(t)})`,r=(await this.server.ldap.system.search({paged:!1,filter:e,attributes:["dn"],scope:"sub"},this.server.ldap.base)).searchEntries??[];if(r.length>1)throw new n(t,r.length);if(0===r.length)return null;const s=r[0].dn;return"string"==typeof s?s:String(s)}onlyRelinks(t){const e=[this.config.ldap_organization_link_attribute,this.config.ldap_organization_path_attribute].filter(t=>"string"==typeof t).map(t=>t.toLowerCase());return[...Object.keys(t.add??{}),...Object.keys(t.replace??{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete??{})].every(t=>e.includes(String(t).split(";")[0].toLowerCase()))}async refusingRule(t,e,r,s){const i=h(e);for(const n of await this.ruleBranches(t)){if(!(h(n)===i||s&&u(n,e)))continue;const a=await this.getUserPermissions(t,n);if(r.some(t=>!a[t]))return n}}shouldSkipAuthorization(t){return!t?.user||!p(t,this.config)}async resolveCaller(t){const{value:e,fellBack:r}=s(t,this.config);r&&!this.fallbackWarned&&(this.fallbackWarned=!0,this.logger.warn(`${this.name}: --authz-identity asks for req.userName and the authenticator published none, so permissions are read for req.user instead. Rules written on logins will not match`));const i=e,n=Date.now(),o=this.resolutionCache.get(i);let h;if(o&&n-o.at<this.cacheTTL)h=o.user;else{if(h=await this.resolveUser(i),this.resolutionCache.size>=AuthzBase.RESOLUTION_CACHE_MAX)for(const[t,e]of this.resolutionCache)n-e.at>=this.cacheTTL&&this.resolutionCache.delete(t);this.resolutionCache.size<AuthzBase.RESOLUTION_CACHE_MAX?this.resolutionCache.set(i,{user:h,at:n}):this.resolutionCacheFullWarned||(this.resolutionCacheFullWarned=!0,this.logger.warn(`${this.name}: more than ${AuthzBase.RESOLUTION_CACHE_MAX} identities resolved within one cache window, so resolutions are no longer cached and each request costs a lookup`))}if(h)return h;if("allow"===this.config.authz_unresolved_user)return this.logger.warn(`User ${i} could not be resolved by ${this.name}; allowed by --authz-unresolved-user allow`),null;throw new a(`[authz-forbidden] User ${i} could not be resolved by ${this.name}, so no permission can be read for them`)}domainLinkAttribute(){const t=this.server.loadedPlugins.ldapOrganizations;return e(t?.schema,"domainLink")||this.config.enterprise_domain_link_attribute||void 0}static organizationLinks(t,e){if(e.some(t=>""===t.trim()))throw new o(`${t} cannot hold an empty value`);const r=[...new Map(e.map(t=>[h(t),t])).values()];if(r.length>1)throw new o(`${t} attaches an entry to one organization, not ${r.length}`);return r}static valuesOf(t,e){if(!t)return[];const r=e.toLowerCase(),s=[];for(const[e,i]of Object.entries(t))if(e.split(";")[0].toLowerCase()===r&&null!=i)for(const t of Array.isArray(i)?i:[i])s.push(String(t));return s}async storedValues(t,e){try{const r=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[e]},t);return AuthzBase.valuesOf(r.searchEntries?.[0],e)}catch{return[]}}async guardDomainLinks(t,e,r,s,i,n){if(0===s.length)return;const a=new Set((n?[]:await this.storedValues(r,i)).map(h)),o=s.filter(t=>!a.has(h(t)));if(0===o.length)return;const l=this.config.ldap_top_organization,d=h(l||this.server.ldap.base),f=new Set;let g=c(r);const w=new Set;for(;g&&!w.has(h(g));){const t=h(g);if(w.add(t),t!==d&&!u(g,d))break;for(const t of await this.storedValues(g,i))f.add(h(t));if(t===d)break;const e=c(g);if(e===g)break;g=e}const p=o.filter(t=>!f.has(h(t)));if(0!==p.length&&!await this.mayLinkAnyDomain(t))throw new Error(`[authz-forbidden] User ${e.user} may not link ${r} to domain ${p[0]}: no organization above it is linked to it`)}async mayLinkAnyDomain(t){const e=this.config.ldap_top_organization;return!!e&&!0===(await this.getUserPermissions(t,e)).write}clearResolutionCache(){this.resolutionCache.clear()}}AuthzBase.widenedForLink=Symbol("widened for the link"),AuthzBase.topOrganizationRead=Symbol("top organization"),AuthzBase.UNRESOLVED_POLICIES=["deny","allow"],AuthzBase.RESOLUTION_CACHE_MAX=1e4;export{AuthzBase as default,mostSpecificBranchRule};
|
|
1
|
+
import t from"../../abstract/plugin.js";import{roleAttribute as e}from"../../config/schema.js";import{assertIdentityMode as r,identityFor as s}from"../auth/base.js";import{ServiceUnavailableError as i,AmbiguousIdentityError as n,AuthzRefusedError as a,BadRequestError as o}from"../errors.js";import{getParentDn as h,isDnInBranch as c,getRdn as u,organizationLink as l,normalizeDn as d,escapeLdapFilter as f,parseDn as g}from"../utils.js";import{authzFor as w,servesRequest as p}from"./composition.js";function mostSpecificBranchRule(t,e){let r=null,s=-1;for(const[i,n]of t){if(!c(e,i))continue;const t=g(i).length;t>s&&([r,s]=[n,t])}return r}class AuthzBase extends t{static widenedForLink=Symbol("widened for the link");static topOrganizationRead=Symbol("top organization");roles=["authz"];cacheTTL;static UNRESOLVED_POLICIES=["deny","allow"];constructor(t){super(t),r(this.config,this.constructor.name),w(this.config,this.constructor.name);const e=this.config.authz_unresolved_user??"deny";if(!AuthzBase.UNRESOLVED_POLICIES.includes(e))throw new Error(`${this.constructor.name}: unknown --authz-unresolved-user "${e}". Known: ${AuthzBase.UNRESOLVED_POLICIES.join(", ")}.`);this.config.authz_transit_branch&&!this.filtersAttachedEntries()&&this.logger.warn(`${this.constructor.name}: --authz-transit-branch is ignored, entries are not judged by their attachment here (--authz-filter-attached-entries)`)}static wantsAttribute(t,e){const r=t?.attributes;if(void 0===r)return!0;const s=(Array.isArray(r)?r:[r]).map(t=>String(t).toLowerCase());return 0===s.length||(!!s.includes("*")||s.includes(e.toLowerCase()))}extractBranchDn(t){return h(t)}filtersAttachedEntries(){return Boolean(this.config.authz_filter_attached_entries)}judgesByAttachment(){return this.filtersAttachedEntries()}globalBranches(){return[]}async searchRefusal(t,e){if(this.filtersAttachedEntries()){if(0===(await this.getAuthorizedBranches(t)).length)return"administers no branch";if(!this.globalBranches().some(t=>c(e,t)))return}return(await this.getUserPermissions(t,e)).read?void 0:`does not have read permission for branch ${e}`}sourcePermission(t){return"read"}async storedDn(t){try{const e=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:["1.1"]},t),r=e.searchEntries?.[0]?.dn;if(r)return String(r)}catch(e){if(32!==e?.code)throw this.logger.error(`${this.name}: ${t} could not be read to be authorized: ${String(e)}`),new i("Directory unavailable")}return t}async storedParent(t){const e=h(t);return e===t?t:`${u(t)},${await this.storedDn(e)}`}async effectiveBranch(t){const e=this.config.ldap_organization_link_attribute;if(!e||!this.filtersAttachedEntries())return this.extractBranchDn(t);try{const r=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[e]},t),s=l(r.searchEntries?.[0]?.[e]);if(s)return s}catch{}return this.extractBranchDn(t)}transitBranch(){if(!this.filtersAttachedEntries())return;const t=String(this.config.authz_transit_branch??"").trim();return t.length>0?t:void 0}inTransit(t,e=this.transitKey()){return void 0===t||void 0!==e&&d(t)===e}transitOriginAttribute(){if(void 0===this.transitBranch())return;const t=String(this.config.authz_transit_origin_attribute??"").trim();return t.length>0?t:void 0}async recordTransitOrigin(t,e){const r=this.transitOriginAttribute(),s=this.config.ldap_organization_link_attribute;if(!r||!s)return e;const same=(t,e)=>t.split(";")[0].toLowerCase()===e.toLowerCase(),named=(t,e)=>void 0!==t&&Object.keys(t).some(t=>same(t,e)),deleted=t=>Array.isArray(e.delete)?e.delete.some(e=>same(e,t)):named(e.delete,t);let i,n;if(named(e.replace,s))i=l(AuthzBase.valuesOf(e.replace,s));else if(named(e.add,s))i=l(AuthzBase.valuesOf(e.add,s));else if(!deleted(s))return e;if(named(e.replace,r)||named(e.add,r)||deleted(r))return e;try{n=(await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[s,r]},t)).searchEntries[0]}catch{}if(!n)return e;const a=l(AuthzBase.valuesOf(n,s)),o=this.inTransit(a),h=this.inTransit(i);return!o&&h&&a?{...e,replace:{...e.replace,[r]:a}}:o&&!h&&AuthzBase.valuesOf(n,r).length>0?{...e,delete:Array.isArray(e.delete)?[...e.delete,r]:{...e.delete,[r]:[]}}:e}transitKey(){const t=this.transitBranch();return void 0===t?void 0:d(t)}async findUserDn(t){const e=`(${this.config.ldap_user_main_attribute||"uid"}=${f(t)})`,r=(await this.server.ldap.system.search({paged:!1,filter:e,attributes:["dn"],scope:"sub"},this.server.ldap.base)).searchEntries??[];if(r.length>1)throw new n(t,r.length);if(0===r.length)return null;const s=r[0].dn;return"string"==typeof s?s:String(s)}onlyRelinks(t){const e=[this.config.ldap_organization_link_attribute,this.config.ldap_organization_path_attribute].filter(t=>"string"==typeof t).map(t=>t.toLowerCase());return[...Object.keys(t.add??{}),...Object.keys(t.replace??{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete??{})].every(t=>e.includes(String(t).split(";")[0].toLowerCase()))}async refusingRule(t,e,r,s){const i=d(e);for(const n of await this.ruleBranches(t)){if(!(d(n)===i||s&&c(n,e)))continue;const a=await this.getUserPermissions(t,n);if(r.some(t=>!a[t]))return n}}shouldSkipAuthorization(t){return!t?.user||!p(t,this.config)}resolutionCache=new Map;static RESOLUTION_CACHE_MAX=1e4;resolutionCacheFullWarned=!1;fallbackWarned=!1;async resolveCaller(t){const{value:e,fellBack:r}=s(t,this.config);r&&!this.fallbackWarned&&(this.fallbackWarned=!0,this.logger.warn(`${this.name}: --authz-identity asks for req.userName and the authenticator published none, so permissions are read for req.user instead. Rules written on logins will not match`));const i=e,n=Date.now(),o=this.resolutionCache.get(i);let h;if(o&&n-o.at<this.cacheTTL)h=o.user;else{if(h=await this.resolveUser(i),this.resolutionCache.size>=AuthzBase.RESOLUTION_CACHE_MAX)for(const[t,e]of this.resolutionCache)n-e.at>=this.cacheTTL&&this.resolutionCache.delete(t);this.resolutionCache.size<AuthzBase.RESOLUTION_CACHE_MAX?this.resolutionCache.set(i,{user:h,at:n}):this.resolutionCacheFullWarned||(this.resolutionCacheFullWarned=!0,this.logger.warn(`${this.name}: more than ${AuthzBase.RESOLUTION_CACHE_MAX} identities resolved within one cache window, so resolutions are no longer cached and each request costs a lookup`))}if(h)return h;if("allow"===this.config.authz_unresolved_user)return this.logger.warn(`User ${i} could not be resolved by ${this.name}; allowed by --authz-unresolved-user allow`),null;throw new a(`User ${i} could not be resolved by ${this.name}, so no permission can be read for them`)}domainLinkAttribute(){const t=this.server.loadedPlugins.ldapOrganizations;return e(t?.schema,"domainLink")||this.config.enterprise_domain_link_attribute||void 0}static organizationLinks(t,e){if(e.some(t=>""===t.trim()))throw new o(`${t} cannot hold an empty value`);const r=[...new Map(e.map(t=>[d(t),t])).values()];if(r.length>1)throw new o(`${t} attaches an entry to one organization, not ${r.length}`);return r}static valuesOf(t,e){if(!t)return[];const r=e.toLowerCase(),s=[];for(const[e,i]of Object.entries(t))if(e.split(";")[0].toLowerCase()===r&&null!=i)for(const t of Array.isArray(i)?i:[i])s.push(String(t));return s}async storedValues(t,e){try{const r=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[e]},t);return AuthzBase.valuesOf(r.searchEntries?.[0],e)}catch{return[]}}async guardDomainLinks(t,e,r,s,i,n){if(0===s.length)return;const o=new Set((n?[]:await this.storedValues(r,i)).map(d)),u=s.filter(t=>!o.has(d(t)));if(0===u.length)return;const l=this.config.ldap_top_organization,f=d(l||this.server.ldap.base),g=new Set;let w=h(r);const p=new Set;for(;w&&!p.has(d(w));){const t=d(w);if(p.add(t),t!==f&&!c(w,f))break;for(const t of await this.storedValues(w,i))g.add(d(t));if(t===f)break;const e=h(w);if(e===w)break;w=e}const m=u.filter(t=>!g.has(d(t)));if(0!==m.length&&!await this.mayLinkAnyDomain(t))throw new a(`User ${e.user} may not link ${r} to domain ${m[0]}: no organization above it is linked to it`)}async mayLinkAnyDomain(t){const e=this.config.ldap_top_organization;return!!e&&!0===(await this.getUserPermissions(t,e)).write}clearResolutionCache(){this.resolutionCache.clear()}hooks={ldaprenamedone:()=>{this.resolutionCache.clear()},ldapdeletedone:()=>{this.resolutionCache.clear()},ldapmodifyrequest:async([t,e,r,s])=>{if(this.shouldSkipAuthorization(s))return[t,await this.recordTransitOrigin(t,e),r,s];const i=await this.resolveCaller(s);if(!i)return[t,await this.recordTransitOrigin(t,e),r,s];const n=await this.storedDn(t),o=this.config.ldap_organization_link_attribute,h=o?AuthzBase.organizationLinks(o,[...AuthzBase.valuesOf(e.replace,o),...AuthzBase.valuesOf(e.add,o)]):[];if(o&&h.length>0){let r,c=this.extractBranchDn(n),u=!1;try{const e=(await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[o]},t)).searchEntries[0];e&&(u=!0,r=l(e[o]),r&&(c=r))}catch{}const d=await this.storedDn(h[0]),f=this.filtersAttachedEntries(),g=f&&u&&this.inTransit(r),w=f&&this.inTransit(d);if(!g){const t=w?"write":this.sourcePermission(c);if(!(await this.getUserPermissions(i,c))[t])throw new a(`User ${s.user} does not have ${t} permission for source branch ${c}`)}if(w){if(g){if(0===(await this.getAuthorizedBranches(i)).length)throw new a(`User ${s.user} administers no branch`)}}else{if(!(await this.getUserPermissions(i,d)).write)throw new a(`User ${s.user} does not have write permission for destination branch ${d}`)}if(!g&&!this.onlyRelinks(e)){const t=f?c:this.extractBranchDn(n);if(!(await this.getUserPermissions(i,t)).write)throw new a(`User ${s.user} does not have write permission for branch ${t}`)}}else{const t=await this.effectiveBranch(n);if(!(await this.getUserPermissions(i,t)).write)throw new a(`User ${s.user} does not have write permission for branch ${t}`)}const c=await this.refusingRule(i,n,["write"],!1);if(c)throw new a(`User ${s.user} does not have write permission for branch ${c}`);const u=this.domainLinkAttribute();return u&&await this.guardDomainLinks(i,s,t,[...AuthzBase.valuesOf(e.add,u),...AuthzBase.valuesOf(e.replace,u)],u,!1),[t,await this.recordTransitOrigin(t,e),r,s]},ldapaddrequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];let i;const n=this.config.ldap_organization_link_attribute,o=n?AuthzBase.organizationLinks(n,AuthzBase.valuesOf(e,n)):[],h=await this.storedParent(t);i=o.length>0?await this.storedDn(o[0]):this.extractBranchDn(h);if(!(await this.getUserPermissions(s,i)).write)throw new a(`User ${r.user} does not have write permission for branch ${i}`);const c=await this.refusingRule(s,h,["write"],!1);if(c)throw new a(`User ${r.user} does not have write permission for branch ${c}`);const u=this.domainLinkAttribute();return u&&await this.guardDomainLinks(s,r,t,AuthzBase.valuesOf(e,u),u,!0),[t,e,r]},ldapsearchrequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];if(t===this.config.ldap_top_organization&&"base"===e.scope)return[t,{...e,[AuthzBase.topOrganizationRead]:!0},r];const i=await this.searchRefusal(s,t);if(i)throw new a(`User ${r.user} ${i}`);if(this.filtersAttachedEntries()){const s=this.config.ldap_organization_link_attribute;if(s&&!AuthzBase.wantsAttribute(e,s)){const t=e.attributes,r=Array.isArray(t)?t.map(String):[String(t)];e={...e,attributes:[...r,s],[AuthzBase.widenedForLink]:!0}}return[t,e,r]}return[t,e,r]},ldapsearchfilter:async([t,e,r])=>{const s=[t,e,r];if(this.shouldSkipAuthorization(e))return s;if(!t?.searchEntries?.length)return s;const i=await this.resolveCaller(e);if(!i)return s;if(!this.filtersAttachedEntries()){if(r?.[AuthzBase.topOrganizationRead])return s;const n=await this.ruleBranches(i),a=[],o=new Map,c=new Set(n.map(d));for(const r of t.searchEntries){const t=String(r.dn),s=d(h(t)),n=c.has(d(t));let u=n?void 0:o.get(s);void 0===u&&(u=!0===(await this.getUserPermissions(i,t)).read,n||o.set(s,u)),u?a.push(r):this.logger.debug(`${this.name}: hiding ${t} from ${e.user}, refused by a narrower branch rule`)}return t.searchEntries=a,s}const n=this.globalBranches();if(n.length){const r=new Map,s=[];for(const a of t.searchEntries){const t=n.find(t=>c(a.dn,t));void 0===t||(r.has(t)||r.set(t,!0===(await this.getUserPermissions(i,t)).read),r.get(t))?s.push(a):this.logger.debug(`${this.name}: hiding ${String(a.dn)} from ${e.user}, in the global branch ${t}`)}t.searchEntries=s}const a=this.config.ldap_organization_link_attribute;if(!a)return s;const o=await this.getAuthorizedBranches(i);if(0===o.length)return s;const u=this.transitKey(),l=new Map,reads=async t=>{if(e=t,!o.some(t=>c(e,t)))return!1;var e;const r=d(t);return l.has(r)||l.set(r,!0===(await this.getUserPermissions(i,t)).read),l.get(r)},f=[];for(const r of t.searchEntries){const t=r[a],s=null==t?[]:(Array.isArray(t)?t:[t]).map(String);if(0===s.length){f.push(r);continue}let i=!1;for(const t of s)if(await reads(t)){i=!0;break}i||s.some(t=>this.inTransit(t,u))?f.push(r):this.logger.debug(`${this.name}: hiding ${String(r.dn)} from ${e.user}, attached to ${s[0]}`)}if(t.searchEntries=f,r?.[AuthzBase.widenedForLink])for(const e of t.searchEntries)delete e[a];return[t,e,r]},getOrganisationTop:async([t,e])=>{if(this.shouldSkipAuthorization(t))return[t,e];const r=await this.resolveCaller(t);if(!r)return[t,e];const s=await this.getAuthorizedBranches(r);if(s.length>0){const e=[];for(const r of s)try{const s=await this.server.ldap.search({paged:!1,scope:"base"},r,t);1===s.searchEntries.length&&e.push(s.searchEntries[0])}catch(t){this.logger.warn(`Failed to fetch authorized branch ${r}: ${t}`)}if(1===e.length)return[t,e[0]];if(e.length>1)return[t,e[0]]}return[t,e]},ldaprenamerequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];const i=await this.storedDn(t),n=await this.storedParent(e),o=this.extractBranchDn(i),h=this.extractBranchDn(n),c=await this.getUserPermissions(s,o),u=this.sourcePermission(o);if(!c[u])throw new a(`User ${r.user} does not have ${u} permission for source branch ${o}`);const l=await this.refusingRule(s,i,["read","write"],!0);if(l)throw new a(`User ${r.user} does not have read and write permission for branch ${l}`);if(!(await this.getUserPermissions(s,h)).write)throw new a(`User ${r.user} does not have write permission for destination branch ${h}`);const d=await this.refusingRule(s,n,["write"],!1);if(d)throw new a(`User ${r.user} does not have write permission for branch ${d}`);return[t,e,r]},ldapdeleterequest:async([t,e])=>{if(this.shouldSkipAuthorization(e))return[t,e];const r=await this.resolveCaller(e);if(!r)return[t,e];const s=Array.isArray(t)?t:[t];for(const t of s){const s=await this.storedDn(t),i=await this.effectiveBranch(s);if(!(await this.getUserPermissions(r,i)).delete)throw new a(`User ${e.user} does not have delete permission for branch ${i}`);const n=await this.refusingRule(r,s,["delete"],!0);if(n)throw new a(`User ${e.user} does not have delete permission for branch ${n}`)}return[t,e]}}}export{AuthzBase as default,mostSpecificBranchRule};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BadRequestError as e}from"../errors.js";import{
|
|
1
|
+
import{BadRequestError as e}from"../errors.js";import{knownAttributeName as t}from"../utils.js";import{toDirectoryEntry as r,missingRequiredAttribute as i,toDirectoryChanges as s,schemaAttribute as a,tooLongValue as n,tooLongMessage as o,maxLengthOf as d,checkDnValues as c,matchesPattern as u,invalidValueMessage as l}from"../../config/schema.js";class EntityValidator{schema;deps;constructor(e,t){this.schema=e,this.deps=t}async validateNew(t){const s=this.renameDeclared(t);if(this.dropUndeclaredEmpty(s),!this.schema)return r(void 0,s),s;applyFixedDefaults(this.schema,s),await this.checkValues(s);const a=i(this.schema,s,this.deps.loadedPlugins);if(a)throw new e(`Attribute "${a}" is required`);return r(this.schema,s),s}async validateChanges(t){t.add&&(t.add=this.renameDeclared(t.add)),t.replace&&(t.replace=this.renameDeclared(t.replace)),t.delete&&(t.delete=Array.isArray(t.delete)?this.renameDeclaredNames(t.delete):this.renameDeclared(t.delete));for(const e of["add","replace"]){const r=t[e];r&&("add"===e&&this.dropUndeclaredEmpty(r),await this.checkValues(r,e))}if(!this.schema)return s(void 0,t),t;if(t.delete){const whole=e=>{if(Array.isArray(t.delete))return!0;const r=t.delete[e];return!r||isNoValue(r)},r=Array.isArray(t.delete)?t.delete:Object.keys(t.delete);for(const t of r){const r=this.attribute(t);if(r?.fixed)throw new e(`Attribute "${t}" is fixed and cannot be deleted`);if(r?.required&&whole(t))throw new e(`Attribute "${t}" is required`)}}return s(this.schema,t),t}renameDeclared(e){const t=Object.keys(e),r=this.renameDeclaredNames(t);if(t.every((e,t)=>e===r[t]))return e;const i=Object.create(null);for(const[s,a]of r.entries())i[a]=e[t[s]];return i}renameDeclaredNames(t){const r=new Map;return t.map(t=>{const i=this.attributeIdentity(t),s=r.get(i);if(void 0!==s&&s!==t)throw new e(`"${s}" and "${t}" name the same attribute`);return r.set(i,t),this.schemaSpelling(t)})}attributeIdentity(e){const r=split(e)[0];return(a(this.schema,r)?.[0]??t(r)??r).toLowerCase()}schemaSpelling(e){const[t,r]=split(e),i=a(this.schema,t)?.[0];return void 0===i?e:r?`${i};${r}`:i}dropUndeclaredEmpty(e){for(const[t,r]of Object.entries(e))null!=r||this.attribute(t)||delete e[t]}async checkValues(t,r){for(const[i,s]of Object.entries(t)){const a=this.attribute(i);if(a?.fixed&&r)throw new e(`Attribute "${i}" is fixed and cannot be modified`);if(a?.required&&!r&&isMissing(s)&&!a.generated)throw new e(`Attribute "${i}" is required`);(a||r)&&(t[i]=asStored(await this.validateValue(i,s)))}}async validateValue(t,r){if(!this.schema)return r;const i=this.attribute(t);if(!i)return r;if(isNoValue(r)){if(i.required)throw new e(`Attribute "${t}" is required`);return r}let s=r;if("array"===i.type){Array.isArray(r)||(s=[r]);for(const e of s)assertScalar(t,e,i.items?.type)}else{if(Array.isArray(r)){if(r.length>1)throw new e(`Attribute "${t}" must be a single value`);s=r[0]}i.normalize&&"string"==typeof s||assertScalar(t,s,i.type)}if(void 0!==n(i,s))throw new e(o(t,d(i)));if(await c(t,i,s,this.deps.entryExists),!u(i,s))throw new e(l(t,i));return s}attribute(t){const r=a(this.schema,t)?.[1];if(!r&&this.schema?.strict)throw new e(`Unknown attribute "${t}" for ${this.deps.singularName}`);return r}}function applyFixedDefaults(e,t){if(e)for(const[r,i]of Object.entries(e.attributes))i.fixed&&void 0!==i.default&&"objectclass"!==r.toLowerCase()&&(t[r]=i.default)}const split=e=>{const[t,...r]=e.split(";");return[t,r.find(Boolean)?.toLowerCase()]},isNoValue=e=>null==e||Array.isArray(e)&&0===e.length,isMissing=e=>isNoValue(e)||""===e,asStored=e=>isNoValue(e)?[]:e,f=/^-?\d+(\.\d+)?$/,h=/^-?\d+$/;function assertScalar(t,r,i){const s="number"==typeof r&&Number.isFinite(r)?String(r):void 0,reads=e=>void 0!==s?e.test(s):"string"==typeof r&&e.test(r);if(!(()=>{switch(i){case"number":return reads(f)&&("number"!=typeof r||!Number.isInteger(r)||Number.isSafeInteger(r));case"integer":return reads(h)&&("number"!=typeof r||Number.isSafeInteger(r));case"boolean":return"boolean"==typeof r||"string"==typeof r&&("TRUE"===r||"FALSE"===r);default:return"string"==typeof r||Buffer.isBuffer(r)}})()){throw new e(`Attribute "${t}" must be ${"number"===i||"integer"===i||"boolean"===i?`a${"integer"===i?"n":""} ${i}`:"a string"}`)}}export{EntityValidator,applyFixedDefaults};
|
package/dist/lib/errors.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
class HttpError extends Error{constructor(r,
|
|
1
|
+
class HttpError extends Error{statusCode;refusal;constructor(r,t=500,o){super(r),this.statusCode=t,this.refusal=o,this.name="HttpError"}}class BadRequestError extends HttpError{constructor(r="Bad request"){super(r,400),this.name="BadRequestError"}}class UnauthorizedError extends HttpError{constructor(r="Unauthorized"){super(r,401),this.name="UnauthorizedError"}}const r="Token does not have permission on this branch",errorBody=(t,o)=>{if(o===r)return{error:o,code:"forbidden"};const e=errorChain(t).find(r=>r instanceof HttpError)?.refusal;return e?{error:o,...e}:{error:o}};class ForbiddenError extends HttpError{constructor(r="Forbidden"){super(r,403),this.name="ForbiddenError"}}class AuthzRefusedError extends ForbiddenError{authzRefusal=!0;constructor(r){super(r),this.name="AuthzRefusedError"}}const errorChain=r=>{const t=[];let o=r;for(;o instanceof Error&&!t.includes(o)&&(t.push(o),32!==t.length);)o=o.cause;return t},isAuthzRefusal=r=>errorChain(r).some(r=>!0===r.authzRefusal),statusCodeOf=r=>{const t=errorChain(r).find(r=>"number"==typeof r.statusCode);return t?.statusCode??500};class AmbiguousIdentityError extends AuthzRefusedError{identity;count;constructor(r,t){super(`${r} names ${t} directory entries, so no single one can be taken for the caller`),this.identity=r,this.count=t,this.name="AmbiguousIdentityError"}}class NotFoundError extends HttpError{constructor(r="Not found"){super(r,404),this.name="NotFoundError"}}class ConflictError extends HttpError{constructor(r="Conflict",t){super(r,409,t),this.name="ConflictError"}}class UriTooLongError extends HttpError{constructor(r="URI Too Long"){super(r,414),this.name="UriTooLongError"}}class UnprocessableEntityError extends HttpError{constructor(r="Unprocessable Entity"){super(r,422),this.name="UnprocessableEntityError"}}class TooManyRequestsError extends HttpError{constructor(r="Too Many Requests"){super(r,429),this.name="TooManyRequestsError"}}class BadGatewayError extends HttpError{constructor(r="Bad Gateway"){super(r,502),this.name="BadGatewayError"}}class ServiceUnavailableError extends HttpError{constructor(r="Service Unavailable"){super(r,503),this.name="ServiceUnavailableError"}}class GatewayTimeoutError extends HttpError{constructor(r="Gateway Timeout"){super(r,504),this.name="GatewayTimeoutError"}}export{r as AUTHZ_REFUSED,AmbiguousIdentityError,AuthzRefusedError,BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UnprocessableEntityError,UriTooLongError,errorBody,errorChain,isAuthzRefusal,statusCodeOf};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{Type as r}from"@sinclair/typebox";import{isAuthzRefusal as t,statusCodeOf as e,errorBody as n,AUTHZ_REFUSED as o}from"./errors.js";let s;const setLogger=r=>{s=r},getLogger=()=>s;let a=!1;const setDebug=r=>{a=r},clientMessageOf=r=>t(r)?o:messageOf(r),messageOf=r=>r instanceof Error?r.message:String(r),errorResponse=(r,o=a)=>{const s=t(r)?403:e(r);return s>=400&&s<500?{status:s,body:n(r,clientMessageOf(r))}:{status:s,body:serverErrorBody(r,o)}},serverErrorBody=(r,t=a)=>({error:"Internal Server Error",message:t?messageOf(r):"An error occurred"}),tryMethod=async(r,t,...e)=>{try{return await t(...e),ok(r)}catch(t){return serverError(r,t)}},tryMethodData=async(r,t,...e)=>{try{const n=await t(...e);return ok(r,n)}catch(t){return serverError(r,t)}},_rejectResponse=(r,t,e)=>{s?.warn(`Client error ${r}: ${e}`),t.status(r).json({error:e})},c=r.Object({success:r.Boolean()}),ok=(r,t={success:!0})=>{r.status(200).json(t)},created=(r,t)=>{r.status(201).json(t)},multiStatus=(r,t)=>{r.status(207).json(t)},badRequest=(r,t="Bad request")=>_rejectResponse(400,r,t),unauthorized=(r,t="Unauthorized")=>_rejectResponse(401,r,t),forbidden=(r,t="Forbidden")=>_rejectResponse(403,r,t),notFound=(r,t="Not found")=>_rejectResponse(404,r,t),serverError=(r,t)=>{const{status:e,body:n}=errorResponse(t,a);if(e<500)return s?.warn(`Client error ${e}: ${n.error}`),void r.status(e).json(n);t instanceof Error?s?.error(t.message,t):"string"==typeof t?s?.error(t):s?.error("Server error",t),r.status(e).json(n)},wantJson=(r,t)=>!1!==r.accepts("json")||(badRequest(t),!1),jsonBody=(r,t,...e)=>{try{if(!wantJson(r,t))return!1;const n=r.body;if(null===n||"object"!=typeof n||Array.isArray(n))return badRequest(t,"Bad content"),!1;if(e.length>0)for(let r=0;r<e.length;r++)if(null==n[e[r]])return badRequest(t,"Bad content"),!1;return n}catch(r){return badRequest(t,"Bad content"),!1}};export{c as WriteResponse,badRequest,clientMessageOf,created,errorResponse,forbidden,getLogger,jsonBody,messageOf,multiStatus,notFound,ok,serverError,serverErrorBody,setDebug,setLogger,tryMethod,tryMethodData,unauthorized,wantJson};
|
package/dist/lib/health.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
import{defineRoute as e}from"./route.js";const t=["/healthz","/readyz"],s=5e3,a=2e3,o=new WeakMap,withTimeout=async(e,t)=>{let s;const a=new Promise((e,a)=>{s=setTimeout(()=>a(new Error("timed out")),t)});try{return await Promise.race([e,a])}finally{clearTimeout(s)}};function readiness(e,t=Date.now(),s=2e3){if(e.stopping)return Promise.resolve({status:"stopping"});if(!e.composed)return Promise.resolve({status:"starting"});const a=o.get(e);if(a&&t>=a.at&&t-a.at<5e3)return a.answer;const r=async function(e,t){const s=withTimeout(e.ldap.ping(t),t).then(()=>!0,()=>!1),a=Object.entries(e.loadedPlugins).filter(([,e])=>e.health).map(async([e,s])=>{try{return[e,await withTimeout(Promise.resolve(s.health()),t)]}catch{return[e,"failed"]}}),[o,r]=await Promise.all([s,Promise.all(a)]);if(!o)return{status:"unavailable",ldap:"failed"};const n={};for(const[e,t]of r)n[e]=t;return{status:Object.values(n).some(e=>"ok"!==e)?"degraded":"ok",ldap:"ok",plugins:n}}(e,s);return o.set(e,{at:t,answer:r}),r}function mountHealth(s,a){const[o,r]=t;e(s,{method:"get",path:o,json:!1,handler:({res:e})=>{e.setHeader("Cache-Control","no-store"),e.json({status:"ok"})}}),e(s,{method:"get",path:r,json:!1,handler:async({res:e})=>{const t=await readiness(a);e.setHeader("Cache-Control","no-store");const s="ok"===t.status||"degraded"===t.status;e.status(s?200:503).json(t)}})}export{t as HEALTH_PATHS,a as PROBE_TIMEOUT_MS,s as PROBE_TTL_MS,mountHealth,readiness};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{statusCodeOf as t,isAuthzRefusal as s}from"./errors.js";import{getLogger as o}from"./expressFormatedResponses.js";import{hookFailures as r}from"./metrics.js";import{runAsPlugin as e,outsideRequest as n}from"./requestContext.js";const i=new WeakMap,recordHookOwner=(t,s,o)=>{i.set(t,{plugin:s,hook:o})},hookOwner=t=>i.get(t),runAsHookOwner=(t,s)=>{const o=hookOwner(t);return e(o?.plugin,o?.hook,s)},isRefusal=o=>403===t(o)||s(o),report=(t,s)=>{const e=(t=>{const s=hookOwner(t);return s?` in ${s.plugin} (${s.hook})`:t.name?` in ${t.name}`:""})(t),n=hookOwner(t);r.inc({hook:n?.hook??(t.name||"anonymous"),plugin:n?.plugin??"none"}),s instanceof Error?o()?.error(`Hook error${e}`,s):o()?.error(`Hook error${e}: ${(t=>{try{return JSON.stringify(t)??String(t)}catch{return String(t)}})(s)}`)},asHooks=t=>t?Array.isArray(t)?t:[t]:[];class HookBus{hooks;tracked={pending:new Set};constructor(t){this.hooks=t}has(t){return asHooks(this.hooks[t]).length>0}subscribers(t){return[...asHooks(this.hooks[t])]}set(t,...s){this.store(t,s)}subscribe(t,s){this.store(t,[...asHooks(this.hooks[t]),s])}insert(t,s,o){const r=asHooks(this.hooks[t]);r.splice(o,0,s),this.store(t,r)}unsubscribe(t,s){const o=s;this.store(t,asHooks(this.hooks[t]).filter(t=>t!==o))}toLast(t,s){const o=asHooks(this.hooks[t]),r=o.indexOf(s);return!(r<0)&&(this.store(t,[...o.slice(0,r),...o.slice(r+1),s]),!0)}async chain(t,s){let r=s;for(const s of asHooks(this.hooks[t]))s&&(r=await runAsHookOwner(s,async()=>{try{return await n(()=>s(r))}catch(t){if(isRefusal(t)){const r=hookOwner(s);o()?.warn(`${r?`${r.plugin} refused ${r.hook}`:`a hook no plugin registered (${s.name||"anonymous"}) refused`}: ${t.message}`)}throw t}}));return r}notify(t,...s){this.fire(t,s)}async notifyStrict(t,...s){let o;for(const r of this.subscribers(t))await runAsHookOwner(r,async()=>{try{await n(()=>r(...s))}catch(t){o??=t}});if(void 0!==o)throw o}async notifyAndWait(t,...s){await this.fire(t,s)}async call(t,...s){const o=[];for(const r of asHooks(this.hooks[t]))r&&o.push(await runAsHookOwner(r,()=>n(()=>r(...s))));return o}prefixed(t){return{chain:async(s,o)=>this.chain(`${t}${s}`,o),notify:(s,...o)=>this.notify(`${t}${s}`,...o)}}async settled(){for(;this.tracked.pending.size>0;)await Promise.all([...this.tracked.pending])}store(t,s){const o=this.hooks;0===s.length?delete o[t]:o[t]=1===s.length?s[0]:s}fire(t,s){const o=(async()=>{for(const o of asHooks(this.hooks[t]))o&&await runAsHookOwner(o,async()=>{try{await n(()=>o(...s))}catch(t){report(o,t)}})})(),{pending:r}=this.tracked;return r.add(o),o.finally(()=>r.delete(o)),o}}export{asHooks,HookBus as default,hookOwner,recordHookOwner,runAsHookOwner};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import{ConflictError as t,GatewayTimeoutError as e,BadGatewayError as i}from"./errors.js";import{timedOutbound as a}from"./metrics.js";const instanceSlug=t=>t.toLowerCase().replace(/[^a-z0-9-]/g,"").replace(/^-+|-+$/g,""),matrixId=t=>{const e=t.lastIndexOf("@"),i=t.slice(0,e),a=t.slice(e+1),s=`@${i}:${a}`.toLowerCase();if(!(e<1||!a||i.includes(":")||/\s/.test(s)||Buffer.byteLength(s)>255))return s};async function refused(t,e){const a=(await e.text().catch(()=>"")).slice(0,500);return new i(`${t}: HTTP ${e.status}${a?` ${a}`:""}`)}class HttpProvider{domain;timeout;constructor(t,e){this.domain=t,this.timeout=e}address(t){return`${t.id}.${this.domain}`.toLowerCase()}async call(t,e={}){try{return await a(this.target,()=>fetch(t,{...e,signal:AbortSignal.timeout(this.timeout)}))}catch(e){throw this.timedOut(e,t)}}async json(t){try{return await t.json()}catch(e){throw this.timedOut(e,t.url)}}timedOut(t,i){return["AbortError","TimeoutError"].includes(t.name??"")?new e(`${new URL(i).host} did not answer within ${this.timeout} ms`):t}}class ClouderyProvider extends HttpProvider{url;token;offer;target="cloudery";constructor(t,e,i,a,s){super(i,s),this.url=t,this.token=e,this.offer=a}headers(){return{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json"}}async create(t){const e=await this.call(`${this.url}/api/v1/instances`,{method:"POST",headers:this.headers(),body:JSON.stringify({email:t.email,internal_email:t.email,publicName:t.publicName,public_name:t.publicName,locale:t.locale,oidc:t.id,slug:t.id,phone:t.phone||"",offer:t.offer||this.offer,domain:this.domain,skip_email_validation:!0,...t.orgId?{org_id:t.orgId}:{},...t.orgDomain?{org_domain:t.orgDomain}:{}})});if(!e.ok)throw await refused("Cloudery refused the instance",e)}async find(e,i){const a=(i??this.address(e)).toLowerCase(),s=new URL(`${this.url}/api/v2/instances`);s.searchParams.set("fqdn",a),s.searchParams.set("limit","1");const o=await this.call(s.toString(),{headers:this.headers()});if(!o.ok)throw await refused("Cloudery instance search failed",o);const{items:r}=await this.json(o),n=r?.[0];if(n){if(n.internal_email&&n.internal_email.toLowerCase()!==e.email.toLowerCase()&&(!e.organization||!await this.organizationHolds(e.id,n._id))||n.oidc&&n.oidc!==e.id)throw new t(`${a} already exists and belongs to another user`);return{fqdn:n.fqdn,ready:Boolean(n.instantiated_at)}}}async organizationHolds(t,e){if(!e)return!1;const i=await this.call(`${this.url}/api/v2/organizations/${encodeURIComponent(t)}`,{headers:this.headers()});if(404===i.status)return!1;if(!i.ok)throw await refused("Cloudery organization lookup failed",i);return(await this.json(i)).instance_id===e}async createOrganization(t){const e=await this.call(`${this.url}/api/v2/organizations`,{method:"POST",headers:this.headers(),body:JSON.stringify({ldap_branch:t.id,name:t.name,custom_domain:t.domain})});if(!e.ok&&409!==e.status)throw await refused("Cloudery refused the organization",e)}async linkOrganization(t,e){const i=await this.call(`${this.url}/api/v2/organizations/${encodeURIComponent(t)}`,{method:"PATCH",headers:this.headers(),body:JSON.stringify({instance_fqdn:e})});if(!i.ok)throw await refused("Cloudery refused to link the organization",i)}}class CozyStackProvider extends HttpProvider{url;context;apps;target="cozy-stack";auth;constructor(t,e,i,a,s,o,r){super(a,r),this.url=t,this.context=s,this.apps=o,this.auth=`Basic ${Buffer.from(`${e}:${i}`).toString("base64")}`}headers(){return{Authorization:this.auth,Accept:"application/json"}}async create(e){const i=this.address(e),a=new URLSearchParams({Domain:i,Email:e.email,PublicName:e.publicName,Locale:e.locale,OIDCID:e.oidc??e.id});e.phone&&a.set("Phone",e.phone),e.orgId&&a.set("OrgID",e.orgId),e.orgDomain&&a.set("OrgDomain",e.orgDomain),this.context&&a.set("ContextName",this.context),this.apps&&a.set("Apps",this.apps);const s=await this.call(`${this.url}/instances?${a.toString()}`,{method:"POST",headers:this.headers()});if(409===s.status){if(!await this.find(e))throw new t(`${i} already exists`);return i}if(!s.ok)throw await refused("cozy-stack refused the instance",s);return await this.finishOnboarding(i),i}async find(e,i){const a=(i??this.address(e)).toLowerCase(),s=await this.read(a);if(s){if(s.email?.toLowerCase()!==e.email.toLowerCase()||s.oidc_id!==(e.oidc??e.id))throw new t(`${a} already exists and belongs to another user`);return s.onboarding_finished||await this.finishOnboarding(a),{fqdn:a,ready:!0}}}async destroy(e,i){const a=this.address({id:e}),s=await this.read(a);if(!s)return;if(s.oidc_id!==i)throw new t(`${a} belongs to another user, left in place`);const o=await this.call(`${this.url}/instances/${encodeURIComponent(a)}`,{method:"DELETE",headers:this.headers()});if(!o.ok&&404!==o.status)throw await refused("cozy-stack refused to destroy the instance",o)}async read(t){const e=await this.call(`${this.url}/instances/${encodeURIComponent(t)}`,{headers:this.headers()});if(404===e.status)return;if(!e.ok)throw await refused("cozy-stack instance read failed",e);const{data:i}=await this.json(e);return i?.attributes??{}}async finishOnboarding(t){const e=await this.call(`${this.url}/instances/${encodeURIComponent(t)}?OnboardingFinished=true`,{method:"PATCH",headers:this.headers()});if(!e.ok)throw await refused("cozy-stack refused to finish onboarding",e)}}export{ClouderyProvider,CozyStackProvider,instanceSlug,matrixId};
|