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
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isDeepStrictEqual as e}from"node:util";import t from"../../abstract/plugin.js";import{asyncHandler as i,normalizeDn as s,escapeLdapFilter as a,validateDnValue as r,escapeDnValue as o}from"../../lib/utils.js";import{jsonBody as n}from"../../lib/expressFormatedResponses.js";import{extractLdapCode as c}from"../../lib/ldapCodes.js";import{NotFoundError as l,ConflictError as d,BadRequestError as p}from"../../lib/errors.js";const u=["pwdChangedTime","pwdAccountLockedTime","pwdFailureTime","pwdGraceUseTime","pwdReset","pwdPolicySubentry"],h={pwdMinAge:"integer",pwdMaxAge:"integer",pwdInHistory:"integer",pwdCheckQuality:"integer",pwdMinLength:"integer",pwdMaxLength:"integer",pwdExpireWarning:"integer",pwdGraceAuthNLimit:"integer",pwdGraceExpiry:"integer",pwdLockout:"boolean",pwdLockoutDuration:"integer",pwdMaxFailure:"integer",pwdFailureCountInterval:"integer",pwdMustChange:"boolean",pwdAllowUserChange:"boolean",pwdSafeModify:"boolean",pwdMinDelay:"integer",pwdMaxDelay:"integer",pwdMaxIdle:"integer",pwdMaxRecordedFailure:"integer",pwdCheckModuleArg:"text",pwdUseCheckModule:"boolean"},w=["pwdCheckModuleArg","pwdUseCheckModule"],y=[...Object.keys(h),"pwdAttribute","pwdCheckModule"],isWritablePolicyAttribute=e=>Object.prototype.hasOwnProperty.call(h,e),values=e=>(Array.isArray(e)?e:void 0===e?[]:[e]).map(e=>Buffer.isBuffer(e)?e.toString():String(e)).filter(e=>""!==e),first=e=>values(e)[0],userEntry=e=>({dn:e.dn,userPassword:first(e.userPassword),uid:first(e.uid),cn:first(e.cn),displayName:first(e.displayName),mail:first(e.mail),pwdChangedTime:first(e.pwdChangedTime),pwdAccountLockedTime:first(e.pwdAccountLockedTime),pwdFailureTime:values(e.pwdFailureTime),pwdGraceUseTime:values(e.pwdGraceUseTime),pwdReset:first(e.pwdReset),pwdPolicySubentry:first(e.pwdPolicySubentry)}),typedPolicy=e=>{const t={};for(const i of y){const s=values(e[i]);if(0!==s.length)switch(h[i]){case"integer":{const e=parseInt(s[0],10);isNaN(e)||(t[i]=e);break}case"boolean":t[i]="TRUE"===s[0];break;default:t[i]=1===s.length?s[0]:s}}return t},g=/[!@#$%^&*()_+\-=[\]{}|;:'",.<>?/]/;class PasswordPolicy extends t{constructor(){super(...arguments),this.name="ldapPasswordPolicy",this.roles=["api","configurable"],this.policyCache=new Map,this.discoveredDefault=null,this.warnedAmbiguousDefault=!1,this.warnedMissingBase=!1,this.warnedUnreadableSchema=!1,this.POLICY_CACHE_TTL=6e4}api(e){const t=`${this.config.api_prefix}/v1`;e.get(`${t}/password-policy`,i(async(e,t)=>{const i=await this.readPolicyConfig();if(i.dn)try{await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:["1.1"],paged:!1},i.dn,e)}catch(e){if(32!==c(e))throw e;return this.forgetPolicies(),void t.json({})}t.json(i)})),e.get(`${t}/users/:id/password-status`,i(async(e,t)=>{const i=this.param(e,"id"),s=await this.getPasswordStatus(i,e);t.json(s)})),e.post(`${t}/users/:id/unlock`,i(async(e,t)=>{const i=this.param(e,"id");await this.unlockAccount(i,e),t.json({success:!0,message:"Account unlocked"})})),e.get(`${t}/password-policy/expiring-soon`,i(async(e,t)=>{const i=parseInt(e.query.days)||this.config.ppolicy_warn_days||14,s=await this.findExpiringPasswords(i,e);t.json({warningDays:i,users:s})})),e.get(`${t}/password-policy/locked-accounts`,i(async(e,t)=>{const i=await this.findLockedAccounts(e);t.json({accounts:i})})),e.get(`${t}/password-policies`,i(async(e,t)=>{t.json({policies:await this.listPolicies(e),attributes:await this.definedAttributes()})})),e.post(`${t}/password-policies`,i(async(e,t)=>{const i=n(e,t);if(!i)return;const s=await this.createPolicy(i,e);t.status(201).json(s)})),e.get(`${t}/password-policies/:cn`,i(async(e,t)=>{const i=await this.findPolicy(this.param(e,"cn"),e);t.json(await this.toPolicyRecord(i))})),e.patch(`${t}/password-policies/:cn`,i(async(e,t)=>{const i=n(e,t);if(!i)return;const s=await this.updatePolicy(this.param(e,"cn"),i,e);t.json(s)})),e.delete(`${t}/password-policies/:cn`,i(async(e,t)=>{await this.deletePolicy(this.param(e,"cn"),e),t.json({success:!0})})),e.put(`${t}/users/:id/password-policy`,i(async(e,t)=>{const i=n(e,t);if(!i)return;const{policy:s}=i;if(null!==s&&("string"!=typeof s||!s))throw new p("policy must be a policy name or null");t.json(await this.assignPolicy(this.param(e,"id"),s,e))})),this.config.ppolicy_validate_complexity&&e.post(`${t}/password/validate`,i(async(e,t)=>{const i=n(e,t);if(!i)return;const{password:s}=i;if(!s)throw new p("password required");const a=this.validateComplexity(s);t.json(a)}))}async getPasswordStatus(e,t){const i=this.resolveUserDn(e),s=await this.readUser(i,e,["*",...u],t),a=await this.policyFor(s);return this.calculateStatus(i,s,a)}async readUser(e,t,i,s){try{const a=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:i,paged:!1},e,s);if(!a.searchEntries||0===a.searchEntries.length)throw new l(`User not found: ${t}`);return userEntry(a.searchEntries[0])}catch(e){if(e instanceof Error&&("NoSuchObjectError"===e.name||e.message.includes("0x20")||e.message.includes("Code: 32")))throw new l(`User not found: ${t}`);throw e}}calculateStatus(e,t,i){const s=Date.now(),a=this.parseGeneralizedTime(t.pwdChangedTime);let r=null,o=!1,n=null;i.pwdMaxAge&&a&&(r=new Date(a.getTime()+1e3*i.pwdMaxAge),o=s>r.getTime(),n=Math.ceil((r.getTime()-s)/864e5));let c=!1;const l=this.config.ppolicy_warn_days||14;null!==n&&n>0&&(c=n<=l);const d=t.pwdAccountLockedTime;let p=!!d;if(p&&!("000001010000Z"===d)&&i.pwdLockoutDuration){const e=this.parseGeneralizedTime(d);if(e){s>new Date(e.getTime()+1e3*i.pwdLockoutDuration).getTime()&&(p=!1)}}const u=t.pwdFailureTime,h=Array.isArray(u)?u.length:0,w=t.pwdGraceUseTime,y=Array.isArray(w)?w.length:0,g="TRUE"===t.pwdReset;return{dn:e,passwordSet:!!t.userPassword,lastChanged:a?.toISOString()||null,expiresAt:r?.toISOString()||null,daysUntilExpiration:n,isExpired:o,isExpiringSoon:c,mustChange:g,isLocked:p,lockedAt:d&&this.parseGeneralizedTime(d)?.toISOString()||null,failureCount:h,graceLoginsUsed:y,policy:i.dn??null}}async unlockAccount(e,t){const i=this.resolveUserDn(e),s=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:["pwdAccountLockedTime","pwdFailureTime"],paged:!1},i,t).catch(t=>{if(32===c(t))throw new l(`User not found: ${e}`);throw t});if(!s.searchEntries||0===s.searchEntries.length)throw new l(`User not found: ${e}`);const a=userEntry(s.searchEntries[0]),r={};a.pwdAccountLockedTime&&(r.pwdAccountLockedTime=[]),a.pwdFailureTime?.length&&(r.pwdFailureTime=[]),Object.keys(r).length>0?(await this.server.ldap.modify(i,{delete:r},t),this.logger.info(`Account unlocked: ${i}`)):this.logger.info(`Account was not locked: ${i}`)}async findExpiringPasswords(e,t){const i=this.config.ldap_users_base||this.config.ldap_base,s=await this.server.ldap.search({filter:"(pwdChangedTime=*)",scope:"sub",attributes:["uid","cn","displayName","mail","pwdChangedTime","pwdPolicySubentry"],paged:!1},i,t),a=Date.now(),r=24*e*60*60*1e3,o=[];for(const e of s.searchEntries){const t=userEntry(e),i=this.parseGeneralizedTime(t.pwdChangedTime);if(!i)continue;const s=await this.policyFor(t);if(!s.pwdMaxAge)continue;const n=new Date(i.getTime()+1e3*s.pwdMaxAge),c=n.getTime()-a;c>0&&c<r&&o.push({dn:t.dn,uid:t.uid,displayName:t.displayName||t.cn,mail:t.mail,expiresAt:n.toISOString(),daysUntilExpiration:Math.ceil(c/864e5)})}return o.sort((e,t)=>e.daysUntilExpiration-t.daysUntilExpiration)}async findLockedAccounts(e){const t=this.config.ldap_users_base||this.config.ldap_base;return(await this.server.ldap.search({filter:"(pwdAccountLockedTime=*)",scope:"sub",attributes:["uid","cn","displayName","mail","pwdAccountLockedTime","pwdFailureTime"],paged:!1},t,e)).searchEntries.map(e=>{const t=userEntry(e);return{dn:t.dn,uid:t.uid,displayName:t.displayName||t.cn,mail:t.mail,lockedAt:this.parseGeneralizedTime(t.pwdAccountLockedTime)?.toISOString(),failureCount:Array.isArray(t.pwdFailureTime)?t.pwdFailureTime.length:0}})}async policyFor(e){if(e.pwdPolicySubentry){const t=await this.readPolicyAt(e.pwdPolicySubentry);if(t)return t}return this.readPolicyConfig()}async readPolicyConfig(){const e=this.config.ppolicy_default_dn||await this.discoverDefaultDn();return(e?await this.readPolicyAt(e):null)??{}}async discoverDefaultDn(){if(this.discoveredDefault&&Date.now()-this.discoveredDefault.time<this.POLICY_CACHE_TTL)return this.discoveredDefault.dn;let e=null;try{const t=(await this.server.ldap.system.search({filter:"(objectClass=pwdPolicy)",scope:"sub",attributes:["1.1"],paged:!1},this.policyBase())).searchEntries.map(e=>e.dn);1===t.length?e=t[0]:t.length>1&&!this.warnedAmbiguousDefault&&(this.warnedAmbiguousDefault=!0,this.logger.warn(`${t.length} password policies found and no --ppolicy-default-dn: accounts without pwdPolicySubentry are reported without a policy`))}catch(e){this.logger.debug(`No ppolicy found: ${e}`)}return this.discoveredDefault={dn:e,time:Date.now()},e}async readPolicyAt(e){const t=s(e),i=this.policyCache.get(t);if(i&&Date.now()-i.time<this.POLICY_CACHE_TTL)return i.policy;let a,r=null;try{const t=await this.server.ldap.system.search({filter:"(objectClass=pwdPolicy)",scope:"base",attributes:y,paged:!1},e);t.searchEntries.length>0&&(r=((e,t)=>{const i=typedPolicy(e);return{dn:t,pwdMaxAge:i.pwdMaxAge,pwdMinAge:i.pwdMinAge,pwdInHistory:i.pwdInHistory,pwdCheckQuality:i.pwdCheckQuality,pwdMinLength:i.pwdMinLength,pwdMaxFailure:i.pwdMaxFailure,pwdLockout:!0===i.pwdLockout,pwdLockoutDuration:i.pwdLockoutDuration,pwdGraceAuthNLimit:i.pwdGraceAuthNLimit,pwdExpireWarning:i.pwdExpireWarning,pwdMustChange:!0===i.pwdMustChange,pwdAllowUserChange:!1!==i.pwdAllowUserChange}})(t.searchEntries[0],e))}catch(e){a=String(e)}const o=this.config.ppolicy_default_dn;return!r&&o&&s(o)===t?this.logger.warn(`--ppolicy-default-dn ${e} holds no password policy${a?`: ${a}`:""}`):a&&this.logger.debug(`Failed to read ppolicy from ${e}: ${a}`),this.policyCache.set(t,{policy:r,time:Date.now()}),r}forgetPolicies(){this.policyCache.clear(),this.discoveredDefault=null}policyBase(){return this.config.ppolicy_base||this.config.ldap_base}async searchPolicies(e,t){try{return(await this.server.ldap.search({filter:`(&(objectClass=pwdPolicy)${e})`,scope:"sub",attributes:["objectClass","cn",...y],paged:!1},this.policyBase(),t)).searchEntries}catch(e){if(32!==c(e))throw e;return this.warnedMissingBase||(this.warnedMissingBase=!0,this.logger.warn(`${this.policyBase()} does not exist: no password policy is found there, check --ppolicy-base`)),[]}}async findPolicy(e,t){const i=await this.searchPolicies(`(cn=${a(e)})`,t);if(0===i.length)throw new l(`No password policy named ${e}`);if(i.length>1)throw new d(`Several password policies are named ${e}`);return i[0]}async listPolicies(e){const t=[];for(const i of await this.searchPolicies("",e))t.push(await this.toPolicyRecord(i));return t.sort((e,t)=>e.cn.localeCompare(t.cn))}async definedAttributes(){const e=Object.keys(h);try{const t=await this.server.ldap.schemaIndex();return e.filter(e=>t.getAttributeType(e))}catch(t){return this.warnedUnreadableSchema||(this.warnedUnreadableSchema=!0,this.logger.warn(`Unable to read the schema, every policy attribute is listed: ${String(t)}`)),e}}async toPolicyRecord(e){const t=(await this.readPolicyConfig()).dn;return{...{dn:e.dn,cn:values(e.cn)[0]??"",isDefault:!!t&&s(t)===s(e.dn)},...typedPolicy(e)}}policyValue(e,t){switch(h[e]){case"integer":if("number"!=typeof t||!Number.isInteger(t)||t<0||"pwdCheckQuality"===e&&t>2)throw new p("pwdCheckQuality"===e?"pwdCheckQuality must be 0, 1 or 2":`${e} must be a non-negative integer`);return String(t);case"boolean":if("boolean"!=typeof t)throw new p(`${e} must be a boolean`);return t?"TRUE":"FALSE";default:if("string"!=typeof t||""===t)throw new p(`${e} must be a non-empty string`);return t}}async createPolicy(e,t){const{cn:i,...s}=e;if("string"!=typeof i)throw new p("cn required");try{r(i,"cn")}catch(e){throw new p(e.message)}const a=await this.findPolicy(i,t).catch(e=>{if(e instanceof l)return null;throw e});if(a)throw new d(`A password policy is named ${i}: ${a.dn}`);const n=["top","person","pwdPolicy"],u={objectClass:n,cn:i,sn:i,pwdAttribute:"userPassword"};for(const[e,t]of Object.entries(s)){if(!isWritablePolicyAttribute(e))throw new p(`${e} cannot be written`);null!==t&&(u[e]=this.policyValue(e,t))}w.some(e=>e in u)&&n.push("pwdPolicyChecker");try{await this.server.ldap.add(`cn=${o(i)},${this.policyBase()}`,u,t)}catch(e){if(32!==c(e))throw e;throw new l(`${this.policyBase()} does not exist: create it or change --ppolicy-base`)}return this.forgetPolicies(),this.toPolicyRecord(await this.findPolicy(i,t))}async updatePolicy(t,i,a){const r=await this.findPolicy(t,a),o=await this.toPolicyRecord(r),n={},c=[];for(const[t,a]of Object.entries(i)){if(!isWritablePolicyAttribute(t)){if("dn"===t?"string"==typeof a&&s(a)===s(o.dn):Object.prototype.hasOwnProperty.call(o,t)&&e(o[t],a))continue;throw new p(`${t} cannot be written`)}null!==a?n[t]=this.policyValue(t,a):void 0!==o[t]&&c.push(t)}const l={};if(Object.keys(n).length>0&&(l.replace=n),c.length>0&&(l.delete=c),!l.replace&&!l.delete)return o;const d=values(r.objectClass).map(e=>e.toLowerCase());return w.some(e=>e in n)&&!d.includes("pwdpolicychecker")&&(l.add={objectClass:"pwdPolicyChecker"}),await this.server.ldap.modify(r.dn,l,a),this.forgetPolicies(),this.toPolicyRecord(await this.findPolicy(t,a))}async deletePolicy(e,t){const i=await this.findPolicy(e,t);await this.server.ldap.authorize("ldapdeleterequest",[i.dn,t]);const r=(await this.readPolicyConfig()).dn;if(r&&s(r)===s(i.dn))throw new d(`${i.dn} is the default password policy`);if((await this.server.ldap.system.search({filter:`(pwdPolicySubentry=${a(i.dn)})`,scope:"sub",attributes:["1.1"],sizeLimit:1,paged:!1},this.config.ldap_base)).searchEntries.length>0)throw new d(`${i.dn} is still applied to at least one entry`);await this.server.ldap.delete(i.dn,t),this.forgetPolicies()}async assignPolicy(e,t,i){const s=this.resolveUserDn(e);if(null===t){try{await this.server.ldap.modify(s,{delete:["pwdPolicySubentry"]},i)}catch(e){if(16!==c(e))throw e}return{dn:s,policy:null}}const a=await this.findPolicy(t,i);return await this.server.ldap.modify(s,{replace:{pwdPolicySubentry:a.dn}},i),{dn:s,policy:a.dn}}param(e,t){const i=e.params[t];return Array.isArray(i)?i[0]:i}parseGeneralizedTime(e){if(!e)return null;const t=e.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);if(!t)return null;const[,i,s,a,r,o,n]=t;return new Date(Date.UTC(+i,+s-1,+a,+r,+o,+n))}validateComplexity(e){const t=[],i=this.config,s=i.ppolicy_min_length||12;return e.length<s&&t.push(`Minimum ${s} characters required`),i.ppolicy_require_uppercase&&!/[A-Z]/.test(e)&&t.push("At least one uppercase letter required"),i.ppolicy_require_lowercase&&!/[a-z]/.test(e)&&t.push("At least one lowercase letter required"),i.ppolicy_require_digit&&!/\d/.test(e)&&t.push("At least one digit required"),i.ppolicy_require_special&&!g.test(e)&&t.push("At least one special character required"),{valid:0===t.length,errors:t}}resolveUserDn(e){if(e.includes("="))return e;const t=this.config.ldap_users_base||this.config.ldap_base;return`${this.config.ldap_user_main_attribute||"uid"}=${o(e)},${t}`}getConfigApiData(){const e=this.config.api_prefix||"/api",t=!!this.config.ppolicy_validate_complexity;return{name:this.name,enabled:!0,endpoints:{getPolicy:`${e}/v1/password-policy`,getUserStatus:`${e}/v1/users/:id/password-status`,unlockUser:`${e}/v1/users/:id/unlock`,getExpiringSoon:`${e}/v1/password-policy/expiring-soon`,getLockedAccounts:`${e}/v1/password-policy/locked-accounts`,listPolicies:`${e}/v1/password-policies`,policy:`${e}/v1/password-policies/:cn`,assignPolicy:`${e}/v1/users/:id/password-policy`,validatePassword:t?`${e}/v1/password/validate`:void 0},config:{warnDays:this.config.ppolicy_warn_days||14,validateComplexity:t,...t&&{complexityRules:{minLength:this.config.ppolicy_min_length||12,requireUppercase:this.config.ppolicy_require_uppercase??!0,requireLowercase:this.config.ppolicy_require_lowercase??!0,requireDigit:this.config.ppolicy_require_digit??!0,requireSpecial:this.config.ppolicy_require_special??!0}}}}}}export{PasswordPolicy as default};
|
|
1
|
+
import{isDeepStrictEqual as e}from"node:util";import{Type as t}from"@sinclair/typebox";import i from"../../abstract/plugin.js";import{normalizeDn as s,escapeLdapFilter as a,validateDnValue as r,escapeDnValue as n}from"../../lib/utils.js";import{jsonBody as o}from"../../lib/expressFormatedResponses.js";import{extractLdapCode as c}from"../../lib/ldapCodes.js";import{passwordPolicyOptions as d}from"./ldap.options.js";import{defineRoute as l}from"../../lib/route.js";import{NotFoundError as p,ConflictError as h,BadRequestError as u}from"../../lib/errors.js";const w=["pwdChangedTime","pwdAccountLockedTime","pwdFailureTime","pwdGraceUseTime","pwdReset","pwdPolicySubentry"],y={pwdMinAge:"integer",pwdMaxAge:"integer",pwdInHistory:"integer",pwdCheckQuality:"integer",pwdMinLength:"integer",pwdMaxLength:"integer",pwdExpireWarning:"integer",pwdGraceAuthNLimit:"integer",pwdGraceExpiry:"integer",pwdLockout:"boolean",pwdLockoutDuration:"integer",pwdMaxFailure:"integer",pwdFailureCountInterval:"integer",pwdMustChange:"boolean",pwdAllowUserChange:"boolean",pwdSafeModify:"boolean",pwdMinDelay:"integer",pwdMaxDelay:"integer",pwdMaxIdle:"integer",pwdMaxRecordedFailure:"integer",pwdCheckModuleArg:"text",pwdUseCheckModule:"boolean"},g=["pwdCheckModuleArg","pwdUseCheckModule"],m=[...Object.keys(y),"pwdAttribute","pwdCheckModule"],isWritablePolicyAttribute=e=>Object.hasOwn(y,e),values=e=>(Array.isArray(e)?e:void 0===e?[]:[e]).map(e=>Buffer.isBuffer(e)?e.toString():String(e)).filter(e=>""!==e),first=e=>values(e)[0],userEntry=e=>({dn:e.dn,userPassword:first(e.userPassword),uid:first(e.uid),cn:first(e.cn),displayName:first(e.displayName),mail:first(e.mail),pwdChangedTime:first(e.pwdChangedTime),pwdAccountLockedTime:first(e.pwdAccountLockedTime),pwdFailureTime:values(e.pwdFailureTime),pwdGraceUseTime:values(e.pwdGraceUseTime),pwdReset:first(e.pwdReset),pwdPolicySubentry:first(e.pwdPolicySubentry)}),typedPolicy=e=>{const t={};for(const i of m){const s=values(e[i]);if(0!==s.length)switch(y[i]){case"integer":{const e=parseInt(s[0],10);isNaN(e)||(t[i]=e);break}case"boolean":t[i]="TRUE"===s[0];break;default:t[i]=1===s.length?s[0]:s}}return t},f=/[!@#$%^&*()_+\-=[\]{}|;:'",.<>?/]/,b=t.Object({cn:t.String({minLength:1})}),P=t.Object({id:t.String({minLength:1})}),A=t.Object({days:t.Optional(t.Integer({minimum:1}))}),C=t.Object({policy:t.Union([t.String({minLength:1}),t.Null()])}),$=t.Object({success:t.Boolean(),message:t.String()}),_=t.Object({password:t.String({minLength:1})});class PasswordPolicy extends i{static optionDefinitions=d;name="ldapPasswordPolicy";roles=["api","configurable"];policyCache=new Map;discoveredDefault=null;warnedAmbiguousDefault=!1;warnedMissingBase=!1;warnedUnreadableSchema=!1;POLICY_CACHE_TTL=6e4;get settings(){return this.options(d)}api(e){const t=`${this.config.api_prefix}/v1`;l(e,{method:"get",path:`${t}/password-policy`,handler:async({req:e,res:t})=>{const i=await this.readPolicyConfig();if(i.dn)try{await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:["1.1"],paged:!1},i.dn,e)}catch(e){if(32!==c(e))throw e;return this.forgetPolicies(),void t.json({})}t.json(i)}}),l(e,{method:"get",path:`${t}/users/:id/password-status`,params:P,handler:async({params:e,req:t,res:i})=>{const s=await this.getPasswordStatus(e.id,t);i.json(s)}}),l(e,{method:"post",path:`${t}/users/:id/unlock`,params:P,responses:{200:$},handler:async({params:e,req:t,res:i})=>{await this.unlockAccount(e.id,t),i.json({success:!0,message:"Account unlocked"})}}),l(e,{method:"get",path:`${t}/password-policy/expiring-soon`,query:A,handler:async({req:e,res:t,query:i})=>{const s=i.days||this.settings.ppolicy_warn_days,a=await this.findExpiringPasswords(s,e);t.json({warningDays:s,users:a})}}),l(e,{method:"get",path:`${t}/password-policy/locked-accounts`,handler:async({req:e,res:t})=>{const i=await this.findLockedAccounts(e);t.json({accounts:i})}}),l(e,{method:"get",path:`${t}/password-policies`,handler:async({req:e,res:t})=>{t.json({policies:await this.listPolicies(e),attributes:await this.definedAttributes()})}}),l(e,{method:"post",path:`${t}/password-policies`,handler:async({req:e,res:t})=>{const i=o(e,t);if(!i)return;const s=await this.createPolicy(i,e);t.status(201).json(s)}}),l(e,{method:"get",path:`${t}/password-policies/:cn`,params:b,handler:async({params:e,req:t,res:i})=>{const s=await this.findPolicy(e.cn,t);i.json(await this.toPolicyRecord(s))}}),l(e,{method:"patch",path:`${t}/password-policies/:cn`,params:b,handler:async({params:e,req:t,res:i})=>{const s=o(t,i);if(!s)return;const a=await this.updatePolicy(e.cn,s,t);i.json(a)}}),l(e,{method:"delete",path:`${t}/password-policies/:cn`,params:b,handler:async({params:e,req:t,res:i})=>{await this.deletePolicy(e.cn,t),i.json({success:!0})}}),l(e,{method:"put",path:`${t}/users/:id/password-policy`,params:P,body:C,handler:async({params:e,req:t,res:i,body:s})=>{i.json(await this.assignPolicy(e.id,s.policy,t))}}),this.settings.ppolicy_validate_complexity&&l(e,{method:"post",path:`${t}/password/validate`,body:_,handler:({res:e,body:t})=>{e.json(this.validateComplexity(t.password))}})}async getPasswordStatus(e,t){const i=this.resolveUserDn(e),s=await this.readUser(i,e,["*",...w],t),a=await this.policyFor(s);return this.calculateStatus(i,s,a)}async readUser(e,t,i,s){try{const a=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:i,paged:!1},e,s);if(!a.searchEntries||0===a.searchEntries.length)throw new p(`User not found: ${t}`);return userEntry(a.searchEntries[0])}catch(e){if(e instanceof Error&&("NoSuchObjectError"===e.name||e.message.includes("0x20")||e.message.includes("Code: 32")))throw new p(`User not found: ${t}`);throw e}}calculateStatus(e,t,i){const s=Date.now(),a=this.parseGeneralizedTime(t.pwdChangedTime);let r=null,n=!1,o=null;i.pwdMaxAge&&a&&(r=new Date(a.getTime()+1e3*i.pwdMaxAge),n=s>r.getTime(),o=Math.ceil((r.getTime()-s)/864e5));let c=!1;const d=this.settings.ppolicy_warn_days;null!==o&&o>0&&(c=o<=d);const l=t.pwdAccountLockedTime;let p=!!l;if(p&&!("000001010000Z"===l)&&i.pwdLockoutDuration){const e=this.parseGeneralizedTime(l);if(e){s>new Date(e.getTime()+1e3*i.pwdLockoutDuration).getTime()&&(p=!1)}}const h=t.pwdFailureTime,u=Array.isArray(h)?h.length:0,w=t.pwdGraceUseTime,y=Array.isArray(w)?w.length:0,g="TRUE"===t.pwdReset;return{dn:e,passwordSet:!!t.userPassword,lastChanged:a?.toISOString()||null,expiresAt:r?.toISOString()||null,daysUntilExpiration:o,isExpired:n,isExpiringSoon:c,mustChange:g,isLocked:p,lockedAt:l&&this.parseGeneralizedTime(l)?.toISOString()||null,failureCount:u,graceLoginsUsed:y,policy:i.dn??null}}async unlockAccount(e,t){const i=this.resolveUserDn(e),s=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:["pwdAccountLockedTime","pwdFailureTime"],paged:!1},i,t).catch(t=>{if(32===c(t))throw new p(`User not found: ${e}`);throw t});if(!s.searchEntries||0===s.searchEntries.length)throw new p(`User not found: ${e}`);const a=userEntry(s.searchEntries[0]),r={};a.pwdAccountLockedTime&&(r.pwdAccountLockedTime=[]),a.pwdFailureTime?.length&&(r.pwdFailureTime=[]),Object.keys(r).length>0?(await this.server.ldap.modify(i,{delete:r},t),this.logger.info(`Account unlocked: ${i}`)):this.logger.info(`Account was not locked: ${i}`)}async findExpiringPasswords(e,t){const i=this.settings.ldap_users_base||this.config.ldap_base,s=await this.server.ldap.search({filter:"(pwdChangedTime=*)",scope:"sub",attributes:["uid","cn","displayName","mail","pwdChangedTime","pwdPolicySubentry"],paged:!1},i,t),a=Date.now(),r=24*e*60*60*1e3,n=[];for(const e of s.searchEntries){const t=userEntry(e),i=this.parseGeneralizedTime(t.pwdChangedTime);if(!i)continue;const s=await this.policyFor(t);if(!s.pwdMaxAge)continue;const o=new Date(i.getTime()+1e3*s.pwdMaxAge),c=o.getTime()-a;c>0&&c<r&&n.push({dn:t.dn,uid:t.uid,displayName:t.displayName||t.cn,mail:t.mail,expiresAt:o.toISOString(),daysUntilExpiration:Math.ceil(c/864e5)})}return n.sort((e,t)=>e.daysUntilExpiration-t.daysUntilExpiration)}async findLockedAccounts(e){const t=this.settings.ldap_users_base||this.config.ldap_base;return(await this.server.ldap.search({filter:"(pwdAccountLockedTime=*)",scope:"sub",attributes:["uid","cn","displayName","mail","pwdAccountLockedTime","pwdFailureTime"],paged:!1},t,e)).searchEntries.map(e=>{const t=userEntry(e);return{dn:t.dn,uid:t.uid,displayName:t.displayName||t.cn,mail:t.mail,lockedAt:this.parseGeneralizedTime(t.pwdAccountLockedTime)?.toISOString(),failureCount:Array.isArray(t.pwdFailureTime)?t.pwdFailureTime.length:0}})}async policyFor(e){if(e.pwdPolicySubentry){const t=await this.readPolicyAt(e.pwdPolicySubentry);if(t)return t}return this.readPolicyConfig()}async readPolicyConfig(){const e=this.settings.ppolicy_default_dn||await this.discoverDefaultDn();return(e?await this.readPolicyAt(e):null)??{}}async discoverDefaultDn(){if(this.discoveredDefault&&Date.now()-this.discoveredDefault.time<this.POLICY_CACHE_TTL)return this.discoveredDefault.dn;let e=null;try{const t=(await this.server.ldap.system.search({filter:"(objectClass=pwdPolicy)",scope:"sub",attributes:["1.1"],paged:!1},this.policyBase())).searchEntries.map(e=>e.dn);1===t.length?e=t[0]:t.length>1&&!this.warnedAmbiguousDefault&&(this.warnedAmbiguousDefault=!0,this.logger.warn(`${t.length} password policies found and no --ppolicy-default-dn: accounts without pwdPolicySubentry are reported without a policy`))}catch(e){this.logger.debug(`No ppolicy found: ${e}`)}return this.discoveredDefault={dn:e,time:Date.now()},e}async readPolicyAt(e){const t=s(e),i=this.policyCache.get(t);if(i&&Date.now()-i.time<this.POLICY_CACHE_TTL)return i.policy;let a,r=null;try{const t=await this.server.ldap.system.search({filter:"(objectClass=pwdPolicy)",scope:"base",attributes:m,paged:!1},e);t.searchEntries.length>0&&(r=((e,t)=>{const i=typedPolicy(e);return{dn:t,pwdMaxAge:i.pwdMaxAge,pwdMinAge:i.pwdMinAge,pwdInHistory:i.pwdInHistory,pwdCheckQuality:i.pwdCheckQuality,pwdMinLength:i.pwdMinLength,pwdMaxFailure:i.pwdMaxFailure,pwdLockout:!0===i.pwdLockout,pwdLockoutDuration:i.pwdLockoutDuration,pwdGraceAuthNLimit:i.pwdGraceAuthNLimit,pwdExpireWarning:i.pwdExpireWarning,pwdMustChange:!0===i.pwdMustChange,pwdAllowUserChange:!1!==i.pwdAllowUserChange}})(t.searchEntries[0],e))}catch(e){a=String(e)}const n=this.settings.ppolicy_default_dn;return!r&&n&&s(n)===t?this.logger.warn(`--ppolicy-default-dn ${e} holds no password policy${a?`: ${a}`:""}`):a&&this.logger.debug(`Failed to read ppolicy from ${e}: ${a}`),this.policyCache.set(t,{policy:r,time:Date.now()}),r}forgetPolicies(){this.policyCache.clear(),this.discoveredDefault=null}policyBase(){return this.settings.ppolicy_base||this.config.ldap_base||""}async searchPolicies(e,t){try{return(await this.server.ldap.search({filter:`(&(objectClass=pwdPolicy)${e})`,scope:"sub",attributes:["objectClass","cn",...m],paged:!1},this.policyBase(),t)).searchEntries}catch(e){if(32!==c(e))throw e;return this.warnedMissingBase||(this.warnedMissingBase=!0,this.logger.warn(`${this.policyBase()} does not exist: no password policy is found there, check --ppolicy-base`)),[]}}async findPolicy(e,t){const i=await this.searchPolicies(`(cn=${a(e)})`,t);if(0===i.length)throw new p(`No password policy named ${e}`);if(i.length>1)throw new h(`Several password policies are named ${e}`);return i[0]}async listPolicies(e){const t=[];for(const i of await this.searchPolicies("",e))t.push(await this.toPolicyRecord(i));return t.sort((e,t)=>e.cn.localeCompare(t.cn))}async definedAttributes(){const e=Object.keys(y);try{const t=await this.server.ldap.schemaIndex();return e.filter(e=>t.getAttributeType(e))}catch(t){return this.warnedUnreadableSchema||(this.warnedUnreadableSchema=!0,this.logger.warn(`Unable to read the schema, every policy attribute is listed: ${String(t)}`)),e}}async toPolicyRecord(e){const t=(await this.readPolicyConfig()).dn;return{...{dn:e.dn,cn:values(e.cn)[0]??"",isDefault:!!t&&s(t)===s(e.dn)},...typedPolicy(e)}}policyValue(e,t){switch(y[e]){case"integer":if("number"!=typeof t||!Number.isInteger(t)||t<0||"pwdCheckQuality"===e&&t>2)throw new u("pwdCheckQuality"===e?"pwdCheckQuality must be 0, 1 or 2":`${e} must be a non-negative integer`);return String(t);case"boolean":if("boolean"!=typeof t)throw new u(`${e} must be a boolean`);return t?"TRUE":"FALSE";default:if("string"!=typeof t||""===t)throw new u(`${e} must be a non-empty string`);return t}}async createPolicy(e,t){const{cn:i,...s}=e;if("string"!=typeof i)throw new u("cn required");try{r(i,"cn")}catch(e){throw new u(e.message)}const a=await this.findPolicy(i,t).catch(e=>{if(e instanceof p)return null;throw e});if(a)throw new h(`A password policy is named ${i}: ${a.dn}`);const o=["top","person","pwdPolicy"],d={objectClass:o,cn:i,sn:i,pwdAttribute:"userPassword"};for(const[e,t]of Object.entries(s)){if(!isWritablePolicyAttribute(e))throw new u(`${e} cannot be written`);null!==t&&(d[e]=this.policyValue(e,t))}g.some(e=>e in d)&&o.push("pwdPolicyChecker");try{await this.server.ldap.add(`cn=${n(i)},${this.policyBase()}`,d,t)}catch(e){if(32!==c(e))throw e;throw new p(`${this.policyBase()} does not exist: create it or change --ppolicy-base`)}return this.forgetPolicies(),this.toPolicyRecord(await this.findPolicy(i,t))}async updatePolicy(t,i,a){const r=await this.findPolicy(t,a),n=await this.toPolicyRecord(r),o={},c=[];for(const[t,a]of Object.entries(i)){if(!isWritablePolicyAttribute(t)){if("dn"===t?"string"==typeof a&&s(a)===s(n.dn):Object.hasOwn(n,t)&&e(n[t],a))continue;throw new u(`${t} cannot be written`)}null!==a?o[t]=this.policyValue(t,a):void 0!==n[t]&&c.push(t)}const d={};if(Object.keys(o).length>0&&(d.replace=o),c.length>0&&(d.delete=c),!d.replace&&!d.delete)return n;const l=values(r.objectClass).map(e=>e.toLowerCase());return g.some(e=>e in o)&&!l.includes("pwdpolicychecker")&&(d.add={objectClass:"pwdPolicyChecker"}),await this.server.ldap.modify(r.dn,d,a),this.forgetPolicies(),this.toPolicyRecord(await this.findPolicy(t,a))}async deletePolicy(e,t){const i=await this.findPolicy(e,t);await this.server.ldap.authorize("ldapdeleterequest",[i.dn,t]);const r=(await this.readPolicyConfig()).dn;if(r&&s(r)===s(i.dn))throw new h(`${i.dn} is the default password policy`);if((await this.server.ldap.system.search({filter:`(pwdPolicySubentry=${a(i.dn)})`,scope:"sub",attributes:["1.1"],sizeLimit:1,paged:!1},this.config.ldap_base||"")).searchEntries.length>0)throw new h(`${i.dn} is still applied to at least one entry`);await this.server.ldap.delete(i.dn,t),this.forgetPolicies()}async assignPolicy(e,t,i){const s=this.resolveUserDn(e);if(null===t){try{await this.server.ldap.modify(s,{delete:["pwdPolicySubentry"]},i)}catch(e){if(16!==c(e))throw e}return{dn:s,policy:null}}const a=await this.findPolicy(t,i);return await this.server.ldap.modify(s,{replace:{pwdPolicySubentry:a.dn}},i),{dn:s,policy:a.dn}}parseGeneralizedTime(e){if(!e)return null;const t=e.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);if(!t)return null;const[,i,s,a,r,n,o]=t;return new Date(Date.UTC(+i,+s-1,+a,+r,+n,+o))}validateComplexity(e){const t=[],i=this.settings,s=i.ppolicy_min_length;return e.length<s&&t.push(`Minimum ${s} characters required`),i.ppolicy_require_uppercase&&!/[A-Z]/.test(e)&&t.push("At least one uppercase letter required"),i.ppolicy_require_lowercase&&!/[a-z]/.test(e)&&t.push("At least one lowercase letter required"),i.ppolicy_require_digit&&!/\d/.test(e)&&t.push("At least one digit required"),i.ppolicy_require_special&&!f.test(e)&&t.push("At least one special character required"),{valid:0===t.length,errors:t}}resolveUserDn(e){if(e.includes("="))return e;const t=this.settings.ldap_users_base||this.config.ldap_base;return`${this.config.ldap_user_main_attribute||"uid"}=${n(e)},${t}`}getConfigApiData(){const e=this.config.api_prefix||"/api",t=!!this.settings.ppolicy_validate_complexity;return{name:this.name,enabled:!0,endpoints:{getPolicy:`${e}/v1/password-policy`,getUserStatus:`${e}/v1/users/:id/password-status`,unlockUser:`${e}/v1/users/:id/unlock`,getExpiringSoon:`${e}/v1/password-policy/expiring-soon`,getLockedAccounts:`${e}/v1/password-policy/locked-accounts`,listPolicies:`${e}/v1/password-policies`,policy:`${e}/v1/password-policies/:cn`,assignPolicy:`${e}/v1/users/:id/password-policy`,validatePassword:t?`${e}/v1/password/validate`:void 0},config:{warnDays:this.settings.ppolicy_warn_days,validateComplexity:t,...t&&{complexityRules:{minLength:this.settings.ppolicy_min_length,requireUppercase:this.settings.ppolicy_require_uppercase,requireLowercase:this.settings.ppolicy_require_lowercase,requireDigit:this.settings.ppolicy_require_digit,requireSpecial:this.settings.ppolicy_require_special}}}}}}export{C as AssignPolicyBody,A as ExpiringQuery,b as PolicyParams,P as PolicyUserParams,$ as UnlockResponse,_ as ValidatePasswordBody,PasswordPolicy as default};
|
package/dist/plugins/ldap/raw.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{Type as t}from"@sinclair/typebox";import{FilterParser as e}from"ldapts";import s from"../../abstract/plugin.js";import{tryMethodData as r}from"../../lib/expressFormatedResponses.js";import{isDnInBranch as i,getRdn as a,getParentDn as n}from"../../lib/utils.js";import{BadRequestError as o,ForbiddenError as h,ServiceUnavailableError as d,NotFoundError as c,isAuthzRefusal as l}from"../../lib/errors.js";import{outsideRequest as p}from"../../lib/requestContext.js";import{rawOptions as u}from"./ldap.options.js";import{defineRoute as f}from"../../lib/route.js";const m=["base","one","sub"],b=["userpassword","pwdhistory","sambantpassword","sambalmpassword","sambapasswordhistory","krbprincipalkey","krbextradata","krbpwdhistory","unicodepwd","dbcspwd","lmpwdhistory","ntpwdhistory","supplementalcredentials","userpkcs12","authpassword"],w=t.Object({base:t.Optional(t.String()),scope:t.Optional(t.String()),filter:t.Optional(t.String()),attributes:t.Optional(t.String()),limit:t.Optional(t.Integer({minimum:1}))}),y=t.Object({dn:t.String({minLength:1})}),g=t.Object({children:t.Optional(t.String())});class LdapRaw extends s{static optionDefinitions=u;name="ldapRaw";roles=["api"];bases;hiddenAttributes;hiddenTypeNames;hiddenFilterNames;showSecrets;maxResults;constructor(t){super(t);const e=this.options(u),s=e.ldap_raw_base.filter(t=>t&&t.length>0);if(0===s.length){if(!this.config.ldap_base)throw new Error("Missing --ldap-base or --ldap-raw-base for plugin core/ldap/raw");s.push(this.config.ldap_base)}this.bases=s,this.showSecrets=e.ldap_raw_show_secrets,this.hiddenAttributes=new Set([...this.showSecrets?[]:b,...e.ldap_raw_hidden_attribute.filter(t=>t&&t.length>0).map(t=>t.toLowerCase())]),this.maxResults=e.ldap_raw_max_results,this.logger.info(`LDAP raw API enabled on ${this.bases.join(", ")} (read-only)`),this.showSecrets&&this.logger.warn("LDAP raw API: --ldap-raw-show-secrets is set, password hashes and other credential attributes are served over the API")}checkDn(t){if(!t||!t.trim())throw new o("DN is required");if(!this.bases.some(e=>i(t,e)||t.toLowerCase()===e.toLowerCase()))throw new h(`DN ${t} is outside the exposed bases`)}async hiddenNames(){if(0===this.hiddenAttributes.size)return this.hiddenAttributes;if(this.hiddenTypeNames)return this.hiddenTypeNames;const t=new Set(this.hiddenAttributes);let e;try{e=await this.getSchemaIndex()}catch(t){throw new d(`The directory schema is needed to hide attributes by type: ${t instanceof Error?t.message:String(t)}`)}const s=new Set(t);for(const r of this.hiddenAttributes){const i=e.getAttributeType(r.replace(/^oid\./i,""));if(!i)continue;let a=i;const n=new Set;for(;a&&!n.has(a.oid);){n.add(a.oid);for(const e of[t,s]){e.add(a.oid.toLowerCase());for(const t of a.names)e.add(t.toLowerCase())}a=a.sup?e.getAttributeType(a.sup):void 0}}return this.hiddenTypeNames=t,this.hiddenFilterNames=s,t}isHidden(t,e){const s=t.replace(/^oid\./i,"").split(";")[0].trim().toLowerCase();return this.hiddenAttributes.has(s)||e.has(s)}checkFilter(t,s){let r;try{r=e.parseString(t)}catch(e){const s=e instanceof Error?e.message:String(e);throw new o(`Invalid LDAP filter ${JSON.stringify(t)}: ${s}. A filter must be parenthesised, e.g. (cn=foo) or (|(cn=*foo*)(ou=*foo*))`)}for(const t of function(t){const e=[],s=[t],r=new Set;for(;s.length>0;){const t=s.pop();if(!t||"object"!=typeof t||r.has(t))continue;r.add(t);const i=t;"string"==typeof i.attribute&&e.push(i.attribute),"string"==typeof i.matchType&&e.push(i.matchType),Array.isArray(i.filters)&&s.push(...i.filters),i.filter&&s.push(i.filter)}return e}(r))if(""!==t){if(this.isHidden(t,s))throw new o("The filter tests an attribute the raw API does not expose")}else if(s.size>0)throw new o("The filter matches on every attribute, which the raw API does not expose while attributes are hidden")}formatEntry(t){const e={},s=this.hiddenTypeNames??this.hiddenAttributes;for(const[r,i]of Object.entries(t)){if("dn"===r)continue;if(this.isHidden(r,s))continue;const t=Array.isArray(i)?i:[i],a=t.some(t=>Buffer.isBuffer(t));e[r]={values:t.map(t=>Buffer.isBuffer(t)?t.toString("base64"):a?Buffer.from(String(t),"utf8").toString("base64"):String(t)),binary:a}}return{dn:t.dn,attributes:e}}async searchDirectory(t,e,s){try{return await this.server.ldap.search({paged:!1,...e},t,s)}catch(e){if(32===e?.code)throw new c(`Entry ${t||"(root DSE)"} not found`);throw e}}async getRootDse(){try{await this.hiddenNames()}catch(t){this.logger.warn(`LDAP raw API: root DSE served without resolving hidden attribute types: ${t instanceof Error?t.message:String(t)}`)}const t=(await p(()=>this.searchDirectory("",{scope:"base",filter:"(objectClass=*)",attributes:["*","+"]}))).searchEntries[0];if(!t)throw new c("Root DSE is not readable");return this.formatEntry({...t,dn:""})}async getSubschemaDn(){return this.server.ldap.subschemaDn()}async getSchemaIndex(){return this.server.ldap.schemaIndex()}async getSchema(){return(await this.getSchemaIndex()).schema}async getEntry(t,e){this.checkDn(t),await this.hiddenNames();const read=async s=>this.searchDirectory(t,{scope:"base",filter:"(objectClass=*)",attributes:s},e);let s;try{s=await read(["*","+"])}catch(e){if(e instanceof Error&&("statusCode"in e||l(e)))throw e;this.logger.debug(`Reading ${t} with operational attributes failed (${e}), retrying without`),s=await read(["*"])}const r=s.searchEntries[0];if(!r)throw new c(`Entry ${t} not found`);return this.formatEntry(r)}async getChildren(t,e,s){if(this.checkDn(t),!await this.server.ldap.readableBy(t,s))throw new c(`Entry ${t} not found`);const r=await this.searchDirectory(t,{scope:"one",filter:"(objectClass=*)",attributes:["objectClass"],sizeLimit:this.maxResults+1},s),i=r.searchEntries.length>this.maxResults,n=[];for(const t of r.searchEntries.slice(0,this.maxResults)){const e=t.objectClass;n.push({dn:t.dn,rdn:a(t.dn),objectClass:Array.isArray(e)?e.map(String):e?[String(e)]:[],hasChildren:!1})}return n.sort((t,e)=>t.rdn.localeCompare(e.rdn)),e&&await Promise.all(n.map(async t=>{t.hasChildren=await this.hasChildren(t.dn,s)})),i&&this.logger.info(`Children of ${t} truncated to ${this.maxResults} entries`),{children:n,truncated:i}}async hasChildren(t,e){try{return(await this.searchDirectory(t,{scope:"one",filter:"(objectClass=*)",attributes:["1.1"],sizeLimit:1},e)).searchEntries.length>0}catch{return!1}}async search(t,e){if(this.checkDn(t.base),!await this.server.ldap.readableBy(t.base,e))throw new c(`Entry ${t.base} not found`);await this.hiddenNames(),this.checkFilter(t.filter,this.hiddenFilterNames??this.hiddenAttributes);const s=Math.min(t.limit||this.maxResults,this.maxResults),r=await this.searchDirectory(t.base,{scope:t.scope,filter:t.filter,attributes:t.attributes?.length?t.attributes:["*"],sizeLimit:s+1},e),i=r.searchEntries.length>s;return{entries:r.searchEntries.slice(0,s).map(t=>this.formatEntry(t)),truncated:i}}parentOf(t){if(this.bases.some(e=>t.toLowerCase()===e.toLowerCase()))return null;const e=n(t);return e&&e!==t?e:null}api(t){const e=`${this.config.api_prefix}/v1/ldap/raw`;f(t,{method:"get",path:`${e}/bases`,handler:async({res:t})=>{await r(t,()=>({bases:this.bases}))}}),f(t,{method:"get",path:`${e}/rootdse`,handler:async({res:t})=>{await r(t,this.getRootDse.bind(this))}}),f(t,{method:"get",path:`${e}/schema`,handler:async({res:t})=>{await r(t,this.getSchema.bind(this))}}),f(t,{method:"get",path:`${e}/search`,query:w,handler:async({req:t,res:e,query:s})=>{const i=s.scope??"sub";if(!m.includes(i))throw new o(`Invalid scope "${i}", expected one of ${m.join(", ")}`);const a=s.attributes?.split(",").map(t=>t.trim()).filter(Boolean);await r(e,this.search.bind(this),{base:s.base||this.bases[0],scope:i,filter:s.filter||"(objectClass=*)",attributes:a,limit:s.limit},t)}}),f(t,{method:"get",path:`${e}/children/:dn`,params:y,query:g,handler:async({req:t,params:e,query:s,res:i})=>{const a=decodeURIComponent(e.dn),n=void 0!==s.children&&"false"!==s.children;await r(i,this.getChildren.bind(this),a,n,t)}}),f(t,{method:"get",path:`${e}/entry/:dn`,params:y,handler:async({req:t,params:e,res:s})=>{const i=decodeURIComponent(e.dn);await r(s,this.getEntry.bind(this),i,t)}})}getConfigApiData(){return{bases:this.bases,readOnly:!0,maxResults:this.maxResults,hiddenAttributes:[...this.hiddenAttributes],showSecrets:this.showSecrets}}}export{g as RawChildrenQuery,y as RawDnParams,w as RawSearchQuery,LdapRaw as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../../abstract/plugin.js";import{parseDn as s,normalizeDn as
|
|
1
|
+
import t from"../../abstract/plugin.js";import{parseDn as s,normalizeDn as a,isDnInBranch as e}from"../../lib/utils.js";import{trashOptions as r}from"./ldap.options.js";class TrashPlugin extends t{static optionDefinitions=r;name="trash";roles=["consistency"];watchedBases=[];trashBase="";addMetadata=!0;autoCreateTrash=!0;trashInitialized=!1;constructor(t){super(t);const i=this.options(r);this.trashBase=i.trash_base;const h=i.trash_watched_bases;"string"==typeof h&&h&&(this.watchedBases=h.split(";").map(t=>t.trim()).filter(t=>t.length>0));const o=this.config.ldap_base;if(o){const t=s(a(o)),holdsBase=s=>s.some((a,e)=>t.every((t,a)=>s[e+a]===t));for(const r of this.watchedBases)e(r,o)&&!holdsBase(s(a(r)).slice(0,-t.length))||this.logger.warn(`Trash watched base "${r}" is not one DN under ${o}: --trash-watched-bases takes full DNs separated by ";", and as written the trash may catch nothing under it`)}this.addMetadata="false"!==i.trash_add_metadata,this.autoCreateTrash="false"!==i.trash_auto_create,this.logger.info(`Trash plugin initialized: base=${this.trashBase}, watched=${this.watchedBases.join(";")}, metadata=${this.addMetadata}`)}isWatched(t){return!e(t,this.trashBase)&&(0===this.watchedBases.length||this.watchedBases.some(s=>e(t,s)))}extractRDN(t){return t.split(",")[0]}buildTrashDN(t){return`${this.extractRDN(t)},${this.trashBase}`}async ensureTrashExists(){if(!this.trashInitialized)if(this.autoCreateTrash){try{if((await this.server.ldap.system.search({paged:!1},this.trashBase)).searchEntries.length>0)return this.logger.debug(`Trash branch ${this.trashBase} already exists`),void(this.trashInitialized=!0)}catch{this.logger.info(`Creating trash branch ${this.trashBase}`)}try{const t=this.extractRDN(this.trashBase).split("=")[1];await this.server.ldap.system.add(this.trashBase,{objectClass:["top","organizationalUnit"],ou:t,description:"LDAP Trash - Deleted entries are moved here"}),this.logger.info(`Trash branch ${this.trashBase} created successfully`),this.trashInitialized=!0}catch(t){throw this.logger.error(`Failed to create trash branch ${this.trashBase}: ${String(t)}`),new Error(`Trash plugin: Unable to create trash branch ${this.trashBase}. Please create it manually or set DM_TRASH_AUTO_CREATE=false`)}}else this.trashInitialized=!0}async removeOldTrashEntry(t){try{(await this.server.ldap.system.search({paged:!1},t)).searchEntries.length>0&&(this.logger.info(`Removing old trash entry ${t} before overwrite`),await this.server.ldap.system.delete(t),this.logger.debug(`Old trash entry ${t} deleted`))}catch(s){this.logger.debug(`No old trash entry to remove at ${t}: ${String(s)}`)}}async addMetadataToTrash(t,s){if(this.addMetadata)try{const a=`Deleted on ${(new Date).toISOString()} from ${s}`;await this.server.ldap.system.modify(t,{replace:{description:a}}),this.logger.debug(`Metadata added to ${t}`)}catch(s){this.logger.warn(`Failed to add metadata to ${t}: ${String(s)}. Entry moved to trash but without metadata.`)}}hooks={ldapdeleterequest:async([t,s])=>{const a=Array.isArray(t)?t:[t],e=[];for(const t of a){if(!this.isWatched(t)){this.logger.debug(`Trash plugin: ${t} not in watched branches, allowing native delete`),e.push(t);continue}this.logger.info(`Trash plugin: Intercepting delete of ${t}`),await this.ensureTrashExists();const a=this.buildTrashDN(t);await this.removeOldTrashEntry(a);try{this.logger.debug(`Moving ${t} to ${a} using LDAP move (atomic)`),await this.server.ldap.move(t,a,s),this.logger.info(`Entry ${t} moved to trash at ${a}`),await this.addMetadataToTrash(a,t)}catch(s){if(this.logger.error(`Trash plugin: Failed to move ${t} to trash: ${String(s)}`),s instanceof Error&&(s.message.includes("Insufficient access")||s.message.includes("permissions")||s.message.includes("access denied")))throw new Error(`Trash plugin: Insufficient LDAP permissions to move ${t} to trash. Please grant modifyDN permissions or disable trash plugin. Original error: ${s.message}`);throw new Error(`Trash plugin: Failed to move ${t} to trash: ${String(s)}`)}}return[Array.isArray(t)?e:e[0]??void 0,s]}}}export{TrashPlugin as default};
|
package/dist/plugins/metrics.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createServer as e}from"node:http";import{Registry as t,collectDefaultMetrics as s,Gauge as o}from"prom-client";import
|
|
1
|
+
import{createServer as e}from"node:http";import{Registry as t,collectDefaultMetrics as s,Gauge as o}from"prom-client";import i from"../abstract/plugin.js";import{registry as r}from"../lib/metrics.js";import{metricsOptions as n}from"./metrics.options.js";class Metrics extends i{static optionDefinitions=n;name="metrics";own=new t;exposed;listener;constructor(e){super(e),s({register:this.own});const i=new o({name:"ldap_rest_ldap_pool_connections",help:"Pooled directory connections, by state",labelNames:["state"],registers:[this.own],collect:()=>{const e=this.server.ldap.poolStats();i.set({state:"busy"},e.busy),i.set({state:"idle"},e.idle)}}),n=new o({name:"ldap_rest_ldap_pool_waiting",help:"Operations waiting for a pooled directory connection",registers:[this.own],collect:()=>{n.set(this.server.ldap.poolStats().waiting)}}),l=new o({name:"ldap_rest_ldap_pool_opening",help:"Directory connections being opened, counted in the pool size",registers:[this.own],collect:()=>{l.set(this.server.ldap.poolStats().opening)}}),a=new o({name:"ldap_rest_ldap_pool_max",help:"Pooled directory connections at most, --ldap-pool-size",registers:[this.own],collect:()=>{a.set(this.server.ldap.poolStats().max)}});this.exposed=t.merge([r,this.own])}async api(t){const s=e((e,t)=>{this.answer(e,t)});await new Promise((e,t)=>{s.once("error",t),s.listen(this.options(n).metrics_port,()=>{s.off("error",t),e()})}),this.listener=s,this.logger.notice(`Metrics served on port ${this.port()}`)}port(){const e=this.listener?.address();return e&&"object"==typeof e?e.port:void 0}async answer(e,t){if("GET"!==e.method||"/metrics"!==e.url?.split("?")[0])return t.statusCode=404,void t.end();try{const e=await this.exposed.metrics();t.setHeader("Content-Type",this.exposed.contentType),t.end(e)}catch(e){this.logger.warn(`Metrics not collected: ${e instanceof Error?e.message:String(e)}`),t.statusCode=500,t.end()}}async close(){const e=this.listener;e&&(this.listener=void 0,e.closeAllConnections(),await new Promise(t=>e.close(()=>t())))}}export{Metrics as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineOptions as t,opt as o,readOptions as n}from"../config/options.js";const r=t({metrics_port:o.number({env:"DM_METRICS_PORT",default:9464})});function metricsOptionsOf(t){return n(r,t)}export{r as metricsOptions,metricsOptionsOf};
|
package/dist/plugins/rabbitmq.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../abstract/plugin.js";import{outsideAnyRequest as i}from"../lib/requestContext.js";class RabbitMq extends t{
|
|
1
|
+
import t from"../abstract/plugin.js";import{outsideAnyRequest as i}from"../lib/requestContext.js";import{rabbitmqOptions as r}from"./rabbitmq.options.js";class RabbitMq extends t{static optionDefinitions=r;name="rabbitmq";rabbitmqUrl;client=null;clientInit=null;constructor(t){super(t),this.rabbitmqUrl=this.options(r).rabbitmq_url,this.rabbitmqUrl||this.logger.warn(`${this.name}: rabbitmq_url is empty — AMQP publishes/subscribes will be skipped`)}isAvailable(){return this.rabbitmqUrl.length>0}health(){return this.rabbitmqUrl?this.client?.isConnected()?"ok":"degraded":"ok"}async publish(t,i,r,e){const n=await this.getClient();n&&await n.publish(t,i,r,e)}async subscribe(t,i,r,e,n){const s=await this.getClient();s&&await s.subscribe(t,i,r,e,n)}async unsubscribe(t){const i=await this.getClient();i&&await i.unsubscribe(t)}async getRawClient(){return this.getClient()}async getClient(){return this.client?this.client:!this.rabbitmqUrl||this.closed?null:(this.clientInit||(this.clientInit=(async()=>{try{const t=await i(()=>this.connect());return this.closed?(await t.close().catch(()=>{}),null):(this.client=t,this.logger.info(`${this.name}: connected to RabbitMQ at ${function(t){try{const i=new URL(t);return i.username="",i.password="",i.toString()}catch{return"amqp://[broker]"}}(this.rabbitmqUrl)}`),t)}catch(t){return this.logger.warn({plugin:this.name,event:"rabbitmq_init",message:"@linagora/rabbitmq-client unavailable or broker unreachable — AMQP disabled",error:`${t}`}),this.clientInit=null,null}})()),this.clientInit)}async connect(){const{RabbitMQClient:t}=await(import("@linagora/rabbitmq-client"));return this.open(t)}async open(t){const i=new t({url:this.rabbitmqUrl,logger:this.clientLogger()});return await i.init(),i}clientLogger(){const write=t=>(i,r)=>{const{error:e,...n}="object"==typeof r&&null!==r?r:{};this.logger.log(t,i,{plugin:this.name,...n,...void 0!==e&&{error:describeError(e)}})};return{debug:write("debug"),info:write("info"),warn:write("warn"),error:write("error")}}closed=!1;async close(){this.closed=!0;const t=this.client;this.client=null,this.clientInit=null,t&&await t.close().catch(t=>{this.logger.warn({plugin:this.name,event:"shutdown",error:`${t}`})})}}function describeError(t){if(!(t instanceof Error))return t;const{errors:i,code:r}=t;return Array.isArray(i)&&i.length?i.map(describeError).join("; "):t.message||"string"!=typeof r?String(t):`${t.name}: ${r}`}export{RabbitMq as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineOptions as t,opt as i,readOptions as n}from"../config/options.js";const o=t({rabbitmq_url:i.string({env:"DM_RABBITMQ_URL",default:""})});function rabbitmqOptionsOf(t){return n(o,t)}export{o as rabbitmqOptions,rabbitmqOptionsOf};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"fs";import{assertIdentityMode as
|
|
1
|
+
import e from"fs";import{readOptions as s}from"../../config/options.js";import{assertIdentityMode as r,identityFor as t}from"../../lib/auth/base.js";import{fillPattern as a,escapeDnValue as o,isChildOf as i}from"../../lib/utils.js";import{scimOptions as u}from"./scim.options.js";class BaseResolver{defaultUserBase;defaultGroupBase;userTemplate;groupTemplate;map;userBaseHeader;groupBaseHeader;headerRoot;config;constructor(t,a=s(u,t)){r(t,"scim base resolver"),this.config=t;const o=t.ldap_base||"";this.defaultUserBase=a.scim_user_base||o,this.defaultGroupBase=a.scim_group_base||o,this.userTemplate=a.scim_user_base_template,this.groupTemplate=a.scim_group_base_template,this.userBaseHeader=a.scim_user_base_header.toLowerCase(),this.groupBaseHeader=a.scim_group_base_header.toLowerCase(),this.headerRoot=a.scim_base_header_root;const i=a.scim_base_map;if(i)try{const s=e.readFileSync(i,"utf8");this.map=JSON.parse(s)}catch(e){throw new Error(`Failed to load SCIM base map from ${i}: ${e instanceof Error?e.message:String(e)}`)}}applyTemplate(e,s){return a(e,"{user}",s?o(s):"")}readHeader(e,s){if(!e||"object"!=typeof e)return;const r=e;if("function"==typeof r.get){const e=r.get(s);if("string"==typeof e&&e.length>0)return e}const t=r.headers?.[s];return"string"==typeof t&&t.length>0?t:Array.isArray(t)&&"string"==typeof t[0]&&t[0].length>0?t[0]:void 0}resolve(e,s){const r=s&&"object"==typeof s?t(s,this.config).value:void 0;if(this.map&&r&&this.map[r]){const s=this.map[r];if("user"===e&&s.userBase)return s.userBase;if("group"===e&&s.groupBase)return s.groupBase}const a="user"===e?this.userBaseHeader:this.groupBaseHeader;if(a&&this.headerRoot){const e=this.readHeader(s,a)?.trim();if(e&&!/[\u0000-\u001f\u007f]/.test(e)&&(e.toLowerCase()===this.headerRoot.toLowerCase()||i(e,this.headerRoot)))return e}if(this.map&&this.map["*"]){const s=this.map["*"];if("user"===e&&s.userBase)return this.applyTemplate(s.userBase,r);if("group"===e&&s.groupBase)return this.applyTemplate(s.groupBase,r)}const o="user"===e?this.userTemplate:this.groupTemplate;return o?this.applyTemplate(o,r):"user"===e?this.defaultUserBase:this.defaultGroupBase}userBase(e){return this.resolve("user",e)}groupBase(e){return this.resolve("group",e)}}export{BaseResolver};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{SCHEMA_BULK_RESPONSE as t}from"./types.js";import{scimInvalidValue as s,kindOf as e,scimErrorFromException as o}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";const r=/^bulkId:([A-Za-z0-9_-]+)$/;class ScimBulk{options;logger;users;groups;scimPrefix;hookBus;constructor(t){this.options=t.options,this.logger=t.logger,this.users=t.users,this.groups=t.groups,this.scimPrefix=t.scimPrefix,this.hookBus=t.hookBus}async execute(e,o){const r=this.options.scim_bulk_max_operations;if(!o.Operations||!Array.isArray(o.Operations))throw s("Missing Operations array");if(o.Operations.length>r)throw s(`Too many operations: ${o.Operations.length} (max ${r})`);const i=new Map,a=[];let n=0;const c=o.failOnErrors??0;for(const t of o.Operations){if(c>0&&n>=c)break;const s=await this.executeOne(e,t,i);a.push(s),parseInt(s.status,10)>=400&&n++}const u={schemas:[t],Operations:a};return this.hookBus.notify("scimbulkdone",u),u}async executeOne(t,i,a){const n=!!i&&"object"==typeof i&&!Array.isArray(i),c={method:n?i.method:void 0,bulkId:n?i.bulkId:void 0,status:"200"};try{if(!n)throw s(`Each operation must be an object, got ${e(i)}`);if("string"!=typeof i.method||"string"!=typeof i.path)throw s("method and path are required, as strings");if("DELETE"!==i.method&&(!i.data||"object"!=typeof i.data||Array.isArray(i.data)))throw s(`data must be an object, got ${e(i.data)}`);i.data&&"object"==typeof i.data&&this.resolveRefs(i.data,a);const o=i.path.replace(/^\//,"").split("/"),u=o[0],h=o[1];if("Users"!==u&&"Groups"!==u)throw s(`Unsupported bulk path '${i.path}'`);const p="Users"===u;let d=h;if(d&&r.test(d)){const t=r.exec(d),e=a.get(t[1]);if(!e)throw s(`Unknown bulkId reference '${t[1]}'`);d=e.id}switch(i.method){case"POST":{if(d)throw s("POST must not include an id");const e=p?await this.users.create(t,i.data):await this.groups.create(t,i.data);return c.status="201",c.location=this.buildLocation(u,e.id,t),c.version=e.meta?.version||void 0,i.bulkId&&a.set(i.bulkId,{id:e.id,type:p?"User":"Group"}),c}case"PUT":{if(!d)throw s("PUT requires an id");const e=p?await this.users.replace(t,d,i.data):await this.groups.replace(t,d,i.data);return c.status="200",c.location=this.buildLocation(u,e.id,t),c}case"PATCH":{if(!d)throw s("PATCH requires an id");const e=p?await this.users.patch(t,d,i.data):await this.groups.patch(t,d,i.data);return c.status="200",c.location=this.buildLocation(u,e.id,t),c}case"DELETE":if(!d)throw s("DELETE requires an id");return p?await this.users.delete(t,d):await this.groups.delete(t,d),c.status="204",c;default:throw s(`Unsupported method '${String(i.method)}'`)}}catch(t){const s=o(t);return c.status=s.status,c.response=s,c}}resolveRefs(t,e,o=0){if(o>64)throw s("Operation data nested too deeply");for(const[s,i]of Object.entries(t)){if("string"==typeof i){const o=r.exec(i);if(o){const r=e.get(o[1]);r&&(t[s]=r.id)}continue}if(Array.isArray(i)){const t=i;for(let s=0;s<t.length;s++){const i=t[s];if("string"==typeof i){const o=r.exec(i);if(o){const r=e.get(o[1]);r&&(t[s]=r.id)}}else i&&"object"==typeof i&&this.resolveRefs(i,e,o+1)}continue}i&&"object"==typeof i&&this.resolveRefs(i,e,o+1)}}buildLocation(t,s,e){const o=this.options.scim_base_url;return`${o?o.replace(/\/$/,""):e?.protocol&&e.get?`${e.protocol}://${String(e.get("host")||"")}`:""}${this.scimPrefix}/${t}/${encodeURIComponent(s)}`}}export{ScimBulk};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"path";import{loadSchemaFile as
|
|
1
|
+
import e from"path";import{loadSchemaFile as s}from"../../lib/schemaFile.js";import{SCHEMA_SERVICE_PROVIDER_CONFIG as t,SCHEMA_USER as o,SCHEMA_RESOURCE_TYPE as i,SCHEMA_GROUP as r,SCHEMA_SCHEMA as n}from"./types.js";class ScimDiscovery{config;options;schemaDir;scimPrefix;loadedPlugins;constructor(e){this.config=e.config,this.options=e.options,this.schemaDir=e.schemaDir,this.scimPrefix=e.scimPrefix,this.loadedPlugins=e.loadedPlugins}baseUrl(e){const s=this.options.scim_base_url;return s?s.replace(/\/$/,""):e?.protocol&&e.get?`${e.protocol}://${String(e.get("host")||"")}`:""}location(e,s){return`${this.baseUrl(e)}${this.scimPrefix}${s}`}authSchemes(){const e=[];return this.loadedPlugins.authToken&&e.push({name:"OAuth Bearer Token",description:'Bearer token configured via --auth-token. Send "Authorization: Bearer <token>".',type:"oauthbearertoken",primary:0===e.length}),this.loadedPlugins.openidconnect&&e.push({name:"OpenID Connect",description:"OIDC-issued access token.",type:"oauth2",specUri:"https://openid.net/specs/openid-connect-core-1_0.html",primary:0===e.length}),this.loadedPlugins.authHmac&&e.push({name:"HMAC",description:"Signed request via HMAC-SHA256.",type:"httpbasic",primary:0===e.length}),0===e.length&&e.push({name:"No authentication",description:"No authentication plugin loaded. NOT RECOMMENDED for production.",type:"httpbasic",primary:!0}),e}serviceProviderConfig(e){return{schemas:[t],patch:{supported:!0},bulk:{supported:!0,maxOperations:this.options.scim_bulk_max_operations,maxPayloadSize:this.options.scim_bulk_max_payload_size},filter:{supported:!0,maxResults:this.options.scim_max_results},changePassword:{supported:!1},sort:{supported:!1},etag:{supported:this.options.scim_etag},authenticationSchemes:this.authSchemes(),meta:{resourceType:"ServiceProviderConfig",location:this.location(e,"/ServiceProviderConfig")}}}resourceTypes(e){return[{schemas:[i],id:"User",name:"User",endpoint:"/Users",description:"User Account",schema:o,meta:{resourceType:"ResourceType",location:this.location(e,"/ResourceTypes/User")}},{schemas:[i],id:"Group",name:"Group",endpoint:"/Groups",description:"Group",schema:r,meta:{resourceType:"ResourceType",location:this.location(e,"/ResourceTypes/Group")}}]}resourceType(e,s){return this.resourceTypes(s).find(s=>s.id===e)}schemas(e){const s=[this.loadSchema("User.json",o),this.loadSchema("Group.json",r)].filter(e=>null!=e);for(const t of s)t.meta={resourceType:"Schema",location:this.location(e,`/Schemas/${t.id}`)};return s}schema(e,s){return this.schemas(s).find(s=>s.id===e)}loadSchema(t,o){const i=e.join(this.schemaDir,t);try{const e=s(i,{config:this.config});return{schemas:[n],id:o,name:e.name||o.split(":").pop()||o,description:e.description,attributes:e.attributes||[]}}catch{return}}}export{ScimDiscovery};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{HttpError as r,AUTHZ_REFUSED as
|
|
1
|
+
import{HttpError as r,isAuthzRefusal as t,errorChain as i,AUTHZ_REFUSED as e}from"../../lib/errors.js";import{messageOf as o}from"../../lib/expressFormatedResponses.js";import{isSchemaRefusal as n,extractLdapCode as s}from"../../lib/ldapCodes.js";import{SCHEMA_ERROR as c}from"./types.js";const a="application/scim+json";class ScimError extends r{scimType;constructor(r,t,i){super(t,r),this.name="ScimError",this.scimType=i}}const kindOf=r=>{if(null===r)return"null";if(Array.isArray(r))return"an array";const t=typeof r;return`${/^[aeiou]/.test(t)?"an":"a"} ${t}`},scimInvalidFilter=(r="Invalid filter")=>new ScimError(400,r,"invalidFilter"),scimInvalidPath=(r="Invalid path")=>new ScimError(400,r,"invalidPath"),scimInvalidValue=(r="Invalid value")=>new ScimError(400,r,"invalidValue"),scimInvalidSyntax=(r="Invalid syntax")=>new ScimError(400,r,"invalidSyntax"),scimNoTarget=(r="No target")=>new ScimError(400,r,"noTarget"),scimMutability=(r="Immutable attribute")=>new ScimError(400,r,"mutability"),scimUniqueness=(r="Uniqueness violation")=>new ScimError(409,r,"uniqueness"),scimTooMany=(r="Too many results")=>new ScimError(400,r,"tooMany"),scimNotFound=(r="Resource not found")=>new ScimError(404,r);function writeScimError(r,t,i,e){const o={schemas:[c],status:String(t),...e?{scimType:e}:{},detail:i};r.status(t).type(a).json(o)}function scimErrorFromException(a,m=500){const body=(r,t,i)=>({schemas:[c],status:String(r),...i?{scimType:i}:{},detail:t}),u=o(a);if(t(a))return body(403,e);if(a instanceof ScimError)return body(a.statusCode,u,a.scimType);const l=i(a).find(t=>t instanceof r);if(l){const r=400===l.statusCode&&n(s(l))?"invalidValue":void 0;return body(l.statusCode,u,r)}const p=s(a);return 32===p?body(404,"Resource not found"):68===p?body(409,u,"uniqueness"):n(p)?body(400,`The directory rejected this write against its schema: ${u}`,"invalidValue"):body(m,u||"Internal error")}function writeScimErrorFromException(r,t,i=500){const e=scimErrorFromException(t,i);r.status(Number(e.status)).type(a).json(e)}const scimAsyncHandler=r=>(t,i,e)=>{Promise.resolve().then(()=>r(t,i,e)).catch(r=>{i.headersSent?e(r):writeScimErrorFromException(i,r)})};export{a as SCIM_CONTENT_TYPE,ScimError,s as extractLdapCode,kindOf,scimAsyncHandler,scimErrorFromException,scimInvalidFilter,scimInvalidPath,scimInvalidSyntax,scimInvalidValue,scimMutability,scimNoTarget,scimNotFound,scimTooMany,scimUniqueness,writeScimError,writeScimErrorFromException};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{escapeLdapFilter as t}from"../../lib/utils.js";import{scimPathToLdapAttribute as e,DEFAULT_LOCK_ATTRIBUTE as s,DEFAULT_LOCK_VALUE as o}from"./mapping.js";import{scimInvalidFilter as n}from"./errors.js";const r=new Set(["and","or","not","pr","eq","ne","co","sw","ew","gt","ge","lt","le","true","false","null"]);const i="\0active";class Parser{constructor(t,e,s={}){this.tokens=t,this.mapping=e,this.opts=s
|
|
1
|
+
import{escapeLdapFilter as t}from"../../lib/utils.js";import{scimPathToLdapAttribute as e,DEFAULT_LOCK_ATTRIBUTE as s,DEFAULT_LOCK_VALUE as o}from"./mapping.js";import{scimInvalidFilter as n}from"./errors.js";const r=new Set(["and","or","not","pr","eq","ne","co","sw","ew","gt","ge","lt","le","true","false","null"]);const i="\0active";class Parser{tokens;mapping;opts;pos=0;depth=0;constructor(t,e,s={}){this.tokens=t,this.mapping=e,this.opts=s}peek(t=0){return this.tokens[this.pos+t]}consume(){return this.tokens[this.pos++]}expect(t,e){const s=this.tokens[this.pos];if(s.kind!==t||e&&s.value.toLowerCase()!==e)throw n(`Expected ${e||t} at position ${s.pos}, got '${s.value}'`);return this.pos++,s}parse(){const t=this.parseOr();if("EOF"!==this.peek().kind)throw n(`Unexpected token '${this.peek().value}' at position ${this.peek().pos}`);return t}parseOr(){const t=[this.parseAnd()];for(;"WORD"===this.peek().kind&&"or"===this.peek().value.toLowerCase();)this.consume(),t.push(this.parseAnd());return 1===t.length?t[0]:`(|${t.join("")})`}parseAnd(){const t=[this.parseNot()];for(;"WORD"===this.peek().kind&&"and"===this.peek().value.toLowerCase();)this.consume(),t.push(this.parseNot());return 1===t.length?t[0]:`(&${t.join("")})`}parseNot(){if("WORD"===this.peek().kind&&"not"===this.peek().value.toLowerCase()){this.consume();return`(!${this.nested(()=>this.parsePrimary())})`}return this.parsePrimary()}nested(t){if(++this.depth>32)throw n("Filter nested too deeply");try{return t()}finally{this.depth--}}parsePrimary(){const t=this.peek();if("LPAREN"===t.kind){this.consume();const t=this.nested(()=>this.parseOr());return this.expect("RPAREN"),t}if("WORD"===t.kind){const t=this.consume().value;if("LBRACK"===this.peek().kind)throw n(`Complex multi-valued filters ('${t}[...]') are not supported; use 'and' at top level instead`);const e=this.consume();if("WORD"!==e.kind)throw n(`Expected comparison operator at position ${e.pos}`);const s=e.value.toLowerCase();if("pr"===s){const e=this.resolvePath(t);return e===i?"(objectClass=*)":`(${e}=*)`}if(!r.has(s)||["and","or","not"].includes(s))throw n(`Unknown operator '${e.value}' at position ${e.pos}`);const o=this.consume(),a=this.tokenToValue(o);return this.emitComparison(t,s,a)}throw n(`Unexpected token '${t.value}' at position ${t.pos}`)}tokenToValue(t){if("STRING"===t.kind)return t.value;if("NUMBER"===t.kind){const e=Number(t.value);if(Number.isNaN(e))throw n(`Invalid number '${t.value}' at position ${t.pos}`);return e}if("WORD"===t.kind){const e=t.value.toLowerCase();return"true"===e||"false"!==e&&("null"===e?null:t.value)}throw n(`Unexpected value token at position ${t.pos}`)}resolvePath(t){if("id"===t)throw n("filter on 'id' only supports 'eq' with a string value");if("active"===t&&this.opts.supportsActive)return i;const s=e(t,this.mapping);if(!s)throw n(`Unknown attribute path '${t}'`);return s}emitComparison(e,r,a){const p=this.resolvePath(e);if(p===i){const e=!0===a||"string"==typeof a&&"true"===a.toLowerCase(),i=this.opts.lockAttribute||s;if(!this.opts.lockValue&&i.toLowerCase()!==s.toLowerCase())throw new Error(`No lock value given for '${i}'`);const p=`(${i}=${t(this.opts.lockValue||o)})`,u=`(!${p})`;if("eq"===r)return e?u:p;if("ne"===r)return e?p:u;throw n(`Operator '${r}' not supported for 'active'`)}if(null===a){if("eq"===r)return`(!(${p}=*))`;if("ne"===r)return`(${p}=*)`;throw n(`Operator '${r}' not supported with null`)}const u=String(a),l=t(u);switch(r){case"eq":return`(${p}=${l})`;case"ne":return`(!(${p}=${l}))`;case"co":return`(${p}=*${l}*)`;case"sw":return`(${p}=${l}*)`;case"ew":return`(${p}=*${l})`;case"gt":return`(&(${p}>=${l})(!(${p}=${l})))`;case"ge":return`(${p}>=${l})`;case"lt":return`(&(${p}<=${l})(!(${p}=${l})))`;case"le":return`(${p}<=${l})`;default:throw n(`Unknown operator '${r}'`)}}}function scimFilterToLdap(t,e,s={}){const o=t.trim();if(!o)return{ldapFilter:"(objectClass=*)",touchesId:!1};const r=/^id\s+eq\s+"([^"\\]*(?:\\.[^"\\]*)*)"\s*$/i.exec(o);if(r)return{ldapFilter:"(objectClass=*)",touchesId:!0,idEquals:r[1].replace(/\\(.)/g,"$1")};const i=function(t){const e=[];let s=0;for(;s<t.length;){const o=t[s];if(" "!==o&&"\t"!==o&&"\n"!==o&&"\r"!==o)if("("!==o)if(")"!==o)if("["!==o){if("]"!==o){if('"'===o){const o=s;s++;let r="";for(;s<t.length&&'"'!==t[s];){if("\\"===t[s]&&s+1<t.length){const e=t[s+1];if('"'===e){r+='"',s+=2;continue}if("\\"===e){r+="\\",s+=2;continue}if("n"===e){r+="\n",s+=2;continue}if("t"===e){r+="\t",s+=2;continue}}r+=t[s],s++}if(s>=t.length)throw n(`Unterminated string literal at position ${o}`);s++,e.push({kind:"STRING",value:r,pos:o});continue}if(o>="0"&&o<="9"){const o=s;for(;s<t.length&&/[0-9.+\-eE]/.test(t[s])&&!/[\s()[\]]/.test(t[s]);)s++;e.push({kind:"NUMBER",value:t.slice(o,s),pos:o});continue}if(/[A-Za-z_$:]/.test(o)){const o=s;for(;s<t.length&&/[A-Za-z0-9_.$:]/.test(t[s]);)s++;e.push({kind:"WORD",value:t.slice(o,s),pos:o});continue}throw n(`Unexpected character '${o}' at position ${s}`)}e.push({kind:"RBRACK",value:"]",pos:s}),s++}else e.push({kind:"LBRACK",value:"[",pos:s}),s++;else e.push({kind:"RPAREN",value:")",pos:s}),s++;else e.push({kind:"LPAREN",value:"(",pos:s}),s++;else s++}return e.push({kind:"EOF",value:"",pos:t.length}),e}(o);return{ldapFilter:new Parser(i,e,s).parse(),touchesId:/\bid\b/.test(o)}}export{scimFilterToLdap};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{escapeDnValue as t,isDummyMemberDn as e,parseDn as s,rdnValue as r,
|
|
1
|
+
import{escapeDnValue as t,isDummyMemberDn as e,parseDn as s,rdnValue as r,validateDnValue as i,isChildOf as o,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as n}from"./types.js";import{withExternalId as u,mergeMapping as c,loadMappingFile as h,DEFAULT_GROUP_MAPPING as p,requiredLdapAttributes as l,ldapToScimGroup as m,scimGroupToLdap as d}from"./mapping.js";import{scimFilterToLdap as f}from"./filter.js";import{pagedSearch as b}from"./list.js";import{writePatch as g,patchToModifyRequest as y}from"./patch.js";import{scimNotFound as w,ScimError as R,scimInvalidValue as v,kindOf as _,scimUniqueness as $}from"./errors.js";import{extractLdapCode as x}from"../../lib/ldapCodes.js";class ScimGroups{ldap;config;options;logger;baseResolver;users;hookBus;mapping;attributes;rdnAttribute;objectClass;idAttribute;maxResults;maxScanned;scimPrefix;constructor(t){this.ldap=t.ldap,this.config=t.config,this.options=t.options,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hookBus=t.hookBus,this.rdnAttribute=this.options.scim_group_rdn_attribute,this.objectClass=this.options.scim_group_object_class,this.idAttribute=this.options.scim_id_attribute,this.maxResults=this.options.scim_max_results,this.maxScanned=this.options.scim_max_scanned,this.scimPrefix=this.options.scim_prefix;const e=this.options.scim_group_mapping;this.mapping=u(c(p,e?h(e):void 0),this.options.scim_group_external_id_attribute),this.attributes=l(this.mapping,[this.rdnAttribute,"member"])}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"Group",baseUrl:this.options.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:"",lockValue:""}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const i=this.baseResolver.userBase(t),o=this.baseResolver.groupBase(t),a=this.scimPrefix,n=this.options.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),u=this.options.scim_user_rdn_attribute,c=this.rdnAttribute,h=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(e(t,h))return;const[p]=s(t),l=p?p.indexOf("="):-1;if(l<=0)return;const m=p.slice(0,l).trim(),d=r(t),f=t.toLowerCase();let b;i&&f.endsWith(i.toLowerCase())?b="User":o&&f.endsWith(o.toLowerCase())?b="Group":m===u?b="User":m===c&&(b="Group");const g={value:d};if(b&&(g.type=b,n)){const t="User"===b?"Users":"Groups";g.$ref=`${n.replace(/\/$/,"")}${a}/${t}/${encodeURIComponent(d)}`}return g}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.attributes},s)}catch(t){if(32===x(t))throw w(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw w(`Group ${e} not found`);return m(r.searchEntries[0],this.mapping,this.ctx(t),this.buildMemberResolver(t))}async list(t,e){const s=this.baseResolver.groupBase(t),r=Math.max(1,e.startIndex||1),i=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let o,a=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=f(e.filter,this.mapping);t.idEquals?o=t.idEquals:a=`(&${a}${t.ldapFilter})`}if(o)try{const e=await this.get(t,o);return{schemas:[n],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof R&&404===t.statusCode)return{schemas:[n],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:u,totalResults:c}=await b({directory:this.ldap.forRequest(t),base:s,filter:a,attributes:this.attributes,startIndex:r,count:i,maxScanned:this.maxScanned}),h=this.buildMemberResolver(t),p=u.map(e=>m(e,this.mapping,this.ctx(t),h));return{schemas:[n],totalResults:c,startIndex:r,itemsPerPage:p.length,Resources:p}}async create(e,s){if(!s.displayName)throw v("displayName is required");const r=this.baseResolver.groupBase(e);d({...s,id:void 0},this.mapping,this.objectClass);const[o]=await this.hookBus.chain("scimgroupcreate",[{...s,id:void 0},e,r]),{rdn:a,attributes:n}=d(o,this.mapping,this.objectClass),u=!n[this.rdnAttribute]&&o.id||a;if(!u)throw v("displayName is required");i(u,this.rdnAttribute),n[this.rdnAttribute]=u;const c=[];if(o.members&&Array.isArray(o.members))for(const t of o.members){if(!t||null==t.value)continue;if("string"!=typeof t.value)throw v(`Each member value must be a string, got ${_(t.value)}`);const s=await this.users.resolveRef(e,t.value);s&&c.push(s)}if(0===c.length){const t=this.config.group_dummy_user||"cn=fakeuser";c.push(t)}n.member=c;const h=`${this.rdnAttribute}=${t(u)},${r}`;try{await this.ldap.forRequest(e).add(h,n)}catch(t){if(68===x(t))throw $(`Group ${u} already exists`);throw t}const p=await this.get(e,u);return this.hookBus.notify("scimgroupcreatedone",p),p}async replace(t,e,s){await this.get(t,e);const r=(await this.hookBus.chain("scimgroupupdate",[e,s,t]))[1],{attributes:i}=d(r,this.mapping,this.objectClass),o=[];if(r.members&&Array.isArray(r.members))for(const e of r.members){if(!e||null==e.value)continue;if("string"!=typeof e.value)throw v(`Each member value must be a string, got ${_(e.value)}`);const s=await this.users.resolveRef(t,e.value);s&&o.push(s)}if(0===o.length){const t=this.config.group_dummy_user||"cn=fakeuser";o.push(t)}const a=this.dnForId(e,t),n={replace:{}};for(const[t,e]of Object.entries(i))"objectClass"!==t&&t!==this.rdnAttribute&&(n.replace[t]=e);n.replace.member=o,await this.ldap.forRequest(t).modify(a,n);const u=await this.get(t,e);return this.hookBus.notify("scimgroupupdatedone",e,u),u}async patch(t,e,s){const r=this.dnForId(e,t);await g(r,async()=>{const i=await this.currentEntry(t,e),o=await y(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e),current:i}),a=o.delete&&!Array.isArray(o.delete)?o.delete.member:void 0;if(void 0!==a){const t=this.config.group_dummy_user||"cn=fakeuser";if(""===a)delete o.delete.member,0===Object.keys(o.delete).length&&delete o.delete,o.replace||(o.replace={}),o.replace.member=[t];else{const e=[].concat(a);0===[].concat(i.member??[],o.add?.member??[]).map(String).filter(t=>!e.includes(t)).length&&(o.add||(o.add={}),o.add.member=t)}}await this.ldap.forRequest(t).modify(r,o,{permissive:!0})});const i=await this.get(t,e);return this.hookBus.notify("scimgroupupdatedone",e,i),i}async currentEntry(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.attributes},s)}catch(t){if(32===x(t))throw w(`Group ${e} not found`);throw t}if(!r.searchEntries?.length)throw w(`Group ${e} not found`);return r.searchEntries[0]}async delete(t,e){await this.get(t,e);const[s]=await this.hookBus.chain("scimgroupdelete",[e,t]),r=this.dnForId(s,t);await this.ldap.forRequest(t).delete(r),this.hookBus.notify("scimgroupdeletedone",s)}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e),i=s.includes("=")&&s.includes(",");if(i&&!o(s,r))return;const n=i?s:`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},n);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==x(t))throw t}if(!i)try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},r);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==x(t))throw t}}get groupMapping(){return this.mapping}}export{ScimGroups};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"fs";import{scimInvalidValue as r,kindOf as t}from"./errors.js";import{SCHEMA_GROUP as a,SCHEMA_USER as i}from"./types.js";const n=["createTimestamp","modifyTimestamp","entryUUID"],o={resourceType:"User",schemas:[i],entries:[{scim:"userName",ldap:"uid"},{scim:"externalId",ldap:"employeeNumber"},{scim:"name",sub:{familyName:"sn",givenName:"givenName",formatted:"cn",middleName:"initials"}},{scim:"displayName",ldap:"displayName"},{scim:"nickName",ldap:"displayName"},{scim:"title",ldap:"title"},{scim:"preferredLanguage",ldap:"preferredLanguage"},{scim:"emails",ldapPrimary:"mail",ldapSecondary:"mailAlternateAddress",multi:"array"},{scim:"phoneNumbers",ldapPrimary:"telephoneNumber",ldapSecondary:"mobile",multi:"array"}]},s={resourceType:"Group",schemas:[a],entries:[{scim:"displayName",ldap:"cn"}]};function withExternalId(e,r){const t=(r||"").trim();return t?e.entries.some(e=>"externalId"===e.scim)?e:{...e,entries:[...e.entries,{scim:"externalId",ldap:t}]}:e}function asString(e){if(null!=e){if(Array.isArray(e)){const r=e[0];return null==r?void 0:String(r)}return Buffer.isBuffer(e)?e.toString():String(e)}}function ldapTimeToIso(e){if(!e)return;const r=/^(\d{4})(\d{2})(\d{2})(\d{2})(?:(\d{2})(?:(\d{2}))?)?(?:[.,](\d+))?(Z|[+-]\d{2}(?:\d{2})?)?$/.exec(e);if(!r)return e;const[,t,a,i,n,o,s,l,c]=r;let u=0;if(l){const e=s?1e3:o?6e4:36e5;u=Math.round(Number(`0.${l}`)*e)}let d=1e3*(60*(60*Number(n)+Number(o||0))+Number(s||0))+u;const m=Math.floor(d/36e5);d-=36e5*m;const f=Math.floor(d/6e4);d-=6e4*f;const p=Math.floor(d/1e3),y=d-1e3*p;let b="Z";c&&"Z"!==c&&(b=`${c[0]}${c.slice(1,3)}:${c.slice(3,5)||"00"}`);const pad=e=>String(e).padStart(2,"0"),g=`${t}-${a}-${i}T${pad(m)}:${pad(f)}:${pad(p)}${y?`.${String(y).padStart(3,"0")}`:""}${b}`;return Number.isNaN(Date.parse(g))?e:g}function asArray(e){return null==e?[]:Array.isArray(e)?e.map(e=>Buffer.isBuffer(e)?e.toString():String(e)):Buffer.isBuffer(e)?[e.toString()]:[String(e)]}function loadMappingFile(r){if(!r)return;const t=e.readFileSync(r,"utf8");return JSON.parse(t)}function mergeMapping(e,r){if(!r)return e;const t=new Map;for(const r of e.entries)t.set(r.scim,r);for(const e of r.entries)t.set(e.scim,e);return{...e,entries:Array.from(t.values())}}const l="pwdAccountLockedTime",c="000001010000Z",u=/^\d{10,14}(\.\d+)?(Z|[+-]\d{2,4})$/;let warn=e=>{console.warn(`WARNING: ${e}`)};function setLockConfigWarn(e){const r=warn;return warn=e,r}const d=/^([A-Za-z][A-Za-z0-9-]*|\d+(\.\d+)+)$/;function resolveLockConfig(e,r,t={attribute:"--scim-user-lock-attribute",value:"--scim-user-lock-value"}){const a=(e||"").trim()||l;if(!d.test(a))throw new Error(`${t.attribute} must be an LDAP attribute name, got '${a}'`);const i=a.toLowerCase()===l.toLowerCase(),n=(r||"").trim();if(!n){if(i)return{attribute:a,value:c};throw new Error(`${t.attribute} is '${a}', so ${t.value} must say what marks an account locked (for nsAccountLock on 389-ds, 'TRUE'). The default '${c}' only means anything to ${l}.`)}return i||n!==c||warn(`${t.value} is the ppolicy convention '${c}' but ${t.attribute} is '${a}', not ${l}. Unless '${a}' is a GeneralizedTime your directory reads as a lock, every deactivation will answer 200 and read back as active:false while the account keeps binding. Verify a deactivation actually prevents a bind.`),i&&!u.test(n)&&warn(`${t.value} '${n}' is not a GeneralizedTime, and ${l} holds one. The directory will refuse the write, or — where the attribute was redefined locally — store a value it does not read as a lock. The ppolicy convention is '${c}'.`),{attribute:a,value:n}}function readActive(e){if(void 0!==e){if("boolean"==typeof e)return e;if("string"==typeof e){const r=e.trim().toLowerCase();if("true"===r)return!0;if("false"===r)return!1}throw r(`Cannot read ${t(e)} as active`)}}function isLocked(e,r){return holdsValue(e,r.lockAttribute,r.lockValue)}function holdsValue(e,r,t){if(!r)return!1;const a=r.toLowerCase(),i=Object.keys(e).find(e=>e.toLowerCase()===a),n=void 0===i?void 0:e[i];if(null==n)return!1;const o=t.toLowerCase();return(Array.isArray(n)?n:[n]).some(e=>null!=e&&e.toString().toLowerCase()===o)}function resolveScimId(e,r){if("rdn"===r.idAttribute)return asString(e[r.rdnAttribute]);return asString(e[r.idAttribute])??asString(e[r.rdnAttribute])}function buildLocation(e,r){if(!r.baseUrl)return;const t="User"===r.resourceType?"Users":"Groups";return`${r.baseUrl.replace(/\/$/,"")}${r.scimPrefix}/${t}/${encodeURIComponent(e)}`}function ldapToScimUser(e,r,t){const a={schemas:[i]},n=resolveScimId(e,t);n&&(a.id=n);for(const t of r.entries){if(t.sub){const r={};for(const[a,i]of Object.entries(t.sub)){const t=asString(e[i]);null!=t&&(r[a]=t)}Object.keys(r).length>0&&(a[t.scim]=r);continue}if(t.ldapPrimary||t.ldapSecondary){const r=[];if(t.ldapPrimary){const a=asString(e[t.ldapPrimary]);null!=a&&r.push({value:a,primary:!0})}if(t.ldapSecondary)for(const a of asArray(e[t.ldapSecondary]))r.push({value:a});r.length>0&&(a[t.scim]=r);continue}if(t.ldap)if("array"===t.multi){const r=asArray(e[t.ldap]);r.length>0&&(a[t.scim]=r)}else{const r=asString(e[t.ldap]);null!=r&&(a[t.scim]=r)}}return a.active=!isLocked(e,t),n&&(a.meta={resourceType:"User",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(n,t)}),a}function scimUserToLdap(e,a,i,n){const o={objectClass:n},s=namingValue(e,"userName")||namingValue(e,"id");for(const i of a.entries){if(i.readOnly||i.operational)continue;const a=e[i.scim];if(null!=a)if(i.sub){if(!a||"object"!=typeof a||Array.isArray(a))throw r(`${i.scim} must be an object, got ${t(a)}`);for(const[e,r]of Object.entries(i.sub)){const t=scalarOrValue(a[e],`${i.scim}.${e}`);void 0!==t&&""!==t&&(o[r]=t)}}else{if(i.ldapPrimary||i.ldapSecondary){if(!Array.isArray(a))throw r(`${i.scim} must be a list, got ${t(a)}`);const e=a.map(e=>multiValued(e,i.scim)).filter(e=>null!=e.value),n=e.find(e=>!0===e.primary)||e[0],s=e.filter(e=>e!==n);i.ldapPrimary&&n&&n.value&&(o[i.ldapPrimary]=n.value),i.ldapSecondary&&s.length>0&&(o[i.ldapSecondary]=s.map(e=>e.value).filter(Boolean));continue}i.ldap&&(o[i.ldap]=scalarValues(a,i.scim))}}const l=readActive(e.active);if(!1===l&&(o[i.lockAttribute]=i.lockValue||c),!o.cn&&s&&(o.cn=s),!o.sn){const r=[e.name?.familyName,e.displayName,s].find(e=>"string"==typeof e&&e.length>0);r&&(o.sn=r)}return{rdn:s,attributes:o,active:l}}function ldapToScimGroup(e,r,t,i){const n={schemas:[a]},o=resolveScimId(e,t);o&&(n.id=o);for(const t of r.entries){if(!t.ldap)continue;const r=e[t.ldap];if(null!=r)if("array"===t.multi){const e=asArray(r);e.length>0&&(n[t.scim]=e)}else{const e=asString(r);null!=e&&(n[t.scim]=e)}}const s=asArray(e.member),l=[];for(const e of s)if(e)if(i){const r=i(e);r&&l.push(r)}else l.push({value:e,type:"User"});return l.length>0&&(n.members=l),o&&(n.meta={resourceType:"Group",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(o,t)}),n}function scimGroupToLdap(e,r,t){const a={objectClass:t},i=namingValue(e,"displayName")||namingValue(e,"id");for(const t of r.entries){if(t.readOnly||t.operational)continue;if(!t.ldap)continue;const r=e[t.scim];null!=r&&(a[t.ldap]=scalarValues(r,t.scim))}return{rdn:i,attributes:a}}function namingValue(e,a){const i=e[a];if(null==i)return"";if("string"!=typeof i)throw r(`${a} must be a string, got ${t(i)}`);return i}function scalarOrValue(e,a){const i=e&&"object"==typeof e&&!Array.isArray(e)&&"value"in e?e.value:e;if(null!=i){if(isScalar(i))return String(i);throw r(`${a} must be a string, got ${t(i)}`)}}const isScalar=e=>"string"==typeof e||"number"==typeof e||"boolean"==typeof e;function scalarValues(e,a){if(isScalar(e))return String(e);if(Array.isArray(e)&&e.every(isScalar))return e.map(String);throw r(`${a} must be a string, a number, a boolean or a list of them, got ${t(e)}`)}function multiValued(e,a){if(e&&"object"==typeof e&&!Array.isArray(e)){const{value:r}=e;if(null==r)return e;if(isScalar(r))return{...e,value:String(r)}}throw r(`Each element of ${a} must be an object whose value is a scalar, got ${t(e)}`)}function requiredLdapAttributes(e,r=[]){const t=new Set(["objectClass",...n,...r]);for(const r of e.entries)if(r.ldap&&t.add(r.ldap),r.ldapPrimary&&t.add(r.ldapPrimary),r.ldapSecondary&&t.add(r.ldapSecondary),r.sub)for(const e of Object.values(r.sub))t.add(e);return Array.from(t)}function scimPathToLdapAttribute(e,r){if("string"!=typeof e)return;const t=e.split(".")[0],a=e.includes(".")?e.split(".").slice(1).join("."):"";for(const e of r.entries)if(e.scim===t){if(e.sub&&a)return Object.
|
|
1
|
+
import e from"fs";import{scimInvalidValue as r,kindOf as t}from"./errors.js";import{SCHEMA_GROUP as a,SCHEMA_USER as i}from"./types.js";const n=["createTimestamp","modifyTimestamp","entryUUID"],o={resourceType:"User",schemas:[i],entries:[{scim:"userName",ldap:"uid"},{scim:"externalId",ldap:"employeeNumber"},{scim:"name",sub:{familyName:"sn",givenName:"givenName",formatted:"cn",middleName:"initials"}},{scim:"displayName",ldap:"displayName"},{scim:"nickName",ldap:"displayName"},{scim:"title",ldap:"title"},{scim:"preferredLanguage",ldap:"preferredLanguage"},{scim:"emails",ldapPrimary:"mail",ldapSecondary:"mailAlternateAddress",multi:"array"},{scim:"phoneNumbers",ldapPrimary:"telephoneNumber",ldapSecondary:"mobile",multi:"array"}]},s={resourceType:"Group",schemas:[a],entries:[{scim:"displayName",ldap:"cn"}]};function withExternalId(e,r){const t=(r||"").trim();return t?e.entries.some(e=>"externalId"===e.scim)?e:{...e,entries:[...e.entries,{scim:"externalId",ldap:t}]}:e}function asString(e){if(null!=e){if(Array.isArray(e)){const r=e[0];return null==r?void 0:String(r)}return Buffer.isBuffer(e)?e.toString():String(e)}}function ldapTimeToIso(e){if(!e)return;const r=/^(\d{4})(\d{2})(\d{2})(\d{2})(?:(\d{2})(?:(\d{2}))?)?(?:[.,](\d+))?(Z|[+-]\d{2}(?:\d{2})?)?$/.exec(e);if(!r)return e;const[,t,a,i,n,o,s,l,c]=r;let u=0;if(l){const e=s?1e3:o?6e4:36e5;u=Math.round(Number(`0.${l}`)*e)}let d=1e3*(60*(60*Number(n)+Number(o||0))+Number(s||0))+u;const m=Math.floor(d/36e5);d-=36e5*m;const f=Math.floor(d/6e4);d-=6e4*f;const p=Math.floor(d/1e3),y=d-1e3*p;let b="Z";c&&"Z"!==c&&(b=`${c[0]}${c.slice(1,3)}:${c.slice(3,5)||"00"}`);const pad=e=>String(e).padStart(2,"0"),g=`${t}-${a}-${i}T${pad(m)}:${pad(f)}:${pad(p)}${y?`.${String(y).padStart(3,"0")}`:""}${b}`;return Number.isNaN(Date.parse(g))?e:g}function asArray(e){return null==e?[]:Array.isArray(e)?e.map(e=>Buffer.isBuffer(e)?e.toString():String(e)):Buffer.isBuffer(e)?[e.toString()]:[String(e)]}function loadMappingFile(r){if(!r)return;const t=e.readFileSync(r,"utf8");return JSON.parse(t)}function mergeMapping(e,r){if(!r)return e;const t=new Map;for(const r of e.entries)t.set(r.scim,r);for(const e of r.entries)t.set(e.scim,e);return{...e,entries:Array.from(t.values())}}const l="pwdAccountLockedTime",c="000001010000Z",u=/^\d{10,14}(\.\d+)?(Z|[+-]\d{2,4})$/;let warn=e=>{console.warn(`WARNING: ${e}`)};function setLockConfigWarn(e){const r=warn;return warn=e,r}const d=/^([A-Za-z][A-Za-z0-9-]*|\d+(\.\d+)+)$/;function resolveLockConfig(e,r,t={attribute:"--scim-user-lock-attribute",value:"--scim-user-lock-value"}){const a=(e||"").trim()||l;if(!d.test(a))throw new Error(`${t.attribute} must be an LDAP attribute name, got '${a}'`);const i=a.toLowerCase()===l.toLowerCase(),n=(r||"").trim();if(!n){if(i)return{attribute:a,value:c};throw new Error(`${t.attribute} is '${a}', so ${t.value} must say what marks an account locked (for nsAccountLock on 389-ds, 'TRUE'). The default '${c}' only means anything to ${l}.`)}return i||n!==c||warn(`${t.value} is the ppolicy convention '${c}' but ${t.attribute} is '${a}', not ${l}. Unless '${a}' is a GeneralizedTime your directory reads as a lock, every deactivation will answer 200 and read back as active:false while the account keeps binding. Verify a deactivation actually prevents a bind.`),i&&!u.test(n)&&warn(`${t.value} '${n}' is not a GeneralizedTime, and ${l} holds one. The directory will refuse the write, or — where the attribute was redefined locally — store a value it does not read as a lock. The ppolicy convention is '${c}'.`),{attribute:a,value:n}}function readActive(e){if(void 0!==e){if("boolean"==typeof e)return e;if("string"==typeof e){const r=e.trim().toLowerCase();if("true"===r)return!0;if("false"===r)return!1}throw r(`Cannot read ${t(e)} as active`)}}function isLocked(e,r){return holdsValue(e,r.lockAttribute,r.lockValue)}function holdsValue(e,r,t){if(!r)return!1;const a=r.toLowerCase(),i=Object.keys(e).find(e=>e.toLowerCase()===a),n=void 0===i?void 0:e[i];if(null==n)return!1;const o=t.toLowerCase();return(Array.isArray(n)?n:[n]).some(e=>null!=e&&e.toString().toLowerCase()===o)}function resolveScimId(e,r){if("rdn"===r.idAttribute)return asString(e[r.rdnAttribute]);return asString(e[r.idAttribute])??asString(e[r.rdnAttribute])}function buildLocation(e,r){if(!r.baseUrl)return;const t="User"===r.resourceType?"Users":"Groups";return`${r.baseUrl.replace(/\/$/,"")}${r.scimPrefix}/${t}/${encodeURIComponent(e)}`}function ldapToScimUser(e,r,t){const a={schemas:[i]},n=resolveScimId(e,t);n&&(a.id=n);for(const t of r.entries){if(t.sub){const r={};for(const[a,i]of Object.entries(t.sub)){const t=asString(e[i]);null!=t&&(r[a]=t)}Object.keys(r).length>0&&(a[t.scim]=r);continue}if(t.ldapPrimary||t.ldapSecondary){const r=[];if(t.ldapPrimary){const a=asString(e[t.ldapPrimary]);null!=a&&r.push({value:a,primary:!0})}if(t.ldapSecondary)for(const a of asArray(e[t.ldapSecondary]))r.push({value:a});r.length>0&&(a[t.scim]=r);continue}if(t.ldap)if("array"===t.multi){const r=asArray(e[t.ldap]);r.length>0&&(a[t.scim]=r)}else{const r=asString(e[t.ldap]);null!=r&&(a[t.scim]=r)}}return a.active=!isLocked(e,t),n&&(a.meta={resourceType:"User",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(n,t)}),a}function scimUserToLdap(e,a,i,n){const o={objectClass:n},s=namingValue(e,"userName")||namingValue(e,"id");for(const i of a.entries){if(i.readOnly||i.operational)continue;const a=e[i.scim];if(null!=a)if(i.sub){if(!a||"object"!=typeof a||Array.isArray(a))throw r(`${i.scim} must be an object, got ${t(a)}`);for(const[e,r]of Object.entries(i.sub)){const t=scalarOrValue(a[e],`${i.scim}.${e}`);void 0!==t&&""!==t&&(o[r]=t)}}else{if(i.ldapPrimary||i.ldapSecondary){if(!Array.isArray(a))throw r(`${i.scim} must be a list, got ${t(a)}`);const e=a.map(e=>multiValued(e,i.scim)).filter(e=>null!=e.value),n=e.find(e=>!0===e.primary)||e[0],s=e.filter(e=>e!==n);i.ldapPrimary&&n&&n.value&&(o[i.ldapPrimary]=n.value),i.ldapSecondary&&s.length>0&&(o[i.ldapSecondary]=s.map(e=>e.value).filter(Boolean));continue}i.ldap&&(o[i.ldap]=scalarValues(a,i.scim))}}const l=readActive(e.active);if(!1===l&&(o[i.lockAttribute]=i.lockValue||c),!o.cn&&s&&(o.cn=s),!o.sn){const r=[e.name?.familyName,e.displayName,s].find(e=>"string"==typeof e&&e.length>0);r&&(o.sn=r)}return{rdn:s,attributes:o,active:l}}function ldapToScimGroup(e,r,t,i){const n={schemas:[a]},o=resolveScimId(e,t);o&&(n.id=o);for(const t of r.entries){if(!t.ldap)continue;const r=e[t.ldap];if(null!=r)if("array"===t.multi){const e=asArray(r);e.length>0&&(n[t.scim]=e)}else{const e=asString(r);null!=e&&(n[t.scim]=e)}}const s=asArray(e.member),l=[];for(const e of s)if(e)if(i){const r=i(e);r&&l.push(r)}else l.push({value:e,type:"User"});return l.length>0&&(n.members=l),o&&(n.meta={resourceType:"Group",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(o,t)}),n}function scimGroupToLdap(e,r,t){const a={objectClass:t},i=namingValue(e,"displayName")||namingValue(e,"id");for(const t of r.entries){if(t.readOnly||t.operational)continue;if(!t.ldap)continue;const r=e[t.scim];null!=r&&(a[t.ldap]=scalarValues(r,t.scim))}return{rdn:i,attributes:a}}function namingValue(e,a){const i=e[a];if(null==i)return"";if("string"!=typeof i)throw r(`${a} must be a string, got ${t(i)}`);return i}function scalarOrValue(e,a){const i=e&&"object"==typeof e&&!Array.isArray(e)&&"value"in e?e.value:e;if(null!=i){if(isScalar(i))return String(i);throw r(`${a} must be a string, got ${t(i)}`)}}const isScalar=e=>"string"==typeof e||"number"==typeof e||"boolean"==typeof e;function scalarValues(e,a){if(isScalar(e))return String(e);if(Array.isArray(e)&&e.every(isScalar))return e.map(String);throw r(`${a} must be a string, a number, a boolean or a list of them, got ${t(e)}`)}function multiValued(e,a){if(e&&"object"==typeof e&&!Array.isArray(e)){const{value:r}=e;if(null==r)return e;if(isScalar(r))return{...e,value:String(r)}}throw r(`Each element of ${a} must be an object whose value is a scalar, got ${t(e)}`)}function requiredLdapAttributes(e,r=[]){const t=new Set(["objectClass",...n,...r]);for(const r of e.entries)if(r.ldap&&t.add(r.ldap),r.ldapPrimary&&t.add(r.ldapPrimary),r.ldapSecondary&&t.add(r.ldapSecondary),r.sub)for(const e of Object.values(r.sub))t.add(e);return Array.from(t)}function scimPathToLdapAttribute(e,r){if("string"!=typeof e)return;const t=e.split(".")[0],a=e.includes(".")?e.split(".").slice(1).join("."):"";for(const e of r.entries)if(e.scim===t){if(e.sub&&a)return Object.hasOwn(e.sub,a)?e.sub[a]:void 0;if((e.ldapPrimary||e.ldapSecondary)&&("value"===a||!a))return e.ldapPrimary||e.ldapSecondary;if(e.ldap)return e.ldap}}export{s as DEFAULT_GROUP_MAPPING,l as DEFAULT_LOCK_ATTRIBUTE,c as DEFAULT_LOCK_VALUE,o as DEFAULT_USER_MAPPING,buildLocation,holdsValue,isLocked,ldapTimeToIso,ldapToScimGroup,ldapToScimUser,loadMappingFile,mergeMapping,readActive,requiredLdapAttributes,resolveLockConfig,resolveScimId,scimGroupToLdap,scimPathToLdapAttribute,scimUserToLdap,setLockConfigWarn,warn,withExternalId};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{extractLdapCode as e}from"../../lib/ldapCodes.js";import{scimInvalidValue as t,kindOf as r,ScimError as o,scimInvalidPath as
|
|
1
|
+
import{extractLdapCode as e}from"../../lib/ldapCodes.js";import{scimInvalidValue as t,kindOf as r,ScimError as o,scimInvalidPath as n,scimNoTarget as a}from"./errors.js";import{DEFAULT_LOCK_ATTRIBUTE as i,DEFAULT_LOCK_VALUE as s,holdsValue as l,scimPathToLdapAttribute as u,readActive as f}from"./mapping.js";function asValues(e){if(null==e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString():String(e)).filter(e=>e.length>0)}class WorkingEntry{current;values=new Map;touched=new Set;whole=new Set;constructor(e){this.current=e}get(e){return this.values.has(e)||this.values.set(e,asValues(this.current[e])),this.values.get(e)}set(e,t){this.whole.add(e),this.write(e,t)}append(e,t){const r=this.get(e);this.write(e,[...r,...t.filter(e=>!r.includes(e))])}drop(e,t){if(!t)return this.set(e,[]);this.write(e,this.get(e).filter(e=>!t.includes(e)))}write(e,t){this.touched.add(e),this.values.set(e,t)}diff(){const e={};for(const t of this.touched){const r=asValues(this.current[t]),o=this.get(t),n=o.filter(e=>!r.includes(e)),a=r.filter(e=>!o.includes(e));if(0!==n.length||0!==a.length)if(this.whole.has(t))if(0!==o.length)0===a.length&&r.length>0?(e.add||(e.add={}),e.add[t]=1===n.length?n[0]:n):(e.replace||(e.replace={}),e.replace[t]=1===o.length?o[0]:o);else{if(0===r.length)continue;e.delete||(e.delete={}),e.delete[t]=""}else n.length>0&&(e.add||(e.add={}),e.add[t]=1===n.length?n[0]:n),a.length>0&&(e.delete||(e.delete={}),e.delete[t]=1===a.length?a[0]:a)}return e}}function normalizeOp(e){if("string"!=typeof e)throw t(`Patch op must be a string, got ${r(e)}`);const o=e.toLowerCase();if("add"===o||"remove"===o||"replace"===o)return o;throw t(`Unknown patch op '${e}'`)}const c=new Set(["__proto__","constructor","prototype"]),p=/^[A-Za-z_$:][\w$:]*$/;function assertSafeKey(e){if(c.has(e))throw n(`Forbidden attribute name '${e}'`)}function parsePath(e){if("string"!=typeof e||0===e.length||e.length>512)throw n(`Malformed path '${String(e)}'`);let t,r,o;const a=e.indexOf("[");if(a>=0){const i=e.lastIndexOf("]");if(i<=a)throw n(`Malformed path '${e}'`);t=e.slice(0,a),r=e.slice(a+1,i);const s=e.slice(i+1);if(s.length>0){if("."!==s[0])throw n(`Malformed path '${e}'`);o=s.slice(1)}}else{const r=e.indexOf(".");r>=0?(t=e.slice(0,r),o=e.slice(r+1)):t=e}if(!p.test(t))throw n(`Malformed path '${e}'`);if(assertSafeKey(t),null!=o){if(!p.test(o))throw n(`Malformed sub-attribute in '${e}'`);assertSafeKey(o)}return{top:t,filter:r,sub:o}}function memberRef(e){const o=e&&"object"==typeof e&&!Array.isArray(e)?e.value:e;if(null==o)return"";if("string"==typeof o||"number"==typeof o)return String(o);throw t(`Each member must be a string or an object whose value is one, got ${r(o)}`)}async function applyOperation(e,o,c){const p=normalizeOp(e.op),h=c.memberAttribute||"member";if(!e.path){if("remove"===p)throw a("PATCH 'remove' requires a path");if(null==e.value||"object"!=typeof e.value||Array.isArray(e.value))throw a("PATCH without path requires an object value");const t=e.value;for(const[r,a]of Object.entries(t)){if(!r)throw n("Empty attribute name");await applyOperation({op:e.op,path:r,value:a},o,c)}return}if("string"!=typeof e.path)throw n(`Path must be a string, got ${r(e.path)}`);const{top:d,sub:y,filter:w}=parsePath(e.path);if("members"===d){const t=c.resolveMemberRef;if(!t)throw a("Member operations require a member resolver");const asMemberValues=async e=>{const r=Array.isArray(e)?e:[e],o=[];for(const e of r){const r=memberRef(e);if(!r)continue;const n=await t(r);n&&o.push(n)}return o};if("add"===p)return void o.append(h,await asMemberValues(e.value));if("replace"===p){const t=await asMemberValues(e.value);if(0===t.length)return;return void o.set(h,t)}if(!w&&null==e.value)return void o.drop(h);const r=w?/value\s+eq\s+"([^"]+)"/i.exec(w)?.[1]??"":"",n=[...r?await asMemberValues(r):[],...null!=e.value?await asMemberValues(e.value):[]];return void o.drop(h,n)}if("active"===d&&c.supportsActive){if(y||w)throw n(`'active' has no sub-attribute (got '${e.path}')`);const r=c.lockAttribute||i,a=Object.keys(c.current).find(e=>e.toLowerCase()===r.toLowerCase())??r,u="remove"===p||function(e){const r=f(e);if(void 0===r)throw t("PATCH on 'active' requires a value");return r}(e.value),h=c.lockValue||s;return void(u?l({[a]:o.get(a)},a,h)&&o.drop(a):o.set(a,[h]))}if(w)throw n(`Complex multi-valued filters are only supported on 'members' (got '${e.path}')`);const m=u(y?`${d}.${y}`:d,c.mapping);if(!m)throw n(`Unknown SCIM attribute path '${e.path}'`);if("remove"===p)return void o.drop(m);const v=function(e){if(null!=e)return"string"==typeof e?e:"number"==typeof e||"boolean"==typeof e?String(e):Array.isArray(e)?e.map(e=>{if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);if(e&&"object"==typeof e&&!Array.isArray(e)&&"value"in e&&("string"==typeof e.value||"number"==typeof e.value||"boolean"==typeof e.value))return String(e.value);throw t(`Unsupported array element in PATCH value: ${r(e)}`)}):void 0}(e.value);if(null==v)throw t(`PATCH ${e.op} ${e.path} missing value`);const g=Array.isArray(v)?v:[v];"add"===p?o.append(m,g):o.set(m,g)}async function patchToModifyRequest(e,o){if(!e.Operations||!Array.isArray(e.Operations))throw t("Missing Operations array");const n=new WorkingEntry(o.current);for(const a of e.Operations){if(!a||"object"!=typeof a||Array.isArray(a))throw t(`Each operation must be an object, got ${r(a)}`);await applyOperation(a,n,o)}return n.diff()}async function writePatch(t,r){for(let n=1;;n++)try{return await r()}catch(r){if(16!==e(r))throw r;if(2===n)throw new o(409,`${t} changed while this PATCH was being applied; nothing was written. Retry the request.`)}}function applyPatchToResource(e,t){const r={...e};for(const e of t.Operations){const t=normalizeOp(e.op);if(!e.path){if(e.value&&"object"==typeof e.value&&!Array.isArray(e.value))for(const[t,o]of Object.entries(e.value))assertSafeKey(t),r[t]=o;continue}const{top:o,sub:n}=parsePath(e.path);if("remove"!==t)if(n){const t=r[o],a=t&&"object"==typeof t&&!Array.isArray(t)?{...t}:Object.create(null);a[n]=e.value,r[o]=a}else r[o]=e.value;else if(n){const e=r[o];e&&"object"==typeof e&&delete e[n]}else delete r[o]}return r}export{applyPatchToResource,patchToModifyRequest,writePatch};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fileURLToPath as s}from"url";import{join as i,dirname as e}from"path";import t from"../../abstract/plugin.js";import{setChangeSource as r}from"../../lib/changeContext.js";import{jsonBodyParser as o}from"../../lib/rawBody.js";import{staticPlugins as a}from"../../lib/staticFiles.js";import{BaseResolver as c}from"./baseResolver.js";import{ScimUsers as n}from"./users.js";import{ScimGroups as p}from"./groups.js";import{ScimBulk as m}from"./bulk.js";import{ScimDiscovery as u}from"./discovery.js";import{SCIM_CONTENT_TYPE as h,scimAsyncHandler as d,writeScimError as l}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";import{projectResource as g,assertProjection as f,projectList as y}from"./projection.js";function scimBodyParser(s){return o({type:["application/json","application/scim+json"],limit:`${s.scim_bulk_max_payload_size||1048576}b`})}class Scim extends t{constructor(t){super(t),this.name="scim",this.roles=["api","configurable"],this.ldap=t.ldap,this.scimPrefix=this.config.scim_prefix||"/scim/v2",this.baseResolver=new c(this.config),this.users=new n({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,hooks:this.registeredHooks}),this.groups=new p({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,users:this.users,hooks:this.registeredHooks}),this.bulk=new m({config:this.config,logger:this.logger,users:this.users,groups:this.groups,scimPrefix:this.scimPrefix,hooks:this.registeredHooks});const r=this.config.schemas_path||i(e(s(import.meta.url)),"..","..","..","static","schemas");this.scimSchemaDir=i(r,"scim"),this.discovery=new u({config:this.config,schemaDir:this.scimSchemaDir,scimPrefix:this.scimPrefix,loadedPlugins:t.loadedPlugins})}scimJson(s,i,e){s.status(i).type(h).json(e)}scimResource(s,i,e,t,r={}){const o=e.id?`${this.scimPrefix}/${t}/${encodeURIComponent(e.id)}`:void 0,a=(this.config.scim_base_url||"")&&e.meta?.location?e.meta.location:o;a&&s.set("Location",a),this.scimJson(s,i,g(e,r))}parseProjection(s){const toCsv=s=>{if("string"!=typeof s)return;const i=s.split(",").map(s=>s.trim()).filter(Boolean);return i.length>0?i:void 0},i={attributes:toCsv(s.query.attributes),excludedAttributes:toCsv(s.query.excludedAttributes)};return f(i),i}readScimBody(s){const i=String(s.headers["content-type"]||"");if(i.startsWith("application/scim+json")||i.startsWith("application/json"))return s.body}api(s){const i=this.scimPrefix;this.server.registerBodyParser(i,scimBodyParser(this.config)),s.use(i,(s,i,e)=>{r(s,"scim"),e()}),s.use(i,d((s,i,e)=>{this.parseProjection(s),e()})),s.get(`${i}/Users`,d(async(s,i)=>{const e=this.parseListQuery(s);f(e);const t=await this.users.list(s,e);this.scimJson(i,200,y(t,e))})),s.get(`${i}/Users/:id`,d(async(s,i)=>{const e=this.parseProjection(s),t=await this.users.get(s,s.params.id);this.scimJson(i,200,g(t,e))})),s.post(`${i}/Users`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return l(i,400,"Missing body","invalidSyntax");const t=await this.users.create(s,e);this.scimResource(i,201,t,"Users",this.parseProjection(s))})),s.put(`${i}/Users/:id`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return l(i,400,"Missing body","invalidSyntax");const t=await this.users.replace(s,s.params.id,e);this.scimResource(i,200,t,"Users",this.parseProjection(s))})),s.patch(`${i}/Users/:id`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return l(i,400,"Missing body","invalidSyntax");const t=await this.users.patch(s,s.params.id,e);this.scimResource(i,200,t,"Users",this.parseProjection(s))})),s.delete(`${i}/Users/:id`,d(async(s,i)=>{await this.users.delete(s,s.params.id),i.status(204).end()})),s.get(`${i}/Groups`,d(async(s,i)=>{const e=this.parseListQuery(s);f(e);const t=await this.groups.list(s,e);this.scimJson(i,200,y(t,e))})),s.get(`${i}/Groups/:id`,d(async(s,i)=>{const e=this.parseProjection(s),t=await this.groups.get(s,s.params.id);this.scimJson(i,200,g(t,e))})),s.post(`${i}/Groups`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return l(i,400,"Missing body","invalidSyntax");const t=await this.groups.create(s,e);this.scimResource(i,201,t,"Groups",this.parseProjection(s))})),s.put(`${i}/Groups/:id`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return l(i,400,"Missing body","invalidSyntax");const t=await this.groups.replace(s,s.params.id,e);this.scimResource(i,200,t,"Groups",this.parseProjection(s))})),s.patch(`${i}/Groups/:id`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return l(i,400,"Missing body","invalidSyntax");const t=await this.groups.patch(s,s.params.id,e);this.scimResource(i,200,t,"Groups",this.parseProjection(s))})),s.delete(`${i}/Groups/:id`,d(async(s,i)=>{await this.groups.delete(s,s.params.id),i.status(204).end()})),s.post(`${i}/Bulk`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return l(i,400,"Missing body","invalidSyntax");const t=this.config.scim_bulk_max_payload_size||1048576;let r;try{r=JSON.stringify(e).length}catch{return l(i,400,"Body nested too deeply","invalidValue")}if(r>t)return l(i,413,`Bulk payload too large: ${r} > ${t}`,"invalidValue");const o=await this.bulk.execute(s,e);this.scimJson(i,200,o)})),s.get(`${i}/ServiceProviderConfig`,d((s,i)=>{this.scimJson(i,200,this.discovery.serviceProviderConfig(s))})),s.get(`${i}/ResourceTypes`,d((s,i)=>{const e=this.discovery.resourceTypes(s);this.scimJson(i,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:e.length,Resources:e,startIndex:1,itemsPerPage:e.length})})),s.get(`${i}/ResourceTypes/:name`,d((s,i)=>{const e=this.discovery.resourceType(s.params.name,s);if(!e)return l(i,404,"ResourceType not found");this.scimJson(i,200,e)})),s.get(`${i}/Schemas`,d((s,i)=>{const e=this.discovery.schemas(s);this.scimJson(i,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:e.length,Resources:e,startIndex:1,itemsPerPage:e.length})})),s.get(`${i}/Schemas/:id`,d((s,i)=>{const e=this.discovery.schema(s.params.id,s);if(!e)return l(i,404,"Schema not found");this.scimJson(i,200,e)}))}parseListQuery(s){const i=s.query,toInt=s=>{if("string"!=typeof s)return;const i=parseInt(s,10);return Number.isFinite(i)?i:void 0},toCsv=s=>{if("string"==typeof s)return s.split(",").map(s=>s.trim()).filter(Boolean)},e="string"==typeof i.sortOrder?i.sortOrder:void 0;return{filter:"string"==typeof i.filter?i.filter:void 0,startIndex:toInt(i.startIndex),count:toInt(i.count),attributes:toCsv(i.attributes),excludedAttributes:toCsv(i.excludedAttributes),sortBy:"string"==typeof i.sortBy?i.sortBy:void 0,sortOrder:"ascending"===e||"descending"===e?e:void 0}}getConfigApiData(){const s=this.scimPrefix,e=["User.json","Group.json","default-mapping.json"].map(s=>i(this.scimSchemaDir,s));let t;for(const s of a(this.server)){const[i,r,o]=e.map(i=>s.schemaUrlOf(i));if(i&&r){t={user:i,group:r,...o?{defaultMapping:o}:{}};break}}const r=this.users.userMapping,o=this.groups.groupMapping,c=r.entries.map(s=>s.scim).concat(["id","active"]),n=o.entries.map(s=>s.scim).concat(["id"]);return{enabled:!0,version:"2.0",prefix:s,endpoints:{users:`${s}/Users`,user:`${s}/Users/:id`,groups:`${s}/Groups`,group:`${s}/Groups/:id`,bulk:`${s}/Bulk`,serviceProviderConfig:`${s}/ServiceProviderConfig`,resourceTypes:`${s}/ResourceTypes`,schemas:`${s}/Schemas`},schemaUrls:t,capabilities:{patch:!0,bulk:!0,filter:!0,sort:!0,etag:Boolean(this.config.scim_etag),changePassword:!1,maxResults:this.config.scim_max_results||200,maxScanned:this.config.scim_max_scanned||1e4,bulkMaxOperations:this.config.scim_bulk_max_operations||100,bulkMaxPayloadSize:this.config.scim_bulk_max_payload_size||1048576},resourceTypes:[{id:"User",endpoint:`${s}/Users`,schema:"urn:ietf:params:scim:schemas:core:2.0:User",rdnAttribute:this.config.scim_user_rdn_attribute||"uid",objectClass:this.config.scim_user_object_class,filterableAttributes:c,mapping:r.entries},{id:"Group",endpoint:`${s}/Groups`,schema:"urn:ietf:params:scim:schemas:core:2.0:Group",rdnAttribute:this.config.scim_group_rdn_attribute||"cn",objectClass:this.config.scim_group_object_class,filterableAttributes:n,mapping:o.entries}],idStrategy:this.config.scim_id_attribute||"rdn",baseResolution:{userBaseTemplate:this.config.scim_user_base_template||void 0,groupBaseTemplate:this.config.scim_group_base_template||void 0,hasBaseMap:Boolean(this.config.scim_base_map)}}}}export{Scim as default,scimBodyParser};
|
|
1
|
+
import{fileURLToPath as s}from"url";import{join as t,dirname as e}from"path";import{Type as i}from"@sinclair/typebox";import r from"../../abstract/plugin.js";import{setChangeSource as o}from"../../lib/changeContext.js";import{jsonBodyParser as a}from"../../lib/rawBody.js";import{staticPlugins as n}from"../../lib/staticFiles.js";import{scimOptions as c}from"./scim.options.js";import{BaseResolver as p}from"./baseResolver.js";import{ScimUsers as m}from"./users.js";import{ScimGroups as h}from"./groups.js";import{ScimBulk as d}from"./bulk.js";import{ScimDiscovery as u}from"./discovery.js";import{SCIM_CONTENT_TYPE as l,scimAsyncHandler as g,writeScimError as y}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";import{projectResource as f,assertProjection as b,projectList as j}from"./projection.js";import{defineRoute as x}from"../../lib/route.js";function scimBodyParser(s){return a({type:["application/json","application/scim+json"],limit:`${s.scim_bulk_max_payload_size}b`})}const _=i.Object({id:i.String({minLength:1})}),v=i.Object({name:i.String({minLength:1})});class Scim extends r{static optionDefinitions=c;name="scim";roles=["api","configurable"];ldap;scimPrefix;users;groups;bulk;discovery;scimSchemaDir;baseResolver;settings;constructor(i){super(i),this.ldap=i.ldap;const r=this.options(c);this.settings=r,this.scimPrefix=r.scim_prefix,this.baseResolver=new p(this.config,r),this.users=new m({ldap:this.ldap,options:r,logger:this.logger,baseResolver:this.baseResolver,hookBus:this.hookBus}),this.groups=new h({ldap:this.ldap,config:this.config,options:r,logger:this.logger,baseResolver:this.baseResolver,users:this.users,hookBus:this.hookBus}),this.bulk=new d({options:r,logger:this.logger,users:this.users,groups:this.groups,scimPrefix:this.scimPrefix,hookBus:this.hookBus});const o=this.config.schemas_path||t(e(s(import.meta.url)),"..","..","..","static","schemas");this.scimSchemaDir=t(o,"scim"),this.discovery=new u({config:this.config,options:r,schemaDir:this.scimSchemaDir,scimPrefix:this.scimPrefix,loadedPlugins:i.loadedPlugins})}scimJson(s,t,e){s.status(t).type(l).json(e)}scimResource(s,t,e,i,r={}){const o=e.id?`${this.scimPrefix}/${i}/${encodeURIComponent(e.id)}`:void 0,a=this.settings.scim_base_url&&e.meta?.location?e.meta.location:o;a&&s.set("Location",a),this.scimJson(s,t,f(e,r))}parseProjection(s){const toCsv=s=>{if("string"!=typeof s)return;const t=s.split(",").map(s=>s.trim()).filter(Boolean);return t.length>0?t:void 0},t={attributes:toCsv(s.query.attributes),excludedAttributes:toCsv(s.query.excludedAttributes)};return b(t),t}readScimBody(s){const t=String(s.headers["content-type"]||"");if(t.startsWith("application/scim+json")||t.startsWith("application/json"))return s.body}api(s){const t=this.scimPrefix;this.server.registerBodyParser(t,scimBodyParser(this.settings)),s.use(t,(s,t,e)=>{o(s,"scim"),e()}),s.use(t,g((s,t,e)=>{this.parseProjection(s),e()}));const e=g(async(s,t)=>{const e=this.parseListQuery(s);b(e);const i=await this.users.list(s,e);this.scimJson(t,200,j(i,e))});x(s,{method:"get",path:`${t}/Users`,json:!1,handler:({req:s,res:t,next:i})=>{e(s,t,i)}});const i=g(async(s,t)=>{const e=this.parseProjection(s),i=await this.users.get(s,s.params.id);this.scimJson(t,200,f(i,e))});x(s,{method:"get",path:`${t}/Users/:id`,json:!1,params:_,handler:({req:s,res:t,next:e})=>{i(s,t,e)}});const r=g(async(s,t)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return y(t,400,"Missing body","invalidSyntax");const i=await this.users.create(s,e);this.scimResource(t,201,i,"Users",this.parseProjection(s))});x(s,{method:"post",path:`${t}/Users`,json:!1,handler:({req:s,res:t,next:e})=>{r(s,t,e)}});const a=g(async(s,t)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return y(t,400,"Missing body","invalidSyntax");const i=await this.users.replace(s,s.params.id,e);this.scimResource(t,200,i,"Users",this.parseProjection(s))});x(s,{method:"put",path:`${t}/Users/:id`,json:!1,params:_,handler:({req:s,res:t,next:e})=>{a(s,t,e)}});const n=g(async(s,t)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return y(t,400,"Missing body","invalidSyntax");const i=await this.users.patch(s,s.params.id,e);this.scimResource(t,200,i,"Users",this.parseProjection(s))});x(s,{method:"patch",path:`${t}/Users/:id`,json:!1,params:_,handler:({req:s,res:t,next:e})=>{n(s,t,e)}});const c=g(async(s,t)=>{await this.users.delete(s,s.params.id),t.status(204).end()});x(s,{method:"delete",path:`${t}/Users/:id`,json:!1,params:_,handler:({req:s,res:t,next:e})=>{c(s,t,e)}});const p=g(async(s,t)=>{const e=this.parseListQuery(s);b(e);const i=await this.groups.list(s,e);this.scimJson(t,200,j(i,e))});x(s,{method:"get",path:`${t}/Groups`,json:!1,handler:({req:s,res:t,next:e})=>{p(s,t,e)}});const m=g(async(s,t)=>{const e=this.parseProjection(s),i=await this.groups.get(s,s.params.id);this.scimJson(t,200,f(i,e))});x(s,{method:"get",path:`${t}/Groups/:id`,json:!1,params:_,handler:({req:s,res:t,next:e})=>{m(s,t,e)}});const h=g(async(s,t)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return y(t,400,"Missing body","invalidSyntax");const i=await this.groups.create(s,e);this.scimResource(t,201,i,"Groups",this.parseProjection(s))});x(s,{method:"post",path:`${t}/Groups`,json:!1,handler:({req:s,res:t,next:e})=>{h(s,t,e)}});const d=g(async(s,t)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return y(t,400,"Missing body","invalidSyntax");const i=await this.groups.replace(s,s.params.id,e);this.scimResource(t,200,i,"Groups",this.parseProjection(s))});x(s,{method:"put",path:`${t}/Groups/:id`,json:!1,params:_,handler:({req:s,res:t,next:e})=>{d(s,t,e)}});const u=g(async(s,t)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return y(t,400,"Missing body","invalidSyntax");const i=await this.groups.patch(s,s.params.id,e);this.scimResource(t,200,i,"Groups",this.parseProjection(s))});x(s,{method:"patch",path:`${t}/Groups/:id`,json:!1,params:_,handler:({req:s,res:t,next:e})=>{u(s,t,e)}});const l=g(async(s,t)=>{await this.groups.delete(s,s.params.id),t.status(204).end()});x(s,{method:"delete",path:`${t}/Groups/:id`,json:!1,params:_,handler:({req:s,res:t,next:e})=>{l(s,t,e)}});const P=g(async(s,t)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return y(t,400,"Missing body","invalidSyntax");const i=this.settings.scim_bulk_max_payload_size;let r;try{r=JSON.stringify(e).length}catch{return y(t,400,"Body nested too deeply","invalidValue")}if(r>i)return y(t,413,`Bulk payload too large: ${r} > ${i}`,"invalidValue");const o=await this.bulk.execute(s,e);this.scimJson(t,200,o)});x(s,{method:"post",path:`${t}/Bulk`,json:!1,handler:({req:s,res:t,next:e})=>{P(s,t,e)}});const S=g((s,t)=>{this.scimJson(t,200,this.discovery.serviceProviderConfig(s))});x(s,{method:"get",path:`${t}/ServiceProviderConfig`,json:!1,handler:({req:s,res:t,next:e})=>{S(s,t,e)}});const $=g((s,t)=>{const e=this.discovery.resourceTypes(s);this.scimJson(t,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:e.length,Resources:e,startIndex:1,itemsPerPage:e.length})});x(s,{method:"get",path:`${t}/ResourceTypes`,json:!1,handler:({req:s,res:t,next:e})=>{$(s,t,e)}});const B=g((s,t)=>{const e=this.discovery.resourceType(s.params.name,s);if(!e)return y(t,404,"ResourceType not found");this.scimJson(t,200,e)});x(s,{method:"get",path:`${t}/ResourceTypes/:name`,json:!1,params:v,handler:({req:s,res:t,next:e})=>{B(s,t,e)}});const R=g((s,t)=>{const e=this.discovery.schemas(s);this.scimJson(t,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:e.length,Resources:e,startIndex:1,itemsPerPage:e.length})});x(s,{method:"get",path:`${t}/Schemas`,json:!1,handler:({req:s,res:t,next:e})=>{R(s,t,e)}});const k=g((s,t)=>{const e=this.discovery.schema(s.params.id,s);if(!e)return y(t,404,"Schema not found");this.scimJson(t,200,e)});x(s,{method:"get",path:`${t}/Schemas/:id`,json:!1,params:_,handler:({req:s,res:t,next:e})=>{k(s,t,e)}})}parseListQuery(s){const t=s.query,toInt=s=>{if("string"!=typeof s)return;const t=parseInt(s,10);return Number.isFinite(t)?t:void 0},toCsv=s=>{if("string"==typeof s)return s.split(",").map(s=>s.trim()).filter(Boolean)},e="string"==typeof t.sortOrder?t.sortOrder:void 0;return{filter:"string"==typeof t.filter?t.filter:void 0,startIndex:toInt(t.startIndex),count:toInt(t.count),attributes:toCsv(t.attributes),excludedAttributes:toCsv(t.excludedAttributes),sortBy:"string"==typeof t.sortBy?t.sortBy:void 0,sortOrder:"ascending"===e||"descending"===e?e:void 0}}getConfigApiData(){const s=this.scimPrefix,e=["User.json","Group.json","default-mapping.json"].map(s=>t(this.scimSchemaDir,s));let i;for(const s of n(this.server)){const[t,r,o]=e.map(t=>s.schemaUrlOf(t));if(t&&r){i={user:t,group:r,...o?{defaultMapping:o}:{}};break}}const r=this.users.userMapping,o=this.groups.groupMapping,a=r.entries.map(s=>s.scim).concat(["id","active"]),c=o.entries.map(s=>s.scim).concat(["id"]);return{enabled:!0,version:"2.0",prefix:s,endpoints:{users:`${s}/Users`,user:`${s}/Users/:id`,groups:`${s}/Groups`,group:`${s}/Groups/:id`,bulk:`${s}/Bulk`,serviceProviderConfig:`${s}/ServiceProviderConfig`,resourceTypes:`${s}/ResourceTypes`,schemas:`${s}/Schemas`},schemaUrls:i,capabilities:{patch:!0,bulk:!0,filter:!0,sort:!0,etag:this.settings.scim_etag,changePassword:!1,maxResults:this.settings.scim_max_results,maxScanned:this.settings.scim_max_scanned,bulkMaxOperations:this.settings.scim_bulk_max_operations,bulkMaxPayloadSize:this.settings.scim_bulk_max_payload_size},resourceTypes:[{id:"User",endpoint:`${s}/Users`,schema:"urn:ietf:params:scim:schemas:core:2.0:User",rdnAttribute:this.settings.scim_user_rdn_attribute,objectClass:this.settings.scim_user_object_class,filterableAttributes:a,mapping:r.entries},{id:"Group",endpoint:`${s}/Groups`,schema:"urn:ietf:params:scim:schemas:core:2.0:Group",rdnAttribute:this.settings.scim_group_rdn_attribute,objectClass:this.settings.scim_group_object_class,filterableAttributes:c,mapping:o.entries}],idStrategy:this.settings.scim_id_attribute,baseResolution:{userBaseTemplate:this.settings.scim_user_base_template||void 0,groupBaseTemplate:this.settings.scim_group_base_template||void 0,hasBaseMap:Boolean(this.settings.scim_base_map)}}}}export{_ as ScimIdParams,v as ScimNameParams,Scim as default,scimBodyParser};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineOptions as _,opt as e,readOptions as s}from"../../config/options.js";const t=_({scim_prefix:e.string({env:"DM_SCIM_PREFIX",default:"/scim/v2"}),scim_user_base:e.string({env:"DM_SCIM_USER_BASE",default:""}),scim_group_base:e.string({env:"DM_SCIM_GROUP_BASE",default:""}),scim_user_base_template:e.string({env:"DM_SCIM_USER_BASE_TEMPLATE",default:""}),scim_group_base_template:e.string({env:"DM_SCIM_GROUP_BASE_TEMPLATE",default:""}),scim_base_map:e.string({env:"DM_SCIM_BASE_MAP",default:""}),scim_user_base_header:e.string({env:"DM_SCIM_USER_BASE_HEADER",default:""}),scim_group_base_header:e.string({env:"DM_SCIM_GROUP_BASE_HEADER",default:""}),scim_base_header_root:e.string({env:"DM_SCIM_BASE_HEADER_ROOT",default:""}),scim_user_object_class:e.array({env:"DM_SCIM_USER_OBJECT_CLASSES",default:["top","inetOrgPerson","organizationalPerson","person"],plural:"scim-user-object-classes",kind:"identifiers"}),scim_user_rdn_attribute:e.string({env:"DM_SCIM_USER_RDN_ATTRIBUTE",default:"uid"}),scim_user_lock_attribute:e.string({env:"DM_SCIM_USER_LOCK_ATTRIBUTE",default:"pwdAccountLockedTime"}),scim_user_lock_value:e.string({env:"DM_SCIM_USER_LOCK_VALUE",default:""}),scim_group_object_class:e.array({env:"DM_SCIM_GROUP_OBJECT_CLASSES",default:["top","groupOfNames"],plural:"scim-group-object-classes",kind:"identifiers"}),scim_group_rdn_attribute:e.string({env:"DM_SCIM_GROUP_RDN_ATTRIBUTE",default:"cn"}),scim_group_external_id_attribute:e.string({env:"DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",default:""}),scim_id_attribute:e.string({env:"DM_SCIM_ID_ATTRIBUTE",default:"rdn"}),scim_user_mapping:e.string({env:"DM_SCIM_USER_MAPPING",default:""}),scim_group_mapping:e.string({env:"DM_SCIM_GROUP_MAPPING",default:""}),scim_max_results:e.number({env:"DM_SCIM_MAX_RESULTS",default:200,min:1}),scim_max_scanned:e.number({env:"DM_SCIM_MAX_SCANNED",default:1e4,min:1}),scim_bulk_max_operations:e.number({env:"DM_SCIM_BULK_MAX_OPERATIONS",default:100,min:1}),scim_bulk_max_payload_size:e.number({env:"DM_SCIM_BULK_MAX_PAYLOAD_SIZE",default:1048576,min:1}),scim_etag:e.boolean({env:"DM_SCIM_ETAG",default:!1}),scim_base_url:e.string({env:"DM_SCIM_BASE_URL",default:""})});function scimOptionsOf(_){return s(t,_)}export{t as scimOptions,scimOptionsOf};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{escapeDnValue as t,
|
|
1
|
+
import{escapeDnValue as t,validateDnValue as e,isChildOf as s,escapeLdapFilter as i}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as r}from"./types.js";import{resolveLockConfig as o,mergeMapping as a,loadMappingFile as c,DEFAULT_USER_MAPPING as n,requiredLdapAttributes as h,ldapToScimUser as u,scimUserToLdap as l,isLocked as d}from"./mapping.js";import{scimFilterToLdap as p}from"./filter.js";import{pagedSearch as m}from"./list.js";import{writePatch as b,patchToModifyRequest as f}from"./patch.js";import{scimNotFound as g,ScimError as k,scimInvalidValue as w,scimUniqueness as y,kindOf as A}from"./errors.js";import{extractLdapCode as R}from"../../lib/ldapCodes.js";class ScimUsers{ldap;options;logger;baseResolver;hookBus;mapping;rdnAttribute;objectClass;idAttribute;maxResults;maxScanned;scimPrefix;lockAttribute;lockValue;constructor(t){this.ldap=t.ldap,this.options=t.options,this.logger=t.logger,this.baseResolver=t.baseResolver,this.hookBus=t.hookBus,this.rdnAttribute=this.options.scim_user_rdn_attribute,this.objectClass=this.options.scim_user_object_class,this.idAttribute=this.options.scim_id_attribute,this.maxResults=this.options.scim_max_results,this.maxScanned=this.options.scim_max_scanned,this.scimPrefix=this.options.scim_prefix;const e=o(this.options.scim_user_lock_attribute,this.options.scim_user_lock_value);this.lockAttribute=e.attribute,this.lockValue=e.value;const s=this.options.scim_user_mapping;this.mapping=a(n,s?c(s):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"User",baseUrl:this.options.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:this.lockAttribute,lockValue:this.lockValue}}ldapAttributes(){return h(this.mapping,[this.lockAttribute])}dnForId(e,s){const i=this.baseResolver.userBase(s);return`${this.rdnAttribute}=${t(e)},${i}`}async get(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===R(t))throw g(`User ${e} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw g(`User ${e} not found`);return u(i.searchEntries[0],this.mapping,this.ctx(t))}async list(t,e){const s=this.baseResolver.userBase(t),i=Math.max(1,e.startIndex||1),o=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let a,c=`(${this.objectClass.includes("inetOrgPerson")?"objectClass=inetOrgPerson":`objectClass=${this.objectClass[0]}`})`;if(e.filter){const t=p(e.filter,this.mapping,{lockAttribute:this.lockAttribute,lockValue:this.lockValue,supportsActive:!0});t.idEquals?a=t.idEquals:c=`(&${c}${t.ldapFilter})`}if(a)try{const e=await this.get(t,a);return{schemas:[r],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof k&&404===t.statusCode)return{schemas:[r],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:n,totalResults:h}=await m({directory:this.ldap.forRequest(t),base:s,filter:c,attributes:this.ldapAttributes(),startIndex:i,count:o,maxScanned:this.maxScanned});e.sortBy,e.sortOrder;const l=n.map(e=>u(e,this.mapping,this.ctx(t)));return{schemas:[r],totalResults:h,startIndex:i,itemsPerPage:l.length,Resources:l}}async create(s,i){if(!i.userName)throw w("userName is required");l(i,this.mapping,this.ctx(s),this.objectClass);const[r]=await this.hookBus.chain("scimusercreate",[i,s]),{rdn:o,attributes:a}=l(r,this.mapping,this.ctx(s),this.objectClass);if(!o)throw w("userName is required");e(o,this.rdnAttribute),a[this.rdnAttribute]=o;const c=this.baseResolver.userBase(s),n=`${this.rdnAttribute}=${t(o)},${c}`;try{await this.ldap.forRequest(s).add(n,a)}catch(t){if(68===R(t))throw y(`User ${o} already exists`);if(void 0!==a[this.lockAttribute])throw this.lockSchemaError(t);throw t}const h=await this.get(s,o);return this.hookBus.notify("scimusercreatedone",h),h}async replace(t,e,s){const i=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},i)}catch(t){if(32===R(t))throw g(`User ${e} not found`);throw t}if(!r.searchEntries?.length)throw g(`User ${e} not found`);const o=r.searchEntries[0],a=(await this.hookBus.chain("scimuserupdate",[e,s,t]))[1],{attributes:c,active:n}=l(a,this.mapping,this.ctx(t),this.objectClass),h={replace:{},delete:[]},u=new Set(["objectClass","entryUUID","createTimestamp","modifyTimestamp",this.rdnAttribute,this.lockAttribute]),hasAttrValue=t=>null!=t&&("string"==typeof t?t.length>0:!Array.isArray(t)||t.length>0&&t.some(t=>null!=t));for(const t of this.ldapAttributes())u.has(t)||(null!=c[t]?h.replace[t]=c[t]:hasAttrValue(o[t])&&h.delete.push(t));if(!1===n?h.replace[this.lockAttribute]=this.lockValue:!0===n&&d(o,{lockAttribute:this.lockAttribute,lockValue:this.lockValue})&&h.delete.push(this.lockAttribute),0===Object.keys(h.replace||{}).length&&delete h.replace,0===h.delete.length&&delete h.delete,h.replace||h.delete)try{await this.ldap.forRequest(t).modify(i,h)}catch(t){if(void 0!==n)throw this.lockSchemaError(t);if(16!==R(t))throw t;throw new k(409,`${i} changed while this PUT was being applied; nothing was written. Retry the request.`)}const p=await this.get(t,e);return this.hookBus.notify("scimuserupdatedone",e,p),p}async patch(t,e,s){const i=this.dnForId(e,t);await b(i,async()=>{const r=await this.currentEntry(t,e),o=await f(s,{mapping:this.mapping,lockAttribute:this.lockAttribute,lockValue:this.lockValue,supportsActive:!0,current:r});try{await this.ldap.forRequest(t).modify(i,o,{permissive:!0})}catch(t){if(function(t,e){const s=e.toLowerCase(),i=t.delete;return[...Object.keys(t.replace||{}),...Object.keys(t.add||{}),...Array.isArray(i)?i:Object.keys(i||{})].some(t=>t.toLowerCase()===s)}(o,this.lockAttribute))throw this.lockSchemaError(t);throw t}});const r=await this.get(t,e);return this.hookBus.notify("scimuserupdatedone",e,r),r}lockSchemaError(t){const e=R(t);if(17!==e&&65!==e)return t;const s=t instanceof Error?t.message:String(t);return w(`The directory rejected '${this.lockAttribute}', the attribute backing 'active': ${s}. Point --scim-user-lock-attribute and --scim-user-lock-value at an attribute this directory defines (nsAccountLock / TRUE on 389-ds); the default needs the ppolicy overlay.`)}async currentEntry(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===R(t))throw g(`User ${e} not found`);throw t}if(!i.searchEntries?.length)throw g(`User ${e} not found`);return i.searchEntries[0]}async delete(t,e){await this.get(t,e);const[s]=await this.hookBus.chain("scimuserdelete",[e,t]),i=this.dnForId(s,t);await this.ldap.forRequest(t).delete(i),this.hookBus.notify("scimuserdeletedone",s)}async resolveRef(e,r){if("string"!=typeof r)throw w(`A member value must be a string, got ${A(r)}`);if(!r)return;const o=this.baseResolver.userBase(e),a=r.includes("=")&&r.includes(",");if(a&&!s(r,o))return;const c=a?r:`${this.rdnAttribute}=${t(r)},${o}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},c);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==R(t))throw t}if(!a)try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${i(r)})`,scope:"sub",paged:!1,attributes:["dn"]},o);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==R(t))throw t}}get userMapping(){return this.mapping}}export{ScimUsers};
|