ldap-rest 0.10.0 → 0.11.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 _}from"url";import{join as a,dirname as e}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",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"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--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",""],["--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"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",a(e(_(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",a(e(_(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes"],["--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"],["--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"],["--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"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--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-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array"],["--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-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"],["--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"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-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"],["--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 _}from"url";import{join as a,dirname as e}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",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"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--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",""],["--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"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",a(e(_(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",a(e(_(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes"],["--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"],["--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"],["--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"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--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"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array"],["--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-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"],["--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"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-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"],["--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 e from"../../abstract/plugin.js";import{assertIdentityMode as t,identityFor as r}from"../auth/base.js";import{AmbiguousIdentityError as s,ForbiddenError as i}from"../errors.js";import{getParentDn as n,escapeLdapFilter as a,isDnInBranch as o}from"../utils.js";import{authzFor as h,servesRequest as u}from"./composition.js";class AuthzBase extends e{constructor(e){super(e),this.roles=["authz"],this.resolutionCache=new Map,this.resolutionCacheFullWarned=!1,this.fallbackWarned=!1,this.hooks={ldaprenamedone:()=>{this.resolutionCache.clear()},ldapdeletedone:()=>{this.resolutionCache.clear()},ldapmodifyrequest:async([e,t,r,s])=>{if(this.shouldSkipAuthorization(s))return[e,t,r,s];const i=await this.resolveCaller(s);if(!i)return[e,t,r,s];const n=this.config.ldap_organization_link_attribute;if(n&&t.replace?.[n]){let r=this.extractBranchDn(e);try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[n]},e),s=t.searchEntries[0]?.[n],i=Array.isArray(s)?s[0]:s;null!=i&&""!==String(i)&&(r=String(i))}catch{}if(!(await this.getUserPermissions(i,r)).read)throw new Error(`[authz-forbidden] User ${s.user} does not have read permission for source branch ${r}`);const a=t.replace[n],o=Array.isArray(a)?String(a[0]):String(a);if(!(await this.getUserPermissions(i,o)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for destination branch ${o}`)}else{const t=await this.effectiveBranch(e);if(!(await this.getUserPermissions(i,t)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for branch ${t}`)}return[e,t,r,s]},ldapaddrequest:async([e,t,r])=>{if(this.shouldSkipAuthorization(r))return[e,t,r];const s=await this.resolveCaller(r);if(!s)return[e,t,r];let i;const n=this.config.ldap_organization_link_attribute;if(n&&t[n]){const e=t[n];i=Array.isArray(e)?String(e[0]):String(e)}else i=this.extractBranchDn(e);if(!(await this.getUserPermissions(s,i)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for branch ${i}`);return[e,t,r]},ldapsearchrequest:async([e,t,r])=>{if(this.shouldSkipAuthorization(r))return[e,t,r];const s=await this.resolveCaller(r);if(!s)return[e,t,r];if(e===this.config.ldap_top_organization&&"base"===t.scope)return[e,t,r];if(this.config.authz_filter_attached_entries){if(0===(await this.getAuthorizedBranches(s)).length)throw new Error(`[authz-forbidden] User ${r.user} administers no branch`);const i=this.config.ldap_organization_link_attribute;if(i&&!AuthzBase.wantsAttribute(t,i)){const e=t.attributes,r=Array.isArray(e)?e.map(String):[String(e)];t={...t,attributes:[...r,i]},AuthzBase.widenedForLink.add(t)}return[e,t,r]}if(!(await this.getUserPermissions(s,e)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for branch ${e}`);return[e,t,r]},ldapsearchfilter:async([e,t,r])=>{const s=[e,t,r];if(!this.config.authz_filter_attached_entries)return s;const i=this.config.ldap_organization_link_attribute;if(!i||this.shouldSkipAuthorization(t))return s;if(!e?.searchEntries?.length)return s;const n=await this.resolveCaller(t);if(!n)return s;const a=await this.getAuthorizedBranches(n);if(0===a.length)return s;const within=e=>a.some(t=>o(e,t));if(e.searchEntries=e.searchEntries.filter(e=>{const r=e[i];if(null==r)return!0;const s=(Array.isArray(r)?r:[r]).map(String);return 0===s.length||(!!s.some(within)||(this.logger.debug(`${this.name}: hiding ${String(e.dn)} from ${t.user}, attached to ${s[0]}`),!1))}),r&&AuthzBase.widenedForLink.has(r))for(const t of e.searchEntries)delete t[i];return[e,t,r]},getOrganisationTop:async([e,t])=>{if(this.shouldSkipAuthorization(e))return[e,t];const r=await this.resolveCaller(e);if(!r)return[e,t];const s=await this.getAuthorizedBranches(r);if(s.length>0){const t=[];for(const r of s)try{const s=await this.server.ldap.search({paged:!1,scope:"base"},r,e);1===s.searchEntries.length&&t.push(s.searchEntries[0])}catch(e){this.logger.warn(`Failed to fetch authorized branch ${r}: ${e}`)}if(1===t.length)return[e,t[0]];if(t.length>1)return[e,t[0]]}return[e,t]},ldaprenamerequest:async([e,t,r])=>{if(this.shouldSkipAuthorization(r))return[e,t,r];const s=await this.resolveCaller(r);if(!s)return[e,t,r];const i=this.extractBranchDn(e),n=this.extractBranchDn(t);if(!(await this.getUserPermissions(s,i)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for source branch ${i}`);if(!(await this.getUserPermissions(s,n)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for destination branch ${n}`);return[e,t,r]},ldapdeleterequest:async([e,t])=>{if(this.shouldSkipAuthorization(t))return[e,t];const r=await this.resolveCaller(t);if(!r)return[e,t];const s=Array.isArray(e)?e:[e];for(const e of s){const s=await this.effectiveBranch(e);if(!(await this.getUserPermissions(r,s)).delete)throw new Error(`[authz-forbidden] User ${t.user} does not have delete permission for branch ${s}`)}return[e,t]}},t(this.config,this.constructor.name),h(this.config,this.constructor.name);const r=this.config.authz_unresolved_user??"deny";if(!AuthzBase.UNRESOLVED_POLICIES.includes(r))throw new Error(`${this.constructor.name}: unknown --authz-unresolved-user "${r}". Known: ${AuthzBase.UNRESOLVED_POLICIES.join(", ")}.`)}static wantsAttribute(e,t){const r=e?.attributes;if(void 0===r)return!0;const s=(Array.isArray(r)?r:[r]).map(e=>String(e).toLowerCase());return 0===s.length||(!!s.includes("*")||s.includes(t.toLowerCase()))}extractBranchDn(e){return n(e)}async effectiveBranch(e){const t=this.config.ldap_organization_link_attribute;if(!t||!this.config.authz_filter_attached_entries)return this.extractBranchDn(e);try{const r=await this.server.ldap.search({paged:!1,scope:"base",attributes:[t]},e),s=(e=>{if(null==e)return;const t=Array.isArray(e)?e[0]:e,r=String(t);return r.length>0?r:void 0})(r.searchEntries?.[0]?.[t]);if(s)return s}catch{}return this.extractBranchDn(e)}async findUserDn(e){const t=`(${this.config.ldap_user_main_attribute||"uid"}=${a(e)})`,r=(await this.server.ldap.search({paged:!1,filter:t,attributes:["dn"],scope:"sub"},this.server.ldap.base)).searchEntries??[];if(r.length>1)throw new s(e,r.length);if(0===r.length)return null;const i=r[0].dn;return"string"==typeof i?i:String(i)}shouldSkipAuthorization(e){return!e?.user||!u(e,this.config)}async resolveCaller(e){const{value:t,fellBack:s}=r(e,this.config);s&&!this.fallbackWarned&&(this.fallbackWarned=!0,this.logger.warn(`${this.name}: --authz-identity asks for req.userName and the authenticator published none, so permissions are read for req.user instead. Rules written on logins will not match`));const n=t,a=Date.now(),o=this.resolutionCache.get(n);let h;if(o&&a-o.at<this.cacheTTL)h=o.user;else{if(h=await this.resolveUser(n),this.resolutionCache.size>=AuthzBase.RESOLUTION_CACHE_MAX)for(const[e,t]of this.resolutionCache)a-t.at>=this.cacheTTL&&this.resolutionCache.delete(e);this.resolutionCache.size<AuthzBase.RESOLUTION_CACHE_MAX?this.resolutionCache.set(n,{user:h,at:a}):this.resolutionCacheFullWarned||(this.resolutionCacheFullWarned=!0,this.logger.warn(`${this.name}: more than ${AuthzBase.RESOLUTION_CACHE_MAX} identities resolved within one cache window, so resolutions are no longer cached and each request costs a lookup`))}if(h)return h;if("allow"===this.config.authz_unresolved_user)return this.logger.warn(`User ${n} could not be resolved by ${this.name}; allowed by --authz-unresolved-user allow`),null;throw new i(`[authz-forbidden] User ${n} could not be resolved by ${this.name}, so no permission can be read for them`)}clearResolutionCache(){this.resolutionCache.clear()}}AuthzBase.widenedForLink=new WeakSet,AuthzBase.UNRESOLVED_POLICIES=["deny","allow"],AuthzBase.RESOLUTION_CACHE_MAX=1e4;export{AuthzBase as default};
1
+ import e from"../../abstract/plugin.js";import{assertIdentityMode as t,identityFor as r}from"../auth/base.js";import{AmbiguousIdentityError as s,ForbiddenError as i}from"../errors.js";import{getParentDn as n,escapeLdapFilter as a,isDnInBranch as o}from"../utils.js";import{authzFor as h,servesRequest as c}from"./composition.js";class AuthzBase extends e{constructor(e){super(e),this.roles=["authz"],this.resolutionCache=new Map,this.resolutionCacheFullWarned=!1,this.fallbackWarned=!1,this.hooks={ldaprenamedone:()=>{this.resolutionCache.clear()},ldapdeletedone:()=>{this.resolutionCache.clear()},ldapmodifyrequest:async([e,t,r,s])=>{if(this.shouldSkipAuthorization(s))return[e,t,r,s];const i=await this.resolveCaller(s);if(!i)return[e,t,r,s];const n=this.config.ldap_organization_link_attribute;if(n&&t.replace?.[n]){let r=this.extractBranchDn(e);try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[n]},e),s=t.searchEntries[0]?.[n],i=Array.isArray(s)?s[0]:s;null!=i&&""!==String(i)&&(r=String(i))}catch{}if(!(await this.getUserPermissions(i,r)).read)throw new Error(`[authz-forbidden] User ${s.user} does not have read permission for source branch ${r}`);const a=t.replace[n],o=Array.isArray(a)?String(a[0]):String(a);if(!(await this.getUserPermissions(i,o)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for destination branch ${o}`)}else{const t=await this.effectiveBranch(e);if(!(await this.getUserPermissions(i,t)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for branch ${t}`)}return[e,t,r,s]},ldapaddrequest:async([e,t,r])=>{if(this.shouldSkipAuthorization(r))return[e,t,r];const s=await this.resolveCaller(r);if(!s)return[e,t,r];let i;const n=this.config.ldap_organization_link_attribute;if(n&&t[n]){const e=t[n];i=Array.isArray(e)?String(e[0]):String(e)}else i=this.extractBranchDn(e);if(!(await this.getUserPermissions(s,i)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for branch ${i}`);return[e,t,r]},ldapsearchrequest:async([e,t,r])=>{if(this.shouldSkipAuthorization(r))return[e,t,r];const s=await this.resolveCaller(r);if(!s)return[e,t,r];if(e===this.config.ldap_top_organization&&"base"===t.scope)return[e,t,r];if(this.filtersAttachedEntries()){if(0===(await this.getAuthorizedBranches(s)).length)throw new Error(`[authz-forbidden] User ${r.user} administers no branch`);const i=this.config.ldap_organization_link_attribute;if(i&&!AuthzBase.wantsAttribute(t,i)){const e=t.attributes,r=Array.isArray(e)?e.map(String):[String(e)];t={...t,attributes:[...r,i]},AuthzBase.widenedForLink.add(t)}return[e,t,r]}if(!(await this.getUserPermissions(s,e)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for branch ${e}`);return[e,t,r]},ldapsearchfilter:async([e,t,r])=>{const s=[e,t,r];if(!this.filtersAttachedEntries())return s;const i=this.config.ldap_organization_link_attribute;if(!i||this.shouldSkipAuthorization(t))return s;if(!e?.searchEntries?.length)return s;const n=await this.resolveCaller(t);if(!n)return s;const a=await this.getAuthorizedBranches(n);if(0===a.length)return s;const within=e=>a.some(t=>o(e,t));if(e.searchEntries=e.searchEntries.filter(e=>{const r=e[i];if(null==r)return!0;const s=(Array.isArray(r)?r:[r]).map(String);return 0===s.length||(!!s.some(within)||(this.logger.debug(`${this.name}: hiding ${String(e.dn)} from ${t.user}, attached to ${s[0]}`),!1))}),r&&AuthzBase.widenedForLink.has(r))for(const t of e.searchEntries)delete t[i];return[e,t,r]},getOrganisationTop:async([e,t])=>{if(this.shouldSkipAuthorization(e))return[e,t];const r=await this.resolveCaller(e);if(!r)return[e,t];const s=await this.getAuthorizedBranches(r);if(s.length>0){const t=[];for(const r of s)try{const s=await this.server.ldap.search({paged:!1,scope:"base"},r,e);1===s.searchEntries.length&&t.push(s.searchEntries[0])}catch(e){this.logger.warn(`Failed to fetch authorized branch ${r}: ${e}`)}if(1===t.length)return[e,t[0]];if(t.length>1)return[e,t[0]]}return[e,t]},ldaprenamerequest:async([e,t,r])=>{if(this.shouldSkipAuthorization(r))return[e,t,r];const s=await this.resolveCaller(r);if(!s)return[e,t,r];const i=this.extractBranchDn(e),n=this.extractBranchDn(t);if(!(await this.getUserPermissions(s,i)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for source branch ${i}`);if(!(await this.getUserPermissions(s,n)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for destination branch ${n}`);return[e,t,r]},ldapdeleterequest:async([e,t])=>{if(this.shouldSkipAuthorization(t))return[e,t];const r=await this.resolveCaller(t);if(!r)return[e,t];const s=Array.isArray(e)?e:[e];for(const e of s){const s=await this.effectiveBranch(e);if(!(await this.getUserPermissions(r,s)).delete)throw new Error(`[authz-forbidden] User ${t.user} does not have delete permission for branch ${s}`)}return[e,t]}},t(this.config,this.constructor.name),h(this.config,this.constructor.name);const r=this.config.authz_unresolved_user??"deny";if(!AuthzBase.UNRESOLVED_POLICIES.includes(r))throw new Error(`${this.constructor.name}: unknown --authz-unresolved-user "${r}". Known: ${AuthzBase.UNRESOLVED_POLICIES.join(", ")}.`)}static wantsAttribute(e,t){const r=e?.attributes;if(void 0===r)return!0;const s=(Array.isArray(r)?r:[r]).map(e=>String(e).toLowerCase());return 0===s.length||(!!s.includes("*")||s.includes(t.toLowerCase()))}extractBranchDn(e){return n(e)}filtersAttachedEntries(){return Boolean(this.config.authz_filter_attached_entries)}async effectiveBranch(e){const t=this.config.ldap_organization_link_attribute;if(!t||!this.filtersAttachedEntries())return this.extractBranchDn(e);try{const r=await this.server.ldap.search({paged:!1,scope:"base",attributes:[t]},e),s=(e=>{if(null==e)return;const t=Array.isArray(e)?e[0]:e,r=String(t);return r.length>0?r:void 0})(r.searchEntries?.[0]?.[t]);if(s)return s}catch{}return this.extractBranchDn(e)}async findUserDn(e){const t=`(${this.config.ldap_user_main_attribute||"uid"}=${a(e)})`,r=(await this.server.ldap.search({paged:!1,filter:t,attributes:["dn"],scope:"sub"},this.server.ldap.base)).searchEntries??[];if(r.length>1)throw new s(e,r.length);if(0===r.length)return null;const i=r[0].dn;return"string"==typeof i?i:String(i)}shouldSkipAuthorization(e){return!e?.user||!c(e,this.config)}async resolveCaller(e){const{value:t,fellBack:s}=r(e,this.config);s&&!this.fallbackWarned&&(this.fallbackWarned=!0,this.logger.warn(`${this.name}: --authz-identity asks for req.userName and the authenticator published none, so permissions are read for req.user instead. Rules written on logins will not match`));const n=t,a=Date.now(),o=this.resolutionCache.get(n);let h;if(o&&a-o.at<this.cacheTTL)h=o.user;else{if(h=await this.resolveUser(n),this.resolutionCache.size>=AuthzBase.RESOLUTION_CACHE_MAX)for(const[e,t]of this.resolutionCache)a-t.at>=this.cacheTTL&&this.resolutionCache.delete(e);this.resolutionCache.size<AuthzBase.RESOLUTION_CACHE_MAX?this.resolutionCache.set(n,{user:h,at:a}):this.resolutionCacheFullWarned||(this.resolutionCacheFullWarned=!0,this.logger.warn(`${this.name}: more than ${AuthzBase.RESOLUTION_CACHE_MAX} identities resolved within one cache window, so resolutions are no longer cached and each request costs a lookup`))}if(h)return h;if("allow"===this.config.authz_unresolved_user)return this.logger.warn(`User ${n} could not be resolved by ${this.name}; allowed by --authz-unresolved-user allow`),null;throw new i(`[authz-forbidden] User ${n} could not be resolved by ${this.name}, so no permission can be read for them`)}clearResolutionCache(){this.resolutionCache.clear()}}AuthzBase.widenedForLink=new WeakSet,AuthzBase.UNRESOLVED_POLICIES=["deny","allow"],AuthzBase.RESOLUTION_CACHE_MAX=1e4;export{AuthzBase as default};
@@ -1 +1 @@
1
- import s from"../../lib/authz/base.js";import{escapeLdapFilter as e}from"../../lib/utils.js";class AuthzLinid1 extends s{constructor(s){super(s),this.name="authzLinid1",this.permissionsCache=new Map,this.cacheTTL=3e5;const e=this.config.authz_local_admin_attribute||"twakeLocalAdminLink";this.logger.info(`AuthzLinid1: Authorization based on ${e} attribute enabled`)}async resolveUser(s){return this.getUserDn(s)}async getUserDn(s){return this.findUserDn(s)}async getUserPermissions(s,e){const t=Date.now(),i=this.permissionsCache.get(s);if(i&&t-i.timestamp<this.cacheTTL){const s=this.findBranchPermissions(i.branches,e);if(s)return s}await this.refreshUserPermissions(s);const r=this.permissionsCache.get(s);if(r){const s=this.findBranchPermissions(r.branches,e);if(s)return s}return{read:!1,write:!1,delete:!1}}async refreshUserPermissions(s){const t=new Map;try{const i=`(${this.config.authz_local_admin_attribute||"twakeLocalAdminLink"}=${e(s)})`,r=this.config.ldap_top_organization||this.config.ldap_base||"",n=await this.server.ldap.search({paged:!1,filter:i,attributes:["dn"],scope:"sub"},r);if(n.searchEntries)for(const s of n.searchEntries){const e="string"==typeof s.dn?s.dn:String(s.dn);t.set(e,{read:!0,write:!0,delete:!0})}}catch(e){this.logger.error(`Failed to refresh permissions for user ${s}: ${e}`)}this.permissionsCache.set(s,{branches:t,timestamp:Date.now()})}async getAuthorizedBranches(s){const e=Date.now(),t=this.permissionsCache.get(s);(!t||e-t.timestamp>=this.cacheTTL)&&await this.refreshUserPermissions(s);const i=this.permissionsCache.get(s);return i?Array.from(i.branches.keys()):[]}findBranchPermissions(s,e){if(s.has(e))return s.get(e)||null;for(const[t,i]of s.entries())if(e.toLowerCase().endsWith(`,${t.toLowerCase()}`))return i;return null}}export{AuthzLinid1 as default};
1
+ import s from"../../lib/authz/base.js";import{escapeLdapFilter as e}from"../../lib/utils.js";class AuthzLinid1 extends s{constructor(s){super(s),this.name="authzLinid1",this.permissionsCache=new Map,this.cacheTTL=3e5;const e=this.config.authz_local_admin_attribute||"twakeLocalAdminLink";this.logger.info(`AuthzLinid1: Authorization based on ${e} attribute enabled`)}filtersAttachedEntries(){return!0}async resolveUser(s){return this.getUserDn(s)}async getUserDn(s){return this.findUserDn(s)}async getUserPermissions(s,e){const t=Date.now(),i=this.permissionsCache.get(s);if(i&&t-i.timestamp<this.cacheTTL){const s=this.findBranchPermissions(i.branches,e);if(s)return s}await this.refreshUserPermissions(s);const r=this.permissionsCache.get(s);if(r){const s=this.findBranchPermissions(r.branches,e);if(s)return s}return{read:!1,write:!1,delete:!1}}async refreshUserPermissions(s){const t=new Map;try{const i=`(${this.config.authz_local_admin_attribute||"twakeLocalAdminLink"}=${e(s)})`,r=this.config.ldap_top_organization||this.config.ldap_base||"",n=await this.server.ldap.search({paged:!1,filter:i,attributes:["dn"],scope:"sub"},r);if(n.searchEntries)for(const s of n.searchEntries){const e="string"==typeof s.dn?s.dn:String(s.dn);t.set(e,{read:!0,write:!0,delete:!0})}}catch(e){this.logger.error(`Failed to refresh permissions for user ${s}: ${e}`)}this.permissionsCache.set(s,{branches:t,timestamp:Date.now()})}async getAuthorizedBranches(s){const e=Date.now(),t=this.permissionsCache.get(s);(!t||e-t.timestamp>=this.cacheTTL)&&await this.refreshUserPermissions(s);const i=this.permissionsCache.get(s);return i?Array.from(i.branches.keys()):[]}findBranchPermissions(s,e){if(s.has(e))return s.get(e)||null;for(const[t,i]of s.entries())if(e.toLowerCase().endsWith(`,${t.toLowerCase()}`))return i;return null}}export{AuthzLinid1 as default};
@@ -0,0 +1 @@
1
+ import e from"../../lib/auth/base.js";class AuthFake extends e{constructor(...e){if(super(...e),this.name="authFake",this.roles=["auth"],"production"===process.env.NODE_ENV)throw new Error(`${this.name}: refused with NODE_ENV=production — it serves every request without authentication`);if(this.user=(this.config.auth_fake_user||"").trim(),!this.user)throw new Error(`${this.name}: --auth-fake-user is required`)}knownIdentities(){return[this.user]}identitySource(){return"req.user and req.userName: --auth-fake-user"}afterLoad(){super.afterLoad(),this.logger.warn(`${this.name}: every request is served as "${this.user}", without authentication`)}authMethod(e,t,r){this.publishIdentity(e,this.user),r()}}export{AuthFake as default};
@@ -1 +1 @@
1
- import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,jsonBody as i,tryMethod as s,ok as a}from"../../lib/expressFormatedResponses.js";import{extractLdapCode as o}from"../../lib/ldapCodes.js";import{transformSchemas as n,asyncHandler as h,isDummyMemberDn as d,escapeDnValue as c,validateDnValue as p,launchHooksChained as m,launchHooks as l,escapeLdapFilter as u,substringSearchFilter as f}from"../../lib/utils.js";import{NotFoundError as g,BadRequestError as b,HttpError as w}from"../../lib/errors.js";import{assertClientMaySet as y,modifiedAttributeNames as $,missingRequiredAttribute as v,checkDnValues as _,matchesPattern as E}from"../../config/schema.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeletedone:async e=>{const t=Array.isArray(e)?e:[e];this.logger.debug(`User deletion detected, removing from groups: ${t.join(", ")}`),await Promise.all(t.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)})}},this.ldap=t.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");this.config.group_schema&&e.readFile(this.config.group_schema,(e,t)=>{if(e)this.logger.error(`Failed to load group schema from ${this.config.group_schema}: ${e}`);else try{this.schema=JSON.parse(n(t.toString(),this.config)),this.logger.debug("Group schema loaded")}catch(e){this.logger.error(`Failed to parse ${this.config.schemas_path}/group.json: ${e}`)}})}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,h(async(e,t)=>{if(!r(e,t))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new b("Invalid match query");if("string"==typeof e.query.attribute&&e.query.attribute)i.filter=f(e.query.match,e.query.attribute);else if(/=/.test(e.query.match)){if(!/^[\w*=()&|, -]+$/.test(e.query.match))throw new b("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=u(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const s=await this.listGroups(i);return a(t,this.hideNeverReturn(s))})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,h(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,h(async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)throw new b("cn is required");const a=i(e,t,"member");if(!a)return;if((Array.isArray(a.member)?a.member:[a.member]).some(e=>"string"!=typeof e))throw new b("member must be a string or string array");await s(t,this.addMember.bind(this),r,a.member)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,h(async(e,t)=>{if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!i||!a)throw new b("cn and member are required");await s(t,this.deleteMember.bind(this),i,a)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,h(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new b("cn is required");const o=i(e,t,"targetOrgDn");if(o){if("string"!=typeof o.targetOrgDn)throw new b("targetOrgDn must be a string");await s(t,this.moveGroup.bind(this),a,o.targetOrgDn,e)}})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,h(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new b("cn is required");const o=i(e,t,"newCn");if(o){if("string"!=typeof o.newCn)throw new b("newCn must be a string");await s(t,this.renameGroup.bind(this),a,o.newCn,e)}}))}isDummyMember(e){return d(e,this.config.group_dummy_user)}withoutDummyMember(e){const t=e.member;if(void 0===t)return e;const r=Array.isArray(t)?t:[t];return{...e,member:r.filter(e=>!this.isDummyMember(e))}}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const e=/,/.test(i)?i:`${this.cn}=${c(i)},${this.base}`,r=await this.ldap.search({paged:!1,scope:"base"},e);if(0===r.searchEntries.length)throw new g("Group not found");t.json(this.hideNeverReturn(this.withoutDummyMember(r.searchEntries[0])))}catch(e){if(e.code&&32===e.code)throw new g("Group not found");throw e}}async apiAdd(e,t,...r){const a=i(e,t,...r);if(!a)return;y(this.schema,Object.keys(a),this.cn);const o=a[this.cn],n=a.member?a.member:[],h=Object.fromEntries(Object.entries(a).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.addGroup.bind(this),o,n,h,e)}async apiDelete(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);if(!i)throw new b("cn is required");await s(t,this.deleteGroup.bind(this),i,e)}async apiModify(e,t){const r=i(e,t);if(!r)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new b("cn is required");y(this.schema,$(r));const o=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.modifyGroup.bind(this),a,o,e)}async addGroup(e,t=[],r={},i){let s;new RegExp(`^${this.cn}=`).test(e)?(s=e,e=e.replace(new RegExp(`^${this.cn}=((?:\\\\.|[^,])+)(?:,.*)?$`),"$1")):(p(e,this.cn),s=`${this.cn}=${c(e)},${this.base}`),await this.validateMembers(s,t),await this.validateNewGroup(s,{objectClass:this.config.group_class,cn:e,member:t,...r});const a=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||a.includes("twakeGroup")||a.includes("twakeStaticGroup")||a.push("twakeGroup");let o,n={objectClass:a,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&n.member.push(...t),[s,n]=await m(this.registeredHooks.ldapgroupadd,[s,n]);try{o=await this.ldap.add(s,n,i)}catch(e){if(e instanceof w)throw e;throw new Error(`Failed to add group ${s}: ${e}`)}return l(this.registeredHooks.ldapgroupadddone,[s,n]),o}async modifyGroup(e,t,r){let i=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;const s=this.opNumber();if([i,t]=await m(this.registeredHooks.ldapgroupmodify,[i,t,s]),t.add){if(t.add.member)throw new Error("Use dedicated API to add members");if(t.add[this.cn])throw new Error(`${this.cn} attribute iq unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new Error("Use dedicated API to delete members");if(t.delete[this.cn])throw new Error(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new Error("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new Error(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new Error("Use dedicated API to replace members");if(t.replace[this.cn])throw new Error(`Use dedicated API to change ${this.cn} attribute`)}await this.validateChanges(i,t);const a=await this.ldap.modify(i,t,r);return l(this.registeredHooks.ldapgroupmodifydone,[i,t,s]),a}async renameGroup(e,t,r){/,/.test(e)||p(e,this.cn),/,/.test(t)||p(t,this.cn);let i=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`,s=/,/.test(t)?t:`${this.cn}=${c(t)},${this.base}`;[i,s]=await m(this.registeredHooks.ldapgrouprename,[i,s]);const a=await this.ldap.rename(i,s,r);return l(this.registeredHooks.ldapgrouprenamedone,[i,s]),a}async deleteGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;r=await m(this.registeredHooks.ldapgroupdelete,r);const i=await this.ldap.delete(r,t);return l(this.registeredHooks.ldapgroupdeletedone,r),i}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await m(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{if(e instanceof w)throw e;throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;if([e,t]=await m(this.registeredHooks.ldapgroupdeletemember,[e,t]),this.isDummyMember(t))throw new b("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{if(e instanceof w)throw e;throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${u(e)}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.ldap.modify(t.dn,{delete:{member:e}}).catch(r=>{if(65===o(r)&&this.config.group_dummy_user)return this.ldap.modify(t.dn,{add:{member:this.config.group_dummy_user},delete:{member:e}});throw r}).catch(r=>{const i=o(r);16!==i&&32!==i&&this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r)})))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0][i];if(!n)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(n===t)throw new Error("Group is already in the target organization");let h;try{h=await this.ldap.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===h.searchEntries.length)throw new Error(`Target organization ${t} not found`);const d=h.searchEntries[0][s];if(!d)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:d}},r),this.logger.info(`Group ${a} moved from ${n} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t}={}){const r=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let i={};for await(const e of r)for(const t of e.searchEntries){const e=t[this.cn];if(Array.isArray(e)&&0===e.length)continue;e&&(i[e]=this.withoutDummyMember(t))}return i=await m(this.registeredHooks._ldapgrouplist,i),i}async searchGroupsByName(e,t=!1,r=[this.cn,"member"]){const i=u(e),s=t?`(${this.cn}=*${i}*)`:`(${this.cn}=${i})`;return await this.listGroups({filter:s,attributes:r})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`)}async validateMembers(e,t){if([e,t]=await m(this.server.hooks.ldapgroupvalidatemembers,[e,t]),!this.config.groups_allow_unexistent_members&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{try{await this.ldap.search({paged:!1,scope:"base"},e)}catch(t){throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await m(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new b(`Invalid value for field ${e}`);const r=v(this.schema,t,this.server.loadedPlugins);if(r)throw new b(`Missing required field ${r}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await m(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))await this._validateOneChange(e,r);if(t.replace)for(const[e,r]of Object.entries(t.replace))await this._validateOneChange(e,r);if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=this.schema.attributes[e];if(!r){if(this.schema.strict)throw new b(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new b(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new b(`Field ${e} must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test&&"pointer"!==r.items.type)for(const i of t)if(typeof i!==r.items.type)throw new b(`Field ${e} must be of type ${r.items.type}`)}else if("pointer"!==r.type&&typeof t!==r.type)return!1;if(await _(e,r,t,async e=>(await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},e)).searchEntries.length>0),!E(r,t))throw new b(`Field ${e} has invalid value ${String(t)}`);return!0}getConfigApiData(){const e=this.config.api_prefix||"/api";let t;if(this.server.loadedPlugins.static&&this.config.group_schema){const e=this.config.static_name||"static",r=this.config.group_schema.indexOf("/schemas/");if(-1!==r){t=`/${e}${this.config.group_schema.substring(r)}`}}return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};
1
+ import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,jsonBody as i,tryMethod as s,ok as a}from"../../lib/expressFormatedResponses.js";import{extractLdapCode as o}from"../../lib/ldapCodes.js";import{transformSchemas as n,asyncHandler as h,isDummyMemberDn as d,escapeDnValue as c,validateDnValue as p,launchHooksChained as m,launchHooks as l,escapeLdapFilter as u,substringSearchFilter as f}from"../../lib/utils.js";import{NotFoundError as g,BadRequestError as b,HttpError as w}from"../../lib/errors.js";import{assertClientMaySet as y,modifiedAttributeNames as $,missingRequiredAttribute as v,checkDnValues as _,matchesPattern as E}from"../../config/schema.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeletedone:async e=>{const t=Array.isArray(e)?e:[e];this.logger.debug(`User deletion detected, removing from groups: ${t.join(", ")}`),await Promise.all(t.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)})}},this.ldap=t.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");this.config.group_schema&&e.readFile(this.config.group_schema,(e,t)=>{if(e)this.logger.error(`Failed to load group schema from ${this.config.group_schema}: ${e}`);else try{this.schema=JSON.parse(n(t.toString(),this.config)),this.logger.debug("Group schema loaded")}catch(e){this.logger.error(`Failed to parse ${this.config.schemas_path}/group.json: ${e}`)}})}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,h(async(e,t)=>{if(!r(e,t))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new b("Invalid match query");if("string"==typeof e.query.attribute&&e.query.attribute)i.filter=f(e.query.match,e.query.attribute);else if(/=/.test(e.query.match)){if(!/^[\w*=()&|, -]+$/.test(e.query.match))throw new b("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=u(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const s=await this.listGroups({...i,req:e});return a(t,this.hideNeverReturn(s))})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,h(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,h(async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)throw new b("cn is required");const a=i(e,t,"member");if(!a)return;if((Array.isArray(a.member)?a.member:[a.member]).some(e=>"string"!=typeof e))throw new b("member must be a string or string array");await s(t,this.addMember.bind(this),r,a.member)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,h(async(e,t)=>{if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!i||!a)throw new b("cn and member are required");await s(t,this.deleteMember.bind(this),i,a)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,h(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new b("cn is required");const o=i(e,t,"targetOrgDn");if(o){if("string"!=typeof o.targetOrgDn)throw new b("targetOrgDn must be a string");await s(t,this.moveGroup.bind(this),a,o.targetOrgDn,e)}})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,h(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new b("cn is required");const o=i(e,t,"newCn");if(o){if("string"!=typeof o.newCn)throw new b("newCn must be a string");await s(t,this.renameGroup.bind(this),a,o.newCn,e)}}))}isDummyMember(e){return d(e,this.config.group_dummy_user)}withoutDummyMember(e){const t=e.member;if(void 0===t)return e;const r=Array.isArray(t)?t:[t];return{...e,member:r.filter(e=>!this.isDummyMember(e))}}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const r=/,/.test(i)?i:`${this.cn}=${c(i)},${this.base}`,s=await this.ldap.search({paged:!1,scope:"base"},r,e);if(0===s.searchEntries.length)throw new g("Group not found");t.json(this.hideNeverReturn(this.withoutDummyMember(s.searchEntries[0])))}catch(e){if(e.code&&32===e.code)throw new g("Group not found");throw e}}async apiAdd(e,t,...r){const a=i(e,t,...r);if(!a)return;y(this.schema,Object.keys(a),this.cn);const o=a[this.cn],n=a.member?a.member:[],h=Object.fromEntries(Object.entries(a).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.addGroup.bind(this),o,n,h,e)}async apiDelete(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);if(!i)throw new b("cn is required");await s(t,this.deleteGroup.bind(this),i,e)}async apiModify(e,t){const r=i(e,t);if(!r)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new b("cn is required");y(this.schema,$(r));const o=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.modifyGroup.bind(this),a,o,e)}async addGroup(e,t=[],r={},i){let s;new RegExp(`^${this.cn}=`).test(e)?(s=e,e=e.replace(new RegExp(`^${this.cn}=((?:\\\\.|[^,])+)(?:,.*)?$`),"$1")):(p(e,this.cn),s=`${this.cn}=${c(e)},${this.base}`),await this.validateMembers(s,t),await this.validateNewGroup(s,{objectClass:this.config.group_class,cn:e,member:t,...r});const a=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||a.includes("twakeGroup")||a.includes("twakeStaticGroup")||a.push("twakeGroup");let o,n={objectClass:a,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&n.member.push(...t),[s,n]=await m(this.registeredHooks.ldapgroupadd,[s,n]);try{o=await this.ldap.add(s,n,i)}catch(e){if(e instanceof w)throw e;throw new Error(`Failed to add group ${s}: ${e}`)}return l(this.registeredHooks.ldapgroupadddone,[s,n]),o}async modifyGroup(e,t,r){let i=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;const s=this.opNumber();if([i,t]=await m(this.registeredHooks.ldapgroupmodify,[i,t,s]),t.add){if(t.add.member)throw new Error("Use dedicated API to add members");if(t.add[this.cn])throw new Error(`${this.cn} attribute iq unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new Error("Use dedicated API to delete members");if(t.delete[this.cn])throw new Error(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new Error("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new Error(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new Error("Use dedicated API to replace members");if(t.replace[this.cn])throw new Error(`Use dedicated API to change ${this.cn} attribute`)}await this.validateChanges(i,t);const a=await this.ldap.modify(i,t,r);return l(this.registeredHooks.ldapgroupmodifydone,[i,t,s]),a}async renameGroup(e,t,r){/,/.test(e)||p(e,this.cn),/,/.test(t)||p(t,this.cn);let i=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`,s=/,/.test(t)?t:`${this.cn}=${c(t)},${this.base}`;[i,s]=await m(this.registeredHooks.ldapgrouprename,[i,s]);const a=await this.ldap.rename(i,s,r);return l(this.registeredHooks.ldapgrouprenamedone,[i,s]),a}async deleteGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;r=await m(this.registeredHooks.ldapgroupdelete,r);const i=await this.ldap.delete(r,t);return l(this.registeredHooks.ldapgroupdeletedone,r),i}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await m(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{if(e instanceof w)throw e;throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;if([e,t]=await m(this.registeredHooks.ldapgroupdeletemember,[e,t]),this.isDummyMember(t))throw new b("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{if(e instanceof w)throw e;throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${u(e)}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.ldap.modify(t.dn,{delete:{member:e}}).catch(r=>{if(65===o(r)&&this.config.group_dummy_user)return this.ldap.modify(t.dn,{add:{member:this.config.group_dummy_user},delete:{member:e}});throw r}).catch(r=>{const i=o(r);16!==i&&32!==i&&this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r)})))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0][i];if(!n)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(n===t)throw new Error("Group is already in the target organization");let h;try{h=await this.ldap.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===h.searchEntries.length)throw new Error(`Target organization ${t} not found`);const d=h.searchEntries[0][s];if(!d)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:d}},r),this.logger.info(`Group ${a} moved from ${n} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t,req:r}={}){const i=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base,r).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let s={};for await(const e of i)for(const t of e.searchEntries){const e=t[this.cn];if(Array.isArray(e)&&0===e.length)continue;e&&(s[e]=this.withoutDummyMember(t))}return s=await m(this.registeredHooks._ldapgrouplist,s),s}async searchGroupsByName(e,t=!1,r=[this.cn,"member"],i){const s=u(e),a=t?`(${this.cn}=*${s}*)`:`(${this.cn}=${s})`;return await this.listGroups({filter:a,attributes:r,req:i})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`)}async validateMembers(e,t){if([e,t]=await m(this.server.hooks.ldapgroupvalidatemembers,[e,t]),!this.config.groups_allow_unexistent_members&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{try{await this.ldap.search({paged:!1,scope:"base"},e)}catch(t){throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await m(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new b(`Invalid value for field ${e}`);const r=v(this.schema,t,this.server.loadedPlugins);if(r)throw new b(`Missing required field ${r}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await m(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))await this._validateOneChange(e,r);if(t.replace)for(const[e,r]of Object.entries(t.replace))await this._validateOneChange(e,r);if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=this.schema.attributes[e];if(!r){if(this.schema.strict)throw new b(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new b(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new b(`Field ${e} must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test&&"pointer"!==r.items.type)for(const i of t)if(typeof i!==r.items.type)throw new b(`Field ${e} must be of type ${r.items.type}`)}else if("pointer"!==r.type&&typeof t!==r.type)return!1;if(await _(e,r,t,async e=>(await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},e)).searchEntries.length>0),!E(r,t))throw new b(`Field ${e} has invalid value ${String(t)}`);return!0}getConfigApiData(){const e=this.config.api_prefix||"/api";let t;if(this.server.loadedPlugins.static&&this.config.group_schema){const e=this.config.static_name||"static",r=this.config.group_schema.indexOf("/schemas/");if(-1!==r){t=`/${e}${this.config.group_schema.substring(r)}`}}return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};
@@ -1 +1 @@
1
- import t from"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 y,ConflictError as b}from"../../lib/errors.js";import{extractLdapCode as _}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,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 y(`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=t.objectClass.map(t=>t.toLowerCase());return this.config.ldap_organization_class.filter(t=>"top"!==t.toLowerCase()).some(t=>i.includes(t.toLowerCase()))}async getOrganisationTop(t){if(!this.config.ldap_top_organization)throw new 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 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 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 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 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 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 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}from"../../lib/utils.js";import{BadRequestError as z,NotFoundError as y,ConflictError as b}from"../../lib/errors.js";import{extractLdapCode as _}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,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 y(`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 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 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 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 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 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 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 +1 @@
1
- var e=["core/storage","core/auth/trustedProxy","core/weblogs","core/auth/crowdsec","core/auth/rateLimit","core/auth/token","core/auth/llng","core/auth/openidconnect","core/auth/authzPerRoute"];export{e as default};
1
+ var e=["core/storage","core/auth/trustedProxy","core/weblogs","core/auth/crowdsec","core/auth/rateLimit","core/auth/token","core/auth/fake","core/auth/llng","core/auth/openidconnect","core/auth/authzPerRoute"];export{e as default};
@@ -87,6 +87,7 @@ export interface Config {
87
87
  llng_ini?: string;
88
88
  llng_username_header?: string;
89
89
  auth_token?: string[];
90
+ auth_fake_user?: string;
90
91
  auth_totp?: string[];
91
92
  auth_totp_window?: number;
92
93
  auth_totp_step?: number;
@@ -104,9 +105,10 @@ export interface Config {
104
105
  * default: where a branch is a tenant rather than a department, opening a
105
106
  * listing across branches is opening a door between customers.
106
107
  *
107
- * Honoured by the plugins built on `AuthzBase` — `authzPerBranch` and
108
- * `authzLinid1`. `authzDynamic` extends `AuthBase` and registers its own
109
- * hooks, so it never sees the filter and this option does nothing there.
108
+ * Honoured by `authzPerBranch`. `authzLinid1` always works this way, its
109
+ * branches being organizations. `authzDynamic` extends `AuthBase` and
110
+ * registers its own hooks, so it never sees the filter and this option does
111
+ * nothing there.
110
112
  */
111
113
  authz_filter_attached_entries?: boolean;
112
114
  authz_unresolved_user?: string;
@@ -52,6 +52,12 @@ export default abstract class AuthzBase extends DmPlugin {
52
52
  * Handles escaped commas in DN values (e.g., "cn=Smith\, John")
53
53
  */
54
54
  extractBranchDn(dn: string): string;
55
+ /**
56
+ * Whether an entry attached to an organization is judged by that
57
+ * organization rather than by the branch it is stored in:
58
+ * `--authz-filter-attached-entries`, off by default.
59
+ */
60
+ protected filtersAttachedEntries(): boolean;
55
61
  /**
56
62
  * The branch a write is judged against.
57
63
  *
@@ -17,6 +17,14 @@ export default class AuthzLinid1 extends AuthzBase {
17
17
  name: string;
18
18
  permissionsCache: Map<string, CachedPermissions>;
19
19
  constructor(server: DM);
20
+ /**
21
+ * Always: the branches this plugin grants are organizations, and what an
22
+ * administrator manages in them is the accounts and groups whose
23
+ * organization link names them, wherever they are stored. Judged by their
24
+ * parent instead, a flat `ou=users` is nobody's branch, and every
25
+ * administrator is refused every account.
26
+ */
27
+ protected filtersAttachedEntries(): boolean;
20
28
  /**
21
29
  * Resolve user - for authzLinid1, convert uid to userDn via LDAP
22
30
  */
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @module plugins/auth/fake
3
+ * @author Xavier Guimard <xguimard@linagora.com>
4
+ *
5
+ * Development authentication: every request is served as the identity given
6
+ * on the command line, without asking for anything.
7
+ * @group Plugins
8
+ */
9
+ import type { Response } from 'express';
10
+ import AuthBase, { type DmRequest } from '../../lib/auth/base';
11
+ import type { Role } from '../../abstract/plugin';
12
+ export default class AuthFake extends AuthBase {
13
+ name: string;
14
+ roles: Role[];
15
+ private readonly user;
16
+ constructor(...args: ConstructorParameters<typeof AuthBase>);
17
+ protected knownIdentities(): string[];
18
+ protected identitySource(): string;
19
+ afterLoad(): void;
20
+ authMethod(req: DmRequest, _res: Response, next: () => void): void;
21
+ }
@@ -148,23 +148,26 @@ export default class LdapGroups extends DmPlugin {
148
148
  }>;
149
149
  /**
150
150
  * List groups from LDAP
151
- * @param param0 {filter, attributes}:
151
+ * @param param0 {filter, attributes, req}:
152
152
  * - filter: LDAP filter (default '(objectClass=*)')
153
153
  * - attributes: attributes to fetch (default ['cn','member'])
154
+ * - req: incoming request, forwarded to the authorization hooks
154
155
  * @returns LdapList (Record<string, AttributesList>)
155
156
  */
156
- listGroups({ filter, attributes, }?: {
157
+ listGroups({ filter, attributes, req, }?: {
157
158
  filter?: string;
158
159
  attributes?: string[];
160
+ req?: Request;
159
161
  }): Promise<LdapList>;
160
162
  /**
161
163
  * Simple formatter for listGroups()
162
164
  * @param cn main attribute value (partial or full)
163
165
  * @param partial boolean, true for partial search (default false)
164
166
  * @param attributes array of attributes to return
167
+ * @param req incoming request, forwarded to the authorization hooks
165
168
  * @returns LdapList (means Record<string, AttributesList> where key is the --ldap-user-main-attribute value [default: cn])
166
169
  */
167
- searchGroupsByName(cn: string, partial?: boolean, attributes?: string[]): Promise<LdapList>;
170
+ searchGroupsByName(cn: string, partial?: boolean, attributes?: string[], req?: Request): Promise<LdapList>;
168
171
  protected fixDn(dn: string): string | false;
169
172
  /**
170
173
  * Verify that each member exists in LDAP
@@ -25,6 +25,7 @@ All LDAP-Rest configuration options.
25
25
  - [core/auth/hmac](#coreauthhmac)
26
26
  - [core/auth/llng](#coreauthllng)
27
27
  - [core/auth/openidconnect](#coreauthopenidconnect)
28
+ - [core/auth/fake](#coreauthfake)
28
29
  - [core/bcl](#corebcl)
29
30
  - [Authorization Plugins](#authorization-plugins)
30
31
  - [Common to every authorization plugin](#common-to-every-authorization-plugin)
@@ -238,6 +239,14 @@ Empty means the plugin guards every path. Scoping several instances to different
238
239
  | -------------- | --------------- | ---------------- | ------- | --------------------- |
239
240
  | `--auth-token` | `--auth-tokens` | `DM_AUTH_TOKENS` | `[]` | Authentication tokens |
240
241
 
242
+ #### `core/auth/fake`
243
+
244
+ | CLI | Env | Default | Description |
245
+ | ------------------ | ------------------- | ------- | ---------------------------------------------- |
246
+ | `--auth-fake-user` | `DM_AUTH_FAKE_USER` | | Identity every request is served as (required) |
247
+
248
+ Development only, refused with `NODE_ENV=production` — see [Fake Authentication](plugins/auth/fake.md).
249
+
241
250
  #### `core/auth/totp`
242
251
 
243
252
  | CLI | Plural | Env | Default | Description |
@@ -324,11 +333,11 @@ whichever of the two names it selects. A value other than `req.user` or
324
333
 
325
334
  #### `core/auth/authzPerBranch`
326
335
 
327
- | CLI | Env | Default | Description |
328
- | --------------------------------- | ---------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
329
- | `--authz-per-branch-config` | `DM_AUTHZ_PER_BRANCH_CONFIG` | `{default:{read:true,write:false,delete:false}}` | Authorization config (JSON) |
330
- | `--authz-per-branch-cache-ttl` | `DM_AUTHZ_PER_BRANCH_CACHE_TTL` | `60` | Cache TTL (seconds) |
331
- | `--authz-filter-attached-entries` | `DM_AUTHZ_FILTER_ATTACHED_ENTRIES` | `false` | Judge an attached entry by the organization it hangs off, and filter listings accordingly. Honoured by `authzPerBranch` and `authzLinid1`; does nothing under `authzDynamic`. |
336
+ | CLI | Env | Default | Description |
337
+ | --------------------------------- | ---------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
338
+ | `--authz-per-branch-config` | `DM_AUTHZ_PER_BRANCH_CONFIG` | `{default:{read:true,write:false,delete:false}}` | Authorization config (JSON) |
339
+ | `--authz-per-branch-cache-ttl` | `DM_AUTHZ_PER_BRANCH_CACHE_TTL` | `60` | Cache TTL (seconds) |
340
+ | `--authz-filter-attached-entries` | `DM_AUTHZ_FILTER_ATTACHED_ENTRIES` | `false` | Judge an attached entry by the organization it hangs off, and filter listings accordingly. Honoured by `authzPerBranch`; always on under `authzLinid1`; does nothing under `authzDynamic`. |
332
341
 
333
342
  #### `core/auth/authzScope`
334
343
 
@@ -11,6 +11,7 @@ LDAP-Rest provides multiple authentication plugins to secure API access. These p
11
11
  | [HMAC](hmac.md) | `core/auth/hmac` | HMAC-SHA256 request signing for backend services |
12
12
  | [LemonLDAP::NG](llng.md) | `core/auth/llng` | Integration with LemonLDAP::NG SSO |
13
13
  | [OpenID Connect](oidc.md) | `core/auth/openidconnect` | OAuth 2.0 / OpenID Connect authentication |
14
+ | [Fake](fake.md) | `core/auth/fake` | Development only: a fixed identity, no login |
14
15
 
15
16
  ## What a rule is keyed on
16
17
 
@@ -25,6 +26,7 @@ of what a caller is called. Every one of them publishes the caller twice:
25
26
  | `core/auth/llng` | LLNG's `whatToTrace` | `--llng-username-header`, or the same |
26
27
  | `core/auth/openidconnect` | the OIDC `sub` | `--oidc-username-claim` (default: `sub`) |
27
28
  | `core/auth/authzDynamic` | the token's tenant | the same |
29
+ | `core/auth/fake` | `--auth-fake-user` | the same |
28
30
 
29
31
  `req.user` is this server's identifier for the caller; `req.userName` is the
30
32
  caller under a name a person would use. `--authz-identity` says which one is
@@ -48,6 +48,24 @@ twakeDepartmentLink: ou=HR,ou=organization,dc=example,dc=com
48
48
 
49
49
  The HR admin (listed in `ou=HR`'s `twakeLocalAdminLink`) can manage John even though he's in `ou=users` because his `twakeDepartmentLink` points to the HR organization.
50
50
 
51
+ ### What an administrator reads
52
+
53
+ `authzLinid1` always judges an attached entry this way, as
54
+ `--authz-filter-attached-entries` does for `authzPerBranch`, whatever that
55
+ option says: judged by its parent instead, an account in a flat `ou=users`
56
+ belongs to nobody's branch, and no administrator could list or change one.
57
+
58
+ - Listings of accounts and groups drop the entries attached to an
59
+ organization outside the caller's branches. A single read of one answers
60
+ `404`.
61
+ - Entries attached to nothing — the organizations, the nomenclature values, a
62
+ group or an account with no `twakeDepartmentLink` — are readable by every
63
+ administrator: the organization tree is reference data. Writes to them are
64
+ judged on their parent. The tree a console draws still starts at the
65
+ caller's own branches: see [getOrganisationTop Hook](#getorganisationtop-hook).
66
+ - A caller named in no `twakeLocalAdminLink` is refused every read (`403`)
67
+ but the top organization's own entry, which `organizations/top` answers.
68
+
51
69
  ## Configuration
52
70
 
53
71
  No configuration file needed - permissions are read from LDAP. Simply load the plugin:
@@ -179,8 +197,9 @@ Intercepts all LDAP search operations:
179
197
  2. Resolves user's DN from username
180
198
  3. Allows base scope searches on top organization (for navigation)
181
199
  4. Allows searches for `twakeLocalAdminLink` (for permission refresh)
182
- 5. Verifies user has read permission for search base
183
- 6. Throws error if access denied
200
+ 5. Refuses a caller who administers no branch; lets the others search, and
201
+ drops from the answer the entries attached outside their branches
202
+ (`ldapsearchfilter`)
184
203
 
185
204
  **Special allowances:**
186
205
 
@@ -461,31 +480,6 @@ User jdoe not found in LDAP
461
480
 
462
481
  3. Verify authentication plugin sets `req.user` correctly
463
482
 
464
- ### Problem: Cannot Search Top Organization
465
-
466
- **Symptoms:**
467
-
468
- ```json
469
- {
470
- "error": "User admin does not have read permission for branch ou=organization,dc=example,dc=com"
471
- }
472
- ```
473
-
474
- **Solutions:**
475
-
476
- This is expected behavior - users can only see organizations they manage. To grant access to top organization:
477
-
478
- ```bash
479
- ldapsearch -x -b "ou=organization,dc=example,dc=com" -s base "(objectClass=*)"
480
- # Modify to add user:
481
- ldapmodify <<EOF
482
- dn: ou=organization,dc=example,dc=com
483
- changetype: modify
484
- add: twakeLocalAdminLink
485
- twakeLocalAdminLink: uid=admin,ou=users,dc=example,dc=com
486
- EOF
487
- ```
488
-
489
483
  ## Integration Examples
490
484
 
491
485
  ### With Token Authentication
@@ -0,0 +1,28 @@
1
+ # Fake Authentication
2
+
3
+ For development only: every request is served as the identity given on the
4
+ command line, without asking for anything. Restarting with another
5
+ `--auth-fake-user` shows an interface as another administrator sees it.
6
+
7
+ ```bash
8
+ npx ldap-rest \
9
+ --plugin core/auth/fake --auth-fake-user alice \
10
+ --plugin core/auth/authzLinid1 \
11
+ ...
12
+ ```
13
+
14
+ **Environment Variable:** `DM_AUTH_FAKE_USER=alice`
15
+
16
+ The identity goes to `req.user` and `req.userName` alike, so the
17
+ authorization plugins judge it as they would the same login coming from a
18
+ real authenticator.
19
+
20
+ ## Safeguards
21
+
22
+ - The server refuses to start when `NODE_ENV` is `production`, as it is in
23
+ the Docker image.
24
+ - It refuses to start without `--auth-fake-user`.
25
+ - A warning naming the identity is logged at every start.
26
+
27
+ Loaded without an authorization plugin, it grants everything to anyone who
28
+ reaches the port. Listen on a local address only.
@@ -4,6 +4,47 @@ 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.11.0
8
+
9
+ ### Group reads are judged
10
+
11
+ **Who is affected:** a deployment loading `core/ldap/groups` with an
12
+ authorization plugin.
13
+
14
+ Listing the groups and reading one went to the directory without the
15
+ request, so no authorization plugin saw them: any authenticated caller read
16
+ every group with its members. They are judged like any other read now.
17
+ Under `core/auth/authzPerBranch` without `--authz-filter-attached-entries`,
18
+ that means a `read` grant on the group base (`ou=groups`, or
19
+ `--ldap-group-base`): **a caller without one is answered `403`** where the
20
+ list used to come back.
21
+
22
+ ### authzLinid1 judges accounts and groups by their organization
23
+
24
+ **Who is affected:** every deployment using `core/auth/authzLinid1`.
25
+
26
+ It now works as with `--authz-filter-attached-entries`, whatever the option
27
+ says. Without it, a flat `ou=users` was nobody's branch: no administrator
28
+ could list or change an account.
29
+
30
+ - An administrator lists the accounts and groups attached to their branches,
31
+ and those attached to none.
32
+ - **The organization tree is readable by every administrator**, not only their
33
+ own branches: an organization is reference data. `organizations/top` still
34
+ answers their own branches, so a console starts there. Writes to the tree
35
+ are judged as before.
36
+ - **A caller named in no `twakeLocalAdminLink` is refused every read** but the
37
+ top organization's own entry.
38
+ - An account stored under the organization tree without a
39
+ `twakeDepartmentLink` becomes readable by every administrator. **Where
40
+ accounts sit under the tree, give them their link before upgrading.** This
41
+ lists the accounts that have none, under `twakeDepartmentLink: missing`:
42
+
43
+ ```bash
44
+ npm run audit:directory -- --schema static/schemas/twake/users.json \
45
+ --base ou=users,<base> --filter '(!(twakeDepartmentLink=*))'
46
+ ```
47
+
7
48
  ## To 0.10.0
8
49
 
9
50
  ### LDAP base is now required
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-rest",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Light directory manager",
5
5
  "keywords": [
6
6
  "Identity-Manager"
@@ -64,6 +64,10 @@
64
64
  "types": "./dist/src/plugins/auth/crowdsec.d.ts",
65
65
  "import": "./dist/plugins/auth/crowdsec.js"
66
66
  },
67
+ "./plugin-auth-fake": {
68
+ "types": "./dist/src/plugins/auth/fake.d.ts",
69
+ "import": "./dist/plugins/auth/fake.js"
70
+ },
67
71
  "./plugin-auth-hmac": {
68
72
  "types": "./dist/src/plugins/auth/hmac.d.ts",
69
73
  "import": "./dist/plugins/auth/hmac.js"