ldap-rest 0.3.10 → 0.4.1
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/dist/config/args.js +1 -1
- package/dist/lib/authz/base.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/plugins/auth/authzDynamic.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/onChange.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/ldap/trash.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/scim/users.js +1 -1
- package/dist/plugins/twake/appAccountsApi.js +1 -1
- package/dist/plugins/twake/clouderyProvision.js +1 -1
- package/dist/src/config/args.d.ts +2 -0
- package/dist/src/hooks.d.ts +1 -1
- package/dist/src/lib/authz/base.d.ts +1 -0
- package/dist/src/lib/ldapActions.d.ts +1 -1
- package/dist/src/plugins/twake/appAccountsApi.d.ts +34 -2
- package/dist/src/plugins/twake/clouderyProvision.d.ts +1 -0
- package/package.json +1 -1
package/dist/config/args.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fileURLToPath as _}from"url";import{join as a,dirname as e}from"path";const r=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",300,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",a(e(_(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",a(e(_(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",a(e(_(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{r as default};
|
|
1
|
+
import{fileURLToPath as _}from"url";import{join as a,dirname as e}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",300,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",a(e(_(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",a(e(_(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",a(e(_(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
|
package/dist/lib/authz/base.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import r from"../../abstract/plugin.js";import{getParentDn as e}from"../utils.js";class AuthzBase extends r{constructor(){super(...arguments),this.roles=["authz"],this.hooks={ldapmodifyrequest:async([r,e,s
|
|
1
|
+
import r from"../../abstract/plugin.js";import{getParentDn as e}from"../utils.js";class AuthzBase extends r{constructor(){super(...arguments),this.roles=["authz"],this.hooks={ldapmodifyrequest:async([r,e,t,s])=>{if(this.shouldSkipAuthorization(s))return[r,e,t,s];const i=await this.resolveUser(s.user);if(!i)return this.logger.warn(`User ${s.user} could not be resolved`),[r,e,t,s];const o=this.config.ldap_organization_link_attribute;if(o&&e.replace?.[o]){try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[o]},r);if(e.searchEntries.length>0){const r=e.searchEntries[0][o],t=Array.isArray(r)?String(r[0]):String(r);if(!(await this.getUserPermissions(i,t)).read)throw new Error(`[authz-forbidden] User ${s.user} does not have read permission for source branch ${t}`)}}catch(e){const t=this.extractBranchDn(r);if(!(await this.getUserPermissions(i,t)).read)throw new Error(`[authz-forbidden] User ${s.user} does not have read permission for source branch ${t}`)}const t=e.replace[o],n=Array.isArray(t)?String(t[0]):String(t);if(!(await this.getUserPermissions(i,n)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for destination branch ${n}`)}else{const e=this.extractBranchDn(r);if(!(await this.getUserPermissions(i,e)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for branch ${e}`)}return[r,e,t,s]},ldapaddrequest:async([r,e,t])=>{if(this.shouldSkipAuthorization(t))return[r,e,t];const s=await this.resolveUser(t.user);if(!s)return this.logger.warn(`User ${t.user} could not be resolved`),[r,e,t];let i;const o=this.config.ldap_organization_link_attribute;if(o&&e[o]){const r=e[o];i=Array.isArray(r)?String(r[0]):String(r)}else i=this.extractBranchDn(r);if(!(await this.getUserPermissions(s,i)).write)throw new Error(`[authz-forbidden] User ${t.user} does not have write permission for branch ${i}`);return[r,e,t]},ldapsearchrequest:async([r,e,t])=>{if(this.shouldSkipAuthorization(t))return[r,e,t];const s=await this.resolveUser(t.user);if(!s)return this.logger.warn(`User ${t.user} could not be resolved`),[r,e,t];if(r===this.config.ldap_top_organization&&"base"===e.scope)return[r,e,t];if(!(await this.getUserPermissions(s,r)).read)throw new Error(`[authz-forbidden] User ${t.user} does not have read permission for branch ${r}`);return[r,e,t]},getOrganisationTop:async([r,e])=>{if(this.shouldSkipAuthorization(r))return[r,e];const t=await this.resolveUser(r.user);if(!t)return[r,e];const s=await this.getAuthorizedBranches(t);if(s.length>0){const e=[];for(const t of s)try{const s=await this.server.ldap.search({paged:!1,scope:"base"},t,r);1===s.searchEntries.length&&e.push(s.searchEntries[0])}catch(r){this.logger.warn(`Failed to fetch authorized branch ${t}: ${r}`)}if(1===e.length)return[r,e[0]];if(e.length>1)return[r,e[0]]}return[r,e]},ldaprenamerequest:async([r,e,t])=>{if(this.shouldSkipAuthorization(t))return[r,e,t];const s=await this.resolveUser(t.user);if(!s)return this.logger.warn(`User ${t.user} could not be resolved`),[r,e,t];const i=this.extractBranchDn(r),o=this.extractBranchDn(e);if(!(await this.getUserPermissions(s,i)).read)throw new Error(`[authz-forbidden] User ${t.user} does not have read permission for source branch ${i}`);if(!(await this.getUserPermissions(s,o)).write)throw new Error(`[authz-forbidden] User ${t.user} does not have write permission for destination branch ${o}`);return[r,e,t]},ldapdeleterequest:async([r,e])=>{if(this.shouldSkipAuthorization(e))return[r,e];const t=await this.resolveUser(e.user);if(!t)return this.logger.warn(`User ${e.user} could not be resolved`),[r,e];const s=Array.isArray(r)?r:[r];for(const r of s){const s=this.extractBranchDn(r);if(!(await this.getUserPermissions(t,s)).delete)throw new Error(`[authz-forbidden] User ${e.user} does not have delete permission for branch ${s}`)}return[r,e]}}}extractBranchDn(r){return e(r)}shouldSkipAuthorization(r){return!r?.user}}export{AuthzBase as default};
|
package/dist/lib/ldapActions.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Client as e,Attribute as t,Change as i}from"ldapts";import{LRUCache as n}from"lru-cache";import o from"p-limit";import{launchHooksChained as s,launchHooks as r,escapeDnValue as a}from"./utils.js";const c={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}};class ldapActions{constructor(e){this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.parent=e,this.logger=e.logger,this.config=e.config,this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=o(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const i="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,s=1e3*(this.config.ldap_cache_ttl||300);if(this.searchCache=new n({max:i,ttl:s,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(`LDAP search cache initialized: max=${i}, ttl=${s/1e3}s`),this.attrSignatureCache=new n({max:1e3}),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP DN is not defined");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined");e.config.ldap_base?this.base=e.config.ldap_base:(this.base=e.config.ldap_dn.split(",",2)[1],this.logger.warn(`LDAP base is not defined, using "${this.base}"`)),this.ldapUrls=e.config.ldap_url,this.logger.info(`LDAP failover configured with ${this.ldapUrls.length} URL(s): ${this.ldapUrls.join(", ")}`),this.options={url:this.ldapUrls[0],timeout:0,connectTimeout:0,strictDN:!1},this.ldapUrls[0].startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const t=[];for(let i=0;i<this.ldapUrls.length;i++){const n=(this.currentUrlIndex+i)%this.ldapUrls.length,o=this.ldapUrls[n];try{this.logger.debug(`Attempting connection to ${o}`);const t={...this.options,url:o,tlsOptions:o.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},i=new e(t);return await i.bind(this.dn,this.pwd),n!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${o}`),this.currentUrlIndex=n),i}catch(e){this.logger.warn(`Failed to connect to ${o}: ${String(e)}`),t.push(e)}}throw this.logger.error(`LDAP connection failed for all ${this.ldapUrls.length} URL(s)`),new Error(`LDAP connection failed for all URLs: ${t.map(e=>e.message).join(", ")}`)}cleanupExpiredConnections(){if(!this.isCleaningUp){this.isCleaningUp=!0;try{const e=Date.now(),t=[];for(let i=this.availableConnections.length-1;i>=0;i--){const n=this.availableConnections[i];if(e-n.createdAt>this.connectionTtl){t.push(n),this.availableConnections.splice(i,1);const e=this.connectionPool.indexOf(n);-1!==e&&this.connectionPool.splice(e,1)}}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}finally{this.isCleaningUp=!1}}}async acquireConnection(){if(this.cleanupExpiredConnections(),this.availableConnections.length>0){const e=this.availableConnections.pop();return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e}if(this.connectionPool.length<this.poolSize){const e={client:await this.createConnection(),createdAt:Date.now(),inUse:!0};return this.connectionPool.push(e),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),e}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((e,t)=>{const i=globalThis.setTimeout(()=>{const e=this.waitingResolvers.indexOf(resolveWrapper);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),resolveWrapper=t=>{globalThis.clearTimeout(i),e(t)};this.waitingResolvers.push(resolveWrapper)})}releaseConnection(e){if(Date.now()-e.createdAt>this.connectionTtl){const t=this.connectionPool.indexOf(e);return-1!==t&&this.connectionPool.splice(t,1),e.client.unbind().catch(()=>{}),void this.logger.debug("Released expired LDAP connection (discarded)")}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t(e)}e.inUse=!1,this.availableConnections.push(e),this.logger.debug("Released LDAP connection back to pool")}getAttributeSignature(e){if(!e||0===e.length)return"*";const t=e.join("|");let i=this.attrSignatureCache.get(t);return i||(i=[...e].sort().join(","),this.attrSignatureCache.set(t,i)),i}getCacheKey(e,t){const i=this.getAttributeSignature(t.attributes),n="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return`${e}:${t.scope||"sub"}:${n}:${i}`}invalidateCache(e){for(const t of this.searchCache.keys())t.startsWith(e)&&this.searchCache.delete(t)}async search(e,t=this.base,i){let n={...c,...e};if(n=await s(this.parent.hooks.ldapsearchopts,n),[t,n]=await s(this.parent.hooks.ldapsearchrequest,[t,n,i]),!n.paged&&"base"===n.scope){const e=this.getCacheKey(t,n),i=this.searchCache.get(e);if(i)return this.logger.debug(`LDAP search cache hit: ${e}`),i}const o=await this.acquireConnection();try{let e=n.paged?o.client.searchPaginated(t,n):o.client.search(t,n);if(e=await s(this.parent.hooks.ldapsearchresult,e),!n.paged&&"base"===n.scope&&e instanceof Promise){const i=await e,o=this.getCacheKey(t,n);return this.searchCache.set(o,i),this.logger.debug(`LDAP search cached: ${o}`),i}return n.paged?this.wrapPaginatedSearch(e,o):e}finally{n.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,i,n){e=this.setDn(e),i.objectClass&&0!==i.objectClass.length||!this.config.user_class||(i.objectClass=this.config.user_class);const o={};for(const[e,t]of Object.entries(i))Buffer.isBuffer(t)?o[e]=t.toString():Array.isArray(t)&&t.length>0&&Buffer.isBuffer(t[0])?o[e]=t.map(e=>e.toString()):o[e]=t;[e,i]=await s(this.parent.hooks.ldapaddrequest,[e,o,n]);const a=[];for(const[e,i]of Object.entries(o)){const n=Array.isArray(i)?i:[i];a.push(new t({type:e,values:n}))}const c=await this.acquireConnection();try{return await c.client.add(e,a),this.invalidateCache(e),r(this.parent.hooks.ldapadddone,[e,i]).catch(e=>{this.logger.error(`Hook ldapadddone failed: ${String(e)}`)}),!0}catch(e){throw new Error(`LDAP add error: ${e}`)}finally{this.releaseConnection(c)}}async modify(e,n,o){e=this.setDn(e);const a=[],c=this.opNumber();if([e,n]=await s(this.parent.hooks.ldapmodifyrequest,[e,n,c,o]),n.add)for(const[e,o]of Object.entries(n.add))a.push(new i({operation:"add",modification:new t({type:e,values:Array.isArray(o)?o:[o]})}));if(n.replace)for(const[e,o]of Object.entries(n.replace))a.push(new i({operation:"replace",modification:new t({type:e,values:Array.isArray(o)?o:[o]})}));if(n.delete)if(Array.isArray(n.delete))for(const e of n.delete)e&&a.push(new i({operation:"delete",modification:new t({type:e,values:[]})}));else for(const[e,o]of Object.entries(n.delete)){const n=new i({operation:"delete",modification:new t(o?{type:e,values:Array.isArray(o)?o:[o]}:{type:e})});a.push(n)}if(0===a.length)return this.logger.error("No changes to apply"),r(this.parent.hooks.ldapmodifydone,[e,{},c]).catch(e=>{this.logger.error(`Hook ldapmodifydone failed: ${String(e)}`)}),!1;{const t=await this.acquireConnection();try{return await t.client.modify(e,a),this.invalidateCache(e),r(this.parent.hooks.ldapmodifydone,[e,n,c]).catch(e=>{this.logger.error(`Hook ldapmodifydone failed: ${String(e)}`)}),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(a)}`),new Error(`LDAP modify error: ${t}`)}finally{this.releaseConnection(t)}}}async rename(e,t,i){e=this.setDn(e),t=this.setDn(t),[e,t]=await s(this.parent.hooks.ldaprenamerequest,[e,t,i]);const n=await this.acquireConnection();try{return await n.client.modifyDN(e,t),r(this.parent.hooks.ldaprenamedone,[e,t]).catch(e=>{this.logger.error(`Hook ldaprenamedone failed: ${String(e)}`)}),!0}catch(e){throw new Error(`LDAP rename error: ${e}`)}finally{this.releaseConnection(n)}}async move(e,t){e=this.setDn(e),t=this.setDn(t);const i=await this.acquireConnection();try{return await i.client.modifyDN(e,t),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(e){throw new Error(`LDAP move error: ${e}`)}finally{this.releaseConnection(i)}}async delete(e){e=Array.isArray(e)?e.map(e=>this.setDn(e)):this.setDn(e),Array.isArray(e)||(e=[e]),e=await s(this.parent?.hooks.ldapdeleterequest,e);const
|
|
1
|
+
import{Client as e,Attribute as t,Change as i}from"ldapts";import{LRUCache as n}from"lru-cache";import o from"p-limit";import{launchHooksChained as s,launchHooks as r,escapeDnValue as a}from"./utils.js";const c={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}};class ldapActions{constructor(e){this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.parent=e,this.logger=e.logger,this.config=e.config,this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=o(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const i="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,s=1e3*(this.config.ldap_cache_ttl||300);if(this.searchCache=new n({max:i,ttl:s,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(`LDAP search cache initialized: max=${i}, ttl=${s/1e3}s`),this.attrSignatureCache=new n({max:1e3}),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP DN is not defined");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined");e.config.ldap_base?this.base=e.config.ldap_base:(this.base=e.config.ldap_dn.split(",",2)[1],this.logger.warn(`LDAP base is not defined, using "${this.base}"`)),this.ldapUrls=e.config.ldap_url,this.logger.info(`LDAP failover configured with ${this.ldapUrls.length} URL(s): ${this.ldapUrls.join(", ")}`),this.options={url:this.ldapUrls[0],timeout:0,connectTimeout:0,strictDN:!1},this.ldapUrls[0].startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const t=[];for(let i=0;i<this.ldapUrls.length;i++){const n=(this.currentUrlIndex+i)%this.ldapUrls.length,o=this.ldapUrls[n];try{this.logger.debug(`Attempting connection to ${o}`);const t={...this.options,url:o,tlsOptions:o.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},i=new e(t);return await i.bind(this.dn,this.pwd),n!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${o}`),this.currentUrlIndex=n),i}catch(e){this.logger.warn(`Failed to connect to ${o}: ${String(e)}`),t.push(e)}}throw this.logger.error(`LDAP connection failed for all ${this.ldapUrls.length} URL(s)`),new Error(`LDAP connection failed for all URLs: ${t.map(e=>e.message).join(", ")}`)}cleanupExpiredConnections(){if(!this.isCleaningUp){this.isCleaningUp=!0;try{const e=Date.now(),t=[];for(let i=this.availableConnections.length-1;i>=0;i--){const n=this.availableConnections[i];if(e-n.createdAt>this.connectionTtl){t.push(n),this.availableConnections.splice(i,1);const e=this.connectionPool.indexOf(n);-1!==e&&this.connectionPool.splice(e,1)}}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}finally{this.isCleaningUp=!1}}}async acquireConnection(){if(this.cleanupExpiredConnections(),this.availableConnections.length>0){const e=this.availableConnections.pop();return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e}if(this.connectionPool.length<this.poolSize){const e={client:await this.createConnection(),createdAt:Date.now(),inUse:!0};return this.connectionPool.push(e),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),e}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((e,t)=>{const i=globalThis.setTimeout(()=>{const e=this.waitingResolvers.indexOf(resolveWrapper);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),resolveWrapper=t=>{globalThis.clearTimeout(i),e(t)};this.waitingResolvers.push(resolveWrapper)})}releaseConnection(e){if(Date.now()-e.createdAt>this.connectionTtl){const t=this.connectionPool.indexOf(e);return-1!==t&&this.connectionPool.splice(t,1),e.client.unbind().catch(()=>{}),void this.logger.debug("Released expired LDAP connection (discarded)")}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t(e)}e.inUse=!1,this.availableConnections.push(e),this.logger.debug("Released LDAP connection back to pool")}getAttributeSignature(e){if(!e||0===e.length)return"*";const t=e.join("|");let i=this.attrSignatureCache.get(t);return i||(i=[...e].sort().join(","),this.attrSignatureCache.set(t,i)),i}getCacheKey(e,t){const i=this.getAttributeSignature(t.attributes),n="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return`${e}:${t.scope||"sub"}:${n}:${i}`}invalidateCache(e){for(const t of this.searchCache.keys())t.startsWith(e)&&this.searchCache.delete(t)}async search(e,t=this.base,i){let n={...c,...e};if(n=await s(this.parent.hooks.ldapsearchopts,n),[t,n]=await s(this.parent.hooks.ldapsearchrequest,[t,n,i]),!n.paged&&"base"===n.scope){const e=this.getCacheKey(t,n),i=this.searchCache.get(e);if(i)return this.logger.debug(`LDAP search cache hit: ${e}`),i}const o=await this.acquireConnection();try{let e=n.paged?o.client.searchPaginated(t,n):o.client.search(t,n);if(e=await s(this.parent.hooks.ldapsearchresult,e),!n.paged&&"base"===n.scope&&e instanceof Promise){const i=await e,o=this.getCacheKey(t,n);return this.searchCache.set(o,i),this.logger.debug(`LDAP search cached: ${o}`),i}return n.paged?this.wrapPaginatedSearch(e,o):e}finally{n.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,i,n){e=this.setDn(e),i.objectClass&&0!==i.objectClass.length||!this.config.user_class||(i.objectClass=this.config.user_class);const o={};for(const[e,t]of Object.entries(i))Buffer.isBuffer(t)?o[e]=t.toString():Array.isArray(t)&&t.length>0&&Buffer.isBuffer(t[0])?o[e]=t.map(e=>e.toString()):o[e]=t;[e,i]=await s(this.parent.hooks.ldapaddrequest,[e,o,n]);const a=[];for(const[e,i]of Object.entries(o)){const n=Array.isArray(i)?i:[i];a.push(new t({type:e,values:n}))}const c=await this.acquireConnection();try{return await c.client.add(e,a),this.invalidateCache(e),r(this.parent.hooks.ldapadddone,[e,i]).catch(e=>{this.logger.error(`Hook ldapadddone failed: ${String(e)}`)}),!0}catch(e){throw new Error(`LDAP add error: ${e}`)}finally{this.releaseConnection(c)}}async modify(e,n,o){e=this.setDn(e);const a=[],c=this.opNumber();if([e,n]=await s(this.parent.hooks.ldapmodifyrequest,[e,n,c,o]),n.add)for(const[e,o]of Object.entries(n.add))a.push(new i({operation:"add",modification:new t({type:e,values:Array.isArray(o)?o:[o]})}));if(n.replace)for(const[e,o]of Object.entries(n.replace))a.push(new i({operation:"replace",modification:new t({type:e,values:Array.isArray(o)?o:[o]})}));if(n.delete)if(Array.isArray(n.delete))for(const e of n.delete)e&&a.push(new i({operation:"delete",modification:new t({type:e,values:[]})}));else for(const[e,o]of Object.entries(n.delete)){const n=new i({operation:"delete",modification:new t(o?{type:e,values:Array.isArray(o)?o:[o]}:{type:e})});a.push(n)}if(0===a.length)return this.logger.error("No changes to apply"),r(this.parent.hooks.ldapmodifydone,[e,{},c]).catch(e=>{this.logger.error(`Hook ldapmodifydone failed: ${String(e)}`)}),!1;{const t=await this.acquireConnection();try{return await t.client.modify(e,a),this.invalidateCache(e),r(this.parent.hooks.ldapmodifydone,[e,n,c]).catch(e=>{this.logger.error(`Hook ldapmodifydone failed: ${String(e)}`)}),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(a)}`),new Error(`LDAP modify error: ${t}`)}finally{this.releaseConnection(t)}}}async rename(e,t,i){e=this.setDn(e),t=this.setDn(t),[e,t]=await s(this.parent.hooks.ldaprenamerequest,[e,t,i]);const n=await this.acquireConnection();try{return await n.client.modifyDN(e,t),r(this.parent.hooks.ldaprenamedone,[e,t]).catch(e=>{this.logger.error(`Hook ldaprenamedone failed: ${String(e)}`)}),!0}catch(e){throw new Error(`LDAP rename error: ${e}`)}finally{this.releaseConnection(n)}}async move(e,t){e=this.setDn(e),t=this.setDn(t);const i=await this.acquireConnection();try{return await i.client.modifyDN(e,t),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(e){throw new Error(`LDAP move error: ${e}`)}finally{this.releaseConnection(i)}}async delete(e,t){e=Array.isArray(e)?e.map(e=>this.setDn(e)):this.setDn(e),Array.isArray(e)||(e=[e]),[e]=await s(this.parent?.hooks.ldapdeleterequest,[e,t]);const i=await this.acquireConnection();try{for(const t of e){try{await i.client.del(t),this.invalidateCache(t)}catch(e){throw new Error(`LDAP delete error: ${e}`)}r(this.parent.hooks.ldapdeletedone,t).catch(e=>{this.logger.error(`Hook ldapdeletedone failed: ${String(e)}`)})}return!0}finally{this.releaseConnection(i)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${a(e)},${this.base}`,e}opNumber(){return this.parent.operationSequence++}}export{ldapActions as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AsyncLocalStorage as t}from"async_hooks";import e from"../../lib/auth/base.js";import{ForbiddenError as i}from"../../lib/errors.js";import{unauthorized as n,ok as r}from"../../lib/expressFormatedResponses.js";import{getParentDn as s,asyncHandler as a}from"../../lib/utils.js";import{verifyLdapPassword as o}from"./authzDynamicHash.js";const h=new t;function asString(t){if(null!=t){if(Array.isArray(t)){const e=t[0];return null==e?void 0:Buffer.isBuffer(e)?e.toString():String(e)}return Buffer.isBuffer(t)?t.toString():String(t)}}function isAtOrUnder(t,e){const i=t.toLowerCase().trim(),n=e.toLowerCase().trim();return!(!i||!n)&&(i===n||i.endsWith(","+n))}class AuthzDynamic extends e{constructor(t){if(super(t),this.name="authzDynamic",this.roles=["auth","authz","configurable"],this.tokens=[],this.lastLoad=0,this.lastFailure=0,this.hooks={ldapsearchrequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,t,"read"),[t,e,i]):[t,e,i]},ldapaddrequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,s(t),"write"),[t,e,i]):[t,e,i]},ldapmodifyrequest:([t,e,i,n])=>{const r=this.activeToken(n);return r?(this.requireBranchPermission(r,s(t),"write"),[t,e,i,n]):[t,e,i,n]},ldapdeleterequest:t=>{const
|
|
1
|
+
import{AsyncLocalStorage as t}from"async_hooks";import e from"../../lib/auth/base.js";import{ForbiddenError as i}from"../../lib/errors.js";import{unauthorized as n,ok as r}from"../../lib/expressFormatedResponses.js";import{getParentDn as s,asyncHandler as a}from"../../lib/utils.js";import{verifyLdapPassword as o}from"./authzDynamicHash.js";const h=new t;function asString(t){if(null!=t){if(Array.isArray(t)){const e=t[0];return null==e?void 0:Buffer.isBuffer(e)?e.toString():String(e)}return Buffer.isBuffer(t)?t.toString():String(t)}}function isAtOrUnder(t,e){const i=t.toLowerCase().trim(),n=e.toLowerCase().trim();return!(!i||!n)&&(i===n||i.endsWith(","+n))}class AuthzDynamic extends e{constructor(t){if(super(t),this.name="authzDynamic",this.roles=["auth","authz","configurable"],this.tokens=[],this.lastLoad=0,this.lastFailure=0,this.hooks={ldapsearchrequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,t,"read"),[t,e,i]):[t,e,i]},ldapaddrequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,s(t),"write"),[t,e,i]):[t,e,i]},ldapmodifyrequest:([t,e,i,n])=>{const r=this.activeToken(n);return r?(this.requireBranchPermission(r,s(t),"write"),[t,e,i,n]):[t,e,i,n]},ldapdeleterequest:([t,e])=>{const i=this.activeToken(e);if(!i)return[t,e];const n=Array.isArray(t)?t:[t];for(const t of n)this.requireBranchPermission(i,s(t),"delete");return[t,e]},ldaprenamerequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,s(t),"read"),this.requireBranchPermission(n,s(e),"write"),[t,e,i]):[t,e,i]}},this.base=this.config.authz_dynamic_base||"",this.cacheTtlMs=1e3*(this.config.authz_dynamic_cache_ttl||60),this.failureBackoffMs=Math.max(Math.floor(this.cacheTtlMs/4),5e3),this.tokenAttr=this.config.authz_dynamic_token_attribute||"userPassword",this.configAttr=this.config.authz_dynamic_config_attribute||"description",this.tenantAttr=this.config.authz_dynamic_tenant_attribute||"cn",this.reloadEndpoint=Boolean(this.config.authz_dynamic_reload_endpoint),!this.base)throw new Error("authzDynamic requires --authz-dynamic-base (the LDAP branch holding token entries)")}api(t){if(super.api(t),this.reloadEndpoint){const e=this.config.api_prefix||"/api";t.post(`${e}/v1/authz-dynamic/reload`,a(async(t,e)=>{t.authzToken?(await this.reload(),r(e,{success:!0,tokens:this.tokens.length})):n(e)})),this.logger.info(`authzDynamic reload endpoint registered at ${e}/v1/authz-dynamic/reload`)}}async reload(){try{const t=Array.from(new Set(["dn","cn",this.tokenAttr,this.configAttr,this.tenantAttr])),e=await this.server.ldap.search({filter:"(objectClass=*)",scope:"sub",paged:!1,attributes:t},this.base),i=[];for(const t of e.searchEntries||[]){const e="string"==typeof t.dn?t.dn:String(t.dn),n=asString(t.cn)||"",r=asString(t[this.tokenAttr])||"";if(!r)continue;let s,a=[];const o=asString(t[this.configAttr]);if(o)try{const t=JSON.parse(o);t&&"string"==typeof t.tenant&&t.tenant&&(s=t.tenant),t&&Array.isArray(t.bases)&&(a=t.bases.filter(t=>t&&"string"==typeof t.dn&&t.dn.length>0).map(t=>({dn:t.dn,read:Boolean(t.read),write:Boolean(t.write),delete:Boolean(t.delete)})))}catch(t){this.logger.warn(`authzDynamic: invalid JSON in ${this.configAttr} of ${e}: ${String(t)}`)}const h="cn"===this.tenantAttr?n:asString(t[this.tenantAttr])||n,c=s||h;i.push({dn:e,cn:n,tenant:c,hash:r,bases:a})}this.tokens=i,this.lastLoad=Date.now(),this.lastFailure=0,this.logger.info(`authzDynamic: loaded ${i.length} token(s) from ${this.base}`)}catch(t){this.lastFailure=Date.now(),this.logger.error(`authzDynamic: failed to load tokens from ${this.base}: ${String(t)}`)}}async ensureFresh(){const t=Date.now();if(!(this.lastLoad>0&&t-this.lastLoad<this.cacheTtlMs||this.lastFailure>0&&t-this.lastFailure<this.failureBackoffMs))return this.loading||(this.loading=this.reload().finally(()=>{this.loading=void 0})),this.loading}authMethod(t,e,i){t.user?i():this.ensureFresh().then(()=>{const r=t.headers.authorization;if(!r||!/^Bearer\s+/.test(r))return this.logger.warn("authzDynamic: missing or invalid Authorization header"),void n(e);const s=r.slice(r.indexOf(" ")+1).trim();if(!s)return void n(e);let a;for(const t of this.tokens)if(o(s,t.hash)){a=t;break}if(!a){const t=s.length>8?`${s.substring(0,8)}...`:"***";return this.logger.warn(`authzDynamic: unauthorized token ${t}`),void n(e)}t.user=a.tenant,t.authzToken=a,h.run(a,i)}).catch(t=>{this.logger.error(`authzDynamic: authMethod error: ${String(t)}`),n(e)})}activeToken(t){return h.getStore()||t?.authzToken}requireBranchPermission(t,e,n){for(const i of t.bases)if(isAtOrUnder(e,i.dn)&&i[n])return;throw this.logger.debug(`authzDynamic: token "${t.tenant}" denied ${n} on ${e}`),new i(`[authz-forbidden] Token does not have ${n} permission on this branch`)}getConfigApiData(){return{enabled:!0,base:this.base,cacheTtlSeconds:this.cacheTtlMs/1e3,tokenCount:this.tokens.length,reloadEndpoint:this.reloadEndpoint?`${this.config.api_prefix||"/api"}/v1/authz-dynamic/reload`:void 0,tokenAttribute:this.tokenAttr,configAttribute:this.configAttr,tenantAttribute:this.tenantAttr}}_tokens(){return this.tokens}}export{h as authzContext,AuthzDynamic as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,jsonBody as i,tryMethod as s,ok as a}from"../../lib/expressFormatedResponses.js";import{transformSchemas as o,asyncHandler as n,escapeDnValue as d,validateDnValue as h,launchHooksChained as c,launchHooks as p,getCompiledRegex as l,escapeRegex as m,escapeLdapFilter as u}from"../../lib/utils.js";import{NotFoundError as f,BadRequestError as g}from"../../lib/errors.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeleterequest:async e=>{let t=e;return Array.isArray(t)||(t=[t]),this.logger.debug(`User deletion detected, removing from groups: ${t.join(", ")}`),await Promise.all(t.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)}),e}},this.ldap=t.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");this.config.group_schema&&e.readFile(this.config.group_schema,(e,t)=>{if(e)this.logger.error(`Failed to load group schema from ${this.config.group_schema}: ${e}`);else try{this.schema=JSON.parse(o(t.toString(),this.config)),this.logger.debug("Group schema loaded")}catch(e){this.logger.error(`Failed to parse ${this.config.schemas_path}/group.json: ${e}`)}})}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,n(async(e,t)=>{if(!r(e,t))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new g("Invalid match query");if(/=/.test(e.query.match)){if(!/^[\w*=()&|, -]+$/.test(e.query.match))throw new g("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=u(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const s=await this.listGroups(i);return a(t,s)})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,n(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,n(async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)throw new g("cn is required");const a=i(e,t,"member");a&&await s(t,this.addMember.bind(this),r,a.member)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,n(async(e,t)=>{if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!i||!a)throw new g("cn and member are required");await s(t,this.deleteMember.bind(this),i,a)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,n(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new g("cn is required");const o=i(e,t,"targetOrgDn");o&&await s(t,this.moveGroup.bind(this),a,o.targetOrgDn,e)})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,n(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new g("cn is required");const o=i(e,t,"newCn");o&&await s(t,this.renameGroup.bind(this),a,o.newCn)}))}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const e=/,/.test(i)?i:`${this.cn}=${d(i)},${this.base}`,r=await this.ldap.search({paged:!1,scope:"base"},e);if(0===r.searchEntries.length)throw new f("Group not found");t.json(r.searchEntries[0])}catch(e){if(e.code&&32===e.code)throw new f("Group not found");throw e}}async apiAdd(e,t,...r){const a=i(e,t,...r);if(!a)return;const o=a[this.cn],n=a.member?a.member:[],d=Object.fromEntries(Object.entries(a).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.addGroup.bind(this),o,n,d)}async apiDelete(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);if(!i)throw new g("cn is required");await s(t,this.deleteGroup.bind(this),i)}async apiModify(e,t){const r=i(e,t);if(!r)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new g("cn is required");const o=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.modifyGroup.bind(this),a,o)}async addGroup(e,t=[],r={}){let i;new RegExp(`^${this.cn}=`).test(e)?(i=e,e=e.replace(new RegExp(`^${this.cn}=((?:\\\\.|[^,])+)(?:,.*)?$`),"$1")):(h(e,this.cn),i=`${this.cn}=${d(e)},${this.base}`),await this.validateMembers(i,t),await this.validateNewGroup(i,{objectClass:this.config.group_class,cn:e,member:t,...r});const s=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||s.includes("twakeGroup")||s.includes("twakeStaticGroup")||s.push("twakeGroup");let a,o={objectClass:s,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&o.member.push(...t),[i,o]=await c(this.registeredHooks.ldapgroupadd,[i,o]);try{a=await this.ldap.add(i,o)}catch(e){throw new Error(`Failed to add group ${i}: ${e}`)}return p(this.registeredHooks.ldapgroupadddone,[i,o]),a}async modifyGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;const i=this.opNumber();if([r,t]=await c(this.registeredHooks.ldapgroupmodify,[r,t,i]),t.add){if(t.add.member)throw new Error("Use dedicated API to add members");if(t.add[this.cn])throw new Error(`${this.cn} attribute iq unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new Error("Use dedicated API to delete members");if(t.delete[this.cn])throw new Error(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new Error("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new Error(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new Error("Use dedicated API to replace members");if(t.replace[this.cn])throw new Error(`Use dedicated API to change ${this.cn} attribute`)}await this.validateChanges(r,t);const s=await this.ldap.modify(r,t);return p(this.registeredHooks.ldapgroupmodifydone,[r,t,i]),s}async renameGroup(e,t){/,/.test(e)||h(e,this.cn),/,/.test(t)||h(t,this.cn);let r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`,i=/,/.test(t)?t:`${this.cn}=${d(t)},${this.base}`;[r,i]=await c(this.registeredHooks.ldapgrouprename,[r,i]);const s=await this.ldap.rename(r,i);return p(this.registeredHooks.ldapgrouprenamedone,[r,i]),s}async deleteGroup(e){let t=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;t=await c(this.registeredHooks.ldapgroupdelete,t);const r=await this.ldap.delete(t);return p(this.registeredHooks.ldapgroupdeletedone,t),r}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await c(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;if([e,t]=await c(this.registeredHooks.ldapgroupdeletemember,[e,t]),t===this.config.group_dummy_user)throw new Error("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${e}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.ldap.modify(t.dn,{delete:{member:e}}).catch(r=>this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r))))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0][i];if(!n)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(n===t)throw new Error("Group is already in the target organization");let h;try{h=await this.ldap.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===h.searchEntries.length)throw new Error(`Target organization ${t} not found`);const c=h.searchEntries[0][s];if(!c)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:c}},r),this.logger.info(`Group ${a} moved from ${n} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t}={}){const r=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let i={};for await(const e of r)e.searchEntries.map(e=>{const t=e[this.cn];t&&(i[t]=e),Array.isArray(i[t].member)||(i[t].member=[i[t].member]),i[t].member=i[t].member.filter(e=>e!==this.config.group_dummy_user)});return i=await c(this.registeredHooks._ldapgrouplist,i),i}async searchGroupsByName(e,t=!1,r=[this.cn,"member"]){const i=t?`(${this.cn}=*${e}*)`:`(${this.cn}=${e})`;return await this.listGroups({filter:i,attributes:r})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`)}async validateMembers(e,t){if([e,t]=await c(this.server.hooks.ldapgroupvalidatemembers,[e,t]),!this.config.groups_allow_unexistent_members&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{try{await this.ldap.search({paged:!1,scope:"base"},e)}catch(t){throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await c(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new Error(`Invalid value for field ${e}`);for(const[e,r]of Object.entries(this.schema.attributes))if(r.required&&null==t[e])throw new Error(`Missing required field ${e}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await c(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))await this._validateOneChange(e,r);if(t.replace)for(const[e,r]of Object.entries(t.replace))await this._validateOneChange(e,r);if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=this.schema.attributes[e];if(!r){if(this.schema.strict)throw new Error(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new Error(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new Error(`Field ${e} must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test){const i="string"==typeof r.items.test?l(r.items.test):r.items.test;for(let s of t){if(typeof s!==r.items.type)throw new Error(`Field ${e} must be of type ${r.items.type}`);if("string"!=typeof s&&(s=s.toString()),!i.test(s))throw new Error(`Field ${e} has invalid value ${s}`)}}}else if("pointer"===r.type){if("string"!=typeof t)throw new Error(`Field ${e} must be a string (DN pointer)`);const i=t;if(r.branch&&r.branch.length>0){if(!r.branch.some(e=>l(`,?${m(e)}$`,"i").test(i)))throw new Error(`Field ${e} must point to a DN within allowed branches: ${r.branch.join(", ")}`)}try{const t=await this.ldap.search({paged:!1,scope:"base"},i);if(!t||!t.searchEntries||0===t.searchEntries.length)throw new Error(`Field ${e} points to non-existent DN: ${i}`)}catch(t){throw new Error(`Field ${e} points to invalid or non-existent DN: ${i}: ${t}`)}if(r.test){if(!("string"==typeof r.test?l(r.test):r.test).test(i))throw new Error(`Field ${e} has invalid value ${i}`)}}else{if(typeof t!==r.type)return!1;if("string"!=typeof t&&(t=t.toString()),r.test){if(!("string"==typeof r.test?l(r.test):r.test).test(t))throw new Error(`Field ${e} has invalid value ${t}`)}}return!0}getConfigApiData(){const e=this.config.api_prefix||"/api";let t;if(this.server.loadedPlugins.static&&this.config.group_schema){const e=this.config.static_name||"static",r=this.config.group_schema.indexOf("/schemas/");if(-1!==r){t=`/${e}${this.config.group_schema.substring(r)}`}}return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};
|
|
1
|
+
import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,jsonBody as i,tryMethod as s,ok as a}from"../../lib/expressFormatedResponses.js";import{transformSchemas as o,asyncHandler as n,escapeDnValue as d,validateDnValue as h,launchHooksChained as c,launchHooks as p,getCompiledRegex as l,escapeRegex as m,escapeLdapFilter as u}from"../../lib/utils.js";import{NotFoundError as f,BadRequestError as g}from"../../lib/errors.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeleterequest:async([e,t])=>{let r=e;return Array.isArray(r)||(r=[r]),this.logger.debug(`User deletion detected, removing from groups: ${r.join(", ")}`),await Promise.all(r.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)}),[e,t]}},this.ldap=t.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");this.config.group_schema&&e.readFile(this.config.group_schema,(e,t)=>{if(e)this.logger.error(`Failed to load group schema from ${this.config.group_schema}: ${e}`);else try{this.schema=JSON.parse(o(t.toString(),this.config)),this.logger.debug("Group schema loaded")}catch(e){this.logger.error(`Failed to parse ${this.config.schemas_path}/group.json: ${e}`)}})}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,n(async(e,t)=>{if(!r(e,t))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new g("Invalid match query");if(/=/.test(e.query.match)){if(!/^[\w*=()&|, -]+$/.test(e.query.match))throw new g("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=u(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const s=await this.listGroups(i);return a(t,s)})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,n(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,n(async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)throw new g("cn is required");const a=i(e,t,"member");a&&await s(t,this.addMember.bind(this),r,a.member)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,n(async(e,t)=>{if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!i||!a)throw new g("cn and member are required");await s(t,this.deleteMember.bind(this),i,a)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,n(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new g("cn is required");const o=i(e,t,"targetOrgDn");o&&await s(t,this.moveGroup.bind(this),a,o.targetOrgDn,e)})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,n(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new g("cn is required");const o=i(e,t,"newCn");o&&await s(t,this.renameGroup.bind(this),a,o.newCn)}))}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const e=/,/.test(i)?i:`${this.cn}=${d(i)},${this.base}`,r=await this.ldap.search({paged:!1,scope:"base"},e);if(0===r.searchEntries.length)throw new f("Group not found");t.json(r.searchEntries[0])}catch(e){if(e.code&&32===e.code)throw new f("Group not found");throw e}}async apiAdd(e,t,...r){const a=i(e,t,...r);if(!a)return;const o=a[this.cn],n=a.member?a.member:[],d=Object.fromEntries(Object.entries(a).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.addGroup.bind(this),o,n,d)}async apiDelete(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);if(!i)throw new g("cn is required");await s(t,this.deleteGroup.bind(this),i)}async apiModify(e,t){const r=i(e,t);if(!r)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new g("cn is required");const o=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.modifyGroup.bind(this),a,o)}async addGroup(e,t=[],r={}){let i;new RegExp(`^${this.cn}=`).test(e)?(i=e,e=e.replace(new RegExp(`^${this.cn}=((?:\\\\.|[^,])+)(?:,.*)?$`),"$1")):(h(e,this.cn),i=`${this.cn}=${d(e)},${this.base}`),await this.validateMembers(i,t),await this.validateNewGroup(i,{objectClass:this.config.group_class,cn:e,member:t,...r});const s=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||s.includes("twakeGroup")||s.includes("twakeStaticGroup")||s.push("twakeGroup");let a,o={objectClass:s,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&o.member.push(...t),[i,o]=await c(this.registeredHooks.ldapgroupadd,[i,o]);try{a=await this.ldap.add(i,o)}catch(e){throw new Error(`Failed to add group ${i}: ${e}`)}return p(this.registeredHooks.ldapgroupadddone,[i,o]),a}async modifyGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;const i=this.opNumber();if([r,t]=await c(this.registeredHooks.ldapgroupmodify,[r,t,i]),t.add){if(t.add.member)throw new Error("Use dedicated API to add members");if(t.add[this.cn])throw new Error(`${this.cn} attribute iq unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new Error("Use dedicated API to delete members");if(t.delete[this.cn])throw new Error(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new Error("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new Error(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new Error("Use dedicated API to replace members");if(t.replace[this.cn])throw new Error(`Use dedicated API to change ${this.cn} attribute`)}await this.validateChanges(r,t);const s=await this.ldap.modify(r,t);return p(this.registeredHooks.ldapgroupmodifydone,[r,t,i]),s}async renameGroup(e,t){/,/.test(e)||h(e,this.cn),/,/.test(t)||h(t,this.cn);let r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`,i=/,/.test(t)?t:`${this.cn}=${d(t)},${this.base}`;[r,i]=await c(this.registeredHooks.ldapgrouprename,[r,i]);const s=await this.ldap.rename(r,i);return p(this.registeredHooks.ldapgrouprenamedone,[r,i]),s}async deleteGroup(e){let t=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;t=await c(this.registeredHooks.ldapgroupdelete,t);const r=await this.ldap.delete(t);return p(this.registeredHooks.ldapgroupdeletedone,t),r}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await c(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;if([e,t]=await c(this.registeredHooks.ldapgroupdeletemember,[e,t]),t===this.config.group_dummy_user)throw new Error("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${e}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.ldap.modify(t.dn,{delete:{member:e}}).catch(r=>this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r))))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0][i];if(!n)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(n===t)throw new Error("Group is already in the target organization");let h;try{h=await this.ldap.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===h.searchEntries.length)throw new Error(`Target organization ${t} not found`);const c=h.searchEntries[0][s];if(!c)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:c}},r),this.logger.info(`Group ${a} moved from ${n} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t}={}){const r=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let i={};for await(const e of r)e.searchEntries.map(e=>{const t=e[this.cn];t&&(i[t]=e),Array.isArray(i[t].member)||(i[t].member=[i[t].member]),i[t].member=i[t].member.filter(e=>e!==this.config.group_dummy_user)});return i=await c(this.registeredHooks._ldapgrouplist,i),i}async searchGroupsByName(e,t=!1,r=[this.cn,"member"]){const i=t?`(${this.cn}=*${e}*)`:`(${this.cn}=${e})`;return await this.listGroups({filter:i,attributes:r})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`)}async validateMembers(e,t){if([e,t]=await c(this.server.hooks.ldapgroupvalidatemembers,[e,t]),!this.config.groups_allow_unexistent_members&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{try{await this.ldap.search({paged:!1,scope:"base"},e)}catch(t){throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await c(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new Error(`Invalid value for field ${e}`);for(const[e,r]of Object.entries(this.schema.attributes))if(r.required&&null==t[e])throw new Error(`Missing required field ${e}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await c(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))await this._validateOneChange(e,r);if(t.replace)for(const[e,r]of Object.entries(t.replace))await this._validateOneChange(e,r);if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=this.schema.attributes[e];if(!r){if(this.schema.strict)throw new Error(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new Error(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new Error(`Field ${e} must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test){const i="string"==typeof r.items.test?l(r.items.test):r.items.test;for(let s of t){if(typeof s!==r.items.type)throw new Error(`Field ${e} must be of type ${r.items.type}`);if("string"!=typeof s&&(s=s.toString()),!i.test(s))throw new Error(`Field ${e} has invalid value ${s}`)}}}else if("pointer"===r.type){if("string"!=typeof t)throw new Error(`Field ${e} must be a string (DN pointer)`);const i=t;if(r.branch&&r.branch.length>0){if(!r.branch.some(e=>l(`,?${m(e)}$`,"i").test(i)))throw new Error(`Field ${e} must point to a DN within allowed branches: ${r.branch.join(", ")}`)}try{const t=await this.ldap.search({paged:!1,scope:"base"},i);if(!t||!t.searchEntries||0===t.searchEntries.length)throw new Error(`Field ${e} points to non-existent DN: ${i}`)}catch(t){throw new Error(`Field ${e} points to invalid or non-existent DN: ${i}: ${t}`)}if(r.test){if(!("string"==typeof r.test?l(r.test):r.test).test(i))throw new Error(`Field ${e} has invalid value ${i}`)}}else{if(typeof t!==r.type)return!1;if("string"!=typeof t&&(t=t.toString()),r.test){if(!("string"==typeof r.test?l(r.test):r.test).test(t))throw new Error(`Field ${e} has invalid value ${t}`)}}return!0}getConfigApiData(){const e=this.config.api_prefix||"/api";let t;if(this.server.loadedPlugins.static&&this.config.group_schema){const e=this.config.static_name||"static",r=this.config.group_schema.indexOf("/schemas/");if(-1!==r){t=`/${e}${this.config.group_schema.substring(r)}`}}return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../../abstract/plugin.js";import{launchHooks as e}from"../../lib/utils.js";const n={mail_attribute:"onLdapMailChange",quota_attribute:"onLdapQuotaChange",alias_attribute:"onLdapAliasChange",forward_attribute:"onLdapForwardChange",display_name_attribute:"onLdapDisplayNameChange",drive_quota_attribute:"onLdapDriveQuotaChange"};class OnLdapChange extends t{constructor(){super(...arguments),this.name="onLdapChange",this.roles=["consistency"],this.stack={},this.pendingDeletions=new Map,this.hooks={ldapadddone:t=>{const[e,n]=t,r={};for(const[t,e]of Object.entries(n))r[t]=[null,e];this.notify(e,r)},ldapmodifyrequest:async([t,e,n])=>{const r=await this.server.ldap.search({paged:!1},t);return 1==r.searchEntries.length?this.stack[n]=r.searchEntries[0]:this.logger.warn(`Could not find unique entry ${t} before modification, got ${r.searchEntries.length} entries`),[t,e,n]},ldapmodifydone:([t,e,n])=>{const r=this.stack[n];if(!r)return delete this.stack[n],void this.logger.warn(`Received a ldapmodifydone for an unknown operation (${n})`);const i={};if(e.add)for(const[t,n]of Object.entries(e.add))i[t]=[null,n];if(e.delete)if(Array.isArray(e.delete))for(const t of e.delete)i[t]=[r[t],null];else for(const[t,n]of Object.entries(e.delete))i[t]=[n,null];if(e.replace)for(const[t,n]of Object.entries(e.replace))i[t]=[r[t],n];this.notify(t,i)},ldapdeleterequest:async
|
|
1
|
+
import t from"../../abstract/plugin.js";import{launchHooks as e}from"../../lib/utils.js";const n={mail_attribute:"onLdapMailChange",quota_attribute:"onLdapQuotaChange",alias_attribute:"onLdapAliasChange",forward_attribute:"onLdapForwardChange",display_name_attribute:"onLdapDisplayNameChange",drive_quota_attribute:"onLdapDriveQuotaChange"};class OnLdapChange extends t{constructor(){super(...arguments),this.name="onLdapChange",this.roles=["consistency"],this.stack={},this.pendingDeletions=new Map,this.hooks={ldapadddone:t=>{const[e,n]=t,r={};for(const[t,e]of Object.entries(n))r[t]=[null,e];this.notify(e,r)},ldapmodifyrequest:async([t,e,n])=>{const r=await this.server.ldap.search({paged:!1},t);return 1==r.searchEntries.length?this.stack[n]=r.searchEntries[0]:this.logger.warn(`Could not find unique entry ${t} before modification, got ${r.searchEntries.length} entries`),[t,e,n]},ldapmodifydone:([t,e,n])=>{const r=this.stack[n];if(!r)return delete this.stack[n],void this.logger.warn(`Received a ldapmodifydone for an unknown operation (${n})`);const i={};if(e.add)for(const[t,n]of Object.entries(e.add))i[t]=[null,n];if(e.delete)if(Array.isArray(e.delete))for(const t of e.delete)i[t]=[r[t],null];else for(const[t,n]of Object.entries(e.delete))i[t]=[n,null];if(e.replace)for(const[t,n]of Object.entries(e.replace))i[t]=[r[t],n];this.notify(t,i)},ldapdeleterequest:async([t,e])=>{const n=Array.isArray(t)?t:[t];for(const t of n)try{const e=await this.server.ldap.search({scope:"base",paged:!1},t),n=e.searchEntries?.[0];n&&this.pendingDeletions.set(t,n)}catch(t){}return[t,e]},ldapdeletedone:t=>{const e=Array.isArray(t)?t:[t];for(const t of e){const e=this.pendingDeletions.get(t);if(e){this.pendingDeletions.delete(t);const n={};for(const[t,r]of Object.entries(e))"dn"!==t&&(n[t]=[r,null]);this.notify(t,n)}}}}}notify(t,r){e(this.server.hooks.onLdapChange,t,r);for(const[i,a]of Object.entries(n))if(this.config[i]&&r[this.config[i]])if("onLdapQuotaChange"===a||"onLdapForwardChange"===a||"onLdapAliasChange"===a)this.notifyAttributeChangeWithMail(this.config[i],a,t,r);else if("onLdapMailChange"===a)this.notifyAttributeChange(this.config[i],a,t,r);else if("onLdapDriveQuotaChange"===a){const[n,a]=r[this.config[i]]||[],s=n?Number(n):null,o=a?Number(a):null;s!==o&&e(this.server.hooks.onLdapDriveQuotaChange,t,s,o)}if(r.cn||r.givenName||r.sn){const n=this.reconstructDisplayName(r,0),i=this.reconstructDisplayName(r,1);e(this.server.hooks.onLdapDisplayNameChange,t,n,i)}}notifyAttributeChange(t,n,r,i,a=!1){const[s,o]=i[t]||[];void 0===s&&void 0===o||(a&&(Array.isArray(s)||Array.isArray(o))?this.logger.error(`Attribute ${t} change detected but one of the values is an array, cannot handle that`):s!==o&&e(this.server.hooks[n],r,s,o))}async notifyAttributeChangeWithMail(t,n,r,i){const[a,s]=i[t]||[];if(void 0===a&&void 0===s)return;const o=this.config.mail_attribute||"mail",h=i[o];let l;if(h)l=Array.isArray(h[1])?String(h[1][0]):String(h[1]);else try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[o]},r);if(1!==t.searchEntries.length)return void this.logger.warn(`Could not find mail for ${r}, skipping ${n} notification`);{const e=t.searchEntries[0][o];l=Array.isArray(e)?String(e[0]):String(e)}}catch(t){return void this.logger.error(`Error fetching mail for ${r}:`,t)}if("onLdapQuotaChange"===n){const t=a?Number(a):0,i=s?Number(s):0;t!==i&&e(this.server.hooks[n],r,l,t,i)}else if("onLdapForwardChange"===n||"onLdapAliasChange"===n){const t=a?Array.isArray(a)?a:[a]:[],i=s?Array.isArray(s)?s:[s]:[];(t.length>0||i.length>0)&&e(this.server.hooks[n],r,l,t,i)}}reconstructDisplayName(t,e){const getValue=n=>{if(!t[n])return null;const r=t[n][e];return r?Array.isArray(r)?r.length>0?String(r[0]):null:String(r):null},n=getValue("cn");if(n)return n;const r=getValue("givenName"),i=getValue("sn");if(r||i){const t=[];return r&&t.push(r),i&&t.push(i),t.join(" ")}return null}}export{OnLdapChange as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"fs";import i from"../../abstract/plugin.js";import{tryMethodData as a,jsonBody as e,tryMethod as n,wantJson as r}from"../../lib/expressFormatedResponses.js";import{transformSchemas as s,asyncHandler as o,validateDnValue as h,escapeDnValue as c,escapeLdapFilter as d,launchHooksChained as g,getParentDn as l,getRdn as p,isChildOf as f}from"../../lib/utils.js";import{BadRequestError as u,NotFoundError as w,ConflictError as $}from"../../lib/errors.js";class LdapOrganizations extends i{constructor(i){if(super(i),this.name="ldapOrganizations",this.roles=["api","consistency","configurable"],this.hooks={ldapaddrequest:async([t,i,a])=>(this.isOu(i)||await this.checkDeptLink(i),this.isOu(i)&&await this.checkDeptPath(i),void 0!==a?[t,i,a]:[t,i]),ldapmodifyrequest:async([t,i,a])=>{let e,n={},r={};const checkIsOu=async()=>{if(void 0!==e)return e;if(i.replace?.objectClass)e=this.isOu({objectClass:i.replace.objectClass});else if(i.add?.objectClass)e=this.isOu({objectClass:i.add.objectClass});else{const i=await this.server.ldap.search({paged:!1,scope:"base"},t);e=i.searchEntries.length>0&&this.isOu(i.searchEntries[0])}return e};if(i.delete){const t=Array.isArray(i.delete)?i.delete.includes(this.linkAttr):i.delete[this.linkAttr],a=Array.isArray(i.delete)?i.delete.includes(this.pathAttr):i.delete[this.pathAttr];if(t||a){if(!await checkIsOu()&&t)throw new u("An organization link cannot be deleted");if(a)throw new u("An organization path cannot be deleted")}}if(i.replace&&(i.replace[this.linkAttr]&&(n={...i.replace}),i.replace[this.pathAttr]&&(r={...i.replace})),i.add&&(i.add[this.linkAttr]&&(n={...n,...i.add}),i.add[this.pathAttr]&&(r={...r,...i.add})),Object.keys(n).length>0){await checkIsOu()||await this.checkDeptLink(n)}if(Object.keys(r).length>0){await checkIsOu()&&await this.checkDeptPath(r)}return[t,i,a]},ldapdeleterequest:async([t])=>(/^ou=/.test(t)&&await this.isEmptyOrganization(t),[t]),ldaprenamerequest:([t,i])=>[t,i]},!this.config.ldap_top_organization)throw new Error("Missing --ldap-top-organization");this.pathAttr=this.config.ldap_organization_path_attribute,this.linkAttr=this.config.ldap_organization_link_attribute,this.config.organization_schema&&t.readFile(this.config.organization_schema,(t,i)=>{if(t)this.logger.error(`Failed to load organization schema from ${this.config.organization_schema}: ${t}`);else try{this.schema=JSON.parse(s(i.toString(),this.config)),this.logger.debug("Organization schema loaded")}catch(t){this.logger.error(`Failed to parse organization schema: ${t}`)}})}api(t){t.get(`${this.config.api_prefix}/v1/ldap/organizations/top`,async(t,i)=>{await a(i,this.getOrganisationTop.bind(this),t)}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,this.getOrganisationByDn.bind(this),e,t)}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,this.getOrganisationSubnodes.bind(this),e,t)}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes/search`,o(async(t,i)=>{const e=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)throw new u('query parameter "q" is required');await a(i,this.searchOrganisationSubnodes.bind(this),e,n)})),t.post(`${this.config.api_prefix}/v1/ldap/organizations`,o(async(t,i)=>this.apiAdd(t,i))),t.put(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,o(async(t,i)=>this.apiModify(t,i))),t.delete(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,o(async(t,i)=>this.apiDelete(t,i))),t.post(`${this.config.api_prefix}/v1/ldap/organizations/:dn/move`,o(async(t,i)=>this.apiMove(t,i)))}async apiAdd(t,i){const a=e(t,i,"ou");if(!a)return;h(a.ou,"ou");const r=a.parentDn||this.config.ldap_top_organization,s=`ou=${c(a.ou)},${r}`,o={objectClass:this.config.ldap_organization_class,ou:a.ou,...Object.fromEntries(Object.entries(a).filter(([t])=>"ou"!==t&&"parentDn"!==t))};await n(i,this.addOrganization.bind(this),s,o,t)}async apiModify(t,i){const a=e(t,i);if(!a)return;const r=decodeURIComponent(t.params.dn);if(!r)throw new u("dn is required");await n(i,this.modifyOrganization.bind(this),r,a)}async apiDelete(t,i){if(!r(t,i))return;const a=decodeURIComponent(t.params.dn);if(!a)throw new u("dn is required");await n(i,this.deleteOrganization.bind(this),a)}async apiMove(t,i){if(!r(t,i))return;const n=e(t,i,"targetOrgDn");if(!n)return;const s=decodeURIComponent(t.params.dn);if(!s)throw new u("dn is required");const{targetOrgDn:o}=n;if(!o||"string"!=typeof o)throw new u("Missing or invalid targetOrgDn in request body");await a(i,this.moveOrganization.bind(this),s,o,t)}async checkDeptLink(t){if(t[this.linkAttr]){const i=t[this.linkAttr],a=Array.isArray(i)?i[0]:i,e=await this.server.ldap.search({paged:!1,scope:"base"},a);if(0===e.searchEntries.length)throw new w(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new u(`Entry ${a} isn't in top organization branch`)}}async checkDeptPath(t){if(t[this.pathAttr]){const i=t[this.pathAttr],a=Array.isArray(i)?i[0]:i,e=this.config.ldap_organization_path_separator||" / ";let n=a;if(this.isOu(t)){const i=t.ou,r=Array.isArray(i)?i[0]:i;if(!a.startsWith(r+e))throw new u(`Organization path must start with its own name followed by separator "${e}"`);n=a.slice(r.length+e.length)}const[r,s]=n.split(e,2),o=await this.server.ldap.search({paged:!1,filter:`(ou=${d(r)})`,scope:"sub"},this.config.ldap_top_organization);if(0===o.searchEntries.length)throw new u(`Invalid organization path ${a}`);if(s){let t=!1;for(const i of o.searchEntries){const a=i[this.pathAttr],e=Array.isArray(a)?a[0]:a;if(e&&e===s){t=!0;break}}if(!t)throw new u(`Invalid organization path ${a}: no matching entry for ${r} with path ${s}`)}else{let t=!1;for(const i of o.searchEntries){const a=i.dn,n=this.config.ldap_top_organization;if(a.toLowerCase()===n.toLowerCase()||a.toLowerCase().endsWith(`,${n.toLowerCase()}`)){const a=i[this.pathAttr],n=Array.isArray(a)?a[0]:a;if(!n||n===r||!n.includes(e)){t=!0;break}}}if(!t)throw new u(`Invalid organization path ${a}: no matching top-level entry for ${r}`)}}}async isEmptyOrganization(t){if((await this.server.ldap.search({paged:!1,filter:`(${this.config.ldap_organization_link_attribute}=${t})`})).searchEntries.length>0)throw new $(`Organization ${t} is not empty`)}isOu(t){if(!t.objectClass)return!1;const i=t.objectClass.map(t=>t.toLowerCase());return this.config.ldap_organization_class.filter(t=>"top"!==t.toLowerCase()).some(t=>i.includes(t.toLowerCase()))}async getOrganisationTop(t){if(!this.config.ldap_top_organization)throw new u("No top organization configured");const i=await this.server.ldap.search({paged:!1,scope:"base"},this.config.ldap_top_organization,t);if(1!==i.searchEntries.length)throw new w("Top organization not found");const[,a]=await g(this.registeredHooks.getOrganisationTop,[t,i.searchEntries[0]]);return a}async getOrganisationByDn(t,i){const a=await this.server.ldap.search({paged:!1,scope:"base",filter:"(objectClass=organizationalUnit)"},t,i);if(1!==a.searchEntries.length)throw new w(`Organization ${t} not found`);return a.searchEntries[0]}async getOrganisationSubnodes(t,i){const a=[],e=this.config.ldap_organization_max_subnodes||50,n=i?.query?.objectClass;if(!n||"organizationalUnit"===n)try{const e=await this.server.ldap.search({paged:!1,scope:"one",filter:"(objectClass=organizationalUnit)"},t,i);this.server.logger.debug(`Found ${e.searchEntries.length} sub-OUs for ${t}`),a.push(...e.searchEntries)}catch(i){this.server.logger.debug(`No sub-OUs found for ${t}: ${i}`)}const r=this.config.ldap_top_organization,s=l(r);let o=`(${this.config.ldap_organization_link_attribute}=${d(t)})`;n&&(o=`(&${o}(objectClass=${d(n)}))`),this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${s}`);const h=await this.server.ldap.search({paged:!0,filter:o},s,i);let c=0,g=0;for await(const t of h){g+=t.searchEntries.length;const i=e-c;if(i>0){const e=t.searchEntries.slice(0,i);a.push(...e),c+=e.length}}return this.server.logger.debug(`Found ${g} linked entities for ${t}, returning ${c}`),g>e&&a.push({dn:`more-${t}`,cn:[`... ${g-e} more elements`],objectClass:["moreIndicator"],_isMoreIndicator:"true",_totalCount:g.toString(),_displayedCount:e.toString()}),a}async addOrganization(t,i,a){return this.validateNewOrganization(t,i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i){return this.validateChanges(t,i),await this.server.ldap.modify(t,i)}validateNewOrganization(t,i){if(!this.schema)return!0;for(const[t,a]of Object.entries(i))if(!this._validateOneChange(t,a))throw new u(`Invalid value for field ${t}`);for(const[t,a]of Object.entries(this.schema.attributes))if(a.required&&null==i[t])throw new u(`Missing required field ${t}`);return!0}validateChanges(t,i){if(!this.schema)return!0;if(i.add)for(const[t,a]of Object.entries(i.add))if(!this._validateOneChange(t,a))throw new u(`Invalid value for field ${t}`);if(i.replace)for(const[t,a]of Object.entries(i.replace))if(!this._validateOneChange(t,a))throw new u(`Invalid value for field ${t}`);return!0}_validateOneChange(t,i){if(!this.schema)return!0;const a=this.schema.attributes[t];if(!a){if(this.schema.strict)throw new Error(`Field ${t} is not allowed`);return!0}if(null==i){if(a.required)throw new Error(`Field ${t} is required`);return!0}if(a.type)switch(a.type){case"string":if(Array.isArray(i))throw new Error(`Field ${t} must be a single value`);break;case"array":if(!Array.isArray(i))throw new Error(`Field ${t} must be an array`)}if(a.test){const e=Array.isArray(i)?i[0]:i;if(!("string"==typeof a.test?new RegExp(a.test):a.test).test(String(e)))throw new Error(`Field ${t} does not match required pattern`)}return!0}async moveOrganization(t,i,a){let e;try{e=await this.server.ldap.search({paged:!1,scope:"base"},i)}catch(t){throw new w(`Target organization ${i} not found: ${t}`)}if(!e.searchEntries||0===e.searchEntries.length)throw new w(`Target organization ${i} not found`);if(!this.isOu(e.searchEntries[0]))throw new u(`Target ${i} is not an organizational unit`);const n=`${p(t)},${i}`;let r;try{r=await this.server.ldap.search({paged:!1,scope:"base"},t)}catch(t){throw new w(`Source organization not found: ${t}`)}if(!r.searchEntries||0===r.searchEntries.length)throw new w(`Source organization ${t} not found`);if(n===t)throw new u("Cannot move organization to its current location");if(i.toLowerCase()===t.toLowerCase()||f(i,t))throw new u("Cannot move organization into itself or its descendant");try{await this.server.ldap.rename(t,n,a),this.logger.info(`Moved organization from ${t} to ${n}`)}catch(t){throw new Error(`Failed to move organization: ${t}`)}return{newDn:n}}async deleteOrganization(t){return await this.server.ldap.delete(t)}async searchOrganisationSubnodes(t,i){const a=[],e=d(i);try{const n=await this.server.ldap.search({paged:!1,scope:"one",filter:`(&(objectClass=organizationalUnit)(|(ou=*${e}*)(description=*${e}*)))`},t);this.server.logger.debug(`Found ${n.searchEntries.length} sub-OUs matching "${i}" for ${t}`),a.push(...n.searchEntries)}catch(a){this.server.logger.debug(`No sub-OUs found matching "${i}" for ${t}: ${a}`)}const n=this.config.ldap_top_organization,r=l(n),s=d(t),o=`(&(${this.config.ldap_organization_link_attribute}=${s})(|(uid=*${e}*)(cn=*${e}*)(mail=*${e}*)(sn=*${e}*)(givenName=*${e}*)))`;this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${r}`);const h=await this.server.ldap.search({paged:!0,filter:o},r);let c=0;for await(const t of h)c+=t.searchEntries.length,a.push(...t.searchEntries);return this.server.logger.debug(`Found ${c} linked entities matching "${i}" for ${t}`),a}getConfigApiData(){const t=this.config.api_prefix||"/api";let i;if(this.server.loadedPlugins.static&&this.config.organization_schema){const t=this.config.static_name||"static",a=this.config.organization_schema.indexOf("/schemas/");if(-1!==a){i=`/${t}${this.config.organization_schema.substring(a)}`}}return{enabled:!0,topOrganization:this.config.ldap_top_organization||"",organizationClass:this.config.ldap_organization_class||["top","organizationalUnit"],linkAttribute:this.linkAttr||"",pathAttribute:this.pathAttr||"",pathSeparator:this.config.ldap_organization_path_separator||" / ",maxSubnodes:this.config.ldap_organization_max_subnodes||50,schema:this.schema,schemaUrl:i,endpoints:{getTop:`${t}/v1/ldap/organizations/top`,get:`${t}/v1/ldap/organizations/:dn`,getSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes`,searchSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes/search`,create:`${t}/v1/ldap/organizations`,update:`${t}/v1/ldap/organizations/:dn`,move:`${t}/v1/ldap/organizations/:dn/move`,delete:`${t}/v1/ldap/organizations/:dn`}}}}export{LdapOrganizations as default};
|
|
1
|
+
import t from"fs";import i from"../../abstract/plugin.js";import{tryMethodData as a,jsonBody as e,tryMethod as n,wantJson as r}from"../../lib/expressFormatedResponses.js";import{transformSchemas as s,asyncHandler as o,validateDnValue as h,escapeDnValue as c,escapeLdapFilter as d,launchHooksChained as g,getParentDn as l,getRdn as p,isChildOf as f}from"../../lib/utils.js";import{BadRequestError as u,NotFoundError as w,ConflictError as $}from"../../lib/errors.js";class LdapOrganizations extends i{constructor(i){if(super(i),this.name="ldapOrganizations",this.roles=["api","consistency","configurable"],this.hooks={ldapaddrequest:async([t,i,a])=>(this.isOu(i)||await this.checkDeptLink(i),this.isOu(i)&&await this.checkDeptPath(i),void 0!==a?[t,i,a]:[t,i]),ldapmodifyrequest:async([t,i,a])=>{let e,n={},r={};const checkIsOu=async()=>{if(void 0!==e)return e;if(i.replace?.objectClass)e=this.isOu({objectClass:i.replace.objectClass});else if(i.add?.objectClass)e=this.isOu({objectClass:i.add.objectClass});else{const i=await this.server.ldap.search({paged:!1,scope:"base"},t);e=i.searchEntries.length>0&&this.isOu(i.searchEntries[0])}return e};if(i.delete){const t=Array.isArray(i.delete)?i.delete.includes(this.linkAttr):i.delete[this.linkAttr],a=Array.isArray(i.delete)?i.delete.includes(this.pathAttr):i.delete[this.pathAttr];if(t||a){if(!await checkIsOu()&&t)throw new u("An organization link cannot be deleted");if(a)throw new u("An organization path cannot be deleted")}}if(i.replace&&(i.replace[this.linkAttr]&&(n={...i.replace}),i.replace[this.pathAttr]&&(r={...i.replace})),i.add&&(i.add[this.linkAttr]&&(n={...n,...i.add}),i.add[this.pathAttr]&&(r={...r,...i.add})),Object.keys(n).length>0){await checkIsOu()||await this.checkDeptLink(n)}if(Object.keys(r).length>0){await checkIsOu()&&await this.checkDeptPath(r)}return[t,i,a]},ldapdeleterequest:async([t,i])=>{const a=Array.isArray(t)?t:[t];for(const t of a)/^ou=/.test(t)&&await this.isEmptyOrganization(t);return[t,i]},ldaprenamerequest:([t,i])=>[t,i]},!this.config.ldap_top_organization)throw new Error("Missing --ldap-top-organization");this.pathAttr=this.config.ldap_organization_path_attribute,this.linkAttr=this.config.ldap_organization_link_attribute,this.config.organization_schema&&t.readFile(this.config.organization_schema,(t,i)=>{if(t)this.logger.error(`Failed to load organization schema from ${this.config.organization_schema}: ${t}`);else try{this.schema=JSON.parse(s(i.toString(),this.config)),this.logger.debug("Organization schema loaded")}catch(t){this.logger.error(`Failed to parse organization schema: ${t}`)}})}api(t){t.get(`${this.config.api_prefix}/v1/ldap/organizations/top`,async(t,i)=>{await a(i,this.getOrganisationTop.bind(this),t)}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,this.getOrganisationByDn.bind(this),e,t)}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,this.getOrganisationSubnodes.bind(this),e,t)}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes/search`,o(async(t,i)=>{const e=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)throw new u('query parameter "q" is required');await a(i,this.searchOrganisationSubnodes.bind(this),e,n)})),t.post(`${this.config.api_prefix}/v1/ldap/organizations`,o(async(t,i)=>this.apiAdd(t,i))),t.put(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,o(async(t,i)=>this.apiModify(t,i))),t.delete(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,o(async(t,i)=>this.apiDelete(t,i))),t.post(`${this.config.api_prefix}/v1/ldap/organizations/:dn/move`,o(async(t,i)=>this.apiMove(t,i)))}async apiAdd(t,i){const a=e(t,i,"ou");if(!a)return;h(a.ou,"ou");const r=a.parentDn||this.config.ldap_top_organization,s=`ou=${c(a.ou)},${r}`,o={objectClass:this.config.ldap_organization_class,ou:a.ou,...Object.fromEntries(Object.entries(a).filter(([t])=>"ou"!==t&&"parentDn"!==t))};await n(i,this.addOrganization.bind(this),s,o,t)}async apiModify(t,i){const a=e(t,i);if(!a)return;const r=decodeURIComponent(t.params.dn);if(!r)throw new u("dn is required");await n(i,this.modifyOrganization.bind(this),r,a)}async apiDelete(t,i){if(!r(t,i))return;const a=decodeURIComponent(t.params.dn);if(!a)throw new u("dn is required");await n(i,this.deleteOrganization.bind(this),a)}async apiMove(t,i){if(!r(t,i))return;const n=e(t,i,"targetOrgDn");if(!n)return;const s=decodeURIComponent(t.params.dn);if(!s)throw new u("dn is required");const{targetOrgDn:o}=n;if(!o||"string"!=typeof o)throw new u("Missing or invalid targetOrgDn in request body");await a(i,this.moveOrganization.bind(this),s,o,t)}async checkDeptLink(t){if(t[this.linkAttr]){const i=t[this.linkAttr],a=Array.isArray(i)?i[0]:i,e=await this.server.ldap.search({paged:!1,scope:"base"},a);if(0===e.searchEntries.length)throw new w(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new u(`Entry ${a} isn't in top organization branch`)}}async checkDeptPath(t){if(t[this.pathAttr]){const i=t[this.pathAttr],a=Array.isArray(i)?i[0]:i,e=this.config.ldap_organization_path_separator||" / ";let n=a;if(this.isOu(t)){const i=t.ou,r=Array.isArray(i)?i[0]:i;if(!a.startsWith(r+e))throw new u(`Organization path must start with its own name followed by separator "${e}"`);n=a.slice(r.length+e.length)}const[r,s]=n.split(e,2),o=await this.server.ldap.search({paged:!1,filter:`(ou=${d(r)})`,scope:"sub"},this.config.ldap_top_organization);if(0===o.searchEntries.length)throw new u(`Invalid organization path ${a}`);if(s){let t=!1;for(const i of o.searchEntries){const a=i[this.pathAttr],e=Array.isArray(a)?a[0]:a;if(e&&e===s){t=!0;break}}if(!t)throw new u(`Invalid organization path ${a}: no matching entry for ${r} with path ${s}`)}else{let t=!1;for(const i of o.searchEntries){const a=i.dn,n=this.config.ldap_top_organization;if(a.toLowerCase()===n.toLowerCase()||a.toLowerCase().endsWith(`,${n.toLowerCase()}`)){const a=i[this.pathAttr],n=Array.isArray(a)?a[0]:a;if(!n||n===r||!n.includes(e)){t=!0;break}}}if(!t)throw new u(`Invalid organization path ${a}: no matching top-level entry for ${r}`)}}}async isEmptyOrganization(t){if((await this.server.ldap.search({paged:!1,filter:`(${this.config.ldap_organization_link_attribute}=${t})`})).searchEntries.length>0)throw new $(`Organization ${t} is not empty`)}isOu(t){if(!t.objectClass)return!1;const i=t.objectClass.map(t=>t.toLowerCase());return this.config.ldap_organization_class.filter(t=>"top"!==t.toLowerCase()).some(t=>i.includes(t.toLowerCase()))}async getOrganisationTop(t){if(!this.config.ldap_top_organization)throw new u("No top organization configured");const i=await this.server.ldap.search({paged:!1,scope:"base"},this.config.ldap_top_organization,t);if(1!==i.searchEntries.length)throw new w("Top organization not found");const[,a]=await g(this.registeredHooks.getOrganisationTop,[t,i.searchEntries[0]]);return a}async getOrganisationByDn(t,i){const a=await this.server.ldap.search({paged:!1,scope:"base",filter:"(objectClass=organizationalUnit)"},t,i);if(1!==a.searchEntries.length)throw new w(`Organization ${t} not found`);return a.searchEntries[0]}async getOrganisationSubnodes(t,i){const a=[],e=this.config.ldap_organization_max_subnodes||50,n=i?.query?.objectClass;if(!n||"organizationalUnit"===n)try{const e=await this.server.ldap.search({paged:!1,scope:"one",filter:"(objectClass=organizationalUnit)"},t,i);this.server.logger.debug(`Found ${e.searchEntries.length} sub-OUs for ${t}`),a.push(...e.searchEntries)}catch(i){this.server.logger.debug(`No sub-OUs found for ${t}: ${i}`)}const r=this.config.ldap_top_organization,s=l(r);let o=`(${this.config.ldap_organization_link_attribute}=${d(t)})`;n&&(o=`(&${o}(objectClass=${d(n)}))`),this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${s}`);const h=await this.server.ldap.search({paged:!0,filter:o},s,i);let c=0,g=0;for await(const t of h){g+=t.searchEntries.length;const i=e-c;if(i>0){const e=t.searchEntries.slice(0,i);a.push(...e),c+=e.length}}return this.server.logger.debug(`Found ${g} linked entities for ${t}, returning ${c}`),g>e&&a.push({dn:`more-${t}`,cn:[`... ${g-e} more elements`],objectClass:["moreIndicator"],_isMoreIndicator:"true",_totalCount:g.toString(),_displayedCount:e.toString()}),a}async addOrganization(t,i,a){return this.validateNewOrganization(t,i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i){return this.validateChanges(t,i),await this.server.ldap.modify(t,i)}validateNewOrganization(t,i){if(!this.schema)return!0;for(const[t,a]of Object.entries(i))if(!this._validateOneChange(t,a))throw new u(`Invalid value for field ${t}`);for(const[t,a]of Object.entries(this.schema.attributes))if(a.required&&null==i[t])throw new u(`Missing required field ${t}`);return!0}validateChanges(t,i){if(!this.schema)return!0;if(i.add)for(const[t,a]of Object.entries(i.add))if(!this._validateOneChange(t,a))throw new u(`Invalid value for field ${t}`);if(i.replace)for(const[t,a]of Object.entries(i.replace))if(!this._validateOneChange(t,a))throw new u(`Invalid value for field ${t}`);return!0}_validateOneChange(t,i){if(!this.schema)return!0;const a=this.schema.attributes[t];if(!a){if(this.schema.strict)throw new Error(`Field ${t} is not allowed`);return!0}if(null==i){if(a.required)throw new Error(`Field ${t} is required`);return!0}if(a.type)switch(a.type){case"string":if(Array.isArray(i))throw new Error(`Field ${t} must be a single value`);break;case"array":if(!Array.isArray(i))throw new Error(`Field ${t} must be an array`)}if(a.test){const e=Array.isArray(i)?i[0]:i;if(!("string"==typeof a.test?new RegExp(a.test):a.test).test(String(e)))throw new Error(`Field ${t} does not match required pattern`)}return!0}async moveOrganization(t,i,a){let e;try{e=await this.server.ldap.search({paged:!1,scope:"base"},i)}catch(t){throw new w(`Target organization ${i} not found: ${t}`)}if(!e.searchEntries||0===e.searchEntries.length)throw new w(`Target organization ${i} not found`);if(!this.isOu(e.searchEntries[0]))throw new u(`Target ${i} is not an organizational unit`);const n=`${p(t)},${i}`;let r;try{r=await this.server.ldap.search({paged:!1,scope:"base"},t)}catch(t){throw new w(`Source organization not found: ${t}`)}if(!r.searchEntries||0===r.searchEntries.length)throw new w(`Source organization ${t} not found`);if(n===t)throw new u("Cannot move organization to its current location");if(i.toLowerCase()===t.toLowerCase()||f(i,t))throw new u("Cannot move organization into itself or its descendant");try{await this.server.ldap.rename(t,n,a),this.logger.info(`Moved organization from ${t} to ${n}`)}catch(t){throw new Error(`Failed to move organization: ${t}`)}return{newDn:n}}async deleteOrganization(t){return await this.server.ldap.delete(t)}async searchOrganisationSubnodes(t,i){const a=[],e=d(i);try{const n=await this.server.ldap.search({paged:!1,scope:"one",filter:`(&(objectClass=organizationalUnit)(|(ou=*${e}*)(description=*${e}*)))`},t);this.server.logger.debug(`Found ${n.searchEntries.length} sub-OUs matching "${i}" for ${t}`),a.push(...n.searchEntries)}catch(a){this.server.logger.debug(`No sub-OUs found matching "${i}" for ${t}: ${a}`)}const n=this.config.ldap_top_organization,r=l(n),s=d(t),o=`(&(${this.config.ldap_organization_link_attribute}=${s})(|(uid=*${e}*)(cn=*${e}*)(mail=*${e}*)(sn=*${e}*)(givenName=*${e}*)))`;this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${r}`);const h=await this.server.ldap.search({paged:!0,filter:o},r);let c=0;for await(const t of h)c+=t.searchEntries.length,a.push(...t.searchEntries);return this.server.logger.debug(`Found ${c} linked entities matching "${i}" for ${t}`),a}getConfigApiData(){const t=this.config.api_prefix||"/api";let i;if(this.server.loadedPlugins.static&&this.config.organization_schema){const t=this.config.static_name||"static",a=this.config.organization_schema.indexOf("/schemas/");if(-1!==a){i=`/${t}${this.config.organization_schema.substring(a)}`}}return{enabled:!0,topOrganization:this.config.ldap_top_organization||"",organizationClass:this.config.ldap_organization_class||["top","organizationalUnit"],linkAttribute:this.linkAttr||"",pathAttribute:this.pathAttr||"",pathSeparator:this.config.ldap_organization_path_separator||" / ",maxSubnodes:this.config.ldap_organization_max_subnodes||50,schema:this.schema,schemaUrl:i,endpoints:{getTop:`${t}/v1/ldap/organizations/top`,get:`${t}/v1/ldap/organizations/:dn`,getSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes`,searchSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes/search`,create:`${t}/v1/ldap/organizations`,update:`${t}/v1/ldap/organizations/:dn`,move:`${t}/v1/ldap/organizations/:dn/move`,delete:`${t}/v1/ldap/organizations/:dn`}}}}export{LdapOrganizations as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../../abstract/plugin.js";class TrashPlugin extends t{constructor(t){super(t),this.name="trash",this.roles=["consistency"],this.watchedBases=[],this.trashBase="",this.addMetadata=!0,this.autoCreateTrash=!0,this.trashInitialized=!1,this.hooks={ldapdeleterequest:async
|
|
1
|
+
import t from"../../abstract/plugin.js";class TrashPlugin extends t{constructor(t){super(t),this.name="trash",this.roles=["consistency"],this.watchedBases=[],this.trashBase="",this.addMetadata=!0,this.autoCreateTrash=!0,this.trashInitialized=!1,this.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 s=this.buildTrashDN(t);await this.removeOldTrashEntry(s);try{this.logger.debug(`Moving ${t} to ${s} using LDAP move (atomic)`),await this.server.ldap.move(t,s),this.logger.info(`Entry ${t} moved to trash at ${s}`),await this.addMetadataToTrash(s,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]}};const s=this.config.trash_base;this.trashBase="string"==typeof s?s:"ou=trash,dc=example,dc=com";const a=this.config.trash_watched_bases;"string"==typeof a&&a&&(this.watchedBases=a.split(",").map(t=>t.trim()).filter(t=>t.length>0)),this.addMetadata="false"!==this.config.trash_add_metadata,this.autoCreateTrash="false"!==this.config.trash_auto_create,this.logger.info(`Trash plugin initialized: base=${this.trashBase}, watched=${this.watchedBases.join(",")}, metadata=${this.addMetadata}`)}isWatched(t){return t!==this.trashBase&&!t.endsWith(","+this.trashBase)&&(0===this.watchedBases.length||this.watchedBases.some(s=>t===s||t.endsWith(","+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.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.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.search({paged:!1},t)).searchEntries.length>0&&(this.logger.info(`Removing old trash entry ${t} before overwrite`),await this.server.ldap.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.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.`)}}}export{TrashPlugin as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{escapeDnValue as t,launchHooksChained as e,validateDnValue as s,launchHooks as r,isChildOf as i,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as o}from"./types.js";import{mergeMapping as n,loadMappingFile as c,DEFAULT_GROUP_MAPPING as h,requiredLdapAttributes as u,ldapToScimGroup as m,scimGroupToLdap as l}from"./mapping.js";import{scimFilterToLdap as p}from"./filter.js";import{patchToModifyRequest as d}from"./patch.js";import{extractLdapCode as g,scimNotFound as f,ScimError as b,scimTooMany as y,scimInvalidValue as w,scimUniqueness as $}from"./errors.js";class ScimGroups{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_group_rdn_attribute||"cn",this.objectClass=this.config.scim_group_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=this.config.scim_group_mapping||"";this.mapping=n(h,e?c(e):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"Group",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const e=this.baseResolver.userBase(t),s=this.baseResolver.groupBase(t),r=this.scimPrefix,i=this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),a=this.config.scim_user_rdn_attribute||"uid",o=this.rdnAttribute,n=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(t.toLowerCase()===n.toLowerCase())return;const c=/^([^=]+)=((?:\\.|[^,])+)/.exec(t);if(!c)return;const h=c[2].replace(/\\(.)/g,"$1"),u=t.toLowerCase();let m;e&&u.endsWith(e.toLowerCase())?m="User":s&&u.endsWith(s.toLowerCase())?m="Group":c[1]===a?m="User":c[1]===o&&(m="Group");const l={value:h};if(m&&(l.type=m,i)){const t="User"===m?"Users":"Groups";l.$ref=`${i.replace(/\/$/,"")}${r}/${t}/${encodeURIComponent(h)}`}return l}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.search({paged:!1,scope:"base",attributes:[...u(this.mapping),"member"]},s)}catch(t){if(32===g(t))throw f(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw f(`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 a,n=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=p(e.filter,this.mapping);t.idEquals?a=t.idEquals:n=`(&${n}${t.ldapFilter})`}if(a)try{const e=await this.get(t,a);return{schemas:[o],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof b&&404===t.statusCode)return{schemas:[o],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const c=(await this.ldap.search({filter:n,scope:"sub",paged:!1,attributes:[...u(this.mapping),"member"],sizeLimit:this.maxResults+1},s)).searchEntries||[],h=c.length;if(h>this.maxResults)throw y(`Filter matched ${h} resources, exceeds max ${this.maxResults}`);const l=c.slice(r-1,r-1+i),d=this.buildMemberResolver(t),g=l.map(e=>m(e,this.mapping,this.ctx(t),d));return{schemas:[o],totalResults:h,startIndex:r,itemsPerPage:g.length,Resources:g}}async create(i,a){if(!a.displayName)throw w("displayName is required");const o=(await e(this.hooks.scimgroupcreate,[a,i]))[0],{rdn:n,attributes:c}=l(o,this.mapping,this.objectClass);if(!n)throw w("displayName is required");s(n,this.rdnAttribute),c[this.rdnAttribute]=n;const h=[];if(o.members&&Array.isArray(o.members))for(const t of o.members){if(!t||!t.value)continue;const e=await this.users.resolveRef(i,t.value);e&&h.push(e)}if(0===h.length){const t=this.config.group_dummy_user||"cn=fakeuser";h.push(t)}c.member=h;const u=this.baseResolver.groupBase(i),m=`${this.rdnAttribute}=${t(n)},${u}`;try{await this.ldap.add(m,c)}catch(t){if(68===g(t))throw $(`Group ${n} already exists`);throw t}const p=await this.get(i,n);return r(this.hooks.scimgroupcreatedone,p),p}async replace(t,s,i){await this.get(t,s);const a=(await e(this.hooks.scimgroupupdate,[s,i,t]))[1],{attributes:o}=l(a,this.mapping,this.objectClass),n=[];if(a.members&&Array.isArray(a.members))for(const e of a.members){if(!e||!e.value)continue;const s=await this.users.resolveRef(t,e.value);s&&n.push(s)}if(0===n.length){const t=this.config.group_dummy_user||"cn=fakeuser";n.push(t)}const c=this.dnForId(s,t),h={replace:{}};for(const[t,e]of Object.entries(o))"objectClass"!==t&&t!==this.rdnAttribute&&(h.replace[t]=e);h.replace.member=n,await this.ldap.modify(c,h);const u=await this.get(t,s);return r(this.hooks.scimgroupupdatedone,s,u),u}async patch(t,e,s){await this.get(t,e);const i=await d(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e)});if(!i.add&&!i.replace&&(!i.delete||Array.isArray(i.delete)&&0===i.delete.length))return this.get(t,e);const a=this.config.group_dummy_user||"cn=fakeuser",o=this.dnForId(e,t);if(i.delete&&!Array.isArray(i.delete)&&i.delete.member){const t=await this.ldap.search({paged:!1,scope:"base",attributes:["member"]},o),e=t.searchEntries[0]?.member,s=Array.isArray(e)?e:e?[String(e)]:[],r=Array.isArray(i.delete.member)?i.delete.member:[String(i.delete.member)];0===s.filter(t=>!r.includes(t)).length&&(i.add||(i.add={}),i.add.member=[a])}await this.ldap.modify(o,i);const n=await this.get(t,e);return r(this.hooks.scimgroupupdatedone,e,n),n}async delete(t,s){await this.get(t,s);const i=(await e(this.hooks.scimgroupdelete,[s,t]))[0],a=this.dnForId(i,t);await this.ldap.delete(a),r(this.hooks.scimgroupdeletedone,i)}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e);if(s.includes("=")&&s.includes(","))return i(s,r)?s:void 0;const o=`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},o);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}try{const t=await this.ldap.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{}}get groupMapping(){return this.mapping}}export{ScimGroups};
|
|
1
|
+
import{escapeDnValue as t,launchHooksChained as e,validateDnValue as s,launchHooks as r,isChildOf as i,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as o}from"./types.js";import{mergeMapping as n,loadMappingFile as c,DEFAULT_GROUP_MAPPING as h,requiredLdapAttributes as u,ldapToScimGroup as m,scimGroupToLdap as l}from"./mapping.js";import{scimFilterToLdap as p}from"./filter.js";import{patchToModifyRequest as d}from"./patch.js";import{extractLdapCode as g,scimNotFound as f,ScimError as b,scimTooMany as y,scimInvalidValue as w,scimUniqueness as $}from"./errors.js";class ScimGroups{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_group_rdn_attribute||"cn",this.objectClass=this.config.scim_group_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=this.config.scim_group_mapping||"";this.mapping=n(h,e?c(e):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"Group",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const e=this.baseResolver.userBase(t),s=this.baseResolver.groupBase(t),r=this.scimPrefix,i=this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),a=this.config.scim_user_rdn_attribute||"uid",o=this.rdnAttribute,n=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(t.toLowerCase()===n.toLowerCase())return;const c=/^([^=]+)=((?:\\.|[^,])+)/.exec(t);if(!c)return;const h=c[2].replace(/\\(.)/g,"$1"),u=t.toLowerCase();let m;e&&u.endsWith(e.toLowerCase())?m="User":s&&u.endsWith(s.toLowerCase())?m="Group":c[1]===a?m="User":c[1]===o&&(m="Group");const l={value:h};if(m&&(l.type=m,i)){const t="User"===m?"Users":"Groups";l.$ref=`${i.replace(/\/$/,"")}${r}/${t}/${encodeURIComponent(h)}`}return l}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.search({paged:!1,scope:"base",attributes:[...u(this.mapping),"member"]},s)}catch(t){if(32===g(t))throw f(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw f(`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 a,n=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=p(e.filter,this.mapping);t.idEquals?a=t.idEquals:n=`(&${n}${t.ldapFilter})`}if(a)try{const e=await this.get(t,a);return{schemas:[o],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof b&&404===t.statusCode)return{schemas:[o],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const c=(await this.ldap.search({filter:n,scope:"sub",paged:!1,attributes:[...u(this.mapping),"member"],sizeLimit:this.maxResults+1},s)).searchEntries||[],h=c.length;if(h>this.maxResults)throw y(`Filter matched ${h} resources, exceeds max ${this.maxResults}`);const l=c.slice(r-1,r-1+i),d=this.buildMemberResolver(t),g=l.map(e=>m(e,this.mapping,this.ctx(t),d));return{schemas:[o],totalResults:h,startIndex:r,itemsPerPage:g.length,Resources:g}}async create(i,a){if(!a.displayName)throw w("displayName is required");const o=(await e(this.hooks.scimgroupcreate,[a,i]))[0],{rdn:n,attributes:c}=l(o,this.mapping,this.objectClass);if(!n)throw w("displayName is required");s(n,this.rdnAttribute),c[this.rdnAttribute]=n;const h=[];if(o.members&&Array.isArray(o.members))for(const t of o.members){if(!t||!t.value)continue;const e=await this.users.resolveRef(i,t.value);e&&h.push(e)}if(0===h.length){const t=this.config.group_dummy_user||"cn=fakeuser";h.push(t)}c.member=h;const u=this.baseResolver.groupBase(i),m=`${this.rdnAttribute}=${t(n)},${u}`;try{await this.ldap.add(m,c,i)}catch(t){if(68===g(t))throw $(`Group ${n} already exists`);throw t}const p=await this.get(i,n);return r(this.hooks.scimgroupcreatedone,p),p}async replace(t,s,i){await this.get(t,s);const a=(await e(this.hooks.scimgroupupdate,[s,i,t]))[1],{attributes:o}=l(a,this.mapping,this.objectClass),n=[];if(a.members&&Array.isArray(a.members))for(const e of a.members){if(!e||!e.value)continue;const s=await this.users.resolveRef(t,e.value);s&&n.push(s)}if(0===n.length){const t=this.config.group_dummy_user||"cn=fakeuser";n.push(t)}const c=this.dnForId(s,t),h={replace:{}};for(const[t,e]of Object.entries(o))"objectClass"!==t&&t!==this.rdnAttribute&&(h.replace[t]=e);h.replace.member=n,await this.ldap.modify(c,h,t);const u=await this.get(t,s);return r(this.hooks.scimgroupupdatedone,s,u),u}async patch(t,e,s){await this.get(t,e);const i=await d(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e)});if(!i.add&&!i.replace&&(!i.delete||Array.isArray(i.delete)&&0===i.delete.length))return this.get(t,e);const a=this.config.group_dummy_user||"cn=fakeuser",o=this.dnForId(e,t);if(i.delete&&!Array.isArray(i.delete)&&i.delete.member){const t=await this.ldap.search({paged:!1,scope:"base",attributes:["member"]},o),e=t.searchEntries[0]?.member,s=Array.isArray(e)?e:e?[String(e)]:[],r=Array.isArray(i.delete.member)?i.delete.member:[String(i.delete.member)];0===s.filter(t=>!r.includes(t)).length&&(i.add||(i.add={}),i.add.member=[a])}await this.ldap.modify(o,i,t);const n=await this.get(t,e);return r(this.hooks.scimgroupupdatedone,e,n),n}async delete(t,s){await this.get(t,s);const i=(await e(this.hooks.scimgroupdelete,[s,t]))[0],a=this.dnForId(i,t);await this.ldap.delete(a,t),r(this.hooks.scimgroupdeletedone,i)}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e);if(s.includes("=")&&s.includes(","))return i(s,r)?s:void 0;const o=`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},o);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}try{const t=await this.ldap.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{}}get groupMapping(){return this.mapping}}export{ScimGroups};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{escapeDnValue as t,launchHooksChained as s,validateDnValue as e,launchHooks as i,isChildOf as r,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as h}from"./types.js";import{mergeMapping as n,loadMappingFile as o,DEFAULT_USER_MAPPING as c,requiredLdapAttributes as l,ldapToScimUser as d,scimUserToLdap as u}from"./mapping.js";import{scimFilterToLdap as p}from"./filter.js";import{patchToModifyRequest as m}from"./patch.js";import{extractLdapCode as g,scimNotFound as f,ScimError as b,scimTooMany as w,scimInvalidValue as y,scimUniqueness as x}from"./errors.js";class ScimUsers{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_user_rdn_attribute||"uid",this.objectClass=this.config.scim_user_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const s=this.config.scim_user_mapping||"";this.mapping=n(c,s?o(s):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"User",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix}}dnForId(s,e){const i=this.baseResolver.userBase(e);return`${this.rdnAttribute}=${t(s)},${i}`}async get(t,s){const e=this.dnForId(s,t);let i;try{i=await this.ldap.search({paged:!1,scope:"base",attributes:l(this.mapping)},e)}catch(t){if(32===g(t))throw f(`User ${s} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw f(`User ${s} not found`);return d(i.searchEntries[0],this.mapping,this.ctx(t))}async list(t,s){const e=this.baseResolver.userBase(t),i=Math.max(1,s.startIndex||1),r=Math.min(this.maxResults,Math.max(0,s.count??this.maxResults));let a,n=`(${this.objectClass.includes("inetOrgPerson")?"objectClass=inetOrgPerson":`objectClass=${this.objectClass[0]}`})`;if(s.filter){const t=p(s.filter,this.mapping);t.idEquals?a=t.idEquals:n=`(&${n}${t.ldapFilter})`}if(a)try{const s=await this.get(t,a);return{schemas:[h],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[s]}}catch(t){if(t instanceof b&&404===t.statusCode)return{schemas:[h],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const o=(await this.ldap.search({filter:n,scope:"sub",paged:!1,attributes:l(this.mapping),sizeLimit:this.maxResults+1},e)).searchEntries||[],c=o.length;if(c>this.maxResults)throw w(`Filter matched ${c} resources, exceeds max ${this.maxResults}`);const u=o.slice(i-1,i-1+r);s.sortBy,s.sortOrder;const m=u.map(s=>d(s,this.mapping,this.ctx(t)));return{schemas:[h],totalResults:c,startIndex:i,itemsPerPage:m.length,Resources:m}}async create(r,a){if(!a.userName)throw y("userName is required");const h=(await s(this.hooks.scimusercreate,[a,r]))[0],{rdn:n,attributes:o}=u(h,this.mapping,this.ctx(r),this.objectClass);if(!n)throw y("userName is required");e(n,this.rdnAttribute),o[this.rdnAttribute]=n;const c=this.baseResolver.userBase(r),l=`${this.rdnAttribute}=${t(n)},${c}`;try{await this.ldap.add(l,o)}catch(t){if(68===g(t))throw x(`User ${n} already exists`);throw t}const d=await this.get(r,n);return i(this.hooks.scimusercreatedone,d),d}async replace(t,e,r){const a=this.dnForId(e,t),h=await this.ldap.search({paged:!1,scope:"base",attributes:l(this.mapping)},a);if(!h.searchEntries?.length)throw f(`User ${e} not found`);const n=h.searchEntries[0],o=(await s(this.hooks.scimuserupdate,[e,r,t]))[1],{attributes:c}=u(o,this.mapping,this.ctx(t),this.objectClass),d={replace:{},delete:[]},p=new Set(["objectClass","entryUUID","createTimestamp","modifyTimestamp",this.rdnAttribute]),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 l(this.mapping))p.has(t)||(null!=c[t]?d.replace[t]=c[t]:hasAttrValue(n[t])&&d.delete.push(t));if(0===Object.keys(d.replace||{}).length&&delete d.replace,0===d.delete.length&&delete d.delete,d.replace||d.delete)try{await this.ldap.modify(a,d)}catch(t){if(16!==g(t))throw t}const m=await this.get(t,e);return i(this.hooks.scimuserupdatedone,e,m),m}async patch(t,s,e){await this.get(t,s);const r=await m(e,{mapping:this.mapping});if(!r.add&&!r.replace&&(!r.delete||Array.isArray(r.delete)&&0===r.delete.length))return this.get(t,s);const a=this.dnForId(s,t);await this.ldap.modify(a,r);const h=await this.get(t,s);return i(this.hooks.scimuserupdatedone,s,h),h}async delete(t,e){await this.get(t,e);const r=(await s(this.hooks.scimuserdelete,[e,t]))[0],a=this.dnForId(r,t);await this.ldap.delete(a),i(this.hooks.scimuserdeletedone,r)}async resolveRef(s,e){if(!e)return;const i=this.baseResolver.userBase(s);if(e.includes("=")&&e.includes(","))return r(e,i)?e:void 0;const h=`${this.rdnAttribute}=${t(e)},${i}`;try{const t=await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},h);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}try{const t=await this.ldap.search({filter:`(${this.rdnAttribute}=${a(e)})`,scope:"sub",paged:!1,attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}}get userMapping(){return this.mapping}}export{ScimUsers};
|
|
1
|
+
import{escapeDnValue as t,launchHooksChained as s,validateDnValue as e,launchHooks as i,isChildOf as r,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as h}from"./types.js";import{mergeMapping as n,loadMappingFile as o,DEFAULT_USER_MAPPING as c,requiredLdapAttributes as l,ldapToScimUser as d,scimUserToLdap as u}from"./mapping.js";import{scimFilterToLdap as p}from"./filter.js";import{patchToModifyRequest as m}from"./patch.js";import{extractLdapCode as g,scimNotFound as f,ScimError as b,scimTooMany as w,scimInvalidValue as y,scimUniqueness as x}from"./errors.js";class ScimUsers{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_user_rdn_attribute||"uid",this.objectClass=this.config.scim_user_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const s=this.config.scim_user_mapping||"";this.mapping=n(c,s?o(s):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"User",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix}}dnForId(s,e){const i=this.baseResolver.userBase(e);return`${this.rdnAttribute}=${t(s)},${i}`}async get(t,s){const e=this.dnForId(s,t);let i;try{i=await this.ldap.search({paged:!1,scope:"base",attributes:l(this.mapping)},e)}catch(t){if(32===g(t))throw f(`User ${s} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw f(`User ${s} not found`);return d(i.searchEntries[0],this.mapping,this.ctx(t))}async list(t,s){const e=this.baseResolver.userBase(t),i=Math.max(1,s.startIndex||1),r=Math.min(this.maxResults,Math.max(0,s.count??this.maxResults));let a,n=`(${this.objectClass.includes("inetOrgPerson")?"objectClass=inetOrgPerson":`objectClass=${this.objectClass[0]}`})`;if(s.filter){const t=p(s.filter,this.mapping);t.idEquals?a=t.idEquals:n=`(&${n}${t.ldapFilter})`}if(a)try{const s=await this.get(t,a);return{schemas:[h],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[s]}}catch(t){if(t instanceof b&&404===t.statusCode)return{schemas:[h],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const o=(await this.ldap.search({filter:n,scope:"sub",paged:!1,attributes:l(this.mapping),sizeLimit:this.maxResults+1},e)).searchEntries||[],c=o.length;if(c>this.maxResults)throw w(`Filter matched ${c} resources, exceeds max ${this.maxResults}`);const u=o.slice(i-1,i-1+r);s.sortBy,s.sortOrder;const m=u.map(s=>d(s,this.mapping,this.ctx(t)));return{schemas:[h],totalResults:c,startIndex:i,itemsPerPage:m.length,Resources:m}}async create(r,a){if(!a.userName)throw y("userName is required");const h=(await s(this.hooks.scimusercreate,[a,r]))[0],{rdn:n,attributes:o}=u(h,this.mapping,this.ctx(r),this.objectClass);if(!n)throw y("userName is required");e(n,this.rdnAttribute),o[this.rdnAttribute]=n;const c=this.baseResolver.userBase(r),l=`${this.rdnAttribute}=${t(n)},${c}`;try{await this.ldap.add(l,o,r)}catch(t){if(68===g(t))throw x(`User ${n} already exists`);throw t}const d=await this.get(r,n);return i(this.hooks.scimusercreatedone,d),d}async replace(t,e,r){const a=this.dnForId(e,t),h=await this.ldap.search({paged:!1,scope:"base",attributes:l(this.mapping)},a);if(!h.searchEntries?.length)throw f(`User ${e} not found`);const n=h.searchEntries[0],o=(await s(this.hooks.scimuserupdate,[e,r,t]))[1],{attributes:c}=u(o,this.mapping,this.ctx(t),this.objectClass),d={replace:{},delete:[]},p=new Set(["objectClass","entryUUID","createTimestamp","modifyTimestamp",this.rdnAttribute]),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 l(this.mapping))p.has(t)||(null!=c[t]?d.replace[t]=c[t]:hasAttrValue(n[t])&&d.delete.push(t));if(0===Object.keys(d.replace||{}).length&&delete d.replace,0===d.delete.length&&delete d.delete,d.replace||d.delete)try{await this.ldap.modify(a,d,t)}catch(t){if(16!==g(t))throw t}const m=await this.get(t,e);return i(this.hooks.scimuserupdatedone,e,m),m}async patch(t,s,e){await this.get(t,s);const r=await m(e,{mapping:this.mapping});if(!r.add&&!r.replace&&(!r.delete||Array.isArray(r.delete)&&0===r.delete.length))return this.get(t,s);const a=this.dnForId(s,t);await this.ldap.modify(a,r);const h=await this.get(t,s);return i(this.hooks.scimuserupdatedone,s,h),h}async delete(t,e){await this.get(t,e);const r=(await s(this.hooks.scimuserdelete,[e,t]))[0],a=this.dnForId(r,t);await this.ldap.delete(a,t),i(this.hooks.scimuserdeletedone,r)}async resolveRef(s,e){if(!e)return;const i=this.baseResolver.userBase(s);if(e.includes("=")&&e.includes(","))return r(e,i)?e:void 0;const h=`${this.rdnAttribute}=${t(e)},${i}`;try{const t=await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},h);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}try{const t=await this.ldap.search({filter:`(${this.rdnAttribute}=${a(e)})`,scope:"sub",paged:!1,attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}}get userMapping(){return this.mapping}}export{ScimUsers};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomInt as t}from"crypto";import r from"../../abstract/plugin.js";import{wantJson as s}from"../../lib/expressFormatedResponses.js";import{escapeDnValue as
|
|
1
|
+
import{randomInt as t}from"crypto";import r from"../../abstract/plugin.js";import{wantJson as s}from"../../lib/expressFormatedResponses.js";import{escapeLdapFilter as e,isDnInBranch as i,escapeDnValue as a}from"../../lib/utils.js";class AppAccountsApi extends r{constructor(t){if(super(t),this.name="appAccountsApi",this.roles=["api","configurable"],this.dependencies={appAccountsConsistency:"core/twake/appAccountsConsistency"},this.applicativeAccountBase=this.config.applicative_account_base,this.maxAppAccounts=this.config.max_app_accounts||5,this.mailAttr=this.config.mail_attribute||"mail",this.userAttr=this.config.app_accounts_user_attribute||this.mailAttr,!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}, max_app_accounts=${this.maxAppAccounts}, user_attribute=${this.userAttr}`)}api(t){const r=this.config.api_prefix||"/api";t.get(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.listAccounts(t,r)),t.post(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.createAccount(t,r)),t.delete(`${r}/v1/users/:user/app-accounts/:uid`,(t,r)=>this.deleteAccount(t,r)),this.logger.info(`AppAccounts API registered at ${r}/v1/users/:user/app-accounts`)}async resolvePrincipal(t,r,s){const a=((await this.server.ldap.search({scope:"sub",filter:`(${this.userAttr}=${e(t)})`,paged:!1,...s?{attributes:s}:{}},this.config.ldap_base||"")).searchEntries||[]).filter(t=>!i(t.dn,this.applicativeAccountBase));return 0===a.length?(r.status(404).json({error:`User ${t} not found`}),null):a.length>1?(this.logger.error(`${this.name}: Ambiguous principal lookup for ${t}: ${a.length} entries share ${this.userAttr}`),r.status(409).json({error:`Multiple users share ${this.userAttr} ${t}`}),null):a[0]}async listAccounts(t,r){if(!s(t,r))return;const i=t.params.user;try{const t=await this.resolvePrincipal(i,r,[this.mailAttr]);if(!t)return;const s=t[this.mailAttr];if(!s)return void r.status(400).json({error:`User ${i} has no mail attribute`});const a=Array.isArray(s)?String(s[0]):String(s),n=await this.server.ldap.search({scope:"sub",filter:`(${this.mailAttr}=${e(a)})`,paged:!1},this.applicativeAccountBase),o=(n.searchEntries||[]).filter(t=>{const r=t.uid;return(Array.isArray(r)?String(r[0]):String(r)).toLowerCase()!==a.toLowerCase()}).map(t=>{const r=t.uid,s=Array.isArray(r)?String(r[0]):String(r),e=t.description,i=e?Array.isArray(e)?String(e[0]):String(e):void 0;return{uid:s,...i?{name:i}:{}}}).sort((t,r)=>t.uid.localeCompare(r.uid));r.json(o)}catch(t){this.logger.error(`${this.name}: Failed to list accounts for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}async createAccount(t,r){if(!s(t,r))return;const i=t.params.user,{name:n}=t.body||{};try{const t=await this.resolvePrincipal(i,r);if(!t)return;const s=t[this.mailAttr];if(!s)return void r.status(400).json({error:`User ${i} has no mail attribute`});const o=Array.isArray(s)?String(s[0]):String(s),c=this.appUidPrefix(i),u=(await this.server.ldap.search({scope:"sub",filter:`(${this.mailAttr}=${e(o)})`,paged:!1},this.applicativeAccountBase)).searchEntries||[];if(u.filter(t=>{const r=t.uid;return(Array.isArray(r)?String(r[0]):String(r)).toLowerCase()!==o.toLowerCase()}).length>=this.maxAppAccounts)return void r.status(400).json({error:`Maximum number of accounts (${this.maxAppAccounts}) reached`});let p,l=0;do{if(p=`${c}_${this.generateAccountId()}`,l++,l>100)throw new Error("Failed to generate unique account ID after 100 attempts")}while(await this.uidExists(p));const h=this.generatePassword(),d={objectClass:["inetOrgPerson"],uid:p,userPassword:h},A=["cn","sn","givenName",this.mailAttr,"displayName"];for(const r of A)if(t[r]){const s=t[r];Array.isArray(s)?d[r]=s.map(t=>Buffer.isBuffer(t)?t.toString():String(t)):d[r]=Buffer.isBuffer(s)?s.toString():String(s)}n&&(d.description=n);const g=`uid=${a(p)},${this.applicativeAccountBase}`;await this.server.ldap.add(g,d),this.logger.info(`${this.name}: Created applicative account ${g} for user ${i}`);const f=`uid=${a(o)},${this.applicativeAccountBase}`;try{await this.server.ldap.modify(f,{add:{userPassword:h}})}catch(t){this.logger.warn(`${this.name}: Failed to add password to principal account ${f}:`,t)}r.json({uid:p,pwd:h,mail:o})}catch(t){this.logger.error(`${this.name}: Failed to create account for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}appUidPrefix(t){return t.replace(/[^A-Za-z0-9_-]/g,"_")}async uidExists(t){return((await this.server.ldap.search({scope:"sub",filter:`(uid=${e(t)})`,paged:!1,attributes:["uid"]},this.applicativeAccountBase)).searchEntries||[]).length>0}async deleteAccount(t,r){if(!s(t,r))return;const i=t.params.user,n=t.params.uid;try{const t=await this.server.ldap.search({scope:"sub",filter:`(uid=${e(n)})`,paged:!1},this.applicativeAccountBase),s=t.searchEntries?.[0];if(!s)return this.logger.warn(`${this.name}: Account ${n} already deleted or not found`),void r.json({uid:n});const o=s[this.mailAttr],c=o?Array.isArray(o)?String(o[0]):String(o):null;let u=i;if(this.userAttr!==this.mailAttr){const t=await this.resolvePrincipal(i,r,[this.mailAttr]);if(!t)return;const s=t[this.mailAttr],e=s?Array.isArray(s)?String(s[0]):String(s):null;if(!e)return void r.status(400).json({error:`User ${i} has no mail attribute`});u=e}if(!c||c.toLowerCase()!==u.toLowerCase())return void r.status(403).json({error:`Account ${n} does not belong to user ${i}`});const p=s.userPassword;if(p||this.logger.warn(`${this.name}: Account ${n} has no userPassword attribute`),p){const t=`uid=${a(c)},${this.applicativeAccountBase}`;try{const r=Array.isArray(p)?p[0]:p;await this.server.ldap.modify(t,{delete:{userPassword:r}})}catch(r){this.logger.warn(`${this.name}: Failed to delete password from principal account ${t}:`,r)}}const l=s.dn;await this.server.ldap.delete(l),this.logger.info(`${this.name}: Deleted applicative account ${l}`),r.json({uid:n})}catch(t){this.logger.error(`${this.name}: Failed to delete account ${n} for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}generateAccountId(){return`c${t(0,1e8).toString().padStart(8,"0")}`}generatePassword(){const r="ABCDEFGHJKMNPQRSTUVWXYZ",s="abcdefghjkmnpqrstuvwxyz",e="23456789",i="@!#$%",a=[];for(let n=0;n<6;n++){const n=t(0,23),o=t(0,23),c=t(0,8),u=t(0,5),p=[r[n],s[o],e[c],i[u]];for(let r=p.length-1;r>0;r--){const s=t(0,r+1);[p[r],p[s]]=[p[s],p[r]]}a.push(p.join(""))}return a.join("-")}getConfigApiData(){const t=this.config.api_prefix||"/api";return{enabled:!0,base:this.applicativeAccountBase,maxAccounts:this.maxAppAccounts,mailAttribute:this.mailAttr,endpoints:{list:`${t}/v1/users/:user/app-accounts`,create:`${t}/v1/users/:user/app-accounts`,delete:`${t}/v1/users/:user/app-accounts/:uid`}}}}export{AppAccountsApi as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{setTimeout as e}from"node:timers/promises";import t from"node-fetch";import r from"../../abstract/plugin.js";import{escapeDnValue as i,isChildOf as a}from"../../lib/utils.js";import{BaseResolver as s}from"../scim/baseResolver.js";const o=new Set(["admin","owner","member"]);class ClouderyProvision extends r{constructor(e){super(e),this.name="clouderyProvision",this.roles=["consistency"],this.dependencies={rabbitmq:"core/rabbitmq"},this.createStash=new Map,this.deleteStash=new Map,this.hooks={scimusercreate:e=>(this.captureCreate(e[0],e[1]),e),scimusercreatedone:async e=>{await this.provision(e)},scimuserdelete:async e=>(await this.captureDelete(e[0],e[1]),e),scimuserdeletedone:async e=>{await this.deprovision(e)}};const t=this.config;this.managerUrl=(t.cloudery_manager_url||"").replace(/\/$/,""),this.managerToken=t.cloudery_manager_token||"",this.offer=t.cloudery_offer||"b2b_twake_default",this.clouderyDomain=t.cloudery_domain||"",this.userBranch=t.cloudery_user_branch||"",this.orgIdHeader=(t.cloudery_org_id_header||"x-cloudery-org-id").toLowerCase(),this.orgRoleHeader=(t.cloudery_org_role_header||"x-cloudery-org-role").toLowerCase();const r=t.cloudery_default_org_role||"member",i=r.trim().toLowerCase();o.has(i)?this.defaultOrgRole=i:(this.defaultOrgRole="member",this.logger.warn(`${this.name}: cloudery_default_org_role "${r}" is not one of ${[...o].join(", ")} — using "member"`)),this.fqdnAttribute=t.cloudery_fqdn_attribute||"twakeWorkspaceUrl",this.orgIdAttribute=t.cloudery_org_id_attribute||"twakeOrganizationId",this.orgRoleAttribute=t.cloudery_org_role_attribute||"twakeOrganizationRole",this.phonesAttribute=t.cloudery_phones_attribute||"twakePhones",this.defaultLocale=t.cloudery_default_locale||"en",this.rdnAttribute=t.scim_user_rdn_attribute||"uid",this.authExchange=t.cozy_auth_exchange||"auth",this.b2bExchange=t.cozy_b2b_exchange||"b2b",this.userCreatedRoutingKey=t.cozy_user_created_routing_key||"user.created",this.userDeletedRoutingKey=t.cozy_user_deleted_routing_key||"domain.user.deleted",this.pollIntervalMs=Number(t.cloudery_workflow_poll_interval_ms)||2e3,this.maxPollAttempts=Number(t.cloudery_workflow_max_attempts)||60,this.baseResolver=new s(this.config),this.managerUrl||this.logger.warn(`${this.name}: cloudery_manager_url is empty — instance provisioning will be skipped`),this.userBranch||this.logger.warn(`${this.name}: cloudery_user_branch is empty — no users will be provisioned`)}captureCreate(e,t){if(this.pruneStash(),!this.managerUrl||!this.userBranch)return;const r=this.extractId(e);if(!r)return;const i=this.baseResolver.userBase(t);if(!this.isAtOrUnder(i,this.userBranch))return;const a=this.readHeader(t,this.orgIdHeader);if(!a)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:`B2B branch create without ${this.orgIdHeader} — skipping provisioning`});const s=this.extractPrimaryEmail(e);if(!s)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:"B2B branch create without a primary email — skipping provisioning"});const o=s.trim();this.createStash.set(o.toLowerCase(),{orgId:a.trim(),email:o,role:this.resolveRole(t,r),base:i,ts:Date.now()})}async provision(e){const t=this.extractId(e);if(!t)return;const r=this.extractPrimaryEmail(e);if(!r)return;const a=r.trim().toLowerCase(),s=this.createStash.get(a);if(!s)return;this.createStash.delete(a);const o=s.email,n=emailDomain(o),l=t.replace(/\./g,"")+s.orgId;const h=this.extractPrimaryPhone(e),u=this.extractPhones(e),c=this.extractPublicName(e)||t,d={plugin:this.name,event:"provision",userName:t,orgId:s.orgId,slug:l};let m;try{m=await this.createInstance({email:o,publicName:c,locale:this.extractLocale(e),oidc:l,phone:h||"",slug:l,public_name:c,offer:this.offer,domain:this.clouderyDomain,skip_email_validation:!0,internal_email:o,org_domain:n,org_id:s.orgId})}catch(e){return void this.logger.error({...d,result:"error",error:`${e}`})}if(!(!m.workflow||await this.waitForWorkflow(m.workflow)))return void this.logger.error({...d,result:"workflow_failed",workflow:m.workflow});const g=m.fqdn,f=`${this.rdnAttribute}=${i(t)},${s.base}`,p={[this.fqdnAttribute]:g,[this.orgIdAttribute]:s.orgId,[this.orgRoleAttribute]:s.role};u.length>0&&(p[this.phonesAttribute]=JSON.stringify(u));try{await this.server.ldap.modify(f,{replace:p})}catch(e){this.logger.error({...d,event:"writeFqdn",dn:f,error:`${e}`})}await this.publishUserCreated({twakeId:t,orgDomain:n,orgId:s.orgId,fqdn:g,email:o,mobile:h}),this.logger.info({...d,result:"success",fqdn:g})}async captureDelete(e,t){if(!this.managerUrl||!this.userBranch)return;const r=this.baseResolver.userBase(t);if(!this.isAtOrUnder(r,this.userBranch))return;const a=`${this.rdnAttribute}=${i(e)},${r}`;try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.fqdnAttribute,"mail"]},a),r=t.searchEntries?.[0],i=r?firstValue(r[this.fqdnAttribute]):void 0,s=i?i.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i,"").split("/")[0]:void 0;if(!s)return;const o=r?firstValue(r.mail):void 0;this.deleteStash.set(e,{fqdn:s,domain:o?emailDomain(o):""})}catch(t){this.logger.warn({plugin:this.name,event:"captureDelete",id:e,error:`${t}`})}}async deprovision(e){const t=this.deleteStash.get(e);if(!t)return;this.deleteStash.delete(e);const r={plugin:this.name,event:"deprovision",id:e,fqdn:t.fqdn};let i=!1;try{const e=await this.findInstanceUuidByFqdn(t.fqdn);e?(await this.deleteInstance(e),i=!0,this.logger.info({...r,result:"deleted",uuid:e})):this.logger.warn({...r,result:"not_found"})}catch(e){this.logger.error({...r,result:"error",error:`${e}`})}i&&await this.publishUserDeleted(t.fqdn,t.domain)}async createInstance(e){const r=await t(`${this.managerUrl}/api/v1/instances`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.managerToken}`},body:JSON.stringify(e)});if(!r.ok)throw new Error(`Cloudery createInstance HTTP ${r.status}`);return await r.json()}async waitForWorkflow(r){for(let i=1;i<=this.maxPollAttempts;i++){try{const e=await t(`${this.managerUrl}/api/v1/workflows/${encodeURIComponent(r)}`,{headers:{Authorization:`Bearer ${this.managerToken}`}});if(e.ok){const t=await e.json();if("succeeded"===t.status)return!0;if("failed"===t.status||"error"===t.status)return!1}}catch(e){this.logger.warn({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,error:`${e}`})}i<this.maxPollAttempts&&await e(this.pollIntervalMs)}return!1}async findInstanceUuidByFqdn(e){const r=new URL(`${this.managerUrl}/api/v2/instances`);r.searchParams.set("fqdn",e),r.searchParams.append("only","_id"),r.searchParams.set("limit","1");const i=await t(r.toString(),{headers:{Authorization:`Bearer ${this.managerToken}`}});if(!i.ok)throw new Error(`Cloudery searchInstances HTTP ${i.status}`);const a=await i.json();return a.items&&a.items.length>0?a.items[0]._id:null}async deleteInstance(e){const r=new URL(`${this.managerUrl}/api/v1/instances/${encodeURIComponent(e)}`);r.searchParams.set("user_request","true");const i=await t(r.toString(),{method:"DELETE",headers:{Authorization:`Bearer ${this.managerToken}`}});if(!i.ok)throw new Error(`Cloudery deleteInstance HTTP ${i.status}`)}async publishUserCreated(e){const t=this.requirePlugin("rabbitmq");if(!t)return;const r={twakeId:e.twakeId,domain:e.orgDomain,organizationDomain:e.orgDomain,workplaceFqdn:e.fqdn,organizationId:e.orgId};e.email&&(r.internalEmail=e.email),e.mobile&&(r.mobile=e.mobile);try{await t.publish(this.authExchange,this.userCreatedRoutingKey,r)}catch(t){this.logger.error({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,error:`${t}`})}}async publishUserDeleted(e,t){const r=this.requirePlugin("rabbitmq");if(r)try{await r.publish(this.b2bExchange,this.userDeletedRoutingKey,{workplaceFqdn:e,domain:t})}catch(t){this.logger.error({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,error:`${t}`})}}readHeader(e,t){if(!e)return;const r=e.get?e.get(t):void 0;if("string"==typeof r&&r.length>0)return r;const i=e.query?.[t];return"string"==typeof i&&i.length>0?i:void 0}resolveRole(e,t){const r=this.readHeader(e,this.orgRoleHeader)?.trim().toLowerCase();return r?o.has(r)?r:(this.logger.warn({plugin:this.name,event:"captureCreate",userName:t,message:`Unknown ${this.orgRoleHeader} "${r}" — falling back to ${this.defaultOrgRole}`}),this.defaultOrgRole):this.defaultOrgRole}isAtOrUnder(e,t){return e.trim().toLowerCase()===t.trim().toLowerCase()||a(e,t)}pruneStash(){const e=Date.now()-3e5;for(const[t,r]of this.createStash)r.ts<e&&this.createStash.delete(t)}extractId(e){return"string"==typeof e.userName&&e.userName.length>0?e.userName:"string"==typeof e.id&&e.id.length>0?e.id:null}extractPrimaryEmail(e){const t=e.emails;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPrimaryPhone(e){const t=e.phoneNumbers;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPhones(e){const t=e.phoneNumbers;return t&&0!==t.length?t.filter(e=>"string"==typeof e.value&&e.value.trim().length>0).map(e=>({number:e.value.trim(),primary:Boolean(e.primary)})):[]}extractPublicName(e){if("string"==typeof e.displayName&&e.displayName.trim().length>0)return e.displayName.trim();const t=e.name?.formatted;if("string"==typeof t&&t.trim().length>0)return t.trim();const r=`${e.name?.givenName?.trim()??""} ${e.name?.familyName?.trim()??""}`.trim();return r.length>0?r:null}extractLocale(e){return"string"==typeof e.locale&&e.locale.length>0?e.locale:"string"==typeof e.preferredLanguage&&e.preferredLanguage.length>0?e.preferredLanguage:this.defaultLocale}}function emailDomain(e){const t=e.lastIndexOf("@");return t>=0?e.slice(t+1):""}function firstValue(e){if(Array.isArray(e)){const t=e[0];return"string"==typeof t?t:void 0}return"string"==typeof e?e:void 0}export{ClouderyProvision as default};
|
|
1
|
+
import{setTimeout as e}from"node:timers/promises";import t from"node-fetch";import r from"../../abstract/plugin.js";import{escapeDnValue as i,isChildOf as a}from"../../lib/utils.js";import{BaseResolver as s}from"../scim/baseResolver.js";const n=new Set(["admin","owner","member"]);class ClouderyProvision extends r{constructor(e){super(e),this.name="clouderyProvision",this.roles=["consistency"],this.dependencies={rabbitmq:"core/rabbitmq"},this.createStash=new Map,this.deleteStash=new Map,this.hooks={scimusercreate:e=>(this.captureCreate(e[0],e[1]),e),scimusercreatedone:async e=>{await this.provision(e)},scimuserdelete:async e=>(await this.captureDelete(e[0],e[1]),e),scimuserdeletedone:async e=>{await this.deprovision(e)}};const t=this.config;this.managerUrl=(t.cloudery_manager_url||"").replace(/\/$/,""),this.managerToken=t.cloudery_manager_token||"",this.offer=t.cloudery_offer||"b2b_twake_default",this.clouderyDomain=t.cloudery_domain||"",this.userBranch=t.cloudery_user_branch||"",this.orgIdHeader=(t.cloudery_org_id_header||"x-cloudery-org-id").toLowerCase(),this.orgRoleHeader=(t.cloudery_org_role_header||"x-cloudery-org-role").toLowerCase();const r=t.cloudery_default_org_role||"member",i=r.trim().toLowerCase();n.has(i)?this.defaultOrgRole=i:(this.defaultOrgRole="member",this.logger.warn(`${this.name}: cloudery_default_org_role "${r}" is not one of ${[...n].join(", ")} — using "member"`)),this.fqdnAttribute=t.cloudery_fqdn_attribute||"twakeWorkspaceUrl",this.orgIdAttribute=t.cloudery_org_id_attribute||"twakeOrganizationId",this.orgRoleAttribute=t.cloudery_org_role_attribute||"twakeOrganizationRole",this.phonesAttribute=t.cloudery_phones_attribute||"twakePhones",this.invitedAttribute=t.cloudery_invited_attribute||"twakeInvited",this.defaultLocale=t.cloudery_default_locale||"en",this.rdnAttribute=t.scim_user_rdn_attribute||"uid",this.authExchange=t.cozy_auth_exchange||"auth",this.b2bExchange=t.cozy_b2b_exchange||"b2b",this.userCreatedRoutingKey=t.cozy_user_created_routing_key||"user.created",this.userDeletedRoutingKey=t.cozy_user_deleted_routing_key||"domain.user.deleted",this.pollIntervalMs=Number(t.cloudery_workflow_poll_interval_ms)||2e3,this.maxPollAttempts=Number(t.cloudery_workflow_max_attempts)||60,this.baseResolver=new s(this.config),this.managerUrl||this.logger.warn(`${this.name}: cloudery_manager_url is empty — instance provisioning will be skipped`),this.userBranch||this.logger.warn(`${this.name}: cloudery_user_branch is empty — no users will be provisioned`)}captureCreate(e,t){if(this.pruneStash(),!this.managerUrl||!this.userBranch)return;const r=this.extractId(e);if(!r)return;const i=this.baseResolver.userBase(t);if(!this.isAtOrUnder(i,this.userBranch))return;const a=this.readHeader(t,this.orgIdHeader);if(!a)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:`B2B branch create without ${this.orgIdHeader} — skipping provisioning`});const s=this.extractPrimaryEmail(e);if(!s)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:"B2B branch create without a primary email — skipping provisioning"});const n=s.trim();this.createStash.set(n.toLowerCase(),{orgId:a.trim(),email:n,role:this.resolveRole(t,r),base:i,ts:Date.now()})}async provision(e){const t=this.extractId(e);if(!t)return;const r=this.extractPrimaryEmail(e);if(!r)return;const a=r.trim().toLowerCase(),s=this.createStash.get(a);if(!s)return;this.createStash.delete(a);const n=s.email,o=emailDomain(n),l=t.replace(/\./g,"")+s.orgId;const h=this.extractPrimaryPhone(e),u=this.extractPhones(e),c=this.extractPublicName(e)||t,d={plugin:this.name,event:"provision",userName:t,orgId:s.orgId,slug:l};let m;try{m=await this.createInstance({email:n,publicName:c,locale:this.extractLocale(e),oidc:l,phone:h||"",slug:l,public_name:c,offer:this.offer,domain:this.clouderyDomain,skip_email_validation:!0,internal_email:n,org_domain:o,org_id:s.orgId})}catch(e){return void this.logger.error({...d,result:"error",error:`${e}`})}if(!(!m.workflow||await this.waitForWorkflow(m.workflow)))return void this.logger.error({...d,result:"workflow_failed",workflow:m.workflow});const g=m.fqdn,f=`${this.rdnAttribute}=${i(t)},${s.base}`,p={[this.fqdnAttribute]:g,[this.orgIdAttribute]:s.orgId,[this.orgRoleAttribute]:s.role,[this.invitedAttribute]:"TRUE"};u.length>0&&(p[this.phonesAttribute]=JSON.stringify(u));try{await this.server.ldap.modify(f,{replace:p})}catch(e){this.logger.error({...d,event:"writeFqdn",dn:f,error:`${e}`})}await this.publishUserCreated({twakeId:t,orgDomain:o,orgId:s.orgId,fqdn:g,email:n,mobile:h}),this.logger.info({...d,result:"success",fqdn:g})}async captureDelete(e,t){if(!this.managerUrl||!this.userBranch)return;const r=this.baseResolver.userBase(t);if(!this.isAtOrUnder(r,this.userBranch))return;const a=`${this.rdnAttribute}=${i(e)},${r}`;try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.fqdnAttribute,"mail"]},a),r=t.searchEntries?.[0],i=r?firstValue(r[this.fqdnAttribute]):void 0,s=i?i.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i,"").split("/")[0]:void 0;if(!s)return;const n=r?firstValue(r.mail):void 0;this.deleteStash.set(e,{fqdn:s,domain:n?emailDomain(n):""})}catch(t){this.logger.warn({plugin:this.name,event:"captureDelete",id:e,error:`${t}`})}}async deprovision(e){const t=this.deleteStash.get(e);if(!t)return;this.deleteStash.delete(e);const r={plugin:this.name,event:"deprovision",id:e,fqdn:t.fqdn};let i=!1;try{const e=await this.findInstanceUuidByFqdn(t.fqdn);e?(await this.deleteInstance(e),i=!0,this.logger.info({...r,result:"deleted",uuid:e})):this.logger.warn({...r,result:"not_found"})}catch(e){this.logger.error({...r,result:"error",error:`${e}`})}i&&await this.publishUserDeleted(t.fqdn,t.domain)}async createInstance(e){const r=await t(`${this.managerUrl}/api/v1/instances`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.managerToken}`},body:JSON.stringify(e)});if(!r.ok)throw new Error(`Cloudery createInstance HTTP ${r.status}`);return await r.json()}async waitForWorkflow(r){for(let i=1;i<=this.maxPollAttempts;i++){try{const e=await t(`${this.managerUrl}/api/v1/workflows/${encodeURIComponent(r)}`,{headers:{Authorization:`Bearer ${this.managerToken}`}});if(e.ok){const t=await e.json();if("succeeded"===t.status)return!0;if("failed"===t.status||"error"===t.status)return!1}}catch(e){this.logger.warn({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,error:`${e}`})}i<this.maxPollAttempts&&await e(this.pollIntervalMs)}return!1}async findInstanceUuidByFqdn(e){const r=new URL(`${this.managerUrl}/api/v2/instances`);r.searchParams.set("fqdn",e),r.searchParams.append("only","_id"),r.searchParams.set("limit","1");const i=await t(r.toString(),{headers:{Authorization:`Bearer ${this.managerToken}`}});if(!i.ok)throw new Error(`Cloudery searchInstances HTTP ${i.status}`);const a=await i.json();return a.items&&a.items.length>0?a.items[0]._id:null}async deleteInstance(e){const r=new URL(`${this.managerUrl}/api/v1/instances/${encodeURIComponent(e)}`);r.searchParams.set("user_request","true");const i=await t(r.toString(),{method:"DELETE",headers:{Authorization:`Bearer ${this.managerToken}`}});if(!i.ok)throw new Error(`Cloudery deleteInstance HTTP ${i.status}`)}async publishUserCreated(e){const t=this.requirePlugin("rabbitmq");if(!t)return;const r={twakeId:e.twakeId,domain:e.orgDomain,organizationDomain:e.orgDomain,workplaceFqdn:e.fqdn,organizationId:e.orgId};e.email&&(r.internalEmail=e.email),e.mobile&&(r.mobile=e.mobile);try{await t.publish(this.authExchange,this.userCreatedRoutingKey,r)}catch(t){this.logger.error({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,error:`${t}`})}}async publishUserDeleted(e,t){const r=this.requirePlugin("rabbitmq");if(r)try{await r.publish(this.b2bExchange,this.userDeletedRoutingKey,{workplaceFqdn:e,domain:t})}catch(t){this.logger.error({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,error:`${t}`})}}readHeader(e,t){if(!e)return;const r=e.get?e.get(t):void 0;if("string"==typeof r&&r.length>0)return r;const i=e.query?.[t];return"string"==typeof i&&i.length>0?i:void 0}resolveRole(e,t){const r=this.readHeader(e,this.orgRoleHeader)?.trim().toLowerCase();return r?n.has(r)?r:(this.logger.warn({plugin:this.name,event:"captureCreate",userName:t,message:`Unknown ${this.orgRoleHeader} "${r}" — falling back to ${this.defaultOrgRole}`}),this.defaultOrgRole):this.defaultOrgRole}isAtOrUnder(e,t){return e.trim().toLowerCase()===t.trim().toLowerCase()||a(e,t)}pruneStash(){const e=Date.now()-3e5;for(const[t,r]of this.createStash)r.ts<e&&this.createStash.delete(t)}extractId(e){return"string"==typeof e.userName&&e.userName.length>0?e.userName:"string"==typeof e.id&&e.id.length>0?e.id:null}extractPrimaryEmail(e){const t=e.emails;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPrimaryPhone(e){const t=e.phoneNumbers;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPhones(e){const t=e.phoneNumbers;return t&&0!==t.length?t.filter(e=>"string"==typeof e.value&&e.value.trim().length>0).map(e=>({number:e.value.trim(),primary:Boolean(e.primary)})):[]}extractPublicName(e){if("string"==typeof e.displayName&&e.displayName.trim().length>0)return e.displayName.trim();const t=e.name?.formatted;if("string"==typeof t&&t.trim().length>0)return t.trim();const r=`${e.name?.givenName?.trim()??""} ${e.name?.familyName?.trim()??""}`.trim();return r.length>0?r:null}extractLocale(e){return"string"==typeof e.locale&&e.locale.length>0?e.locale:"string"==typeof e.preferredLanguage&&e.preferredLanguage.length>0?e.preferredLanguage:this.defaultLocale}}function emailDomain(e){const t=e.lastIndexOf("@");return t>=0?e.slice(t+1):""}function firstValue(e){if(Array.isArray(e)){const t=e[0];return"string"==typeof t?t:void 0}return"string"==typeof e?e:void 0}export{ClouderyProvision as default};
|
|
@@ -144,12 +144,14 @@ export interface Config {
|
|
|
144
144
|
cloudery_org_id_attribute?: string;
|
|
145
145
|
cloudery_org_role_attribute?: string;
|
|
146
146
|
cloudery_phones_attribute?: string;
|
|
147
|
+
cloudery_invited_attribute?: string;
|
|
147
148
|
cloudery_default_locale?: string;
|
|
148
149
|
cloudery_workflow_poll_interval_ms?: number;
|
|
149
150
|
cloudery_workflow_max_attempts?: number;
|
|
150
151
|
rabbitmq_url?: string;
|
|
151
152
|
applicative_account_base?: string;
|
|
152
153
|
max_app_accounts?: number;
|
|
154
|
+
app_accounts_user_attribute?: string;
|
|
153
155
|
ldap_operational_attribute?: string[];
|
|
154
156
|
trash_base?: string;
|
|
155
157
|
trash_watched_bases?: string;
|
package/dist/src/hooks.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface Hooks {
|
|
|
26
26
|
ldapadddone?: (args: [string, AttributesList]) => MaybePromise<void>;
|
|
27
27
|
ldapmodifyrequest?: ChainedHook<[string, ModifyRequest, number, Request?]>;
|
|
28
28
|
ldapmodifydone?: (args: [string, ModifyRequest, number]) => MaybePromise<void>;
|
|
29
|
-
ldapdeleterequest?: ChainedHook<string | string[]>;
|
|
29
|
+
ldapdeleterequest?: ChainedHook<[string | string[], Request?]>;
|
|
30
30
|
ldapdeletedone?: (dn: string | string[]) => MaybePromise<void>;
|
|
31
31
|
ldaprenamerequest?: ChainedHook<[string, string, Request?]>;
|
|
32
32
|
ldaprenamedone?: (args: [string, string]) => MaybePromise<void>;
|
|
@@ -59,5 +59,6 @@ export default abstract class AuthzBase extends DmPlugin {
|
|
|
59
59
|
ldapsearchrequest: ([base, opts, req]: [string, SearchOptions, DmRequest?]) => Promise<[string, SearchOptions, DmRequest?]>;
|
|
60
60
|
getOrganisationTop: ([req, defaultTop]: [DmRequest | undefined, AttributesList | null]) => Promise<[DmRequest | undefined, AttributesList | null]>;
|
|
61
61
|
ldaprenamerequest: ([oldDn, newDn, req]: [string, string, DmRequest?]) => Promise<[string, string, DmRequest?]>;
|
|
62
|
+
ldapdeleterequest: ([dn, req]: [string | string[], DmRequest?]) => Promise<[string | string[], DmRequest?]>;
|
|
62
63
|
};
|
|
63
64
|
}
|
|
@@ -91,7 +91,7 @@ declare class ldapActions {
|
|
|
91
91
|
* ldapts will extract newRDN="uid=user1" and newSuperior="ou=trash,dc=example,dc=com"
|
|
92
92
|
*/
|
|
93
93
|
move(dn: string, newDn: string): Promise<boolean>;
|
|
94
|
-
delete(dn: string | string[]): Promise<boolean>;
|
|
94
|
+
delete(dn: string | string[], req?: Request): Promise<boolean>;
|
|
95
95
|
private setDn;
|
|
96
96
|
opNumber(): number;
|
|
97
97
|
}
|
|
@@ -16,8 +16,10 @@ import type { DM } from '../../bin';
|
|
|
16
16
|
* type: string
|
|
17
17
|
* description: |
|
|
18
18
|
* Unique identifier of the app account. Format:
|
|
19
|
-
* `<
|
|
20
|
-
*
|
|
19
|
+
* `<prefix>_c<8-digits>`, where `<prefix>` is the `:user` path param
|
|
20
|
+
* (the resolution value — the mail by default) sanitized into a
|
|
21
|
+
* uid-safe token (e.g. `alice@example.com` -> `alice_example_com`).
|
|
22
|
+
* example: alice_example_com_c04729183
|
|
21
23
|
* name:
|
|
22
24
|
* type: string
|
|
23
25
|
* description: Human-readable label (stored as LDAP `description`).
|
|
@@ -73,11 +75,30 @@ export default class AppAccountsApi extends DmPlugin {
|
|
|
73
75
|
private applicativeAccountBase;
|
|
74
76
|
private maxAppAccounts;
|
|
75
77
|
private mailAttr;
|
|
78
|
+
private userAttr;
|
|
76
79
|
constructor(server: DM);
|
|
77
80
|
/**
|
|
78
81
|
* Register API routes
|
|
79
82
|
*/
|
|
80
83
|
api(app: Express): void;
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the principal user entry from the `:user` path param.
|
|
86
|
+
*
|
|
87
|
+
* `:user` is matched against the configured resolution attribute
|
|
88
|
+
* (`app_accounts_user_attribute`, default the mail attribute). Mail is
|
|
89
|
+
* globally unique; resolving by the LDAP `uid` instead is only safe when uid
|
|
90
|
+
* is unique directory-wide, as the same `uid` may repeat under different
|
|
91
|
+
* subtrees and resolve to an arbitrary same-named user (see #88).
|
|
92
|
+
*
|
|
93
|
+
* On failure (no match, or an ambiguous one) this writes the HTTP error
|
|
94
|
+
* response and returns null, so callers can simply `return` when it does.
|
|
95
|
+
*
|
|
96
|
+
* @param principal - The `:user` path param value
|
|
97
|
+
* @param res - The Express response, used to emit error statuses
|
|
98
|
+
* @param attributes - Optional attribute projection for the search
|
|
99
|
+
* @returns The single matching LDAP entry, or null when none/ambiguous
|
|
100
|
+
*/
|
|
101
|
+
private resolvePrincipal;
|
|
81
102
|
/**
|
|
82
103
|
* List applicative accounts for a user
|
|
83
104
|
*/
|
|
@@ -86,6 +107,17 @@ export default class AppAccountsApi extends DmPlugin {
|
|
|
86
107
|
* Create a new applicative account
|
|
87
108
|
*/
|
|
88
109
|
private createAccount;
|
|
110
|
+
/**
|
|
111
|
+
* Derive a uid-safe prefix for app-account uids from the (unique) `:user`
|
|
112
|
+
* value. Characters awkward in a uid/RDN (e.g. `@` and `.` in a mail) are
|
|
113
|
+
* replaced with `_`. Sanitization may be lossy, but app-account uids are made
|
|
114
|
+
* unique across the whole branch at creation time, so this never collides.
|
|
115
|
+
*/
|
|
116
|
+
private appUidPrefix;
|
|
117
|
+
/**
|
|
118
|
+
* Whether a uid already exists anywhere under the applicative branch.
|
|
119
|
+
*/
|
|
120
|
+
private uidExists;
|
|
89
121
|
/**
|
|
90
122
|
* Delete an applicative account
|
|
91
123
|
*/
|
|
@@ -19,6 +19,7 @@ export default class ClouderyProvision extends DmPlugin {
|
|
|
19
19
|
private readonly orgIdAttribute;
|
|
20
20
|
private readonly orgRoleAttribute;
|
|
21
21
|
private readonly phonesAttribute;
|
|
22
|
+
private readonly invitedAttribute;
|
|
22
23
|
private readonly defaultLocale;
|
|
23
24
|
private readonly rdnAttribute;
|
|
24
25
|
private readonly authExchange;
|