ldap-rest 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{fileURLToPath as e}from"url";import{join as _,dirname as a}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins","identifiers"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains","identifiers"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls","identifiers",!0],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",_(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","identifiers"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes","identifiers"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--organization-schema","DM_ORGANIZATION_SCHEMA",""],["--enterprise-domain-name-attribute","DM_ENTERPRISE_DOMAIN_NAME_ATTRIBUTE","associatedDomain"],["--enterprise-domain-link-attribute","DM_ENTERPRISE_DOMAIN_LINK_ATTRIBUTE",""],["--storage-backend","DM_STORAGE_BACKEND",""],["--storage-sweep-interval","DM_STORAGE_SWEEP_INTERVAL",600,"number"],["--storage-ldap-base","DM_STORAGE_LDAP_BASE",""],["--storage-ldap-object-class","DM_STORAGE_LDAP_OBJECT_CLASS","applicationProcess"],["--storage-file-directory","DM_STORAGE_FILE_DIRECTORY",""],["--storage-postgres-url","DM_STORAGE_POSTGRES_URL",""],["--storage-postgres-table","DM_STORAGE_POSTGRES_TABLE","ldap_rest_storage"],["--storage-valkey-url","DM_STORAGE_VALKEY_URL",""],["--storage-valkey-prefix","DM_STORAGE_VALKEY_PREFIX","ldap-rest:"],["--bcl-retention","DM_BCL_RETENTION",604800,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes","identifiers"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",_(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","identifiers"],["--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"],["--ldap-flat-auto-repair","DM_LDAP_FLAT_AUTO_REPAIR",!0,"boolean"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases","dns"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes","identifiers"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches","dns"],["--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",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--twake-lifecycle-role-attribute","DM_TWAKE_LIFECYCLE_ROLE_ATTRIBUTE",""],["--twake-lifecycle-lock-attribute","DM_TWAKE_LIFECYCLE_LOCK_ATTRIBUTE",""],["--twake-lifecycle-lock-value","DM_TWAKE_LIFECYCLE_LOCK_VALUE",""],["--twake-lifecycle-deleted-attribute","DM_TWAKE_LIFECYCLE_DELETED_ATTRIBUTE",""],["--twake-lifecycle-deleted-value","DM_TWAKE_LIFECYCLE_DELETED_VALUE","TRUE"],["--twake-lifecycle-deleted-at-attribute","DM_TWAKE_LIFECYCLE_DELETED_AT_ATTRIBUTE",""],["--twake-lifecycle-deleted-at-format","DM_TWAKE_LIFECYCLE_DELETED_AT_FORMAT","iso8601"],["--twake-lifecycle-reason-attribute","DM_TWAKE_LIFECYCLE_REASON_ATTRIBUTE",""],["--twake-lifecycle-rules","DM_TWAKE_LIFECYCLE_RULES",""],["--twake-lifecycle-member-attribute","DM_TWAKE_LIFECYCLE_MEMBER_ATTRIBUTE","member"],["--twake-group-base","DM_TWAKE_GROUP_BASE",""],["--twake-group-user-base","DM_TWAKE_GROUP_USER_BASE",""],["--twake-group-organization-dn","DM_TWAKE_GROUP_ORGANIZATION_DN",""],["--twake-group-organization-status-attribute","DM_TWAKE_GROUP_ORGANIZATION_STATUS_ATTRIBUTE","twakeOrgStatus"],["--twake-group-organization-deleted-value","DM_TWAKE_GROUP_ORGANIZATION_DELETED_VALUE","deleted"],["--twake-group-display-name-attribute","DM_TWAKE_GROUP_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-group-color-attribute","DM_TWAKE_GROUP_COLOR_ATTRIBUTE","twakeGroupColor"],["--twake-group-created-at-attribute","DM_TWAKE_GROUP_CREATED_AT_ATTRIBUTE","twakeCreatedAt"],["--twake-group-max-page-limit","DM_TWAKE_GROUP_MAX_PAGE_LIMIT",1e3,"number"],["--twake-group-member-fields","DM_TWAKE_GROUP_MEMBER_FIELDS",{},"json"],["--twake-space-base","DM_TWAKE_SPACE_BASE",""],["--twake-space-class","DM_TWAKE_SPACE_CLASS",["top","twakeSpace"],"array",null,"identifiers"],["--twake-space-display-name-attribute","DM_TWAKE_SPACE_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-space-admin-attribute","DM_TWAKE_SPACE_ADMIN_ATTRIBUTE","twakeSpaceAdmin"],["--twake-space-editor-attribute","DM_TWAKE_SPACE_EDITOR_ATTRIBUTE","twakeSpaceEditor"],["--twake-space-viewer-attribute","DM_TWAKE_SPACE_VIEWER_ATTRIBUTE","twakeSpaceViewer"],["--twake-space-user-role-attribute","DM_TWAKE_SPACE_USER_ROLE_ATTRIBUTE",""],["--twake-space-exchange","DM_TWAKE_SPACE_EXCHANGE","space"],["--twake-tombstone-dn","DM_TWAKE_TOMBSTONE_DN",[],"array",null,"dns"],["--twake-tombstone-default-reason","DM_TWAKE_TOMBSTONE_DEFAULT_REASON","deleted"],["--twake-tombstone-reason-header","DM_TWAKE_TOMBSTONE_REASON_HEADER","x-deletion-reason"],["--twake-tombstone-reasons","DM_TWAKE_TOMBSTONE_REASONS",[],"array",null,"identifiers"],["--twake-tombstone-clear-attributes","DM_TWAKE_TOMBSTONE_CLEAR_ATTRIBUTES",[],"array",null,"identifiers"],["--twake-tombstone-erase-min-age","DM_TWAKE_TOMBSTONE_ERASE_MIN_AGE",2592e3,"number"],["--twake-tombstone-group-bases","DM_TWAKE_TOMBSTONE_GROUP_BASES",[],"array",null,"dns"],["--twake-instance-dn","DM_TWAKE_INSTANCE_DN",[],"array",null,"dns"],["--twake-instance-skip-attribute","DM_TWAKE_INSTANCE_SKIP_ATTRIBUTE",""],["--twake-instance-skip-value","DM_TWAKE_INSTANCE_SKIP_VALUE",""],["--twake-instance-provider","DM_TWAKE_INSTANCE_PROVIDER","cloudery"],["--twake-instance-id","DM_TWAKE_INSTANCE_ID","{uid}"],["--twake-instance-fqdn-attribute","DM_TWAKE_INSTANCE_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--twake-instance-sent-attribute","DM_TWAKE_INSTANCE_SENT_ATTRIBUTE","twakeCreatedEventAt"],["--twake-instance-locale","DM_TWAKE_INSTANCE_LOCALE",""],["--twake-instance-timeout","DM_TWAKE_INSTANCE_TIMEOUT",3e4,"number"],["--twake-instance-cloudery-url","DM_TWAKE_INSTANCE_CLOUDERY_URL",""],["--twake-instance-cloudery-token","DM_TWAKE_INSTANCE_CLOUDERY_TOKEN",""],["--twake-instance-cloudery-offer","DM_TWAKE_INSTANCE_CLOUDERY_OFFER","b2b_twake_default"],["--twake-instance-cloudery-domain","DM_TWAKE_INSTANCE_CLOUDERY_DOMAIN",""],["--twake-instance-cloudery-organization-offer","DM_TWAKE_INSTANCE_CLOUDERY_ORGANIZATION_OFFER",""],["--twake-instance-cozy-url","DM_TWAKE_INSTANCE_COZY_URL",""],["--twake-instance-cozy-user","DM_TWAKE_INSTANCE_COZY_USER","admin"],["--twake-instance-cozy-passphrase","DM_TWAKE_INSTANCE_COZY_PASSPHRASE",""],["--twake-instance-cozy-domain","DM_TWAKE_INSTANCE_COZY_DOMAIN",""],["--twake-instance-cozy-context","DM_TWAKE_INSTANCE_COZY_CONTEXT","default"],["--twake-instance-cozy-apps","DM_TWAKE_INSTANCE_COZY_APPS","home,drive,settings,notes,dataproxy"],["--twake-instance-cozy-org-id","DM_TWAKE_INSTANCE_COZY_ORG_ID",""],["--twake-instance-cozy-org-domain","DM_TWAKE_INSTANCE_COZY_ORG_DOMAIN",""],["--twake-instance-auth-exchange","DM_TWAKE_INSTANCE_AUTH_EXCHANGE","auth"],["--twake-instance-b2b-exchange","DM_TWAKE_INSTANCE_B2B_EXCHANGE","b2b"],["--twake-instance-user-created-key","DM_TWAKE_INSTANCE_USER_CREATED_KEY","user.created"],["--twake-instance-organization-created-key","DM_TWAKE_INSTANCE_ORGANIZATION_CREATED_KEY","organization.created"],["--twake-instance-workplace-created-key","DM_TWAKE_INSTANCE_WORKPLACE_CREATED_KEY","workplace.created"],["--twake-instance-queue","DM_TWAKE_INSTANCE_QUEUE","workplace.created.ldap-rest"],["--twake-instance-organization-base","DM_TWAKE_INSTANCE_ORGANIZATION_BASE",""],["--twake-instance-organization-id-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_ID_ATTRIBUTE","ou"],["--twake-instance-organization-domain-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_DOMAIN_ATTRIBUTE","twakeDomain"],["--twake-instance-organization-name-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_NAME_ATTRIBUTE","description"],["--twake-instance-organization-fqdn-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_FQDN_ATTRIBUTE",""],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes","identifiers"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes","identifiers"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--llng-username-header","DM_LLNG_USERNAME_HEADER",""],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-fake-user","DM_AUTH_FAKE_USER",""],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps","phrases"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs","phrases"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array",null,"identifiers"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-transit-branch","DM_AUTHZ_TRANSIT_BRANCH",""],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-bypass","DM_AUTHZ_DYNAMIC_BYPASS",[],"array",null,"identifiers"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-username-claim","DM_OIDC_USERNAME_CLAIM","sub"],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies","identifiers"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes","identifiers"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes","identifiers"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
1
+ import{fileURLToPath as e}from"url";import{join as _,dirname as a}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins","identifiers"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains","identifiers"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls","identifiers",!0],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",_(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","identifiers"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes","identifiers"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--organization-schema","DM_ORGANIZATION_SCHEMA",""],["--enterprise-domain-name-attribute","DM_ENTERPRISE_DOMAIN_NAME_ATTRIBUTE","associatedDomain"],["--enterprise-domain-link-attribute","DM_ENTERPRISE_DOMAIN_LINK_ATTRIBUTE",""],["--storage-backend","DM_STORAGE_BACKEND",""],["--storage-sweep-interval","DM_STORAGE_SWEEP_INTERVAL",600,"number"],["--storage-ldap-base","DM_STORAGE_LDAP_BASE",""],["--storage-ldap-object-class","DM_STORAGE_LDAP_OBJECT_CLASS","applicationProcess"],["--storage-file-directory","DM_STORAGE_FILE_DIRECTORY",""],["--storage-postgres-url","DM_STORAGE_POSTGRES_URL",""],["--storage-postgres-table","DM_STORAGE_POSTGRES_TABLE","ldap_rest_storage"],["--storage-valkey-url","DM_STORAGE_VALKEY_URL",""],["--storage-valkey-prefix","DM_STORAGE_VALKEY_PREFIX","ldap-rest:"],["--bcl-retention","DM_BCL_RETENTION",604800,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes","identifiers"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",_(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","identifiers"],["--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"],["--ldap-flat-auto-repair","DM_LDAP_FLAT_AUTO_REPAIR",!0,"boolean"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases","dns"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes","identifiers"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches","dns"],["--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",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--twake-lifecycle-role-attribute","DM_TWAKE_LIFECYCLE_ROLE_ATTRIBUTE",""],["--twake-lifecycle-lock-attribute","DM_TWAKE_LIFECYCLE_LOCK_ATTRIBUTE",""],["--twake-lifecycle-lock-value","DM_TWAKE_LIFECYCLE_LOCK_VALUE",""],["--twake-lifecycle-deleted-attribute","DM_TWAKE_LIFECYCLE_DELETED_ATTRIBUTE",""],["--twake-lifecycle-deleted-value","DM_TWAKE_LIFECYCLE_DELETED_VALUE","TRUE"],["--twake-lifecycle-deleted-at-attribute","DM_TWAKE_LIFECYCLE_DELETED_AT_ATTRIBUTE",""],["--twake-lifecycle-deleted-at-format","DM_TWAKE_LIFECYCLE_DELETED_AT_FORMAT","iso8601"],["--twake-lifecycle-reason-attribute","DM_TWAKE_LIFECYCLE_REASON_ATTRIBUTE",""],["--twake-lifecycle-rules","DM_TWAKE_LIFECYCLE_RULES",""],["--twake-lifecycle-member-attribute","DM_TWAKE_LIFECYCLE_MEMBER_ATTRIBUTE","member"],["--twake-group-base","DM_TWAKE_GROUP_BASE",""],["--twake-group-user-base","DM_TWAKE_GROUP_USER_BASE",""],["--twake-group-organization-dn","DM_TWAKE_GROUP_ORGANIZATION_DN",""],["--twake-group-organization-status-attribute","DM_TWAKE_GROUP_ORGANIZATION_STATUS_ATTRIBUTE","twakeOrgStatus"],["--twake-group-organization-deleted-value","DM_TWAKE_GROUP_ORGANIZATION_DELETED_VALUE","deleted"],["--twake-group-display-name-attribute","DM_TWAKE_GROUP_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-group-color-attribute","DM_TWAKE_GROUP_COLOR_ATTRIBUTE","twakeGroupColor"],["--twake-group-created-at-attribute","DM_TWAKE_GROUP_CREATED_AT_ATTRIBUTE","twakeCreatedAt"],["--twake-group-max-page-limit","DM_TWAKE_GROUP_MAX_PAGE_LIMIT",1e3,"number"],["--twake-group-member-fields","DM_TWAKE_GROUP_MEMBER_FIELDS",{},"json"],["--twake-space-base","DM_TWAKE_SPACE_BASE",""],["--twake-space-class","DM_TWAKE_SPACE_CLASS",["top","twakeSpace"],"array",null,"identifiers"],["--twake-space-display-name-attribute","DM_TWAKE_SPACE_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-space-admin-attribute","DM_TWAKE_SPACE_ADMIN_ATTRIBUTE","twakeSpaceAdmin"],["--twake-space-editor-attribute","DM_TWAKE_SPACE_EDITOR_ATTRIBUTE","twakeSpaceEditor"],["--twake-space-viewer-attribute","DM_TWAKE_SPACE_VIEWER_ATTRIBUTE","twakeSpaceViewer"],["--twake-space-user-role-attribute","DM_TWAKE_SPACE_USER_ROLE_ATTRIBUTE",""],["--twake-space-exchange","DM_TWAKE_SPACE_EXCHANGE","space"],["--twake-space-sync-queue","DM_TWAKE_SPACE_SYNC_QUEUE","twake.space.sync.requested.ldap-rest"],["--twake-tombstone-dn","DM_TWAKE_TOMBSTONE_DN",[],"array",null,"dns"],["--twake-tombstone-default-reason","DM_TWAKE_TOMBSTONE_DEFAULT_REASON","deleted"],["--twake-tombstone-reason-header","DM_TWAKE_TOMBSTONE_REASON_HEADER","x-deletion-reason"],["--twake-tombstone-reasons","DM_TWAKE_TOMBSTONE_REASONS",[],"array",null,"identifiers"],["--twake-tombstone-clear-attributes","DM_TWAKE_TOMBSTONE_CLEAR_ATTRIBUTES",[],"array",null,"identifiers"],["--twake-tombstone-erase-min-age","DM_TWAKE_TOMBSTONE_ERASE_MIN_AGE",2592e3,"number"],["--twake-tombstone-group-bases","DM_TWAKE_TOMBSTONE_GROUP_BASES",[],"array",null,"dns"],["--twake-instance-dn","DM_TWAKE_INSTANCE_DN",[],"array",null,"dns"],["--twake-instance-skip-attribute","DM_TWAKE_INSTANCE_SKIP_ATTRIBUTE",""],["--twake-instance-skip-value","DM_TWAKE_INSTANCE_SKIP_VALUE",""],["--twake-instance-provider","DM_TWAKE_INSTANCE_PROVIDER","cloudery"],["--twake-instance-id","DM_TWAKE_INSTANCE_ID","{uid}"],["--twake-instance-fqdn-attribute","DM_TWAKE_INSTANCE_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--twake-instance-sent-attribute","DM_TWAKE_INSTANCE_SENT_ATTRIBUTE","twakeCreatedEventAt"],["--twake-instance-locale","DM_TWAKE_INSTANCE_LOCALE",""],["--twake-instance-timeout","DM_TWAKE_INSTANCE_TIMEOUT",3e4,"number"],["--twake-instance-cloudery-url","DM_TWAKE_INSTANCE_CLOUDERY_URL",""],["--twake-instance-cloudery-token","DM_TWAKE_INSTANCE_CLOUDERY_TOKEN",""],["--twake-instance-cloudery-offer","DM_TWAKE_INSTANCE_CLOUDERY_OFFER","b2b_twake_default"],["--twake-instance-cloudery-domain","DM_TWAKE_INSTANCE_CLOUDERY_DOMAIN",""],["--twake-instance-cloudery-organization-offer","DM_TWAKE_INSTANCE_CLOUDERY_ORGANIZATION_OFFER",""],["--twake-instance-cozy-url","DM_TWAKE_INSTANCE_COZY_URL",""],["--twake-instance-cozy-user","DM_TWAKE_INSTANCE_COZY_USER","admin"],["--twake-instance-cozy-passphrase","DM_TWAKE_INSTANCE_COZY_PASSPHRASE",""],["--twake-instance-cozy-domain","DM_TWAKE_INSTANCE_COZY_DOMAIN",""],["--twake-instance-cozy-context","DM_TWAKE_INSTANCE_COZY_CONTEXT","default"],["--twake-instance-cozy-apps","DM_TWAKE_INSTANCE_COZY_APPS","home,drive,settings,notes,dataproxy"],["--twake-instance-cozy-org-id","DM_TWAKE_INSTANCE_COZY_ORG_ID",""],["--twake-instance-cozy-org-domain","DM_TWAKE_INSTANCE_COZY_ORG_DOMAIN",""],["--twake-instance-auth-exchange","DM_TWAKE_INSTANCE_AUTH_EXCHANGE","auth"],["--twake-instance-b2b-exchange","DM_TWAKE_INSTANCE_B2B_EXCHANGE","b2b"],["--twake-instance-user-created-key","DM_TWAKE_INSTANCE_USER_CREATED_KEY","user.created"],["--twake-instance-organization-created-key","DM_TWAKE_INSTANCE_ORGANIZATION_CREATED_KEY","organization.created"],["--twake-instance-workplace-created-key","DM_TWAKE_INSTANCE_WORKPLACE_CREATED_KEY","workplace.created"],["--twake-instance-queue","DM_TWAKE_INSTANCE_QUEUE","workplace.created.ldap-rest"],["--twake-instance-organization-base","DM_TWAKE_INSTANCE_ORGANIZATION_BASE",""],["--twake-instance-organization-id-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_ID_ATTRIBUTE","ou"],["--twake-instance-organization-domain-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_DOMAIN_ATTRIBUTE","twakeDomain"],["--twake-instance-organization-name-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_NAME_ATTRIBUTE","description"],["--twake-instance-organization-fqdn-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_FQDN_ATTRIBUTE",""],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes","identifiers"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes","identifiers"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--llng-username-header","DM_LLNG_USERNAME_HEADER",""],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-fake-user","DM_AUTH_FAKE_USER",""],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps","phrases"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs","phrases"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array",null,"identifiers"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-transit-branch","DM_AUTHZ_TRANSIT_BRANCH",""],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-bypass","DM_AUTHZ_DYNAMIC_BYPASS",[],"array",null,"identifiers"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-username-claim","DM_OIDC_USERNAME_CLAIM","sub"],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies","identifiers"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes","identifiers"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes","identifiers"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as 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 s}from"../../lib/expressFormatedResponses.js";import{transformSchemas as r,asyncHandler as o,validateDnValue as h,escapeDnValue as c,getRdn as d,isDnInBranch as l,rdnValue as g,normalizeDn as p,getParentDn as f,escapeLdapFilter as u,launchHooksChained as w,isChildOf as m}from"../../lib/utils.js";import{BadRequestError as z,NotFoundError as _,ConflictError as b}from"../../lib/errors.js";import{extractLdapCode as y}from"../../lib/ldapCodes.js";import{assertClientMaySet as $,modifiedAttributeNames as v,missingRequiredAttribute as O,checkDnValues as C,matchesPattern as E}from"../../config/schema.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,t),void 0!==a?[t,i,a]:[t,i]),ldapmodifyrequest:async([t,i,a,e])=>{let n,s={},r={};const checkIsOu=async()=>{if(void 0!==n)return n;if(i.replace?.objectClass)n=this.isOu({objectClass:i.replace.objectClass});else if(i.add?.objectClass)n=this.isOu({objectClass:i.add.objectClass});else{const i=await this.server.ldap.search({paged:!1,scope:"base"},t);n=i.searchEntries.length>0&&this.isOu(i.searchEntries[0])}return n};if(i.delete){const t=(Array.isArray(i.delete)?i.delete.map(String):Object.keys(i.delete)).map(t=>t.split(";")[0].toLowerCase()),a=t.includes(this.linkAttr.toLowerCase()),e=t.includes(this.pathAttr.toLowerCase());if(a||e){if(!await checkIsOu()&&a)throw new z("An organization link cannot be deleted");if(e)throw new z("An organization path cannot be deleted")}}if(i.replace&&(i.replace[this.linkAttr]&&(s={...i.replace}),i.replace[this.pathAttr]&&(r={...i.replace})),i.add&&(i.add[this.linkAttr]&&(s={...s,...i.add}),i.add[this.pathAttr]&&(r={...r,...i.add})),Object.keys(s).length>0){await checkIsOu()||await this.checkDeptLink(s)}if(Object.keys(r).length>0){await checkIsOu()&&await this.checkDeptPath(r,t)}return[t,i,a,e]},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,a])=>[t,i,a]},this.sizeLimitWarned=new Set,!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,i.ldap.declareObjectClasses(this.config.ldap_top_organization,this.config.ldap_organization_class||[],!0),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(r(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,async()=>this.hideNeverReturn(await this.getOrganisationTop(t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,async()=>this.hideNeverReturn(await this.getOrganisationByDn(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,async()=>this.hideNeverReturn(await this.getOrganisationSubnodes(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 z('query parameter "q" is required');await a(i,async()=>this.hideNeverReturn(await this.searchOrganisationSubnodes(e,n,t)))})),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;$(this.schema,Object.keys(a),"ou"),h(a.ou,"ou");const s=a.parentDn||this.config.ldap_top_organization,r=`ou=${c(a.ou)},${s}`,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),r,o,t)}async apiModify(t,i){const a=e(t,i);if(!a)return;const s=decodeURIComponent(t.params.dn);if(!s)throw new z("dn is required");$(this.schema,v(a)),await n(i,this.modifyOrganization.bind(this),s,a,t)}async apiDelete(t,i){if(!s(t,i))return;const a=decodeURIComponent(t.params.dn);if(!a)throw new z("dn is required");await n(i,this.deleteOrganization.bind(this),a,t)}async apiMove(t,i){if(!s(t,i))return;const n=e(t,i,"targetOrgDn");if(!n)return;const r=decodeURIComponent(t.params.dn);if(!r)throw new z("dn is required");const{targetOrgDn:o}=n;if(!o||"string"!=typeof o)throw new z("Missing or invalid targetOrgDn in request body");await a(i,this.moveOrganization.bind(this),r,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 _(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new z(`Entry ${a} isn't in top organization branch`)}}async checkDeptPath(t,i){if(t[this.pathAttr]){const a=t[this.pathAttr],e=Array.isArray(a)?a[0]:a,n=this.config.ldap_organization_path_separator||" / ",s=this.config.ldap_top_organization||"",r=i&&s&&/^ou=/i.test(d(i))&&l(i,s)?g(i):void 0,o=t.ou,h=(Array.isArray(o)?o[0]:o)||r;let c=e;if((this.isOu(t)||void 0!==r)&&h){const t=s?g(s):"";if(t&&e.startsWith(h+n)&&e.endsWith(n+t))return;if(e===h){const t=s?p(s):"";if(!i||!t||!l(i,s)||p(i)===t||p(f(i))===t)return;throw new z(`Organization path "${e}" names no parent, but ${i} is not directly under ${s}`)}if(!e.endsWith(n+h))throw new z(`Organization path must end with its own name, preceded by separator "${n}"`);c=e.slice(0,e.length-n.length-h.length)}const w=await this.server.ldap.search({paged:!1,scope:"sub",filter:`(${this.pathAttr}=${u(c)})`,attributes:["dn"]},this.config.ldap_top_organization);if(!w.searchEntries||0===w.searchEntries.length)throw new z(`Invalid organization path ${e}: no organization holds "${c}"`)}}async isEmptyOrganization(t){if((await this.server.ldap.search({paged:!1,filter:`(${this.config.ldap_organization_link_attribute}=${u(t)})`})).searchEntries.length>0)throw new b(`Organization ${t} is not empty`)}isOu(t){if(!t.objectClass)return!1;const i=[].concat(t.objectClass).map(t=>String(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 z("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 _("Top organization not found");const[,a]=await w(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 _(`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;n&&"organizationalUnit"!==n||a.push(...await this.childOrganizations(t,"(objectClass=organizationalUnit)",e,i));const s=this.config.ldap_top_organization,r=f(s);let o=`(${this.config.ldap_organization_link_attribute}=${u(t)})`;return n&&(o=`(&${o}(objectClass=${u(n)}))`),this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${r}`),a.push(...await this.linkedEntities(r,o,e,t,i)),a}warnSizeLimit(t,i){this.sizeLimitWarned.has(t)?this.server.logger.debug(i):(this.sizeLimitWarned.size<LdapOrganizations.SIZE_LIMIT_WARNED_MAX&&this.sizeLimitWarned.add(t),this.server.logger.warn(i))}async childOrganizations(t,i,a,e){try{const a=await this.server.ldap.search({paged:!0,scope:"one",filter:i},t,e),n=[];for await(const t of a)n.push(...t.searchEntries);return this.server.logger.debug(`Found ${n.length} sub-OUs for ${t}`),n}catch(i){const a=y(i);if(32===a)return this.server.logger.debug(`No sub-OUs for ${t}: the node holds none`),[];if(4!==a)throw i}const n=(await this.server.ldap.search({paged:!1,scope:"one",filter:i,sizeLimit:a+1},t,e)).searchEntries.slice(0,a);return this.warnSizeLimit(`children:${t}`,`Organization ${t} holds more child organizations than the directory will list in one answer, so ${n.length} are returned and the rest are hidden. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to list them all.`),[...n,{dn:`more-organizations-${t}`,cn:["... more organizations than the directory will list"],objectClass:["moreIndicator"],_isMoreIndicator:"true",_displayedCount:n.length.toString()}]}async linkedEntities(t,i,a,e,n){const s=[];let r=0;try{const e=await this.server.ldap.search({paged:!0,filter:i},t,n);for await(const t of e){r+=t.searchEntries.length;const i=a-s.length;i>0&&s.push(...t.searchEntries.slice(0,i))}}catch(s){if(4!==y(s))throw s;const r=await this.server.ldap.search({paged:!1,filter:i,sizeLimit:a+1},t,n);return this.warnSizeLimit(`linked:${e}`,`More entries are attached to ${e} than the directory will list in one answer. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to count them all.`),[...r.searchEntries.slice(0,a),{dn:`more-${e}`,cn:["... more elements than the directory will list"],objectClass:["moreIndicator"],_isMoreIndicator:"true",_displayedCount:Math.min(r.searchEntries.length,a).toString()}]}return this.server.logger.debug(`Found ${r} linked entities for ${e}, returning ${s.length}`),r<=a?s:[...s,{dn:`more-${e}`,cn:[`... ${r-a} more elements`],objectClass:["moreIndicator"],_isMoreIndicator:"true",_totalCount:r.toString(),_displayedCount:a.toString()}]}async addOrganization(t,i,a){return await this.validateNewOrganization(t,i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i,a){return await this.validateChanges(t,i),await this.server.ldap.modify(t,i,a)}async validateNewOrganization(t,i){if(!this.schema)return!0;for(const[t,a]of Object.entries(i))if(!await this._validateOneChange(t,a))throw new z(this.invalidValueMessage(t));const a=O(this.schema,i,this.server.loadedPlugins);if(a)throw new z(`Missing required field ${a}`);return!0}invalidValueMessage(t){const i=this.schema?.attributes[t],a=i?.hint||i?.items?.hint;return a?`Invalid value for field ${t}: ${a}`:`Invalid value for field ${t}`}async validateChanges(t,i){if(!this.schema)return!0;if(i.add)for(const[t,a]of Object.entries(i.add))if(!await this._validateOneChange(t,a))throw new z(this.invalidValueMessage(t));if(i.replace)for(const[t,a]of Object.entries(i.replace))if(!await this._validateOneChange(t,a))throw new z(this.invalidValueMessage(t));return!0}async _validateOneChange(t,i){if(!this.schema)return!0;const a=this.schema.attributes[t];if(!a){if(this.schema.strict)throw new z(`Field ${t} is not allowed`);return!0}if(null==i){if(a.required)throw new z(`Field ${t} is required`);return!0}if("string"===a.type&&Array.isArray(i))throw new z(`Field ${t} must be a single value`);return await C(t,a,i,t=>this.entryExists(t)),E(a,i)}async entryExists(t){try{return(await this.server.ldap.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}}async moveOrganization(t,i,a){let e;try{e=await this.server.ldap.search({paged:!1,scope:"base"},i)}catch(t){throw new _(`Target organization ${i} not found: ${t instanceof Error?t.message:String(t)}`)}if(!e.searchEntries||0===e.searchEntries.length)throw new _(`Target organization ${i} not found`);if(!this.isOu(e.searchEntries[0]))throw new z(`Target ${i} is not an organizational unit`);const n=`${d(t)},${i}`;let s;try{s=await this.server.ldap.search({paged:!1,scope:"base"},t)}catch(t){throw new _(`Source organization not found: ${t}`)}if(!s.searchEntries||0===s.searchEntries.length)throw new _(`Source organization ${t} not found`);if(n===t)throw new z("Cannot move organization to its current location");if(i.toLowerCase()===t.toLowerCase()||m(i,t))throw new z("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,i){return await this.server.ldap.delete(t,i)}async searchOrganisationSubnodes(t,i,a){const e=[],n=this.config.ldap_organization_max_subnodes||50,s=u(i);e.push(...await this.childOrganizations(t,`(&(objectClass=organizationalUnit)(|(ou=*${s}*)(description=*${s}*)))`,n,a));const r=this.config.ldap_top_organization,o=f(r),h=u(t),c=`(&(${this.config.ldap_organization_link_attribute}=${h})(|(uid=*${s}*)(cn=*${s}*)(mail=*${s}*)(sn=*${s}*)(givenName=*${s}*)))`;return this.server.logger.debug(`Searching for linked entities with filter: ${c} in ${o}`),e.push(...await this.linkedEntities(o,c,n,t,a)),e}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`}}}}LdapOrganizations.SIZE_LIMIT_WARNED_MAX=500;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 s}from"../../lib/expressFormatedResponses.js";import{transformSchemas as r,asyncHandler as o,validateDnValue as h,escapeDnValue as c,getRdn as d,isDnInBranch as l,rdnValue as g,normalizeDn as p,getParentDn as f,escapeLdapFilter as u,launchHooksChained as w,isChildOf as m,substringSearchFilter as z}from"../../lib/utils.js";import{BadRequestError as _,NotFoundError as y,ConflictError as b}from"../../lib/errors.js";import{extractLdapCode as $}from"../../lib/ldapCodes.js";import{assertClientMaySet as v,modifiedAttributeNames as O,missingRequiredAttribute as C,checkDnValues as E,matchesPattern as A}from"../../config/schema.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,t),void 0!==a?[t,i,a]:[t,i]),ldapmodifyrequest:async([t,i,a,e])=>{let n,s={},r={};const checkIsOu=async()=>{if(void 0!==n)return n;if(i.replace?.objectClass)n=this.isOu({objectClass:i.replace.objectClass});else if(i.add?.objectClass)n=this.isOu({objectClass:i.add.objectClass});else{const i=await this.server.ldap.search({paged:!1,scope:"base"},t);n=i.searchEntries.length>0&&this.isOu(i.searchEntries[0])}return n};if(i.delete){const t=(Array.isArray(i.delete)?i.delete.map(String):Object.keys(i.delete)).map(t=>t.split(";")[0].toLowerCase()),a=t.includes(this.linkAttr.toLowerCase()),e=t.includes(this.pathAttr.toLowerCase());if(a||e){if(!await checkIsOu()&&a)throw new _("An organization link cannot be deleted");if(e)throw new _("An organization path cannot be deleted")}}if(i.replace&&(i.replace[this.linkAttr]&&(s={...i.replace}),i.replace[this.pathAttr]&&(r={...i.replace})),i.add&&(i.add[this.linkAttr]&&(s={...s,...i.add}),i.add[this.pathAttr]&&(r={...r,...i.add})),Object.keys(s).length>0){await checkIsOu()||await this.checkDeptLink(s)}if(Object.keys(r).length>0){await checkIsOu()&&await this.checkDeptPath(r,t)}return[t,i,a,e]},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,a])=>[t,i,a]},this.sizeLimitWarned=new Set,!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,i.ldap.declareObjectClasses(this.config.ldap_top_organization,this.config.ldap_organization_class||[],!0),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(r(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,async()=>this.hideNeverReturn(await this.getOrganisationTop(t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,async()=>this.hideNeverReturn(await this.getOrganisationByDn(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,async()=>this.hideNeverReturn(await this.getOrganisationSubnodes(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 _('query parameter "q" is required');await a(i,async()=>this.hideNeverReturn(await this.searchOrganisationSubnodes(e,n,t)))})),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;v(this.schema,Object.keys(a),"ou"),h(a.ou,"ou");const s=a.parentDn||this.config.ldap_top_organization,r=`ou=${c(a.ou)},${s}`,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),r,o,t)}async apiModify(t,i){const a=e(t,i);if(!a)return;const s=decodeURIComponent(t.params.dn);if(!s)throw new _("dn is required");v(this.schema,O(a)),await n(i,this.modifyOrganization.bind(this),s,a,t)}async apiDelete(t,i){if(!s(t,i))return;const a=decodeURIComponent(t.params.dn);if(!a)throw new _("dn is required");await n(i,this.deleteOrganization.bind(this),a,t)}async apiMove(t,i){if(!s(t,i))return;const n=e(t,i,"targetOrgDn");if(!n)return;const r=decodeURIComponent(t.params.dn);if(!r)throw new _("dn is required");const{targetOrgDn:o}=n;if(!o||"string"!=typeof o)throw new _("Missing or invalid targetOrgDn in request body");await a(i,this.moveOrganization.bind(this),r,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 y(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new _(`Entry ${a} isn't in top organization branch`)}}async checkDeptPath(t,i){if(t[this.pathAttr]){const a=t[this.pathAttr],e=Array.isArray(a)?a[0]:a,n=this.config.ldap_organization_path_separator||" / ",s=this.config.ldap_top_organization||"",r=i&&s&&/^ou=/i.test(d(i))&&l(i,s)?g(i):void 0,o=t.ou,h=(Array.isArray(o)?o[0]:o)||r;let c=e;if((this.isOu(t)||void 0!==r)&&h){const t=s?g(s):"";if(t&&e.startsWith(h+n)&&e.endsWith(n+t))return;if(e===h){const t=s?p(s):"";if(!i||!t||!l(i,s)||p(i)===t||p(f(i))===t)return;throw new _(`Organization path "${e}" names no parent, but ${i} is not directly under ${s}`)}if(!e.endsWith(n+h))throw new _(`Organization path must end with its own name, preceded by separator "${n}"`);c=e.slice(0,e.length-n.length-h.length)}const w=await this.server.ldap.search({paged:!1,scope:"sub",filter:`(${this.pathAttr}=${u(c)})`,attributes:["dn"]},this.config.ldap_top_organization);if(!w.searchEntries||0===w.searchEntries.length)throw new _(`Invalid organization path ${e}: no organization holds "${c}"`)}}async isEmptyOrganization(t){if((await this.server.ldap.search({paged:!1,filter:`(${this.config.ldap_organization_link_attribute}=${u(t)})`})).searchEntries.length>0)throw new b(`Organization ${t} is not empty`)}isOu(t){if(!t.objectClass)return!1;const i=[].concat(t.objectClass).map(t=>String(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 _("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 y("Top organization not found");const[,a]=await w(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 y(`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;n&&"organizationalUnit"!==n||a.push(...await this.childOrganizations(t,"(objectClass=organizationalUnit)",e,i));const s=this.config.ldap_top_organization,r=f(s);let o=`(${this.config.ldap_organization_link_attribute}=${u(t)})`;return n&&(o=`(&${o}(objectClass=${u(n)}))`),this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${r}`),a.push(...await this.linkedEntities(r,o,e,t,i)),a}warnSizeLimit(t,i){this.sizeLimitWarned.has(t)?this.server.logger.debug(i):(this.sizeLimitWarned.size<LdapOrganizations.SIZE_LIMIT_WARNED_MAX&&this.sizeLimitWarned.add(t),this.server.logger.warn(i))}async childOrganizations(t,i,a,e){try{const a=await this.server.ldap.search({paged:!0,scope:"one",filter:i},t,e),n=[];for await(const t of a)n.push(...t.searchEntries);return this.server.logger.debug(`Found ${n.length} sub-OUs for ${t}`),n}catch(i){const a=$(i);if(32===a)return this.server.logger.debug(`No sub-OUs for ${t}: the node holds none`),[];if(4!==a)throw i}const n=(await this.server.ldap.search({paged:!1,scope:"one",filter:i,sizeLimit:a+1},t,e)).searchEntries.slice(0,a);return this.warnSizeLimit(`children:${t}`,`Organization ${t} holds more child organizations than the directory will list in one answer, so ${n.length} are returned and the rest are hidden. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to list them all.`),[...n,{dn:`more-organizations-${t}`,cn:["... more organizations than the directory will list"],objectClass:["moreIndicator"],_isMoreIndicator:"true",_displayedCount:n.length.toString()}]}async linkedEntities(t,i,a,e,n){const s=[];let r=0;try{const e=await this.server.ldap.search({paged:!0,filter:i},t,n);for await(const t of e){r+=t.searchEntries.length;const i=a-s.length;i>0&&s.push(...t.searchEntries.slice(0,i))}}catch(s){if(4!==$(s))throw s;const r=await this.server.ldap.search({paged:!1,filter:i,sizeLimit:a+1},t,n);return this.warnSizeLimit(`linked:${e}`,`More entries are attached to ${e} than the directory will list in one answer. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to count them all.`),[...r.searchEntries.slice(0,a),{dn:`more-${e}`,cn:["... more elements than the directory will list"],objectClass:["moreIndicator"],_isMoreIndicator:"true",_displayedCount:Math.min(r.searchEntries.length,a).toString()}]}return this.server.logger.debug(`Found ${r} linked entities for ${e}, returning ${s.length}`),r<=a?s:[...s,{dn:`more-${e}`,cn:[`... ${r-a} more elements`],objectClass:["moreIndicator"],_isMoreIndicator:"true",_totalCount:r.toString(),_displayedCount:a.toString()}]}async addOrganization(t,i,a){return await this.validateNewOrganization(t,i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i,a){return await this.validateChanges(t,i),await this.server.ldap.modify(t,i,a)}async validateNewOrganization(t,i){if(!this.schema)return!0;for(const[t,a]of Object.entries(i))if(!await this._validateOneChange(t,a))throw new _(this.invalidValueMessage(t));const a=C(this.schema,i,this.server.loadedPlugins);if(a)throw new _(`Missing required field ${a}`);return!0}invalidValueMessage(t){const i=this.schema?.attributes[t],a=i?.hint||i?.items?.hint;return a?`Invalid value for field ${t}: ${a}`:`Invalid value for field ${t}`}async validateChanges(t,i){if(!this.schema)return!0;if(i.add)for(const[t,a]of Object.entries(i.add))if(!await this._validateOneChange(t,a))throw new _(this.invalidValueMessage(t));if(i.replace)for(const[t,a]of Object.entries(i.replace))if(!await this._validateOneChange(t,a))throw new _(this.invalidValueMessage(t));return!0}async _validateOneChange(t,i){if(!this.schema)return!0;const a=this.schema.attributes[t];if(!a){if(this.schema.strict)throw new _(`Field ${t} is not allowed`);return!0}if(null==i){if(a.required)throw new _(`Field ${t} is required`);return!0}if("string"===a.type&&Array.isArray(i))throw new _(`Field ${t} must be a single value`);return await E(t,a,i,t=>this.entryExists(t)),A(a,i)}async entryExists(t){try{return(await this.server.ldap.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}}async moveOrganization(t,i,a){let e;try{e=await this.server.ldap.search({paged:!1,scope:"base"},i)}catch(t){throw new y(`Target organization ${i} not found: ${t instanceof Error?t.message:String(t)}`)}if(!e.searchEntries||0===e.searchEntries.length)throw new y(`Target organization ${i} not found`);if(!this.isOu(e.searchEntries[0]))throw new _(`Target ${i} is not an organizational unit`);const n=`${d(t)},${i}`;let s;try{s=await this.server.ldap.search({paged:!1,scope:"base"},t)}catch(t){throw new y(`Source organization not found: ${t}`)}if(!s.searchEntries||0===s.searchEntries.length)throw new y(`Source organization ${t} not found`);if(n===t)throw new _("Cannot move organization to its current location");if(i.toLowerCase()===t.toLowerCase()||m(i,t))throw new _("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,i){return await this.server.ldap.delete(t,i)}async searchOrganisationSubnodes(t,i,a){const e=[],n=this.config.ldap_organization_max_subnodes||50;e.push(...await this.childOrganizations(t,`(&(objectClass=organizationalUnit)${z(i,"ou,description")})`,n,a));const s=this.config.ldap_top_organization,r=f(s),o=u(t),h=this.config.display_name_attribute||"displayName",c=`(&(${this.config.ldap_organization_link_attribute}=${o})${z(i,`uid,cn,mail,sn,givenName,${h}`)})`;return this.server.logger.debug(`Searching for linked entities with filter: ${c} in ${r}`),e.push(...await this.linkedEntities(r,c,n,t,a)),e}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`}}}}LdapOrganizations.SIZE_LIMIT_WARNED_MAX=500;export{LdapOrganizations as default};
@@ -1 +1 @@
1
- import{randomUUID as t}from"node:crypto";import{BadRequestError as e,AUTHZ_REFUSED as r,HttpError as s}from"../../lib/errors.js";import{extractLdapCode as o}from"../../lib/ldapCodes.js";import{parseDn as i,escapeDnValue as a,isDummyMemberDn as n,escapeLdapFilter as c,isAuthzRefusal as u,unescapeDnValue as p,normalizeDn as h}from"../../lib/utils.js";import d from"../ldap/groups.js";import{BaseResolver as m}from"../scim/baseResolver.js";import{mergeMapping as g,loadMappingFile as l,DEFAULT_GROUP_MAPPING as f}from"../scim/mapping.js";import{valueOf as w,lifecycleAttributes as y,first as b}from"./lifecycleAttributes.js";const read=(t,e)=>b(w(t,e)),_="{org}",O=["displayName","description","createdAt"],v=["uid","displayName","mail","jobTitle"],$={uid:"uid",_id:"entryUUID",cn:"cn",sn:"sn",givenName:"givenName",displayName:"displayName",mail:"mail",mobile:"mobile",jobTitle:"twakeJobTitle",company:"twakeCompany",organizationRole:"twakeOrganizationRole",organizationId:"twakeOrganizationId"},I={familyName:"sn",givenName:"givenName",additionalName:"twakeAdditionalName",namePrefix:"twakeNamePrefix"},R="twakeIsTechnical";class RouteError extends Error{constructor(t,e,r){super(e),this.status=t,this.code=r}}const notFound=t=>new RouteError(404,`${t[0].toUpperCase()}${t.slice(1)} not found`,`${t.toUpperCase()}_NOT_FOUND`),invalid=(t,e="INVALID_INPUT")=>new RouteError(400,t,e),N="name must be a non-blank string of at most 256 characters with no control characters";function isDisplayName(t){return"string"==typeof t&&t.trim().length>0&&t.length<=256&&!/[\x00-\x1f\x7f]/.test(t)}const j=/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/,isColor=t=>"string"==typeof t&&j.test(t),text=t=>"string"==typeof t||"number"==typeof t?String(t):"";function bodyOf(t){const e=t.body;if(!e||"object"!=typeof e||Array.isArray(e))throw invalid("Request body must be a JSON object");return e}function values(t){return null==t?[]:(Array.isArray(t)?t:[t]).map(t=>t.toString())}function dnKey(t){try{return h(t)}catch{return t.toLowerCase()}}function parentOf(t){return i(t).slice(1).join(",")}function rdnValue(t){const[e]=i(t),r=e.indexOf("=");return r>0?p(e.slice(r+1).trim()):t}function branchPattern(t){const[e,r]=i(t).join(",").split(_).map(t=>t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"));return new RegExp(`^${e}(?<org>(?:\\\\.|[^,])+)${r}$`,"i")}function movedInto(t,e,r){const s=organizationIn(t,r);return void 0!==s&&dnKey(parentOf(r))!==dnKey(parentOf(e))?s:void 0}const k=Symbol("whole member lists");function organizationIn(t,e){const r=t.exec(parentOf(i(e).join(",")));return r?.groups?p(r.groups.org):void 0}class TwakeGroups extends d{constructor(r){super(r),this.name="twakeGroups",this.hooks={ldapaddrequest:([t,e,r])=>(this.checkMembers(t,e.member),void 0!==this.organizationOf(t)&&void 0!==w(e,this.displayName)&&void 0===w(e,this.createdAt)&&(e={...e,[this.createdAt]:(new Date).toISOString()}),[t,e,r]),ldapmodifyrequest:([t,e,r,s])=>(this.checkMembers(t,e.add?.member),this.checkMembers(t,e.replace?.member),[t,e,r,s]),ldaprenamerequest:async([t,r,s])=>{const i=movedInto(this.groupPattern,t,r);if(void 0===i)return[t,r,s];let a;try{const{searchEntries:e}=await this.ldap.search({paged:!1,scope:"base",attributes:["member"],[k]:!0},t);a=e[0]?.member}catch(t){if(32!==o(t))throw t}if(void 0!==this.foreign(i,a))throw new e(`The group holds members of another organization than ${i}`);return[t,r,s]},ldapsearchfilter:async([t,e,r])=>[r?.[k]?t:await this.hideTombstones(t),e,r],scimgroupcreate:([e,r,s])=>(s??(s=new m(this.config).groupBase(r)),[this.groupPattern.test(i(s).join(","))?{...e,id:t()}:e,r,s])},this.groupBase=this.config.twake_group_base||"",this.userBase=this.config.twake_group_user_base||"";for(const[t,e]of[["--twake-group-base",this.groupBase],["--twake-group-user-base",this.userBase]])if(!e.includes(_))throw new Error(`${this.name}: ${t} must hold ${_}`);this.groupPattern=branchPattern(this.groupBase),this.attrs=y(this.config),this.orgPattern=this.config.twake_group_organization_dn||"",this.displayName=this.config.twake_group_display_name_attribute||"twakeDisplayName",this.color=this.config.twake_group_color_attribute||"twakeGroupColor",this.createdAt=this.config.twake_group_created_at_attribute||"twakeCreatedAt",this.maxPage=this.config.twake_group_max_page_limit||1e3,this.memberFields={...$,...this.config.twake_group_member_fields}}adaptSchema(t){const e={[this.displayName]:{type:"string",required:!0},[this.color]:{type:"string",test:j.source},[this.createdAt]:{type:"string"}};for(const[r,s]of Object.entries(e)){const e=Object.keys(t.attributes).find(t=>t.toLowerCase()===r.toLowerCase()),o=e?t.attributes[e]:s;e&&delete t.attributes[e],t.attributes[r]=o}return t}afterLoad(){if(!this.server.loadedPlugins.scim)return;const t=this.config.scim_group_rdn_attribute||"cn",e=this.config.scim_group_mapping||"",r=g(f,e?l(e):void 0).entries.find(e=>e.ldap?.toLowerCase()===t.toLowerCase());r&&this.logger.warn(`${this.name}: the SCIM group mapping writes ${r.scim} to ${t}, so SCIM groups keep it as their RDN instead of a generated cn: map ${r.scim} to another attribute with --scim-group-mapping`)}organizationOf(t){return organizationIn(this.groupPattern,t)}groupBaseOf(t){return this.groupBase.replace(_,a(t))}userBaseOf(t){return this.userBase.replace(_,a(t))}checkMembers(t,r){const s=this.organizationOf(t);if(!s)return;const o=this.foreign(s,r);if(void 0!==o)throw new e(`${o} is not a user of organization ${s}`)}foreign(t,e){const r=dnKey(this.userBaseOf(t));return values(e).find(t=>!n(t,this.config.group_dummy_user)&&dnKey(parentOf(t))!==r)}async hideTombstones(t){if(!this.attrs.deleted)return t;const e=new Set;for(const r of t.searchEntries){const t=this.organizationOf(r.dn);t&&void 0!==r.member&&e.add(t)}if(0===e.size)return t;const r=new Set;for(const t of e)for(const e of await this.tombstonesOf(t))r.add(dnKey(e));return 0===r.size?t:{...t,searchEntries:t.searchEntries.map(t=>void 0!==t.member&&this.organizationOf(t.dn)?{...t,member:values(t.member).filter(t=>!r.has(dnKey(t)))}:t)}}async tombstonesOf(t){const{deleted:e,deletedValue:r}=this.attrs;if(!e)return[];try{const{searchEntries:s}=await this.ldap.search({paged:!1,scope:"one",filter:`(${e}=${c(r)})`,attributes:["dn"]},this.userBaseOf(t));return s.map(t=>t.dn)}catch(t){if(32===o(t))return[];throw t}}api(t){const e=`${this.config.api_prefix}/v1/organizations/:id/groups`,r=this.organizationRoute.bind(this);t.get(e,r("listing groups",(t,e,r)=>this.listRoute(t,e,r))),t.post(e,r("creating group",(t,e,r)=>this.createRoute(t,e,r))),t.get(`${e}/:groupId`,r("getting group",async(t,e,r)=>{const s=await this.readGroup(r,t.params.groupId);if(!s)throw notFound("group");e.json(s)})),t.get(`${e}/:groupId/members`,r("listing group members",(t,e,r)=>this.membersRoute(t,e,r))),t.patch(`${e}/:groupId`,r("updating group",(t,e,r)=>this.updateRoute(t,e,r))),t.delete(`${e}/:groupId`,r("deleting group",async(t,e,r)=>{await this.onGroup(()=>this.deleteGroup(this.groupDn(r,t.params.groupId),t)),e.json({success:!0})})),t.post(`${e}/:groupId/members`,r("adding group members",(t,e,r)=>this.addMembersRoute(t,e,r))),t.delete(`${e}/:groupId/members/:userId`,r("removing group member",(t,e,r)=>this.removeMemberRoute(t,e,r)))}organizationRoute(t,e,o=this.name){return async(i,a)=>{try{const t=i.params.id;await this.checkOrganization(t),await e(i,a,t)}catch(e){if(e instanceof RouteError)return void a.status(e.status).json({error:e.message,code:e.code});if(u(e))return void a.status(403).json({error:r,code:"REFUSED"});if(e instanceof s&&e.statusCode<500)return void a.status(e.statusCode).json({error:e.message,code:400===e.statusCode?"INVALID_INPUT":"REFUSED"});this.logger.error({plugin:o,context:t,error:String(e)}),a.status(500).json({error:`Error ${t}`,code:"INTERNAL_ERROR"})}}}groupDn(t,e){return`${this.cn}=${a(e)},${this.groupBaseOf(t)}`}get userAttribute(){return this.config.ldap_user_main_attribute||"uid"}userDn(t,e){return`${this.userAttribute}=${a(e)},${this.userBaseOf(t)}`}async checkOrganization(t){if(!this.orgPattern)return;const e=this.config.twake_group_organization_status_attribute||"twakeOrgStatus";let r;try{r=(await this.ldap.search({paged:!1,scope:"base",attributes:[e]},this.orgPattern.replace(_,a(t)))).searchEntries[0]}catch(t){if(32!==o(t))throw t}if(!r)throw notFound("organization");const s=this.config.twake_group_organization_deleted_value||"deleted";if(read(r,e)===s)throw new RouteError(410,"Organization has been deleted","ORGANIZATION_DELETED")}page(t,e){const r=Math.max(1,parseInt(t.query.page)||1),s=Math.max(1,Math.min(parseInt(t.query.limit)||20,this.maxPage)),o=t.query.search||void 0;if(void 0!==o&&("string"!=typeof o||o.length<2))throw invalid("Search query must be at least 2 characters","INVALID_SEARCH_QUERY");const i=t.query.sortBy;if(void 0!==i&&!e.includes(i))throw invalid(`sortBy must be one of: ${e.join(", ")}`,"INVALID_SORT_FIELD");return{page:r,limit:s,offset:(r-1)*s,search:o,sortBy:i,desc:"desc"===t.query.sortOrder}}sorted(t,e,r){if(!e)return t;const s=r?-1:1;return t.sort((t,r)=>text(t[e]).localeCompare(text(r[e]))*s)}groupOf(t,e){const r=read(e,this.cn)||"",s=dnKey(this.userBaseOf(t)),o=values(e.member).filter(t=>!n(t,this.config.group_dummy_user)).map(t=>dnKey(parentOf(t))===s?rdnValue(t):t);return{id:r,cn:r,displayName:read(e,this.displayName)??r,description:read(e,"description"),color:read(e,this.color),organizationId:t,baseDN:this.groupDn(t,r),members:o,createdAt:read(e,this.createdAt)}}async groups(t,e,r,s){try{const{searchEntries:o}=await this.ldap.search({paged:!1,scope:"one",filter:e,...r&&{attributes:r}},this.groupBaseOf(t),s);return o}catch(t){if(32===o(t))return[];throw t}}async readGroup(t,e){try{const{searchEntries:r}=await this.ldap.search({paged:!1,scope:"base"},this.groupDn(t,e));return r[0]&&this.groupOf(t,r[0])}catch(t){if(32===o(t))return;throw t}}async named(t,e){const[r]=await this.groups(t,`(${this.displayName}=${c(e)})`);return r&&read(r,this.cn)}async onGroup(t){try{return await t()}catch(t){if(32===o(t))throw notFound("group");throw t}}async listRoute(t,e,r){const s=this.page(t,O);let o=`(${this.cn}=*)`;if(s.search){const t=c(s.search);o=`(&${o}(|(${this.displayName}=*${t}*)(description=*${t}*)))`}const i=this.sorted((await this.groups(r,o)).map(t=>this.groupOf(r,t)),s.sortBy,s.desc);e.json({organizationId:r,groups:i.slice(s.offset,s.offset+s.limit),pagination:{page:s.page,limit:s.limit,total:i.length,totalPages:Math.ceil(i.length/s.limit)}})}async createRoute(e,r,s){const o=bodyOf(e);if(!isDisplayName(o.name))throw invalid(N);if(void 0!==o.description&&"string"!=typeof o.description)throw invalid("description must be a string");if(void 0!==o.color&&!isColor(o.color))throw invalid("color must be a hex code (e.g. #RRGGBB or #RGB)");if(await this.named(s,o.name))throw new RouteError(409,"Group already exists","GROUP_EXISTS");const i=t(),a={objectClass:this.config.group_class||["top","groupOfNames"],[this.displayName]:o.name,[this.createdAt]:(new Date).toISOString()};o.description&&(a.description=o.description),o.color&&(a[this.color]=o.color),await this.addGroup(this.groupDn(s,i),[],a,e);const n=await this.readGroup(s,i);r.status(201).json(n)}async updateRoute(t,e,r){const s=bodyOf(t),o=t.params.groupId,i=Object.keys(s).filter(t=>!["name","description","color"].includes(t));if(i.length)throw invalid(`Only "name", "description" and "color" can be updated; unsupported field(s): ${i.join(", ")}`);const{name:a,description:n,color:c}=s;if(void 0===a&&void 0===n&&void 0===c)throw invalid('at least one of "name", "description" or "color" must be provided');if(void 0!==a&&!isDisplayName(a))throw invalid(N);if(void 0!==n&&"string"!=typeof n)throw invalid("description must be a string");if(void 0!==c&&""!==c&&!isColor(c))throw invalid("color must be a hex code (e.g. #RRGGBB or #RGB) or empty to clear it");const u={};if(void 0!==a){const t=await this.named(r,a);if(t&&t!==o)throw new RouteError(409,"Group already exists","GROUP_EXISTS");u[this.displayName]=a}void 0!==n&&(u.description=n||[]),void 0!==c&&(u[this.color]=c||[]),await this.onGroup(()=>this.modifyGroup(this.groupDn(r,o),{replace:u},t)),e.json({success:!0})}async membersRoute(t,e,r){const s=this.page(t,v),o=t.params.groupId,i=await this.readGroup(r,o);if(!i)throw notFound("group");e.json({organizationId:r,id:o,...await this.memberPage(s,r,i.members)})}async memberPage(t,e,r,s=()=>({}),o){const i=[...new Map(r.map(t=>[t.toLowerCase(),t])).values()],a=await this.profiles(e,i,o);let n=i.map(t=>({...a.get(t.toLowerCase())??{uid:t},...s(t)}));if(t.search){const e=t.search.toLowerCase();n=n.filter(t=>[t.uid,t.displayName||t.cn,t.mail].some(t=>text(t).toLowerCase().includes(e)))}n=this.sorted(n,t.sortBy??"uid",t.desc);const c=Math.ceil(n.length/t.limit);return{members:n.slice(t.offset,t.offset+t.limit),pagination:{page:t.page,limit:t.limit,total:n.length,totalPages:c,hasNextPage:t.page<c,hasPreviousPage:t.page>1}}}async users(t,e,r,s){const{deleted:i,deletedValue:a}=this.attrs,n=i?`(!(${i}=${c(a)}))`:"",u=[];for(let i=0;i<e.length;i+=this.maxPage){const a=`(&(|${e.slice(i,i+this.maxPage).map(t=>`(${this.userAttribute}=${c(t)})`).join("")})${n})`;try{const{searchEntries:e}=await this.ldap.search({paged:!1,scope:"one",filter:a,attributes:r},this.userBaseOf(t),s);u.push(...e)}catch(t){if(32!==o(t))throw t}}return u}async profiles(t,e,r){const s=[...new Set([...Object.values(this.memberFields),...Object.values(I),R])],o=new Map;for(const i of await this.users(t,e,s,r)){const t=read(i,this.userAttribute);if(!t)continue;const e={};for(const[t,r]of Object.entries(this.memberFields)){const s=read(i,r);void 0!==s&&(e[t]=s)}const r={};for(const[t,e]of Object.entries(I)){const s=read(i,e);void 0!==s&&(r[t]=s)}Object.keys(r).length&&(e.name=r),e.isTechnical="TRUE"===read(i,R)?.toUpperCase(),o.set(t.toLowerCase(),e)}return o}async addMembersRoute(t,e,r){const{usernames:s}=bodyOf(t);if(!Array.isArray(s)||0===s.length)throw invalid("usernames must be a non-empty array");if(!s.every(t=>"string"==typeof t&&t.length>0))throw invalid("usernames must be an array of non-empty strings");if(s.length>this.maxPage)throw invalid(`usernames cannot exceed ${this.maxPage} entries per request`);const o=t.params.groupId,i=await this.readGroup(r,o);if(!i)throw notFound("group");const a=[...new Map(s.map(t=>[t.toLowerCase(),t])).keys()],n=new Map((await this.users(r,a,[this.userAttribute])).map(t=>{const e=read(t,this.userAttribute)||"";return[e.toLowerCase(),e]}));if(a.some(t=>!n.has(t)))throw notFound("user");const c=new Set(i.members.map(t=>t.toLowerCase())),u=a.filter(t=>!c.has(t)).map(t=>this.userDn(r,n.get(t))),p=this.groupDn(r,o);u.length&&(await this.onGroup(()=>this.ldap.modify(p,{add:{member:u}},t)),await this.dropPlaceholder(p,t)),e.json({success:!0})}async dropPlaceholder(t,e){const r=this.config.group_dummy_user;if(r)try{await this.ldap.modify(t,{delete:{member:r}},e)}catch(t){if(![16,65].includes(o(t)??0))throw t}}async removeMemberRoute(t,e,r){const s=this.groupDn(r,t.params.groupId),i=this.userDn(r,t.params.userId);try{await this.ldap.modify(s,{delete:{member:i}},t)}catch(e){const r=o(e);if(32===r)throw notFound("group");if(16===r)throw notFound("member");if(65!==r||!this.config.group_dummy_user)throw e;await this.ldap.modify(s,{replace:{member:[this.config.group_dummy_user]}},t)}e.json({success:!0})}}export{N as NAME_RULE,_ as ORG,RouteError,bodyOf,branchPattern,TwakeGroups as default,dnKey,invalid,isDisplayName,movedInto,notFound,organizationIn,parentOf,rdnValue,read,values};
1
+ import{randomUUID as t}from"node:crypto";import{BadRequestError as e,AUTHZ_REFUSED as r,HttpError as s}from"../../lib/errors.js";import{extractLdapCode as o}from"../../lib/ldapCodes.js";import{parseDn as i,escapeDnValue as a,isDummyMemberDn as n,escapeLdapFilter as c,isAuthzRefusal as u,unescapeDnValue as p,normalizeDn as h}from"../../lib/utils.js";import d from"../ldap/groups.js";import{BaseResolver as m}from"../scim/baseResolver.js";import{mergeMapping as g,loadMappingFile as l,DEFAULT_GROUP_MAPPING as f}from"../scim/mapping.js";import{valueOf as w,lifecycleAttributes as y,first as b}from"./lifecycleAttributes.js";const read=(t,e)=>b(w(t,e)),_="{org}",O=["displayName","description","createdAt"],v=["uid","displayName","mail","jobTitle"],$={uid:"uid",_id:"entryUUID",cn:"cn",sn:"sn",givenName:"givenName",displayName:"displayName",mail:"mail",mobile:"mobile",jobTitle:"twakeJobTitle",company:"twakeCompany",organizationRole:"twakeOrganizationRole",organizationId:"twakeOrganizationId"},I={familyName:"sn",givenName:"givenName",additionalName:"twakeAdditionalName",namePrefix:"twakeNamePrefix"},R="twakeIsTechnical";class RouteError extends Error{constructor(t,e,r){super(e),this.status=t,this.code=r}}const notFound=t=>new RouteError(404,`${t[0].toUpperCase()}${t.slice(1)} not found`,`${t.toUpperCase()}_NOT_FOUND`),invalid=(t,e="INVALID_INPUT")=>new RouteError(400,t,e),N="name must be a non-blank string of at most 256 characters with no control characters";function isDisplayName(t){return"string"==typeof t&&t.trim().length>0&&t.length<=256&&!/[\x00-\x1f\x7f]/.test(t)}const j=/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/,isColor=t=>"string"==typeof t&&j.test(t),text=t=>"string"==typeof t||"number"==typeof t?String(t):"";function bodyOf(t){const e=t.body;if(!e||"object"!=typeof e||Array.isArray(e))throw invalid("Request body must be a JSON object");return e}function values(t){return null==t?[]:(Array.isArray(t)?t:[t]).map(t=>t.toString())}function dnKey(t){try{return h(t)}catch{return t.toLowerCase()}}function parentOf(t){return i(t).slice(1).join(",")}function rdnValue(t){const[e]=i(t),r=e.indexOf("=");return r>0?p(e.slice(r+1).trim()):t}function branchPattern(t){const[e,r]=i(t).join(",").split(_).map(t=>t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"));return new RegExp(`^${e}(?<org>(?:\\\\.|[^,])+)${r}$`,"i")}function movedInto(t,e,r){const s=organizationIn(t,r);return void 0!==s&&dnKey(parentOf(r))!==dnKey(parentOf(e))?s:void 0}const k=Symbol("whole member lists");function organizationIn(t,e){const r=t.exec(parentOf(i(e).join(",")));return r?.groups?p(r.groups.org):void 0}class TwakeGroups extends d{constructor(r){super(r),this.name="twakeGroups",this.hooks={ldapaddrequest:([t,e,r])=>(this.checkMembers(t,e.member),void 0!==this.organizationOf(t)&&void 0!==w(e,this.displayName)&&void 0===w(e,this.createdAt)&&(e={...e,[this.createdAt]:(new Date).toISOString()}),[t,e,r]),ldapmodifyrequest:([t,e,r,s])=>(this.checkMembers(t,e.add?.member),this.checkMembers(t,e.replace?.member),[t,e,r,s]),ldaprenamerequest:async([t,r,s])=>{const i=movedInto(this.groupPattern,t,r);if(void 0===i)return[t,r,s];let a;try{const{searchEntries:e}=await this.ldap.search({paged:!1,scope:"base",attributes:["member"],[k]:!0},t);a=e[0]?.member}catch(t){if(32!==o(t))throw t}if(void 0!==this.foreign(i,a))throw new e(`The group holds members of another organization than ${i}`);return[t,r,s]},ldapsearchfilter:async([t,e,r])=>[r?.[k]?t:await this.hideTombstones(t),e,r],scimgroupcreate:([e,r,s])=>(s??(s=new m(this.config).groupBase(r)),[this.groupPattern.test(i(s).join(","))?{...e,id:t()}:e,r,s])},this.groupBase=this.config.twake_group_base||"",this.userBase=this.config.twake_group_user_base||"";for(const[t,e]of[["--twake-group-base",this.groupBase],["--twake-group-user-base",this.userBase]])if(!e.includes(_))throw new Error(`${this.name}: ${t} must hold ${_}`);this.groupPattern=branchPattern(this.groupBase),this.attrs=y(this.config),this.orgPattern=this.config.twake_group_organization_dn||"",this.displayName=this.config.twake_group_display_name_attribute||"twakeDisplayName",this.color=this.config.twake_group_color_attribute||"twakeGroupColor",this.createdAt=this.config.twake_group_created_at_attribute||"twakeCreatedAt",this.maxPage=this.config.twake_group_max_page_limit||1e3,this.memberFields={...$,...this.config.twake_group_member_fields}}adaptSchema(t){const e={[this.displayName]:{type:"string",required:!0},[this.color]:{type:"string",test:j.source},[this.createdAt]:{type:"string"}};for(const[r,s]of Object.entries(e)){const e=Object.keys(t.attributes).find(t=>t.toLowerCase()===r.toLowerCase()),o=e?t.attributes[e]:s;e&&delete t.attributes[e],t.attributes[r]=o}return t}afterLoad(){if(!this.server.loadedPlugins.scim)return;const t=this.config.scim_group_rdn_attribute||"cn",e=this.config.scim_group_mapping||"",r=g(f,e?l(e):void 0).entries.find(e=>e.ldap?.toLowerCase()===t.toLowerCase());r&&this.logger.warn(`${this.name}: the SCIM group mapping writes ${r.scim} to ${t}, so SCIM groups keep it as their RDN instead of a generated cn: map ${r.scim} to another attribute with --scim-group-mapping`)}organizationOf(t){return organizationIn(this.groupPattern,t)}groupBaseOf(t){return this.groupBase.replace(_,a(t))}userBaseOf(t){return this.userBase.replace(_,a(t))}checkMembers(t,r){const s=this.organizationOf(t);if(!s)return;const o=this.foreign(s,r);if(void 0!==o)throw new e(`${o} is not a user of organization ${s}`)}foreign(t,e){const r=dnKey(this.userBaseOf(t));return values(e).find(t=>!n(t,this.config.group_dummy_user)&&dnKey(parentOf(t))!==r)}async hideTombstones(t){if(!this.attrs.deleted)return t;const e=new Set;for(const r of t.searchEntries){const t=this.organizationOf(r.dn);t&&void 0!==r.member&&e.add(t)}if(0===e.size)return t;const r=new Set;for(const t of e)for(const e of await this.tombstonesOf(t))r.add(dnKey(e));return 0===r.size?t:{...t,searchEntries:t.searchEntries.map(t=>void 0!==t.member&&this.organizationOf(t.dn)?{...t,member:values(t.member).filter(t=>!r.has(dnKey(t)))}:t)}}async tombstonesOf(t){const{deleted:e,deletedValue:r}=this.attrs;if(!e)return[];try{const{searchEntries:s}=await this.ldap.search({paged:!1,scope:"one",filter:`(${e}=${c(r)})`,attributes:["dn"]},this.userBaseOf(t));return s.map(t=>t.dn)}catch(t){if(32===o(t))return[];throw t}}api(t){const e=`${this.config.api_prefix}/v1/organizations/:id/groups`,r=this.organizationRoute.bind(this);t.get(e,r("listing groups",(t,e,r)=>this.listRoute(t,e,r))),t.post(e,r("creating group",(t,e,r)=>this.createRoute(t,e,r))),t.get(`${e}/:groupId`,r("getting group",async(t,e,r)=>{const s=await this.readGroup(r,t.params.groupId);if(!s)throw notFound("group");e.json(s)})),t.get(`${e}/:groupId/members`,r("listing group members",(t,e,r)=>this.membersRoute(t,e,r))),t.patch(`${e}/:groupId`,r("updating group",(t,e,r)=>this.updateRoute(t,e,r))),t.delete(`${e}/:groupId`,r("deleting group",async(t,e,r)=>{await this.onGroup(()=>this.deleteGroup(this.groupDn(r,t.params.groupId),t)),e.json({success:!0})})),t.post(`${e}/:groupId/members`,r("adding group members",(t,e,r)=>this.addMembersRoute(t,e,r))),t.delete(`${e}/:groupId/members/:userId`,r("removing group member",(t,e,r)=>this.removeMemberRoute(t,e,r)))}organizationRoute(t,e,o=this.name){return async(i,a)=>{try{const t=i.params.id;await this.checkOrganization(t),await e(i,a,t)}catch(e){if(e instanceof RouteError)return void a.status(e.status).json({error:e.message,code:e.code});if(u(e))return void a.status(403).json({error:r,code:"REFUSED"});if(e instanceof s&&e.statusCode<500)return void a.status(e.statusCode).json({error:e.message,code:400===e.statusCode?"INVALID_INPUT":"REFUSED"});this.logger.error({plugin:o,context:t,error:String(e)}),a.status(500).json({error:`Error ${t}`,code:"INTERNAL_ERROR"})}}}groupDn(t,e){return`${this.cn}=${a(e)},${this.groupBaseOf(t)}`}get userAttribute(){return this.config.ldap_user_main_attribute||"uid"}userDn(t,e){return`${this.userAttribute}=${a(e)},${this.userBaseOf(t)}`}async checkOrganization(t){if(!this.orgPattern)return;const e=this.config.twake_group_organization_status_attribute||"twakeOrgStatus";let r;try{r=(await this.ldap.search({paged:!1,scope:"base",attributes:[e]},this.orgPattern.replace(_,a(t)))).searchEntries[0]}catch(t){if(32!==o(t))throw t}if(!r)throw notFound("organization");const s=this.config.twake_group_organization_deleted_value||"deleted";if(read(r,e)===s)throw new RouteError(410,"Organization has been deleted","ORGANIZATION_DELETED")}page(t,e){const r=Math.max(1,parseInt(t.query.page)||1),s=Math.max(1,Math.min(parseInt(t.query.limit)||20,this.maxPage)),o=t.query.search||void 0;if(void 0!==o&&("string"!=typeof o||o.length<2))throw invalid("Search query must be at least 2 characters","INVALID_SEARCH_QUERY");const i=t.query.sortBy;if(void 0!==i&&!e.includes(i))throw invalid(`sortBy must be one of: ${e.join(", ")}`,"INVALID_SORT_FIELD");return{page:r,limit:s,offset:(r-1)*s,search:o,sortBy:i,desc:"desc"===t.query.sortOrder}}sorted(t,e,r){if(!e)return t;const s=r?-1:1;return t.sort((t,r)=>text(t[e]).localeCompare(text(r[e]))*s)}groupOf(t,e){const r=read(e,this.cn)||"",s=dnKey(this.userBaseOf(t)),o=values(e.member).filter(t=>!n(t,this.config.group_dummy_user)).map(t=>dnKey(parentOf(t))===s?rdnValue(t):t);return{id:r,cn:r,displayName:read(e,this.displayName)??r,description:read(e,"description"),color:read(e,this.color),organizationId:t,baseDN:this.groupDn(t,r),members:o,createdAt:read(e,this.createdAt)}}async groups(t,e,r,s){try{const{searchEntries:o}=await this.ldap.search({paged:!1,scope:"one",filter:e,...r&&{attributes:r}},this.groupBaseOf(t),s);return o}catch(t){if(32===o(t))return[];throw t}}async readGroup(t,e){try{const{searchEntries:r}=await this.ldap.search({paged:!1,scope:"base"},this.groupDn(t,e));return r[0]&&this.groupOf(t,r[0])}catch(t){if(32===o(t))return;throw t}}async named(t,e){const[r]=await this.groups(t,`(${this.displayName}=${c(e)})`);return r&&read(r,this.cn)}async onGroup(t){try{return await t()}catch(t){if(32===o(t))throw notFound("group");throw t}}async listRoute(t,e,r){const s=this.page(t,O);let o=`(${this.cn}=*)`;if(s.search){const t=c(s.search);o=`(&${o}(|(${this.displayName}=*${t}*)(description=*${t}*)(&(!(${this.displayName}=*))(${this.cn}=*${t}*))))`}const i=this.sorted((await this.groups(r,o)).map(t=>this.groupOf(r,t)),s.sortBy,s.desc);e.json({organizationId:r,groups:i.slice(s.offset,s.offset+s.limit),pagination:{page:s.page,limit:s.limit,total:i.length,totalPages:Math.ceil(i.length/s.limit)}})}async createRoute(e,r,s){const o=bodyOf(e);if(!isDisplayName(o.name))throw invalid(N);if(void 0!==o.description&&"string"!=typeof o.description)throw invalid("description must be a string");if(void 0!==o.color&&!isColor(o.color))throw invalid("color must be a hex code (e.g. #RRGGBB or #RGB)");if(await this.named(s,o.name))throw new RouteError(409,"Group already exists","GROUP_EXISTS");const i=t(),a={objectClass:this.config.group_class||["top","groupOfNames"],[this.displayName]:o.name,[this.createdAt]:(new Date).toISOString()};o.description&&(a.description=o.description),o.color&&(a[this.color]=o.color),await this.addGroup(this.groupDn(s,i),[],a,e);const n=await this.readGroup(s,i);r.status(201).json(n)}async updateRoute(t,e,r){const s=bodyOf(t),o=t.params.groupId,i=Object.keys(s).filter(t=>!["name","description","color"].includes(t));if(i.length)throw invalid(`Only "name", "description" and "color" can be updated; unsupported field(s): ${i.join(", ")}`);const{name:a,description:n,color:c}=s;if(void 0===a&&void 0===n&&void 0===c)throw invalid('at least one of "name", "description" or "color" must be provided');if(void 0!==a&&!isDisplayName(a))throw invalid(N);if(void 0!==n&&"string"!=typeof n)throw invalid("description must be a string");if(void 0!==c&&""!==c&&!isColor(c))throw invalid("color must be a hex code (e.g. #RRGGBB or #RGB) or empty to clear it");const u={};if(void 0!==a){const t=await this.named(r,a);if(t&&t!==o)throw new RouteError(409,"Group already exists","GROUP_EXISTS");u[this.displayName]=a}void 0!==n&&(u.description=n||[]),void 0!==c&&(u[this.color]=c||[]),await this.onGroup(()=>this.modifyGroup(this.groupDn(r,o),{replace:u},t)),e.json({success:!0})}async membersRoute(t,e,r){const s=this.page(t,v),o=t.params.groupId,i=await this.readGroup(r,o);if(!i)throw notFound("group");e.json({organizationId:r,id:o,...await this.memberPage(s,r,i.members)})}async memberPage(t,e,r,s=()=>({}),o){const i=[...new Map(r.map(t=>[t.toLowerCase(),t])).values()],a=await this.profiles(e,i,o);let n=i.map(t=>({...a.get(t.toLowerCase())??{uid:t},...s(t)}));if(t.search){const e=t.search.toLowerCase();n=n.filter(t=>[t.uid,t.displayName||t.cn,t.mail].some(t=>text(t).toLowerCase().includes(e)))}n=this.sorted(n,t.sortBy??"uid",t.desc);const c=Math.ceil(n.length/t.limit);return{members:n.slice(t.offset,t.offset+t.limit),pagination:{page:t.page,limit:t.limit,total:n.length,totalPages:c,hasNextPage:t.page<c,hasPreviousPage:t.page>1}}}async users(t,e,r,s){const{deleted:i,deletedValue:a}=this.attrs,n=i?`(!(${i}=${c(a)}))`:"",u=[];for(let i=0;i<e.length;i+=this.maxPage){const a=`(&(|${e.slice(i,i+this.maxPage).map(t=>`(${this.userAttribute}=${c(t)})`).join("")})${n})`;try{const{searchEntries:e}=await this.ldap.search({paged:!1,scope:"one",filter:a,attributes:r},this.userBaseOf(t),s);u.push(...e)}catch(t){if(32!==o(t))throw t}}return u}async profiles(t,e,r){const s=[...new Set([...Object.values(this.memberFields),...Object.values(I),R])],o=new Map;for(const i of await this.users(t,e,s,r)){const t=read(i,this.userAttribute);if(!t)continue;const e={};for(const[t,r]of Object.entries(this.memberFields)){const s=read(i,r);void 0!==s&&(e[t]=s)}const r={};for(const[t,e]of Object.entries(I)){const s=read(i,e);void 0!==s&&(r[t]=s)}Object.keys(r).length&&(e.name=r),e.isTechnical="TRUE"===read(i,R)?.toUpperCase(),o.set(t.toLowerCase(),e)}return o}async addMembersRoute(t,e,r){const{usernames:s}=bodyOf(t);if(!Array.isArray(s)||0===s.length)throw invalid("usernames must be a non-empty array");if(!s.every(t=>"string"==typeof t&&t.length>0))throw invalid("usernames must be an array of non-empty strings");if(s.length>this.maxPage)throw invalid(`usernames cannot exceed ${this.maxPage} entries per request`);const o=t.params.groupId,i=await this.readGroup(r,o);if(!i)throw notFound("group");const a=[...new Map(s.map(t=>[t.toLowerCase(),t])).keys()],n=new Map((await this.users(r,a,[this.userAttribute])).map(t=>{const e=read(t,this.userAttribute)||"";return[e.toLowerCase(),e]}));if(a.some(t=>!n.has(t)))throw notFound("user");const c=new Set(i.members.map(t=>t.toLowerCase())),u=a.filter(t=>!c.has(t)).map(t=>this.userDn(r,n.get(t))),p=this.groupDn(r,o);u.length&&(await this.onGroup(()=>this.ldap.modify(p,{add:{member:u}},t)),await this.dropPlaceholder(p,t)),e.json({success:!0})}async dropPlaceholder(t,e){const r=this.config.group_dummy_user;if(r)try{await this.ldap.modify(t,{delete:{member:r}},e)}catch(t){if(![16,65].includes(o(t)??0))throw t}}async removeMemberRoute(t,e,r){const s=this.groupDn(r,t.params.groupId),i=this.userDn(r,t.params.userId);try{await this.ldap.modify(s,{delete:{member:i}},t)}catch(e){const r=o(e);if(32===r)throw notFound("group");if(16===r)throw notFound("member");if(65!==r||!this.config.group_dummy_user)throw e;await this.ldap.modify(s,{replace:{member:[this.config.group_dummy_user]}},t)}e.json({success:!0})}}export{N as NAME_RULE,_ as ORG,RouteError,bodyOf,branchPattern,TwakeGroups as default,dnKey,invalid,isDisplayName,movedInto,notFound,organizationIn,parentOf,rdnValue,read,values};
@@ -1 +1 @@
1
- import{randomUUID as e}from"node:crypto";import t from"../../abstract/plugin.js";import{BadRequestError as s}from"../../lib/errors.js";import{extractLdapCode as a}from"../../lib/ldapCodes.js";import{currentOperation as r}from"../../lib/operation.js";import{escapeDnValue as i,escapeLdapFilter as n,parseDn as o}from"../../lib/utils.js";import{organizationIn as h,dnKey as c,movedInto as l,ORG as d,branchPattern as p,values as u,parentOf as g,read as m,rdnValue as f,notFound as w,invalid as b,bodyOf as y,isDisplayName as v,NAME_RULE as k,RouteError as O}from"./groups.js";import{isTombstone as $,valueOf as I,lifecycleAttributes as _}from"./lifecycleAttributes.js";const C=["viewer","editor","admin"],isRole=e=>C.includes(e),L=[...C].reverse(),M=["name"],S=["uid","displayName","mail","jobTitle","role"],j=new RegExp(`:(${C.join("|")})$`,"i"),conflict=(e,t)=>new O(409,e,t),lastAdmin=()=>conflict("A space keeps at least one admin","LAST_ADMIN"),isAdmin=e=>"member"===e.kind&&"admin"===e.role;function roleOf(e){if(!isRole(e.role))throw b(`role must be one of: ${C.join(", ")}`);return e.role}function checkLength(e,t,s){if(e.length>s)throw b(`${t} cannot exceed ${s} entries per request`)}function namesOf(e,t,s){if(!Array.isArray(e)||0===e.length||!e.every(e=>"string"==typeof e&&e.length>0))throw b(`${t} must be a non-empty array of non-empty strings`);return checkLength(e,t,s),[...new Map(e.map(e=>[e.toLowerCase(),e])).values()]}function holdersOf(e,t,s){const a="id"===t?"groups":"members";if(!Array.isArray(e))throw b(`${a} must be an array of { ${t}, role }`);checkLength(e,a,s);const r=new Set;return e.map(e=>{const{[t]:s,role:i}=e??{};if("string"!=typeof s||0===s.length||!isRole(i))throw b(`${a} must be an array of { ${t}, role }, role one of: ${C.join(", ")}`);if(r.has(s.toLowerCase()))throw b(`${s} is listed twice in ${a}`);return r.add(s.toLowerCase()),{name:s,role:i}})}class TwakeSpaces extends t{constructor(e){if(super(e),this.name="twakeSpaces",this.roles=["api"],this.dependencies={twakeGroups:"core/twake/groups",onLdapChange:"core/ldap/onChange"},this.following=Promise.resolve(),this.publishing=Promise.resolve(),this.unlinking=new Map,this.announced=new Map,this.handing=new Map,this.hooks={ldapaddrequest:([e,t,s])=>{for(const s of Object.values(this.roleAttributes))this.checkHolders(e,I(t,s));return[e,t,s]},ldapmodifyrequest:([e,t,s,a])=>{for(const s of Object.values(this.roleAttributes))for(const a of[t.add,t.replace])a&&this.checkHolders(e,I(a,s));return[e,t,s,a]},ldaprenamerequest:async([e,t,a])=>{const r=l(this.spacePattern,e,t);if(void 0===r)return[e,t,a];const i=Object.values(this.roleAttributes),n=await this.entry(e,i);if(n&&i.some(e=>void 0!==this.foreign(r,I(n,e))))throw new s(`The space holds users or groups of another organization than ${r}`);return[e,t,a]},ldapdeleterequest:async([e,t])=>{const s=r();if(void 0!==s)for(const t of[e].flat()){const e=h(this.userPattern,t);if(void 0!==e){if(this.tombstone?.keeps(t))continue;const a=await this.administered(e,t);if(!a.length)continue;this.handing.has(s)||this.handing.set(s,new Map),this.handing.get(s).set(c(t),{org:e,ids:a})}else if(this.rabbitmq){const e=await this.deletedGroup(t);e&&this.unlinking.set(c(t),{...e,op:s})}}return[e,t]},ldapdeletedone:(e,t={})=>{const s=r(),a=void 0===s?void 0:this.handing.get(s);for(const s of[e].flat()){const e=this.unlinking.get(c(s));e&&delete e.op;const r=a?.get(c(s));r&&this.queue(s,()=>this.handOver(r.org,s,r.ids,t))}},ldapdeleteend:e=>{this.handing.delete(e);for(const[t,s]of this.unlinking)if(s.op===e){this.unlinking.delete(t);for(const{id:e,context:t,fields:a}of s.owed??[])this.publishEvent(s.org,e,"group.unlinked",t,a)}},onLdapEntryChange:(e,t,s,a)=>{this.follows&&this.queue(e,()=>this.follow(e,t,s,a));const r=h(this.userPattern,e);void 0!==r&&this.lifecycle.deleted&&t&&s&&!$(t,this.lifecycle)&&$(s,this.lifecycle)&&this.queue(e,async()=>this.handOver(r,e,await this.administered(r,e),a))}},this.spaceBase=this.config.twake_space_base||"",!this.spaceBase.includes(d))throw new Error(`${this.name}: --twake-space-base must hold ${d}`);this.spacePattern=p(this.spaceBase),this.userPattern=p(this.config.twake_group_user_base),this.lifecycle=_(this.config),this.displayName=this.config.twake_space_display_name_attribute||"twakeDisplayName",this.roleAttributes={admin:this.config.twake_space_admin_attribute||"twakeSpaceAdmin",editor:this.config.twake_space_editor_attribute||"twakeSpaceEditor",viewer:this.config.twake_space_viewer_attribute||"twakeSpaceViewer"},this.userRole=this.config.twake_space_user_role_attribute||"",this.exchange=this.config.twake_space_exchange||"space",this.config.rabbitmq_url&&(this.dependencies.rabbitmq="core/rabbitmq")}get follows(){return Boolean(this.userRole||this.config.rabbitmq_url)}get groups(){return this.server.loadedPlugins.twakeGroups}get tombstone(){return this.server.loadedPlugins.twakeTombstone}async assertComposition(){if(this.config.rabbitmq_url&&!await(this.rabbitmq?.getRawClient()))throw new Error(`${this.name}: RabbitMQ at --rabbitmq-url cannot be reached`);if(!this.follows||!this.server.loadedPlugins.trash)return;const type=e=>e.split("=")[0].toLowerCase(),e=[this.config.twake_group_base||"",this.spaceBase].map(e=>{const t=o(e).reverse();return e=>o(e).reverse().slice(0,t.length).every((e,s)=>t[s].includes(d)?type(e)===type(t[s]):c(e)===c(t[s]))}),t=String(this.config.trash_watched_bases||"").split(";").map(e=>e.trim()).filter(Boolean);if(0===t.length||t.some(t=>e.some(e=>e(t))))throw new Error(`${this.name}: core/ldap/trash watches the organization groups or spaces, and the deletion of one would not be followed: no user role value or event would say it went. Leave the group and space branches out of --trash-watched-bases`)}queue(e,t){this.following=this.following.then(()=>t().catch(t=>{this.logger.error({plugin:this.name,event:"follow",dn:e,error:String(t)})}))}organizationOf(e){return h(this.spacePattern,e)}spaceBaseOf(e){return this.spaceBase.replace(d,i(e))}spaceDn(e,t){return`cn=${i(t)},${this.spaceBaseOf(e)}`}async entry(e,t){try{return(await this.server.ldap.search({paged:!1,scope:"base",attributes:t},e)).searchEntries[0]}catch(e){if(32===a(e))return;throw e}}checkHolders(e,t){const a=this.organizationOf(e);if(!a)return;const r=this.foreign(a,t);if(void 0!==r)throw new s(`${r} is neither a user nor a group of organization ${a}`)}foreign(e,t){const s=[this.groups.userBaseOf(e),this.groups.groupBaseOf(e)].map(c);return u(t).find(e=>!s.includes(c(g(e))))}api(e){const t=`${this.config.api_prefix}/v1/organizations/:id/spaces`,route=(e,t)=>this.groups.organizationRoute(e,t,this.name),done=e=>{e.json({success:!0})},spaceId=e=>e.params.spaceId;e.get(t,route("listing spaces",(e,t,s)=>this.listRoute(e,t,s))),e.post(t,route("creating space",(e,t,s)=>this.createRoute(e,t,s))),e.get(`${t}/:spaceId`,route("getting space",async(e,t,s)=>{t.json(this.spaceOf(s,await this.space(s,spaceId(e),e)))})),e.patch(`${t}/:spaceId`,route("renaming space",async(e,t,s)=>{const a=y(e),r=Object.keys(a).filter(e=>"name"!==e);if(r.length)throw b(`Only "name" can be updated; unsupported field(s): ${r.join(", ")}`);if(!v(a.name))throw b(k);await this.write(s,spaceId(e),{replace:{[this.displayName]:a.name}},e),done(t)})),e.delete(`${t}/:spaceId`,route("deleting space",async(e,t,s)=>{try{await this.server.ldap.delete(this.spaceDn(s,spaceId(e)),e)}catch(e){if(32===a(e))throw w("space");throw e}done(t)})),e.get(`${t}/:spaceId/members`,route("listing space members",async(e,t,s)=>{const a=this.groups.page(e,S),r=await this.space(s,spaceId(e),e),i=r.holders.filter(e=>"member"===e.kind),n=new Map(i.map(e=>[e.name.toLowerCase(),e.role]));t.json({organizationId:s,id:r.id,...await this.groups.memberPage(a,s,i.map(e=>e.name),e=>({role:n.get(e.toLowerCase())}),e)})})),e.post(`${t}/:spaceId/members`,route("adding space members",async(e,t,s)=>{const a=y(e),r=namesOf(a.usernames,"usernames",this.groups.maxPage);await this.add(e,s,"member",roleOf(a),()=>this.liveUsers(s,r,e)),done(t)})),e.patch(`${t}/:spaceId/members/:userId`,route("changing space member role",async(e,t,s)=>{await this.change(e,s,"member",roleOf(y(e))),done(t)})),e.delete(`${t}/:spaceId/members/:userId`,route("removing space member",async(e,t,s)=>{await this.change(e,s,"member"),done(t)})),e.get(`${t}/:spaceId/groups`,route("listing space groups",async(e,t,s)=>{const a=await this.space(s,spaceId(e),e),r=a.holders.filter(e=>"group"===e.kind),i=new Map((await this.orgGroups(s,r.map(e=>e.name),[],e)).map(e=>[c(e.dn),this.groups.groupOf(s,e).displayName])),n=r.map(({name:e,role:t,dn:s})=>({id:e,name:i.get(c(s))??e,role:t})).sort((e,t)=>e.name.localeCompare(t.name));t.json({organizationId:s,id:a.id,groups:n})})),e.post(`${t}/:spaceId/groups`,route("linking space groups",async(e,t,s)=>{const a=y(e),r=namesOf(a.groupIds,"groupIds",this.groups.maxPage);await this.add(e,s,"group",roleOf(a),()=>this.groupDns(s,r,e)),done(t)})),e.patch(`${t}/:spaceId/groups/:groupId`,route("changing space group role",async(e,t,s)=>{await this.change(e,s,"group",roleOf(y(e))),done(t)})),e.delete(`${t}/:spaceId/groups/:groupId`,route("unlinking space group",async(e,t,s)=>{await this.change(e,s,"group"),done(t)}))}spaceOf(e,t){return{id:t.id,name:t.name,organizationId:e,members:t.holders.filter(e=>"member"===e.kind).map(({name:e,role:t})=>({username:e,role:t})),groups:t.holders.filter(e=>"group"===e.kind).map(({name:e,role:t})=>({id:e,role:t}))}}entryOf(e,t,s){const a=new Map([[c(this.groups.userBaseOf(e)),"member"],[c(this.groups.groupBaseOf(e)),"group"]]),r=m(t,"cn")||"",i=new Map;for(const e of L)for(const r of u(I(t,this.roleAttributes[e]))){const t=c(r),n=a.get(c(g(r)));if(!n||s.has(t))continue;const o=i.get(t);o?o.roles.push(e):i.set(t,{kind:n,name:f(r),role:e,roles:[e],dn:r})}return{id:r,name:m(t,this.displayName)??r,holders:[...i.values()]}}async spaces(e,t,s,r,i){const[n,o]=await Promise.all([(async()=>{try{return(await this.server.ldap.search({paged:!1,scope:void 0===s?"one":"base",filter:t,attributes:["cn",this.displayName,...Object.values(this.roleAttributes)]},void 0===s?this.spaceBaseOf(e):this.spaceDn(e,s),r)).searchEntries}catch(e){if(32===a(e))return[];throw e}})(),i??this.hidden(e)]);return n.map(t=>this.entryOf(e,t,o))}async hidden(e){return new Set((await this.groups.tombstonesOf(e)).map(c))}holding(e){return`(|${e.flatMap(e=>Object.values(this.roleAttributes).map(t=>`(${t}=${n(e)})`)).join("")})`}async space(e,t,s){const[a]=await this.spaces(e,"(objectClass=*)",t,s);if(!a)throw w("space");return a}async write(e,t,s,r){try{await this.server.ldap.modify(this.spaceDn(e,t),s,r)}catch(e){if(32===a(e))throw w("space");throw e}}async again(e){for(let t=0;;t++)try{return await e()}catch(e){const s=a(e);if(16!==s&&20!==s||t>=2)throw e}}async liveUsers(e,t,s){const a=this.groups.userAttribute,r=new Map((await this.groups.users(e,t,[a],s)).map(e=>[(m(e,a)||"").toLowerCase(),e.dn]));if(t.some(e=>!r.has(e.toLowerCase())))throw w("user");return r}get groupName(){return this.config.twake_group_display_name_attribute||"twakeDisplayName"}async orgGroups(e,t,s=[],a){const r=this.groups.cn,i=[];for(let o=0;o<t.length;o+=this.groups.maxPage){const h=t.slice(o,o+this.groups.maxPage);i.push(...await this.groups.groups(e,`(|${h.map(e=>`(${r}=${n(e)})`).join("")})`,[r,this.groupName,...s],a))}return i}async follow(e,t,s,a){let r=this.organizationOf(e);if(void 0!==r){const[e,i]=[t,s].map(e=>e?this.entryOf(r,e,new Set):void 0),n=[...new Set([e,i].flatMap(e=>e?[e.id]:[]))],o=[e,i].map(e=>new Map(e?.holders.map(e=>[c(e.dn),e.role]))),h=[e,i].flatMap(e=>e?.holders??[]).filter(e=>n.length>1||o[0].get(c(e.dn))!==o[1].get(c(e.dn))),l=new Set(h.filter(e=>"group"===e.kind).map(e=>e.name)),d=await this.orgGroups(r,[...l],["member"]),p=(i??e).id,g=await this.settle(r,[...h.filter(e=>"member"===e.kind).map(e=>e.dn),...d.flatMap(e=>u(e.member))],{id:p,space:e},await this.hidden(r));return void(this.rabbitmq&&await this.announce(r,g,a,n,{was:e,is:i,linked:d}))}if(r=this.groups.organizationOf(e),void 0===r||!t)return;const i=u(t.member),n=u(s?.member),[o,h]=[i,n].map(e=>new Set(e.map(c))),l=s?void 0:this.unlinking.get(c(e));l&&this.unlinking.delete(c(e));const d=await this.hidden(r),p=await this.settle(r,[...i.filter(e=>!h.has(c(e))),...n.filter(e=>!o.has(c(e)))],{group:e,members:o,spaces:l?.spaces},d);if(!this.rabbitmq||!l&&!p.length)return;const g=(await this.spaces(r,this.holding([e]),void 0,void 0,d)).map(e=>e.id);l&&this.unlinked(e,l,g,a),await this.announce(r,p,a,[...l?.spaces.map(e=>e.id)??[],...g])}async administered(e,t){try{const s=`(${this.roleAttributes.admin}=${n(t)})`;return(await this.spaces(e,s)).map(e=>e.id)}catch(e){return this.logger.error({plugin:this.name,event:"handOver",dn:t,error:String(e)}),[]}}async handOver(e,t,s,a){for(const r of s)try{await this.again(()=>this.handOverOnce(e,t,r,a))}catch(e){this.logger.error({plugin:this.name,event:"handOver",space:r,error:String(e)})}}async handOverOnce(e,t,s,a){const r=c(t),[i]=await this.spaces(e,"(objectClass=*)",s),n=i?.holders.filter(e=>"member"===e.kind&&c(e.dn)!==r)??[];if(!i||n.some(e=>"admin"===e.role))return;if(!n.length)return void await this.server.ldap.delete(this.spaceDn(e,s),void 0,{context:a});const o=n.some(e=>"editor"===e.role)?"editor":"viewer",h=n.filter(e=>e.role===o).map(e=>e.dn);await this.server.ldap.modify(this.spaceDn(e,s),{delete:{[this.roleAttributes[o]]:h},add:{[this.roleAttributes.admin]:h}},void 0,{context:a})}async deletedGroup(e){const t=this.groups.organizationOf(e);if(void 0!==t)try{const s=(await this.spaces(t,this.holding([e]))).flatMap(t=>{const s=t.holders.find(t=>c(t.dn)===c(e));return s?[{id:t.id,role:s.role}]:[]});if(!s.length)return;const[a]=await this.orgGroups(t,[f(e)]),r=f(e);return{org:t,id:r,name:a&&m(a,this.groupName)||r,spaces:s}}catch(t){return void this.logger.error({plugin:this.name,event:"deleting",dn:e,error:String(t)})}}unlinked(e,t,s,a){const{org:r,id:i,name:n,spaces:o}=t,h=new Set(s.map(e=>e.toLowerCase()));for(const{id:t,role:s}of o){const o=c(this.spaceDn(r,t));h.has(t.toLowerCase())&&(this.announced.has(o)||this.announced.set(o,new Set),this.announced.get(o).add(c(e))),this.publishEvent(r,t,"group.unlinked",a,{groups:[{id:i,name:n,role:s}]})}}async settle(e,t,s,a){const r=c(this.groups.userBaseOf(e)),i=new Map(t.filter(e=>c(g(e))===r).map(e=>[c(e),e])),n=[];for(const[t,r]of i)if(!a.has(t))try{const t=await this.rolesOf(e,r,a),i=this.userRole?await this.writeRoles(r,t):await this.rolesOf(e,r,a,s);i&&n.push({dn:r,before:i,after:t})}catch(e){this.logger.error({plugin:this.name,event:"settle",dn:r,error:String(e)})}return n}async userIn(e,t,s,a){const r=(await this.groups.groups(e,`(member=${n(t)})`,[this.groups.cn],s)).map(e=>c(e.dn)),i=new Set([c(t),...r]),o=[t,...r];if(a&&"group"in a){const e=c(a.group);i.delete(e),a.members.has(c(t))&&i.add(e),o.push(a.group)}return{filter:this.holding(o),roleIn:e=>e.holders.find(e=>i.has(c(e.dn)))?.role}}async announce(e,t,s,a,r){const i=new Set;if(r){const{was:a,is:n,linked:o}=r,h=(n??a).id,l=new Map(o.map(e=>[c(e.dn),m(e,this.groupName)||f(e.dn)])),groupOf=e=>({id:e.name,name:l.get(c(e.dn))??e.name,role:e.role}),publish=(t,a)=>this.publishEvent(e,h,t,s,a),d=c(this.spaceDn(e,h)),p=new Set;for(const t of[a,n]){const s=t&&c(this.spaceDn(e,t.id));for(const e of s&&this.announced.get(s)||[])p.add(e);s&&this.announced.delete(s)}if(a&&n){a.name!==n.name&&publish("updated",{name:n.name});const e=new Map(a.holders.filter(e=>"group"===e.kind).map(e=>[c(e.dn),e]));for(const t of n.holders.filter(e=>"group"===e.kind)){const s=e.get(c(t.dn));e.delete(c(t.dn)),s?.role!==t.role&&publish(s?"group.role.changed":"group.linked",{groups:[groupOf(t)]})}const byDeletion=e=>{if(p.delete(c(e.dn)))return!0;const t=this.unlinking.get(c(e.dn));return!!t?.spaces.some(e=>e.id.toLowerCase()===h.toLowerCase())&&(void 0!==t.op&&(t.owed??(t.owed=[])).push({id:h,context:s,fields:{groups:[groupOf(e)]}}),!0)};for(const t of e.values())byDeletion(t)||publish("group.unlinked",{groups:[groupOf(t)]});const[t,r]=[a,n].map(e=>new Set(e.holders.map(e=>c(e.dn)))),i=[...p].filter(e=>t.has(e)&&r.has(e));i.length&&this.announced.set(d,new Set(i))}else if(i.add(h.toLowerCase()),n){const s=t.flatMap(e=>{const t=e.after.get(h);return t?[{dn:e.dn,role:t}]:[]}),a=await this.profiles(e,s.map(e=>e.dn));publish("created",{name:n.name,members:s.flatMap(({dn:e,role:t})=>{const s=a.get(c(e));return s?[{...s,role:t}]:[]}),groups:n.holders.filter(e=>"group"===e.kind).map(groupOf)})}else publish("deleted")}const n=new Set(a.map(e=>e.toLowerCase())),o=t.flatMap(({dn:e,before:t,after:s})=>[...new Set([...t.keys(),...s.keys()])].flatMap(a=>{const[r,o]=[t.get(a),s.get(a)];if(r===o||i.has(a.toLowerCase()))return[];const h=!n.has(a.toLowerCase());if(h&&!this.userRole)return[];return[{dn:e,id:a,event:r?o?"member.role.changed":"member.removed":"member.added",role:o??r,caught:h}]}));if(!o.length)return;const h=await this.profiles(e,o.map(e=>e.dn));for(const{dn:t,id:a,event:r,role:i,caught:n}of o){const o=h.get(c(t));o&&this.publishEvent(e,a,r,n?{}:s,{members:[{...o,role:i}]})}}async profiles(e,t){if(!t.length)return new Map;const s=this.groups.userAttribute,a=this.config.mail_attribute||"mail",r=new Map((await this.groups.users(e,[...new Set(t.map(f))],[s,"entryUUID",a,"givenName","sn"])).map(e=>[(m(e,s)||"").toLowerCase(),e]));return new Map(t.flatMap(e=>{const t=r.get(f(e).toLowerCase());if(!t)return[];const i={uuid:m(t,"entryUUID"),username:m(t,s),email:m(t,a),firstName:m(t,"givenName"),lastName:m(t,"sn")};return[[c(e),i]]}))}publishEvent(e,t,s,a,r={}){const i=this.rabbitmq;if(!i)return;const n={organizationId:e,id:t,...r,actor:a.actor,timestamp:(new Date).toISOString()};this.publishing=this.publishing.then(()=>this.publish(i,s,n))}get rabbitmq(){return this.config.rabbitmq_url?this.requirePlugin("rabbitmq"):null}async publish(t,s,a){const r={plugin:this.name,exchange:this.exchange,routingKey:`twake.space.${s}`,messageId:e()};try{if(!await t.getRawClient())return void this.logger.error({...r,result:"no broker"});await t.publish(r.exchange,r.routingKey,a,{messageId:r.messageId}),this.logger.info({...r,result:"published"})}catch(e){this.logger.error({...r,result:"error",error:String(e)})}}async rolesOf(e,t,s,a){const{filter:r,roleIn:i}=await this.userIn(e,t,void 0,a);let n=await this.spaces(e,r,void 0,void 0,s);if(a&&"id"in a){const e=a.id.toLowerCase();n=n.filter(t=>t.id.toLowerCase()!==e),a.space&&n.push(a.space)}const o=new Map(n.flatMap(e=>{const t=i(e);return t?[[e.id,t]]:[]}));if(a&&"group"in a&&a.members.has(c(t)))for(const{id:e,role:t}of a.spaces??[]){const s=o.get(e);(!s||C.indexOf(t)>C.indexOf(s))&&o.set(e,t)}return o}async writeRoles(e,t){const s=this.userRole,r=[...t].map(([e,t])=>`${e}:${t}`),i=new Set(r.map(e=>e.toLowerCase()));for(let t=0;;t++)try{const[t]=(await this.server.ldap.search({paged:!1,scope:"base",attributes:[s]},e)).searchEntries;if(!t)return;const a=u(I(t,s)),n=new Set(a.map(e=>e.toLowerCase())),o=a.filter(e=>j.test(e)&&!i.has(e.toLowerCase())),h=r.filter(e=>!n.has(e.toLowerCase()));return(o.length||h.length)&&await this.server.ldap.modify(e,{...o.length&&{delete:{[s]:o}},...h.length&&{add:{[s]:h}}}),new Map(a.flatMap(e=>{const t=j.exec(e)?.[1].toLowerCase();return t?[[e.slice(0,e.lastIndexOf(":")),t]]:[]}))}catch(e){const s=a(e);if(32===s)return;if(16!==s&&20!==s||t>=2)throw e}}async groupDns(e,t,s){const a=new Map((await this.orgGroups(e,t,[],s)).map(e=>[(m(e,this.groups.cn)||"").toLowerCase(),e.dn]));if(t.some(e=>!a.has(e.toLowerCase())))throw w("group");return a}async add(e,t,s,a,r){await this.again(()=>this.addOnce(e,t,s,a,r))}async addOnce(e,t,s,a,r){const i=await this.space(t,e.params.spaceId,e),n=await r(),o=new Map(i.holders.filter(e=>e.kind===s).map(e=>[e.name.toLowerCase(),e.role])),h=[];for(const[e,t]of n){const r=o.get(e);if(void 0===r)h.push(t);else if(r!==a)throw"member"===s?conflict(`${e} is already a member, as ${r}`,"MEMBER_EXISTS"):conflict(`${e} is already linked, as ${r}`,"GROUP_ALREADY_LINKED")}h.length&&await this.write(t,i.id,{add:{[this.roleAttributes[a]]:h}},e)}async change(e,t,s,a){await this.again(()=>this.changeOnce(e,t,s,a))}async changeOnce(e,t,s,a){const r=await this.space(t,e.params.spaceId,e),i="member"===s?e.params.userId:e.params.groupId,n=r.holders.find(e=>e.kind===s&&e.name.toLowerCase()===i.toLowerCase());if(!n)throw w(s);if(1===n.roles.length&&n.role===a)return;const o="member"===s&&"admin"===n.role&&"admin"!==a;if(o&&!r.holders.some(e=>e!==n&&isAdmin(e)))throw lastAdmin();const h=Object.fromEntries(n.roles.filter(e=>e!==a).map(e=>[this.roleAttributes[e],n.dn])),c=a&&!n.roles.includes(a)?{[this.roleAttributes[a]]:n.dn}:void 0;if(await this.write(t,r.id,{delete:h,add:c},e),o&&!(await this.space(t,r.id,e)).holders.some(isAdmin))throw await this.write(t,r.id,{add:h,delete:c},e),lastAdmin()}async listRoute(e,t,s){const a=this.groups.page(e,M);let r="(cn=*)";a.search&&(r=`(&${r}(${this.displayName}=*${n(a.search)}*))`);const i=e.query.user;let o;if(void 0!==i){if("string"!=typeof i||!i)throw b("user must be a username");const[t]=(await this.liveUsers(s,[i],e)).values(),a=await this.userIn(s,t,e);r=`(&${r}${a.filter})`,o=a.roleIn}const h=this.groups.sorted((await this.spaces(s,r,void 0,e)).map(e=>({...this.spaceOf(s,e),...o&&{role:o(e)}})),a.sortBy,a.desc);t.json({organizationId:s,spaces:h.slice(a.offset,a.offset+a.limit),pagination:{page:a.page,limit:a.limit,total:h.length,totalPages:Math.ceil(h.length/a.limit)}})}async createRoute(t,s,a){const r=y(t);if(!v(r.name))throw b(k);const i=this.groups.maxPage,n=holdersOf(r.members,"username",i),o=holdersOf(r.groups??[],"id",i);if(!n.some(e=>"admin"===e.role))throw b("A space needs an admin among its members","ADMIN_REQUIRED");const[h,c]=await Promise.all([this.liveUsers(a,n.map(e=>e.name),t),this.groupDns(a,o.map(e=>e.name),t)]),l=e(),d={objectClass:this.config.twake_space_class||["top","twakeSpace"],cn:l,[this.displayName]:r.name};for(const[e,t]of[[n,h],[o,c]])for(const{name:s,role:a}of e){const e=this.roleAttributes[a];d[e]=[...u(d[e]),t.get(s.toLowerCase())]}await this.server.ldap.add(this.spaceDn(a,l),d,t),s.status(201).json(this.spaceOf(a,await this.space(a,l,t)))}}export{C as SPACE_ROLES,TwakeSpaces as default};
1
+ import{randomUUID as e}from"node:crypto";import t from"../../abstract/plugin.js";import{BadRequestError as s}from"../../lib/errors.js";import{extractLdapCode as a}from"../../lib/ldapCodes.js";import{currentOperation as i}from"../../lib/operation.js";import{parseDn as r,unescapeDnValue as n,escapeDnValue as o,escapeLdapFilter as h}from"../../lib/utils.js";import{organizationIn as c,dnKey as d,movedInto as l,ORG as p,branchPattern as u,values as g,parentOf as m,read as f,rdnValue as w,notFound as b,RouteError as y,invalid as v,bodyOf as k,isDisplayName as O,NAME_RULE as $}from"./groups.js";import{isTombstone as _,valueOf as I,lifecycleAttributes as S}from"./lifecycleAttributes.js";const C=["viewer","editor","admin"],isRole=e=>C.includes(e),M=[...C].reverse(),L="twake.space.sync.requested",q=["name"],j=["uid","displayName","mail","jobTitle","role"],A=new RegExp(`:(${C.join("|")})$`,"i"),conflict=(e,t)=>new y(409,e,t),lastAdmin=()=>conflict("A space keeps at least one admin","LAST_ADMIN"),isAdmin=e=>"member"===e.kind&&"admin"===e.role;function roleOf(e){if(!isRole(e.role))throw v(`role must be one of: ${C.join(", ")}`);return e.role}function checkLength(e,t,s){if(e.length>s)throw v(`${t} cannot exceed ${s} entries per request`)}function namesOf(e,t,s){if(!Array.isArray(e)||0===e.length||!e.every(e=>"string"==typeof e&&e.length>0))throw v(`${t} must be a non-empty array of non-empty strings`);return checkLength(e,t,s),[...new Map(e.map(e=>[e.toLowerCase(),e])).values()]}function holdersOf(e,t,s){const a="id"===t?"groups":"members";if(!Array.isArray(e))throw v(`${a} must be an array of { ${t}, role }`);checkLength(e,a,s);const i=new Set;return e.map(e=>{const{[t]:s,role:r}=e??{};if("string"!=typeof s||0===s.length||!isRole(r))throw v(`${a} must be an array of { ${t}, role }, role one of: ${C.join(", ")}`);if(i.has(s.toLowerCase()))throw v(`${s} is listed twice in ${a}`);return i.add(s.toLowerCase()),{name:s,role:r}})}class TwakeSpaces extends t{constructor(e){if(super(e),this.name="twakeSpaces",this.roles=["api"],this.dependencies={twakeGroups:"core/twake/groups",onLdapChange:"core/ldap/onChange"},this.domains=new Map,this.following=Promise.resolve(),this.publishing=Promise.resolve(),this.unlinking=new Map,this.announced=new Map,this.handing=new Map,this.hooks={ldapaddrequest:([e,t,s])=>{for(const s of Object.values(this.roleAttributes))this.checkHolders(e,I(t,s));return[e,t,s]},ldapmodifyrequest:([e,t,s,a])=>{for(const s of Object.values(this.roleAttributes))for(const a of[t.add,t.replace])a&&this.checkHolders(e,I(a,s));return[e,t,s,a]},ldaprenamerequest:async([e,t,a])=>{const i=l(this.spacePattern,e,t);if(void 0===i)return[e,t,a];const r=Object.values(this.roleAttributes),n=await this.entry(e,r);if(n&&r.some(e=>void 0!==this.foreign(i,I(n,e))))throw new s(`The space holds users or groups of another organization than ${i}`);return[e,t,a]},ldapdeleterequest:async([e,t])=>{const s=i();if(void 0!==s)for(const t of[e].flat()){const e=c(this.userPattern,t);if(void 0!==e){if(this.tombstone?.keeps(t))continue;const a=await this.administered(e,t);if(!a.length)continue;this.handing.has(s)||this.handing.set(s,new Map),this.handing.get(s).set(d(t),{org:e,ids:a})}else if(this.rabbitmq){const e=await this.deletedGroup(t);e&&this.unlinking.set(d(t),{...e,op:s})}}return[e,t]},ldapdeletedone:(e,t={})=>{const s=i(),a=void 0===s?void 0:this.handing.get(s);for(const s of[e].flat()){const e=this.unlinking.get(d(s));e&&delete e.op;const i=a?.get(d(s));i&&this.queue(s,()=>this.handOver(i.org,s,i.ids,t))}},ldapdeleteend:e=>{this.handing.delete(e);for(const[t,s]of this.unlinking)if(s.op===e){this.unlinking.delete(t);for(const{id:e,context:t,fields:a}of s.owed??[])this.publishEvent(s.org,e,"group.unlinked",t,a)}},onLdapEntryChange:(e,t,s,a)=>{const i=this.orgPattern?.exec(r(e).join(","))?.groups;i&&this.rabbitmq&&(this.publishing=this.publishing.then(()=>{this.domains.delete(n(i.org).toLowerCase())})),this.follows&&this.queue(e,()=>this.follow(e,t,s,a));const o=c(this.userPattern,e);void 0!==o&&this.lifecycle.deleted&&t&&s&&!_(t,this.lifecycle)&&_(s,this.lifecycle)&&this.queue(e,async()=>this.handOver(o,e,await this.administered(o,e),a))}},this.spaceBase=this.config.twake_space_base||"",!this.spaceBase.includes(p))throw new Error(`${this.name}: --twake-space-base must hold ${p}`);this.spacePattern=u(this.spaceBase),this.userPattern=u(this.config.twake_group_user_base),this.lifecycle=S(this.config),this.displayName=this.config.twake_space_display_name_attribute||"twakeDisplayName",this.roleAttributes={admin:this.config.twake_space_admin_attribute||"twakeSpaceAdmin",editor:this.config.twake_space_editor_attribute||"twakeSpaceEditor",viewer:this.config.twake_space_viewer_attribute||"twakeSpaceViewer"},this.userRole=this.config.twake_space_user_role_attribute||"",this.exchange=this.config.twake_space_exchange||"space",this.domainAttribute=this.config.twake_instance_organization_domain_attribute||"twakeDomain",this.config.twake_group_organization_dn&&(this.orgPattern=u(this.config.twake_group_organization_dn)),this.config.rabbitmq_url&&(this.dependencies.rabbitmq="core/rabbitmq")}get follows(){return Boolean(this.userRole||this.config.rabbitmq_url)}get groups(){return this.server.loadedPlugins.twakeGroups}get tombstone(){return this.server.loadedPlugins.twakeTombstone}async assertComposition(){if(this.config.rabbitmq_url&&!await(this.rabbitmq?.getRawClient()))throw new Error(`${this.name}: RabbitMQ at --rabbitmq-url cannot be reached`);if(this.config.rabbitmq_url&&this.orgPattern&&!(await this.server.ldap.schemaIndex()).getAttributeType(this.domainAttribute))throw new Error(`${this.name}: the directory schema defines no ${this.domainAttribute}; load a schema that does, or set --twake-instance-organization-domain-attribute`);if(!this.follows||!this.server.loadedPlugins.trash)return;const type=e=>e.split("=")[0].toLowerCase(),e=[this.config.twake_group_base||"",this.spaceBase].map(e=>{const t=r(e).reverse();return e=>r(e).reverse().slice(0,t.length).every((e,s)=>t[s].includes(p)?type(e)===type(t[s]):d(e)===d(t[s]))}),t=String(this.config.trash_watched_bases||"").split(";").map(e=>e.trim()).filter(Boolean);if(0===t.length||t.some(t=>e.some(e=>e(t))))throw new Error(`${this.name}: core/ldap/trash watches the organization groups or spaces, and the deletion of one would not be followed: no user role value or event would say it went. Leave the group and space branches out of --trash-watched-bases`)}queue(e,t){this.following=this.following.then(()=>t().catch(t=>{this.logger.error({plugin:this.name,event:"follow",dn:e,error:String(t)})}))}organizationOf(e){return c(this.spacePattern,e)}spaceBaseOf(e){return this.spaceBase.replace(p,o(e))}spaceDn(e,t){return`cn=${o(t)},${this.spaceBaseOf(e)}`}async entry(e,t){try{return(await this.server.ldap.search({paged:!1,scope:"base",attributes:t},e)).searchEntries[0]}catch(e){if(32===a(e))return;throw e}}checkHolders(e,t){const a=this.organizationOf(e);if(!a)return;const i=this.foreign(a,t);if(void 0!==i)throw new s(`${i} is neither a user nor a group of organization ${a}`)}foreign(e,t){const s=[this.groups.userBaseOf(e),this.groups.groupBaseOf(e)].map(d);return g(t).find(e=>!s.includes(d(m(e))))}async api(e){const t=`${this.config.api_prefix}/v1/organizations/:id/spaces`,route=(e,t)=>this.groups.organizationRoute(e,t,this.name),done=e=>{e.json({success:!0})},spaceId=e=>e.params.spaceId;e.get(t,route("listing spaces",(e,t,s)=>this.listRoute(e,t,s))),e.post(t,route("creating space",(e,t,s)=>this.createRoute(e,t,s))),e.get(`${t}/:spaceId`,route("getting space",async(e,t,s)=>{t.json(this.spaceOf(s,await this.space(s,spaceId(e),e)))})),e.patch(`${t}/:spaceId`,route("renaming space",async(e,t,s)=>{const a=k(e),i=Object.keys(a).filter(e=>"name"!==e);if(i.length)throw v(`Only "name" can be updated; unsupported field(s): ${i.join(", ")}`);if(!O(a.name))throw v($);await this.write(s,spaceId(e),{replace:{[this.displayName]:a.name}},e),done(t)})),e.delete(`${t}/:spaceId`,route("deleting space",async(e,t,s)=>{try{await this.server.ldap.delete(this.spaceDn(s,spaceId(e)),e)}catch(e){if(32===a(e))throw b("space");throw e}done(t)})),e.get(`${t}/:spaceId/members`,route("listing space members",async(e,t,s)=>{const a=this.groups.page(e,j),i=await this.space(s,spaceId(e),e),r=i.holders.filter(e=>"member"===e.kind),n=new Map(r.map(e=>[e.name.toLowerCase(),e.role]));t.json({organizationId:s,id:i.id,...await this.groups.memberPage(a,s,r.map(e=>e.name),e=>({role:n.get(e.toLowerCase())}),e)})})),e.post(`${t}/:spaceId/members`,route("adding space members",async(e,t,s)=>{const a=k(e),i=namesOf(a.usernames,"usernames",this.groups.maxPage);await this.add(e,s,"member",roleOf(a),()=>this.liveUsers(s,i,e)),done(t)})),e.patch(`${t}/:spaceId/members/:userId`,route("changing space member role",async(e,t,s)=>{await this.change(e,s,"member",roleOf(k(e))),done(t)})),e.delete(`${t}/:spaceId/members/:userId`,route("removing space member",async(e,t,s)=>{await this.change(e,s,"member"),done(t)})),e.get(`${t}/:spaceId/groups`,route("listing space groups",async(e,t,s)=>{const a=await this.space(s,spaceId(e),e),i=a.holders.filter(e=>"group"===e.kind),r=new Map((await this.orgGroups(s,i.map(e=>e.name),[],e)).map(e=>[d(e.dn),this.groups.groupOf(s,e).displayName])),n=i.map(({name:e,role:t,dn:s})=>({id:e,name:r.get(d(s))??e,role:t})).sort((e,t)=>e.name.localeCompare(t.name));t.json({organizationId:s,id:a.id,groups:n})})),e.post(`${t}/:spaceId/groups`,route("linking space groups",async(e,t,s)=>{const a=k(e),i=namesOf(a.groupIds,"groupIds",this.groups.maxPage);await this.add(e,s,"group",roleOf(a),()=>this.groupDns(s,i,e)),done(t)})),e.patch(`${t}/:spaceId/groups/:groupId`,route("changing space group role",async(e,t,s)=>{await this.change(e,s,"group",roleOf(k(e))),done(t)})),e.delete(`${t}/:spaceId/groups/:groupId`,route("unlinking space group",async(e,t,s)=>{await this.change(e,s,"group"),done(t)})),await(this.rabbitmq?.subscribe(this.exchange,L,this.config.twake_space_sync_queue||`${L}.ldap-rest`,e=>this.onSyncRequested(e)))}spaceOf(e,t){return{id:t.id,name:t.name,organizationId:e,members:t.holders.filter(e=>"member"===e.kind).map(({name:e,role:t})=>({username:e,role:t})),groups:t.holders.filter(e=>"group"===e.kind).map(({name:e,role:t})=>({id:e,role:t}))}}entryOf(e,t,s){const a=new Map([[d(this.groups.userBaseOf(e)),"member"],[d(this.groups.groupBaseOf(e)),"group"]]),i=f(t,"cn")||"",r=new Map;for(const e of M)for(const i of g(I(t,this.roleAttributes[e]))){const t=d(i),n=a.get(d(m(i)));if(!n||s.has(t))continue;const o=r.get(t);o?o.roles.push(e):r.set(t,{kind:n,name:w(i),role:e,roles:[e],dn:i})}return{id:i,name:f(t,this.displayName)??i,holders:[...r.values()]}}async spaces(e,t,s,i,r){const[n,o]=await Promise.all([(async()=>{try{return(await this.server.ldap.search({paged:!1,scope:void 0===s?"one":"base",filter:t,attributes:["cn",this.displayName,...Object.values(this.roleAttributes)]},void 0===s?this.spaceBaseOf(e):this.spaceDn(e,s),i)).searchEntries}catch(e){if(32===a(e))return[];throw e}})(),r??this.hidden(e)]);return n.map(t=>this.entryOf(e,t,o))}async hidden(e){return new Set((await this.groups.tombstonesOf(e)).map(d))}holding(e){return`(|${e.flatMap(e=>Object.values(this.roleAttributes).map(t=>`(${t}=${h(e)})`)).join("")})`}async space(e,t,s){const[a]=await this.spaces(e,"(objectClass=*)",t,s);if(!a)throw b("space");return a}async write(e,t,s,i){try{await this.server.ldap.modify(this.spaceDn(e,t),s,i)}catch(e){if(32===a(e))throw b("space");throw e}}async again(e){for(let t=0;;t++)try{return await e()}catch(e){const s=a(e);if(16!==s&&20!==s||t>=2)throw e}}async liveUsers(e,t,s){const a=this.groups.userAttribute,i=new Map((await this.groups.users(e,t,[a],s)).map(e=>[(f(e,a)||"").toLowerCase(),e.dn]));if(t.some(e=>!i.has(e.toLowerCase())))throw b("user");return i}get groupName(){return this.config.twake_group_display_name_attribute||"twakeDisplayName"}async orgGroups(e,t,s=[],a){const i=this.groups.cn,r=[];for(let n=0;n<t.length;n+=this.groups.maxPage){const o=t.slice(n,n+this.groups.maxPage);r.push(...await this.groups.groups(e,`(|${o.map(e=>`(${i}=${h(e)})`).join("")})`,[i,this.groupName,...s],a))}return r}async follow(e,t,s,a){let i=this.organizationOf(e);if(void 0!==i){const[e,r]=[t,s].map(e=>e?this.entryOf(i,e,new Set):void 0),n=[...new Set([e,r].flatMap(e=>e?[e.id]:[]))],o=[e,r].map(e=>new Map(e?.holders.map(e=>[d(e.dn),e.role]))),h=[e,r].flatMap(e=>e?.holders??[]).filter(e=>n.length>1||o[0].get(d(e.dn))!==o[1].get(d(e.dn))),c=new Set(h.filter(e=>"group"===e.kind).map(e=>e.name)),l=await this.orgGroups(i,[...c],["member"]),p=(r??e).id,u=await this.settle(i,[...h.filter(e=>"member"===e.kind).map(e=>e.dn),...l.flatMap(e=>g(e.member))],{id:p,space:e},await this.hidden(i));return void(this.rabbitmq&&await this.announce(i,u,a,n,{was:e,is:r,linked:l}))}if(i=this.groups.organizationOf(e),void 0===i||!t)return;const r=g(t.member),n=g(s?.member),[o,h]=[r,n].map(e=>new Set(e.map(d))),c=s?void 0:this.unlinking.get(d(e));c&&this.unlinking.delete(d(e));const l=await this.hidden(i),p=await this.settle(i,[...r.filter(e=>!h.has(d(e))),...n.filter(e=>!o.has(d(e)))],{group:e,members:o,spaces:c?.spaces},l);if(!this.rabbitmq||!c&&!p.length)return;const u=(await this.spaces(i,this.holding([e]),void 0,void 0,l)).map(e=>e.id);c&&this.unlinked(e,c,u,a),await this.announce(i,p,a,[...c?.spaces.map(e=>e.id)??[],...u])}async administered(e,t){try{const s=`(${this.roleAttributes.admin}=${h(t)})`;return(await this.spaces(e,s)).map(e=>e.id)}catch(e){return this.logger.error({plugin:this.name,event:"handOver",dn:t,error:String(e)}),[]}}async handOver(e,t,s,a){for(const i of s)try{await this.again(()=>this.handOverOnce(e,t,i,a))}catch(e){this.logger.error({plugin:this.name,event:"handOver",space:i,error:String(e)})}}async handOverOnce(e,t,s,a){const i=d(t),[r]=await this.spaces(e,"(objectClass=*)",s),n=r?.holders.filter(e=>"member"===e.kind&&d(e.dn)!==i)??[];if(!r||n.some(e=>"admin"===e.role))return;if(!n.length)return void await this.server.ldap.delete(this.spaceDn(e,s),void 0,{context:a});const o=n.some(e=>"editor"===e.role)?"editor":"viewer",h=n.filter(e=>e.role===o).map(e=>e.dn);await this.server.ldap.modify(this.spaceDn(e,s),{delete:{[this.roleAttributes[o]]:h},add:{[this.roleAttributes.admin]:h}},void 0,{context:a})}async deletedGroup(e){const t=this.groups.organizationOf(e);if(void 0!==t)try{const s=(await this.spaces(t,this.holding([e]))).flatMap(t=>{const s=t.holders.find(t=>d(t.dn)===d(e));return s?[{id:t.id,role:s.role}]:[]});if(!s.length)return;const[a]=await this.orgGroups(t,[w(e)]),i=w(e);return{org:t,id:i,name:a&&f(a,this.groupName)||i,spaces:s}}catch(t){return void this.logger.error({plugin:this.name,event:"deleting",dn:e,error:String(t)})}}unlinked(e,t,s,a){const{org:i,id:r,name:n,spaces:o}=t,h=new Set(s.map(e=>e.toLowerCase()));for(const{id:t,role:s}of o){const o=d(this.spaceDn(i,t));h.has(t.toLowerCase())&&(this.announced.has(o)||this.announced.set(o,new Set),this.announced.get(o).add(d(e))),this.publishEvent(i,t,"group.unlinked",a,{groups:[{id:r,name:n,role:s}]})}}async settle(e,t,s,a){const i=d(this.groups.userBaseOf(e)),r=new Map(t.filter(e=>d(m(e))===i).map(e=>[d(e),e])),n=[];for(const[t,i]of r)if(!a.has(t))try{const t=await this.rolesOf(e,i,a),r=this.userRole?await this.writeRoles(i,t):await this.rolesOf(e,i,a,s);r&&n.push({dn:i,before:r,after:t})}catch(e){this.logger.error({plugin:this.name,event:"settle",dn:i,error:String(e)})}return n}async userIn(e,t,s,a){const i=(await this.groups.groups(e,`(member=${h(t)})`,[this.groups.cn],s)).map(e=>d(e.dn)),r=new Set([d(t),...i]),n=[t,...i];if(a&&"group"in a){const e=d(a.group);r.delete(e),a.members.has(d(t))&&r.add(e),n.push(a.group)}return{filter:this.holding(n),roleIn:e=>e.holders.find(e=>r.has(d(e.dn)))?.role}}async announce(e,t,s,a,i){const r=new Set;if(i){const{was:a,is:n,linked:o}=i,h=(n??a).id,c=new Map(o.map(e=>[d(e.dn),f(e,this.groupName)||w(e.dn)])),groupOf=e=>({id:e.name,name:c.get(d(e.dn))??e.name,role:e.role}),publish=(t,a)=>this.publishEvent(e,h,t,s,a),l=d(this.spaceDn(e,h)),p=new Set;for(const t of[a,n]){const s=t&&d(this.spaceDn(e,t.id));for(const e of s&&this.announced.get(s)||[])p.add(e);s&&this.announced.delete(s)}if(a&&n){a.name!==n.name&&publish("updated",{name:n.name});const e=new Map(a.holders.filter(e=>"group"===e.kind).map(e=>[d(e.dn),e]));for(const t of n.holders.filter(e=>"group"===e.kind)){const s=e.get(d(t.dn));e.delete(d(t.dn)),s?.role!==t.role&&publish(s?"group.role.changed":"group.linked",{groups:[groupOf(t)]})}const byDeletion=e=>{if(p.delete(d(e.dn)))return!0;const t=this.unlinking.get(d(e.dn));return!!t?.spaces.some(e=>e.id.toLowerCase()===h.toLowerCase())&&(void 0!==t.op&&(t.owed??(t.owed=[])).push({id:h,context:s,fields:{groups:[groupOf(e)]}}),!0)};for(const t of e.values())byDeletion(t)||publish("group.unlinked",{groups:[groupOf(t)]});const[t,i]=[a,n].map(e=>new Set(e.holders.map(e=>d(e.dn)))),r=[...p].filter(e=>t.has(e)&&i.has(e));r.length&&this.announced.set(l,new Set(r))}else if(r.add(h.toLowerCase()),n){const s=t.flatMap(e=>{const t=e.after.get(h);return t?[{dn:e.dn,role:t}]:[]}),a=await this.profiles(e,s.map(e=>e.dn));publish("created",{name:n.name,members:s.flatMap(({dn:e,role:t})=>{const s=a.get(d(e));return s?[{...s,role:t}]:[]}),groups:n.holders.filter(e=>"group"===e.kind).map(groupOf)})}else publish("deleted")}const n=new Set(a.map(e=>e.toLowerCase())),o=t.flatMap(({dn:e,before:t,after:s})=>[...new Set([...t.keys(),...s.keys()])].flatMap(a=>{const[i,o]=[t.get(a),s.get(a)];if(i===o||r.has(a.toLowerCase()))return[];const h=!n.has(a.toLowerCase());if(h&&!this.userRole)return[];return[{dn:e,id:a,event:i?o?"member.role.changed":"member.removed":"member.added",role:o??i,caught:h}]}));if(!o.length)return;const h=await this.profiles(e,o.map(e=>e.dn));for(const{dn:t,id:a,event:i,role:r,caught:n}of o){const o=h.get(d(t));o&&this.publishEvent(e,a,i,n?{}:s,{members:[{...o,role:r}]})}}async profiles(e,t){if(!t.length)return new Map;const s=this.groups.userAttribute,a=this.config.mail_attribute||"mail",i=new Map((await this.groups.users(e,[...new Set(t.map(w))],[s,"entryUUID",a,"givenName","sn"])).map(e=>[(f(e,s)||"").toLowerCase(),e]));return new Map(t.flatMap(e=>{const t=i.get(w(e).toLowerCase());if(!t)return[];const r={uuid:f(t,"entryUUID"),username:f(t,s),email:f(t,a),firstName:f(t,"givenName"),lastName:f(t,"sn")};return[[d(e),r]]}))}publishEvent(e,t,s,a,i={}){this.enqueue(e,s,{id:t,...i,actor:a.actor},(new Date).toISOString())}enqueue(e,t,s,a){const i=this.rabbitmq;if(!i)return Promise.resolve();const r=this.publishing.then(async()=>this.publish(i,t,{organizationId:e,...await this.domainOf(e),...s,timestamp:a}));return this.publishing=r.catch(()=>{}),r}async onSyncRequested(t){const malformed=()=>new Error(`${this.name}: malformed sync request ${JSON.stringify(t)}`);if("object"!=typeof t||!t||Array.isArray(t))throw malformed();const{organizationId:s,id:a}=t,valid=e=>void 0===e||"string"==typeof e&&e.length>0;if(!valid(s)||!valid(a)||void 0===s&&void 0!==a)throw malformed();if(void 0===s){for(const t of await this.organizations())await this.rabbitmq.publish(this.exchange,L,{organizationId:t,timestamp:(new Date).toISOString()},{messageId:e()});return}let i=!1;try{await this.groups.checkOrganization(s)}catch(e){if(!(e instanceof y))throw e;if(this.logger.info({plugin:this.name,event:"sync",organization:s,result:e.code}),410!==e.status)return;i=!0}await this.sync(s,a,i)}async organizations(){const e=r(this.spaceBase),t=e.findIndex(e=>e.includes(p)),s=e.slice(0,t),a=[],i=await this.server.ldap.search({paged:{pageSize:500},scope:"one",filter:"(objectClass=*)",attributes:["dn"]},e.slice(t+1).join(","));for await(const e of i)for(const{dn:t}of e.searchEntries){const e=c(this.spacePattern,["cn=x",...s,t].join(","));void 0!==e&&a.push(e)}return a}async sync(e,t,s){const a=(new Date).toISOString(),i=s?[]:await this.spaces(e,"(objectClass=*)",t),r=[];void 0===t||i.length||r.push(this.enqueue(e,"deleted",{id:t},a));for(const t of i)r.push(this.enqueue(e,"synced",await this.whole(e,t),a));void 0===t&&r.push(this.enqueue(e,"sync.completed",{spaceIds:i.map(e=>e.id)},a));const n=(await Promise.allSettled(r)).find(e=>"rejected"===e.status);if(n)throw n.reason}async whole(e,t){const s=d(this.groups.userBaseOf(e)),a=await this.orgGroups(e,t.holders.filter(e=>"group"===e.kind).map(e=>e.name),["member"]),i=new Map(a.map(e=>[d(e.dn),{name:f(e,this.groupName)||w(e.dn),members:g(e.member)}])),r=new Map,hold=(e,t)=>{r.has(d(e))||r.set(d(e),{dn:e,role:t})};for(const e of t.holders)if("member"===e.kind)hold(e.dn,e.role);else for(const t of i.get(d(e.dn))?.members??[])d(m(t))===s&&hold(t,e.role);const n=[...r.values()],o=await this.profiles(e,n.map(e=>e.dn));return{id:t.id,name:t.name,members:n.flatMap(({dn:e,role:t})=>{const s=o.get(d(e));return s?[{...s,role:t}]:[]}),groups:t.holders.filter(e=>"group"===e.kind).map(e=>({id:e.name,name:i.get(d(e.dn))?.name??e.name,role:e.role}))}}async domainOf(e){const t=this.config.twake_group_organization_dn;if(!t)return{};const s=e.toLowerCase();let a=this.domains.get(s);if(void 0===a){const i=this.domainAttribute;try{const s=await this.entry(t.replace(p,o(e)),[i]);a=s&&f(s,i)}catch(t){this.logger.warn({plugin:this.name,event:"organizationDomain",organization:e,error:String(t)})}a&&this.domains.set(s,a)}return a?{organizationDomain:a}:{}}get rabbitmq(){return this.config.rabbitmq_url?this.requirePlugin("rabbitmq"):null}async publish(t,s,a){const i={plugin:this.name,exchange:this.exchange,routingKey:`twake.space.${s}`,messageId:e()};if(!await t.getRawClient())throw this.logger.error({...i,result:"no broker"}),new Error(`${this.name}: no broker for ${i.routingKey}`);try{await t.publish(i.exchange,i.routingKey,a,{messageId:i.messageId})}catch(e){throw this.logger.error({...i,result:"error",error:String(e)}),e}this.logger.info({...i,result:"published"})}async rolesOf(e,t,s,a){const{filter:i,roleIn:r}=await this.userIn(e,t,void 0,a);let n=await this.spaces(e,i,void 0,void 0,s);if(a&&"id"in a){const e=a.id.toLowerCase();n=n.filter(t=>t.id.toLowerCase()!==e),a.space&&n.push(a.space)}const o=new Map(n.flatMap(e=>{const t=r(e);return t?[[e.id,t]]:[]}));if(a&&"group"in a&&a.members.has(d(t)))for(const{id:e,role:t}of a.spaces??[]){const s=o.get(e);(!s||C.indexOf(t)>C.indexOf(s))&&o.set(e,t)}return o}async writeRoles(e,t){const s=this.userRole,i=[...t].map(([e,t])=>`${e}:${t}`),r=new Set(i.map(e=>e.toLowerCase()));for(let t=0;;t++)try{const[t]=(await this.server.ldap.search({paged:!1,scope:"base",attributes:[s]},e)).searchEntries;if(!t)return;const a=g(I(t,s)),n=new Set(a.map(e=>e.toLowerCase())),o=a.filter(e=>A.test(e)&&!r.has(e.toLowerCase())),h=i.filter(e=>!n.has(e.toLowerCase()));return(o.length||h.length)&&await this.server.ldap.modify(e,{...o.length&&{delete:{[s]:o}},...h.length&&{add:{[s]:h}}}),new Map(a.flatMap(e=>{const t=A.exec(e)?.[1].toLowerCase();return t?[[e.slice(0,e.lastIndexOf(":")),t]]:[]}))}catch(e){const s=a(e);if(32===s)return;if(16!==s&&20!==s||t>=2)throw e}}async groupDns(e,t,s){const a=new Map((await this.orgGroups(e,t,[],s)).map(e=>[(f(e,this.groups.cn)||"").toLowerCase(),e.dn]));if(t.some(e=>!a.has(e.toLowerCase())))throw b("group");return a}async add(e,t,s,a,i){await this.again(()=>this.addOnce(e,t,s,a,i))}async addOnce(e,t,s,a,i){const r=await this.space(t,e.params.spaceId,e),n=await i(),o=new Map(r.holders.filter(e=>e.kind===s).map(e=>[e.name.toLowerCase(),e.role])),h=[];for(const[e,t]of n){const i=o.get(e);if(void 0===i)h.push(t);else if(i!==a)throw"member"===s?conflict(`${e} is already a member, as ${i}`,"MEMBER_EXISTS"):conflict(`${e} is already linked, as ${i}`,"GROUP_ALREADY_LINKED")}h.length&&await this.write(t,r.id,{add:{[this.roleAttributes[a]]:h}},e)}async change(e,t,s,a){await this.again(()=>this.changeOnce(e,t,s,a))}async changeOnce(e,t,s,a){const i=await this.space(t,e.params.spaceId,e),r="member"===s?e.params.userId:e.params.groupId,n=i.holders.find(e=>e.kind===s&&e.name.toLowerCase()===r.toLowerCase());if(!n)throw b(s);if(1===n.roles.length&&n.role===a)return;const o="member"===s&&"admin"===n.role&&"admin"!==a;if(o&&!i.holders.some(e=>e!==n&&isAdmin(e)))throw lastAdmin();const h=Object.fromEntries(n.roles.filter(e=>e!==a).map(e=>[this.roleAttributes[e],n.dn])),c=a&&!n.roles.includes(a)?{[this.roleAttributes[a]]:n.dn}:void 0;if(await this.write(t,i.id,{delete:h,add:c},e),o&&!(await this.space(t,i.id,e)).holders.some(isAdmin))throw await this.write(t,i.id,{add:h,delete:c},e),lastAdmin()}async listRoute(e,t,s){const a=this.groups.page(e,q);let i="(cn=*)";a.search&&(i=`(&${i}(${this.displayName}=*${h(a.search)}*))`);const r=e.query.user;let n;if(void 0!==r){if("string"!=typeof r||!r)throw v("user must be a username");const[t]=(await this.liveUsers(s,[r],e)).values(),a=await this.userIn(s,t,e);i=`(&${i}${a.filter})`,n=a.roleIn}const o=this.groups.sorted((await this.spaces(s,i,void 0,e)).map(e=>({...this.spaceOf(s,e),...n&&{role:n(e)}})),a.sortBy,a.desc);t.json({organizationId:s,spaces:o.slice(a.offset,a.offset+a.limit),pagination:{page:a.page,limit:a.limit,total:o.length,totalPages:Math.ceil(o.length/a.limit)}})}async createRoute(t,s,a){const i=k(t);if(!O(i.name))throw v($);const r=this.groups.maxPage,n=holdersOf(i.members,"username",r),o=holdersOf(i.groups??[],"id",r);if(!n.some(e=>"admin"===e.role))throw v("A space needs an admin among its members","ADMIN_REQUIRED");const[h,c]=await Promise.all([this.liveUsers(a,n.map(e=>e.name),t),this.groupDns(a,o.map(e=>e.name),t)]),d=e(),l={objectClass:this.config.twake_space_class||["top","twakeSpace"],cn:d,[this.displayName]:i.name};for(const[e,t]of[[n,h],[o,c]])for(const{name:s,role:a}of e){const e=this.roleAttributes[a];l[e]=[...g(l[e]),t.get(s.toLowerCase())]}await this.server.ldap.add(this.spaceDn(a,d),l,t),s.status(201).json(this.spaceOf(a,await this.space(a,d,t)))}}export{C as SPACE_ROLES,TwakeSpaces as default};
@@ -237,6 +237,7 @@ export interface Config {
237
237
  twake_space_viewer_attribute?: string;
238
238
  twake_space_user_role_attribute?: string;
239
239
  twake_space_exchange?: string;
240
+ twake_space_sync_queue?: string;
240
241
  twake_instance_dn?: string[];
241
242
  twake_instance_skip_attribute?: string;
242
243
  twake_instance_skip_value?: string;
@@ -113,7 +113,7 @@ export default class TwakeGroups extends LdapGroups {
113
113
  get userAttribute(): string;
114
114
  userDn(org: string, username: string): string;
115
115
  /** Every route answers 404 for a missing organization, 410 for a deleted one. */
116
- private checkOrganization;
116
+ checkOrganization(org: string): Promise<void>;
117
117
  /** Page, search and sort of a list route, or a refusal of them. */
118
118
  page(req: Request, sortable: string[]): Page;
119
119
  sorted<T extends Record<string, unknown>>(items: T[], sortBy: string | undefined, desc: boolean): T[];
@@ -30,6 +30,9 @@ export default class TwakeSpaces extends DmPlugin {
30
30
  readonly roleAttributes: Record<SpaceRole, string>;
31
31
  private readonly userRole;
32
32
  private readonly exchange;
33
+ private readonly domainAttribute;
34
+ private readonly orgPattern?;
35
+ private readonly domains;
33
36
  /**
34
37
  * Changes are followed one at a time, each from the directory as it is
35
38
  * then, so the order they are followed in does not matter.
@@ -62,6 +65,9 @@ export default class TwakeSpaces extends DmPlugin {
62
65
  * core/rabbitmq connects lazily and hands back no client when it cannot:
63
66
  * every event would then be lost, so the server does not start.
64
67
  *
68
+ * The organization domain is only read, and a search for an attribute the
69
+ * schema does not define finds none: every event would go out without it.
70
+ *
65
71
  * core/ldap/trash moves a deleted group or space away instead of deleting
66
72
  * it: nothing follows the move, so users would keep the roles it gave,
67
73
  * and no event would say it went.
@@ -78,7 +84,7 @@ export default class TwakeSpaces extends DmPlugin {
78
84
  private checkHolders;
79
85
  /** A holder that is neither a user nor a group of the organization. */
80
86
  private foreign;
81
- api(app: Express): void;
87
+ api(app: Express): Promise<void>;
82
88
  /** A space as the routes answer it, strongest role first. */
83
89
  private spaceOf;
84
90
  /**
@@ -161,6 +167,26 @@ export default class TwakeSpaces extends DmPlugin {
161
167
  /** Users as space events name them, by DN key, live users only. */
162
168
  private profiles;
163
169
  private publishEvent;
170
+ private enqueue;
171
+ /**
172
+ * Every organization is asked in a request of its own, through the queue:
173
+ * the broker closes the channel of a consumer that holds a message longer
174
+ * than its consumer_timeout (30 minutes by default), and delivers every
175
+ * message in flight on it again.
176
+ */
177
+ private onSyncRequested;
178
+ /** The entries where `--twake-space-base` puts an organization. */
179
+ private organizations;
180
+ /**
181
+ * The events bear the time before the read, so an event of a later write
182
+ * is newer. Fails once they are all out if one was not sent, so the
183
+ * request is retried.
184
+ */
185
+ private sync;
186
+ /** A space as `twake.space.created` gives it. */
187
+ private whole;
188
+ /** Kept once found, until the organization entry changes. */
189
+ private domainOf;
164
190
  private get rabbitmq();
165
191
  private publish;
166
192
  /** A user's roles, in the directory as it is or with a change undone. */
@@ -606,18 +606,20 @@ See [groups](plugins/integrations/groups.md).
606
606
 
607
607
  Requires `core/twake/groups`, whose options it reads for users, groups and the
608
608
  organization entry, and `core/ldap/onChange`; with `--rabbitmq-url`, it
609
- requires `core/rabbitmq`.
610
-
611
- | CLI | Env | Default | Description |
612
- | -------------------------------------- | --------------------------------------- | ------------------ | -------------------------------------------------- |
613
- | `--twake-space-base` | `DM_TWAKE_SPACE_BASE` | | Space branch of an organization, with `{org}` |
614
- | `--twake-space-class` | `DM_TWAKE_SPACE_CLASS` | `top,twakeSpace` | Object classes of a space |
615
- | `--twake-space-display-name-attribute` | `DM_TWAKE_SPACE_DISPLAY_NAME_ATTRIBUTE` | `twakeDisplayName` | Space name attribute |
616
- | `--twake-space-admin-attribute` | `DM_TWAKE_SPACE_ADMIN_ATTRIBUTE` | `twakeSpaceAdmin` | DNs holding the admin role |
617
- | `--twake-space-editor-attribute` | `DM_TWAKE_SPACE_EDITOR_ATTRIBUTE` | `twakeSpaceEditor` | DNs holding the editor role |
618
- | `--twake-space-viewer-attribute` | `DM_TWAKE_SPACE_VIEWER_ATTRIBUTE` | `twakeSpaceViewer` | DNs holding the viewer role |
619
- | `--twake-space-user-role-attribute` | `DM_TWAKE_SPACE_USER_ROLE_ATTRIBUTE` | | User attribute holding `<space id>:<role>`, if set |
620
- | `--twake-space-exchange` | `DM_TWAKE_SPACE_EXCHANGE` | `space` | Topic exchange of the space events |
609
+ requires `core/rabbitmq` and reads the organization domain from
610
+ `--twake-instance-organization-domain-attribute`.
611
+
612
+ | CLI | Env | Default | Description |
613
+ | -------------------------------------- | --------------------------------------- | -------------------------------------- | -------------------------------------------------- |
614
+ | `--twake-space-base` | `DM_TWAKE_SPACE_BASE` | | Space branch of an organization, with `{org}` |
615
+ | `--twake-space-class` | `DM_TWAKE_SPACE_CLASS` | `top,twakeSpace` | Object classes of a space |
616
+ | `--twake-space-display-name-attribute` | `DM_TWAKE_SPACE_DISPLAY_NAME_ATTRIBUTE` | `twakeDisplayName` | Space name attribute |
617
+ | `--twake-space-admin-attribute` | `DM_TWAKE_SPACE_ADMIN_ATTRIBUTE` | `twakeSpaceAdmin` | DNs holding the admin role |
618
+ | `--twake-space-editor-attribute` | `DM_TWAKE_SPACE_EDITOR_ATTRIBUTE` | `twakeSpaceEditor` | DNs holding the editor role |
619
+ | `--twake-space-viewer-attribute` | `DM_TWAKE_SPACE_VIEWER_ATTRIBUTE` | `twakeSpaceViewer` | DNs holding the viewer role |
620
+ | `--twake-space-user-role-attribute` | `DM_TWAKE_SPACE_USER_ROLE_ATTRIBUTE` | | User attribute holding `<space id>:<role>`, if set |
621
+ | `--twake-space-exchange` | `DM_TWAKE_SPACE_EXCHANGE` | `space` | Topic exchange of the space events |
622
+ | `--twake-space-sync-queue` | `DM_TWAKE_SPACE_SYNC_QUEUE` | `twake.space.sync.requested.ldap-rest` | Queue of the space sync requests |
621
623
 
622
624
  See [spaces](plugins/integrations/spaces.md).
623
625
 
@@ -62,8 +62,9 @@ A group reads:
62
62
  display name reads its `cn`.
63
63
 
64
64
  - `GET /groups?page&limit&search&sortBy&sortOrder`: `search` (2 characters
65
- or more; an empty one lists everything) matches the display name and the
66
- description; `sortBy` is `displayName`, `description` or `createdAt`. Answers
65
+ or more; an empty one lists everything) matches the display name — the
66
+ `cn` when the group has none — and the description; `sortBy` is
67
+ `displayName`, `description` or `createdAt`. Answers
67
68
  `{ organizationId, groups, pagination: { page, limit, total, totalPages } }`.
68
69
  - `POST /groups` with `{ name, description?, color? }`: `name` is at most 256
69
70
  characters, not blank, without control characters, and unique in the
@@ -69,6 +69,7 @@ every write, whichever API makes it, shortly after it.
69
69
  ```json
70
70
  {
71
71
  "organizationId": "acme",
72
+ "organizationDomain": "acme.com",
72
73
  "id": "3b9e2c71-5d4a-4f0e-9c8b-1a2d6e7f8091",
73
74
  "members": [
74
75
  {
@@ -89,6 +90,16 @@ Every event carries `organizationId`, `id` (the space), `actor` (who made
89
90
  the write, when known) and `timestamp`. A member is described as above, its
90
91
  `uuid` the user's `entryUUID`; a group is `{ id, name, role }`.
91
92
 
93
+ `organizationDomain` is the organization's domain, read on its entry
94
+ (`--twake-group-organization-dn`) from
95
+ `--twake-instance-organization-domain-attribute` (default `twakeDomain`), the
96
+ option `twake/instances` reads too. It is kept until the entry is changed
97
+ through this server: another replica, or a write made straight to the
98
+ directory, keeps the old domain until restart. It is left out when
99
+ `--twake-group-organization-dn` is empty or the entry has no such value. The
100
+ server does not start when the directory schema does not define that
101
+ attribute.
102
+
92
103
  Writes made close together blur the actor: the member events of two writes
93
104
  of one space or group followed together may go out under the first one's.
94
105
  With `--twake-space-user-role-attribute`, the follow of a write may find a
@@ -111,11 +122,50 @@ announce.
111
122
  followed, is unlinked too, under its id for a name. Renaming a linked group
112
123
  publishes nothing.
113
124
 
125
+ - `synced`: the whole space, as `created` gives it, published by a sync.
126
+ - `sync.completed`: `spaceIds`, the ids of every space of the organization,
127
+ after its `synced` events. It has no `id`.
128
+
114
129
  A user who is deleted or becomes a tombstone publishes no member event.
115
130
 
131
+ ## Sync
132
+
133
+ An app repairs its copy of the spaces by publishing
134
+ `twake.space.sync.requested` on the exchange:
135
+
136
+ - `{ "organizationId", "id" }`: the space's `synced`, or `deleted` when it
137
+ is gone;
138
+ - `{ "organizationId" }`: a `synced` for each space of the organization,
139
+ then `sync.completed`;
140
+ - `{}`: a `twake.space.sync.requested` with `organizationId` for each
141
+ organization, the entries one level under the parent of `{org}` in
142
+ `--twake-space-base`, published on the exchange, so every organization is
143
+ synced as above.
144
+
145
+ A request reaches every queue bound to its key, `twake.space.#` included.
146
+ An app that follows the events binds `twake.space.*`, `twake.space.member.#`,
147
+ `twake.space.group.#` and `twake.space.sync.completed`, or ignores
148
+ `twake.space.sync.requested`.
149
+
150
+ The plugin consumes these requests from the `--twake-space-sync-queue`
151
+ quorum queue, with a dead letter queue `<queue>.dlq`. Every replica consumes
152
+ it, several requests at once, so the events of two syncs of one organization
153
+ may interleave: their timestamps order them. A request it cannot read goes
154
+ to the dead letter queue. A missing organization is skipped. A deleted one
155
+ has no space left: its `sync.completed` lists none, and a space asked is
156
+ `deleted`. A sync whose events the broker does not take fails, and its
157
+ request is retried, then goes to the dead letter queue.
158
+
159
+ The events of a sync carry no `actor`. They all carry the time just before
160
+ the spaces were read, so, with synchronized clocks, the event of a write
161
+ followed after that is newer.
162
+ An app that applies only events newer than the last one keeps the right
163
+ state, and keeps a space created after a `sync.completed` that does not list
164
+ it.
165
+
116
166
  The server does not start when the broker cannot be reached. An event the
117
- broker cannot take once the server runs is logged with `result: "error"` and
118
- not sent again.
167
+ broker cannot take once the server runs is logged with `result: "error"`;
168
+ the event of a write is not sent again.
119
169
 
120
170
  Member events compare each moved user's roles before and after a change:
121
171
 
@@ -210,6 +260,9 @@ organization's users, groups and organization entry.
210
260
  Unset, no user entry is written.
211
261
  - `--twake-space-exchange` (default `space`): the topic exchange of the
212
262
  events, published only when `--rabbitmq-url` is set.
263
+ - `--twake-space-sync-queue` (default
264
+ `twake.space.sync.requested.ldap-rest`): the queue of the sync requests,
265
+ consumed only when `--rabbitmq-url` is set.
213
266
 
214
267
  ## Dependencies
215
268
 
@@ -4,6 +4,18 @@ What to check before deploying, newest first. Only releases that need a
4
4
  decision or a configuration change appear here; see the
5
5
  [CHANGELOG](../../CHANGELOG.md) for everything else.
6
6
 
7
+ ## To 0.16.0
8
+
9
+ ### Space events need the organization domain attribute
10
+
11
+ **Who is affected:** a deployment of `core/twake/spaces` with `--rabbitmq-url`
12
+ and `--twake-group-organization-dn`.
13
+
14
+ The space events now carry the organization's domain, read from
15
+ `--twake-instance-organization-domain-attribute` (default `twakeDomain`). The
16
+ server does not start when the directory schema does not define that
17
+ attribute: load a schema that does, or point the option at one it defines.
18
+
7
19
  ## To 0.15.0
8
20
 
9
21
  ### A DN option read from the environment splits on `;` only
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-rest",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Light directory manager",
5
5
  "keywords": [
6
6
  "Identity-Manager"
@@ -775,7 +775,6 @@
775
775
  "eslint-plugin-import": "^2.32.0",
776
776
  "eslint-plugin-node": "^11.1.0",
777
777
  "eslint-plugin-prettier": "^5.5.4",
778
- "fast-glob": "^3.3.3",
779
778
  "globals": "^14.0.0",
780
779
  "js-yaml": "^4.1.1",
781
780
  "mocha": "^12.0.0",
@@ -786,6 +785,7 @@
786
785
  "rollup-plugin-postcss": "^4.0.2",
787
786
  "sort-package-json": "^3.4.0",
788
787
  "supertest": "^7.1.4",
788
+ "tinyglobby": "^0.2.17",
789
789
  "tslib": "^2.8.1",
790
790
  "tsx": "^4.20.5",
791
791
  "typescript": "^5.9.2"