ldap-rest 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abstract/twakePlugin.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/authz/base.js +1 -1
- package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/passwordPolicy.js +1 -1
- package/dist/plugins/twake/james.js +1 -1
- package/dist/src/config/args.d.ts +1 -0
- package/dist/src/lib/authz/base.d.ts +42 -0
- package/dist/src/plugins/ldap/externalUsersInGroups.d.ts +11 -0
- package/dist/src/plugins/ldap/groups.d.ts +34 -3
- package/dist/src/plugins/ldap/passwordPolicy.d.ts +5 -0
- package/dist/src/plugins/twake/james.d.ts +54 -3
- package/docs/client-development/api/rest-api.md +2 -0
- package/docs/usage/configuration.md +13 -12
- package/docs/usage/plugins/auth/authz-linid1.md +21 -0
- package/docs/usage/plugins/integrations/james-mail.md +67 -10
- package/docs/usage/plugins/integrations/james-mailboxes.md +14 -7
- package/docs/usage/plugins/ldap/external-users.md +23 -2
- package/docs/usage/upgrading.md +18 -0
- package/package.json +1 -1
- package/static/schemas/twake/groups.json +34 -17
- package/static/schemas/twake/nomenclature/twakeAccountStatus.json +16 -8
- package/static/schemas/twake/nomenclature/twakeDeliveryMode.json +14 -7
- package/static/schemas/twake/nomenclature/twakeDomain.json +14 -7
- package/static/schemas/twake/nomenclature/twakeListType.json +14 -7
- package/static/schemas/twake/nomenclature/twakeMailboxType.json +14 -7
- package/static/schemas/twake/nomenclature/twakeTitle.json +24 -12
- package/static/schemas/twake/organizations.json +26 -13
- package/static/schemas/twake/positions.json +10 -5
- package/static/schemas/twake/users.json +76 -38
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node-fetch";import e from"p-limit";import r from"./plugin.js";class TwakePlugin extends r{constructor(t,r,s,i){if(super(t),this.retryAttempts=1,this.retryDelay=0,this.webadminUrl=this.config[r]||"",this.webadminToken=this.config[s]||"",!this.webadminUrl)throw new Error(`Twake plugin: ${r} is required`);this.webadminToken||this.logger.warn(`Twake plugin: No authentication token configured (${s}) - requests will be unauthenticated`);const a="number"==typeof this.config[i]?this.config[i]:10;this.requestLimit=e(a),this.logger.info(`Twake plugin HTTP request concurrency limit set to ${a}`),this.mailAttr=this.config.mail_attribute||"mail",this.displayNameAttr=this.config.display_name_attribute||"displayName"}createHeaders(t){const e={};return this.webadminToken&&(e.Authorization=`Bearer ${this.webadminToken}`),t&&(e["Content-Type"]=t),e}async callWebAdminApi(e,r,s,i,a,n,o=[]){const u={plugin:this.name,event:e,result:"error",dn:i,...n},l={method:s,headers:this.createHeaders(a?"application/json":void 0)};a&&(l.body=a);for(let s=1;;s++){try{const i=await this.requestLimit(()=>t(r,l));if(i.ok)this.logger.info({...u,result:"success",http_status:i.status,url:r});else{if(this.shouldRetry(i.status,o,s)){this.logger.warn({...u,result:"retry",attempt:s,http_status:i.status,http_status_text:i.statusText,url:r}),await this.waitBeforeRetry(s);continue}this.shouldIgnoreError(i.status,e)?this.logger.debug({...u,result:"ignored",http_status:i.status,http_status_text:i.statusText,url:r}):this.logger.error({...u,...s>1?{attempts:s}:{},http_status:i.status,http_status_text:i.statusText,url:r})}}catch(t){this.logger.error({...u,error:t,url:r})}return}}shouldRetry(t,e,r){return r<this.retryAttempts&&e.includes(t)}waitBeforeRetry(t){return new Promise(e=>setTimeout(e,this.retryDelay*t))}shouldIgnoreError(t,e){return 409===t
|
|
1
|
+
import t from"node-fetch";import e from"p-limit";import r from"./plugin.js";class TwakePlugin extends r{constructor(t,r,s,i){if(super(t),this.retryAttempts=1,this.retryDelay=0,this.webadminUrl=this.config[r]||"",this.webadminToken=this.config[s]||"",!this.webadminUrl)throw new Error(`Twake plugin: ${r} is required`);this.webadminToken||this.logger.warn(`Twake plugin: No authentication token configured (${s}) - requests will be unauthenticated`);const a="number"==typeof this.config[i]?this.config[i]:10;this.requestLimit=e(a),this.logger.info(`Twake plugin HTTP request concurrency limit set to ${a}`),this.mailAttr=this.config.mail_attribute||"mail",this.displayNameAttr=this.config.display_name_attribute||"displayName"}createHeaders(t){const e={};return this.webadminToken&&(e.Authorization=`Bearer ${this.webadminToken}`),t&&(e["Content-Type"]=t),e}async callWebAdminApi(e,r,s,i,a,n,o=[]){const u={plugin:this.name,event:e,result:"error",dn:i,...n},l={method:s,headers:this.createHeaders(a?"application/json":void 0)};a&&(l.body=a);for(let s=1;;s++){try{const i=await this.requestLimit(()=>t(r,l));if(i.ok)this.logger.info({...u,result:"success",http_status:i.status,url:r});else{if(this.shouldRetry(i.status,o,s)){this.logger.warn({...u,result:"retry",attempt:s,http_status:i.status,http_status_text:i.statusText,url:r}),await this.waitBeforeRetry(s);continue}this.shouldIgnoreError(i.status,e)?this.logger.debug({...u,result:"ignored",http_status:i.status,http_status_text:i.statusText,url:r}):this.logger.error({...u,...s>1?{attempts:s}:{},http_status:i.status,http_status_text:i.statusText,url:r})}}catch(t){this.logger.error({...u,error:t,url:r})}return}}shouldRetry(t,e,r){return r<this.retryAttempts&&e.includes(t)}waitBeforeRetry(t){return new Promise(e=>setTimeout(e,this.retryDelay*t))}shouldIgnoreError(t,e){return 409===t&&/alias/i.test(e)}attributeToString(t){return t?Array.isArray(t)?t.length>0?String(t[0]):null:String(t):null}async ldapGetAttributes(t,e){try{const r=e&&e.length>0?e:void 0,s=await this.server.ldap.search({paged:!1,scope:"base",attributes:r},t);return s.searchEntries&&s.searchEntries.length>0?s.searchEntries[0]:null}catch(e){return this.logger.debug(`Could not fetch attributes from DN ${t}: ${e}`),null}}getAliases(t){if(!t)return[];return(Array.isArray(t)?t:[t]).map(t=>Buffer.isBuffer(t)?t.toString("utf-8"):String(t)).map(t=>this.normalizeAlias(t))}normalizeAlias(t){return t.toLowerCase().startsWith("smtp:")?t.substring(5):t}extractMailDomain(t){const e=t.split("@");return 2===e.length?e[1]:null}extractDomainFromDn(t){const e=t.match(/dc=([^,]+)/gi);return e?e.map(t=>t.substring(3)).join("."):"example.com"}}export{TwakePlugin,TwakePlugin as default};
|
package/dist/config/args.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fileURLToPath as e}from"url";import{join as _,dirname as a}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins","identifiers"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains","identifiers"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls","identifiers",!0],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",_(a(e(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuotaSize"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes","identifiers"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes","identifiers"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--organization-schema","DM_ORGANIZATION_SCHEMA",""],["--enterprise-domain-name-attribute","DM_ENTERPRISE_DOMAIN_NAME_ATTRIBUTE","associatedDomain"],["--enterprise-domain-link-attribute","DM_ENTERPRISE_DOMAIN_LINK_ATTRIBUTE",""],["--storage-backend","DM_STORAGE_BACKEND",""],["--storage-sweep-interval","DM_STORAGE_SWEEP_INTERVAL",600,"number"],["--storage-ldap-base","DM_STORAGE_LDAP_BASE",""],["--storage-ldap-object-class","DM_STORAGE_LDAP_OBJECT_CLASS","applicationProcess"],["--storage-file-directory","DM_STORAGE_FILE_DIRECTORY",""],["--storage-postgres-url","DM_STORAGE_POSTGRES_URL",""],["--storage-postgres-table","DM_STORAGE_POSTGRES_TABLE","ldap_rest_storage"],["--storage-valkey-url","DM_STORAGE_VALKEY_URL",""],["--storage-valkey-prefix","DM_STORAGE_VALKEY_PREFIX","ldap-rest:"],["--bcl-retention","DM_BCL_RETENTION",604800,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes","identifiers"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",_(a(e(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes","identifiers"],["--static-path","DM_STATIC_PATH",_(a(e(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-flat-auto-repair","DM_LDAP_FLAT_AUTO_REPAIR",!0,"boolean"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases","dns"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes","identifiers"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-retry-attempts","DM_JAMES_RETRY_ATTEMPTS",5,"number"],["--james-retry-delay","DM_JAMES_RETRY_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches","dns"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--twake-lifecycle-role-attribute","DM_TWAKE_LIFECYCLE_ROLE_ATTRIBUTE",""],["--twake-lifecycle-lock-attribute","DM_TWAKE_LIFECYCLE_LOCK_ATTRIBUTE",""],["--twake-lifecycle-lock-value","DM_TWAKE_LIFECYCLE_LOCK_VALUE",""],["--twake-lifecycle-deleted-attribute","DM_TWAKE_LIFECYCLE_DELETED_ATTRIBUTE",""],["--twake-lifecycle-deleted-value","DM_TWAKE_LIFECYCLE_DELETED_VALUE","TRUE"],["--twake-lifecycle-deleted-at-attribute","DM_TWAKE_LIFECYCLE_DELETED_AT_ATTRIBUTE",""],["--twake-lifecycle-deleted-at-format","DM_TWAKE_LIFECYCLE_DELETED_AT_FORMAT","iso8601"],["--twake-lifecycle-reason-attribute","DM_TWAKE_LIFECYCLE_REASON_ATTRIBUTE",""],["--twake-lifecycle-rules","DM_TWAKE_LIFECYCLE_RULES",""],["--twake-lifecycle-member-attribute","DM_TWAKE_LIFECYCLE_MEMBER_ATTRIBUTE","member"],["--twake-group-base","DM_TWAKE_GROUP_BASE",""],["--twake-group-user-base","DM_TWAKE_GROUP_USER_BASE",""],["--twake-group-organization-dn","DM_TWAKE_GROUP_ORGANIZATION_DN",""],["--twake-group-organization-status-attribute","DM_TWAKE_GROUP_ORGANIZATION_STATUS_ATTRIBUTE","twakeOrgStatus"],["--twake-group-organization-deleted-value","DM_TWAKE_GROUP_ORGANIZATION_DELETED_VALUE","deleted"],["--twake-group-display-name-attribute","DM_TWAKE_GROUP_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-group-color-attribute","DM_TWAKE_GROUP_COLOR_ATTRIBUTE","twakeGroupColor"],["--twake-group-created-at-attribute","DM_TWAKE_GROUP_CREATED_AT_ATTRIBUTE","twakeCreatedAt"],["--twake-group-max-page-limit","DM_TWAKE_GROUP_MAX_PAGE_LIMIT",1e3,"number"],["--twake-group-member-fields","DM_TWAKE_GROUP_MEMBER_FIELDS",{},"json"],["--twake-space-base","DM_TWAKE_SPACE_BASE",""],["--twake-space-class","DM_TWAKE_SPACE_CLASS",["top","twakeSpace"],"array",null,"identifiers"],["--twake-space-display-name-attribute","DM_TWAKE_SPACE_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-space-admin-attribute","DM_TWAKE_SPACE_ADMIN_ATTRIBUTE","twakeSpaceAdmin"],["--twake-space-editor-attribute","DM_TWAKE_SPACE_EDITOR_ATTRIBUTE","twakeSpaceEditor"],["--twake-space-viewer-attribute","DM_TWAKE_SPACE_VIEWER_ATTRIBUTE","twakeSpaceViewer"],["--twake-space-user-role-attribute","DM_TWAKE_SPACE_USER_ROLE_ATTRIBUTE",""],["--twake-space-exchange","DM_TWAKE_SPACE_EXCHANGE","space"],["--twake-space-sync-queue","DM_TWAKE_SPACE_SYNC_QUEUE","twake.space.sync.requested.ldap-rest"],["--twake-tombstone-dn","DM_TWAKE_TOMBSTONE_DN",[],"array",null,"dns"],["--twake-tombstone-default-reason","DM_TWAKE_TOMBSTONE_DEFAULT_REASON","deleted"],["--twake-tombstone-reason-header","DM_TWAKE_TOMBSTONE_REASON_HEADER","x-deletion-reason"],["--twake-tombstone-reasons","DM_TWAKE_TOMBSTONE_REASONS",[],"array",null,"identifiers"],["--twake-tombstone-clear-attributes","DM_TWAKE_TOMBSTONE_CLEAR_ATTRIBUTES",[],"array",null,"identifiers"],["--twake-tombstone-erase-min-age","DM_TWAKE_TOMBSTONE_ERASE_MIN_AGE",2592e3,"number"],["--twake-tombstone-group-bases","DM_TWAKE_TOMBSTONE_GROUP_BASES",[],"array",null,"dns"],["--twake-instance-dn","DM_TWAKE_INSTANCE_DN",[],"array",null,"dns"],["--twake-instance-skip-attribute","DM_TWAKE_INSTANCE_SKIP_ATTRIBUTE",""],["--twake-instance-skip-value","DM_TWAKE_INSTANCE_SKIP_VALUE",""],["--twake-instance-provider","DM_TWAKE_INSTANCE_PROVIDER","cloudery"],["--twake-instance-id","DM_TWAKE_INSTANCE_ID","{uid}"],["--twake-instance-fqdn-attribute","DM_TWAKE_INSTANCE_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--twake-instance-sent-attribute","DM_TWAKE_INSTANCE_SENT_ATTRIBUTE","twakeCreatedEventAt"],["--twake-instance-locale","DM_TWAKE_INSTANCE_LOCALE",""],["--twake-instance-timeout","DM_TWAKE_INSTANCE_TIMEOUT",3e4,"number"],["--twake-instance-cloudery-url","DM_TWAKE_INSTANCE_CLOUDERY_URL",""],["--twake-instance-cloudery-token","DM_TWAKE_INSTANCE_CLOUDERY_TOKEN",""],["--twake-instance-cloudery-offer","DM_TWAKE_INSTANCE_CLOUDERY_OFFER","b2b_twake_default"],["--twake-instance-cloudery-domain","DM_TWAKE_INSTANCE_CLOUDERY_DOMAIN",""],["--twake-instance-cloudery-organization-offer","DM_TWAKE_INSTANCE_CLOUDERY_ORGANIZATION_OFFER",""],["--twake-instance-cozy-url","DM_TWAKE_INSTANCE_COZY_URL",""],["--twake-instance-cozy-user","DM_TWAKE_INSTANCE_COZY_USER","admin"],["--twake-instance-cozy-passphrase","DM_TWAKE_INSTANCE_COZY_PASSPHRASE",""],["--twake-instance-cozy-domain","DM_TWAKE_INSTANCE_COZY_DOMAIN",""],["--twake-instance-cozy-context","DM_TWAKE_INSTANCE_COZY_CONTEXT","default"],["--twake-instance-cozy-apps","DM_TWAKE_INSTANCE_COZY_APPS","home,drive,settings,notes,dataproxy"],["--twake-instance-cozy-org-id","DM_TWAKE_INSTANCE_COZY_ORG_ID",""],["--twake-instance-cozy-org-domain","DM_TWAKE_INSTANCE_COZY_ORG_DOMAIN",""],["--twake-instance-auth-exchange","DM_TWAKE_INSTANCE_AUTH_EXCHANGE","auth"],["--twake-instance-b2b-exchange","DM_TWAKE_INSTANCE_B2B_EXCHANGE","b2b"],["--twake-instance-user-created-key","DM_TWAKE_INSTANCE_USER_CREATED_KEY","user.created"],["--twake-instance-organization-created-key","DM_TWAKE_INSTANCE_ORGANIZATION_CREATED_KEY","organization.created"],["--twake-instance-workplace-created-key","DM_TWAKE_INSTANCE_WORKPLACE_CREATED_KEY","workplace.created"],["--twake-instance-queue","DM_TWAKE_INSTANCE_QUEUE","workplace.created.ldap-rest"],["--twake-instance-organization-base","DM_TWAKE_INSTANCE_ORGANIZATION_BASE",""],["--twake-instance-organization-id-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_ID_ATTRIBUTE","ou"],["--twake-instance-organization-domain-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_DOMAIN_ATTRIBUTE","twakeDomain"],["--twake-instance-organization-name-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_NAME_ATTRIBUTE","description"],["--twake-instance-organization-fqdn-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_FQDN_ATTRIBUTE",""],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes","identifiers"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes","identifiers"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--llng-username-header","DM_LLNG_USERNAME_HEADER",""],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-fake-user","DM_AUTH_FAKE_USER",""],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps","phrases"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs","phrases"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array",null,"identifiers"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-transit-branch","DM_AUTHZ_TRANSIT_BRANCH",""],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-bypass","DM_AUTHZ_DYNAMIC_BYPASS",[],"array",null,"identifiers"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-username-claim","DM_OIDC_USERNAME_CLAIM","sub"],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies","identifiers"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes","identifiers"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes","identifiers"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
|
|
1
|
+
import{fileURLToPath as e}from"url";import{join as _,dirname as a}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins","identifiers"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains","identifiers"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls","identifiers",!0],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",_(a(e(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuotaSize"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes","identifiers"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes","identifiers"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--organization-schema","DM_ORGANIZATION_SCHEMA",""],["--enterprise-domain-name-attribute","DM_ENTERPRISE_DOMAIN_NAME_ATTRIBUTE","associatedDomain"],["--enterprise-domain-link-attribute","DM_ENTERPRISE_DOMAIN_LINK_ATTRIBUTE",""],["--storage-backend","DM_STORAGE_BACKEND",""],["--storage-sweep-interval","DM_STORAGE_SWEEP_INTERVAL",600,"number"],["--storage-ldap-base","DM_STORAGE_LDAP_BASE",""],["--storage-ldap-object-class","DM_STORAGE_LDAP_OBJECT_CLASS","applicationProcess"],["--storage-file-directory","DM_STORAGE_FILE_DIRECTORY",""],["--storage-postgres-url","DM_STORAGE_POSTGRES_URL",""],["--storage-postgres-table","DM_STORAGE_POSTGRES_TABLE","ldap_rest_storage"],["--storage-valkey-url","DM_STORAGE_VALKEY_URL",""],["--storage-valkey-prefix","DM_STORAGE_VALKEY_PREFIX","ldap-rest:"],["--bcl-retention","DM_BCL_RETENTION",604800,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes","identifiers"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",_(a(e(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes","identifiers"],["--static-path","DM_STATIC_PATH",_(a(e(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-flat-auto-repair","DM_LDAP_FLAT_AUTO_REPAIR",!0,"boolean"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases","dns"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes","identifiers"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-retry-attempts","DM_JAMES_RETRY_ATTEMPTS",5,"number"],["--james-retry-delay","DM_JAMES_RETRY_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches","dns"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--james-sync-forwards","DM_JAMES_SYNC_FORWARDS",!1,"boolean"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--twake-lifecycle-role-attribute","DM_TWAKE_LIFECYCLE_ROLE_ATTRIBUTE",""],["--twake-lifecycle-lock-attribute","DM_TWAKE_LIFECYCLE_LOCK_ATTRIBUTE",""],["--twake-lifecycle-lock-value","DM_TWAKE_LIFECYCLE_LOCK_VALUE",""],["--twake-lifecycle-deleted-attribute","DM_TWAKE_LIFECYCLE_DELETED_ATTRIBUTE",""],["--twake-lifecycle-deleted-value","DM_TWAKE_LIFECYCLE_DELETED_VALUE","TRUE"],["--twake-lifecycle-deleted-at-attribute","DM_TWAKE_LIFECYCLE_DELETED_AT_ATTRIBUTE",""],["--twake-lifecycle-deleted-at-format","DM_TWAKE_LIFECYCLE_DELETED_AT_FORMAT","iso8601"],["--twake-lifecycle-reason-attribute","DM_TWAKE_LIFECYCLE_REASON_ATTRIBUTE",""],["--twake-lifecycle-rules","DM_TWAKE_LIFECYCLE_RULES",""],["--twake-lifecycle-member-attribute","DM_TWAKE_LIFECYCLE_MEMBER_ATTRIBUTE","member"],["--twake-group-base","DM_TWAKE_GROUP_BASE",""],["--twake-group-user-base","DM_TWAKE_GROUP_USER_BASE",""],["--twake-group-organization-dn","DM_TWAKE_GROUP_ORGANIZATION_DN",""],["--twake-group-organization-status-attribute","DM_TWAKE_GROUP_ORGANIZATION_STATUS_ATTRIBUTE","twakeOrgStatus"],["--twake-group-organization-deleted-value","DM_TWAKE_GROUP_ORGANIZATION_DELETED_VALUE","deleted"],["--twake-group-display-name-attribute","DM_TWAKE_GROUP_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-group-color-attribute","DM_TWAKE_GROUP_COLOR_ATTRIBUTE","twakeGroupColor"],["--twake-group-created-at-attribute","DM_TWAKE_GROUP_CREATED_AT_ATTRIBUTE","twakeCreatedAt"],["--twake-group-max-page-limit","DM_TWAKE_GROUP_MAX_PAGE_LIMIT",1e3,"number"],["--twake-group-member-fields","DM_TWAKE_GROUP_MEMBER_FIELDS",{},"json"],["--twake-space-base","DM_TWAKE_SPACE_BASE",""],["--twake-space-class","DM_TWAKE_SPACE_CLASS",["top","twakeSpace"],"array",null,"identifiers"],["--twake-space-display-name-attribute","DM_TWAKE_SPACE_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-space-admin-attribute","DM_TWAKE_SPACE_ADMIN_ATTRIBUTE","twakeSpaceAdmin"],["--twake-space-editor-attribute","DM_TWAKE_SPACE_EDITOR_ATTRIBUTE","twakeSpaceEditor"],["--twake-space-viewer-attribute","DM_TWAKE_SPACE_VIEWER_ATTRIBUTE","twakeSpaceViewer"],["--twake-space-user-role-attribute","DM_TWAKE_SPACE_USER_ROLE_ATTRIBUTE",""],["--twake-space-exchange","DM_TWAKE_SPACE_EXCHANGE","space"],["--twake-space-sync-queue","DM_TWAKE_SPACE_SYNC_QUEUE","twake.space.sync.requested.ldap-rest"],["--twake-tombstone-dn","DM_TWAKE_TOMBSTONE_DN",[],"array",null,"dns"],["--twake-tombstone-default-reason","DM_TWAKE_TOMBSTONE_DEFAULT_REASON","deleted"],["--twake-tombstone-reason-header","DM_TWAKE_TOMBSTONE_REASON_HEADER","x-deletion-reason"],["--twake-tombstone-reasons","DM_TWAKE_TOMBSTONE_REASONS",[],"array",null,"identifiers"],["--twake-tombstone-clear-attributes","DM_TWAKE_TOMBSTONE_CLEAR_ATTRIBUTES",[],"array",null,"identifiers"],["--twake-tombstone-erase-min-age","DM_TWAKE_TOMBSTONE_ERASE_MIN_AGE",2592e3,"number"],["--twake-tombstone-group-bases","DM_TWAKE_TOMBSTONE_GROUP_BASES",[],"array",null,"dns"],["--twake-instance-dn","DM_TWAKE_INSTANCE_DN",[],"array",null,"dns"],["--twake-instance-skip-attribute","DM_TWAKE_INSTANCE_SKIP_ATTRIBUTE",""],["--twake-instance-skip-value","DM_TWAKE_INSTANCE_SKIP_VALUE",""],["--twake-instance-provider","DM_TWAKE_INSTANCE_PROVIDER","cloudery"],["--twake-instance-id","DM_TWAKE_INSTANCE_ID","{uid}"],["--twake-instance-fqdn-attribute","DM_TWAKE_INSTANCE_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--twake-instance-sent-attribute","DM_TWAKE_INSTANCE_SENT_ATTRIBUTE","twakeCreatedEventAt"],["--twake-instance-locale","DM_TWAKE_INSTANCE_LOCALE",""],["--twake-instance-timeout","DM_TWAKE_INSTANCE_TIMEOUT",3e4,"number"],["--twake-instance-cloudery-url","DM_TWAKE_INSTANCE_CLOUDERY_URL",""],["--twake-instance-cloudery-token","DM_TWAKE_INSTANCE_CLOUDERY_TOKEN",""],["--twake-instance-cloudery-offer","DM_TWAKE_INSTANCE_CLOUDERY_OFFER","b2b_twake_default"],["--twake-instance-cloudery-domain","DM_TWAKE_INSTANCE_CLOUDERY_DOMAIN",""],["--twake-instance-cloudery-organization-offer","DM_TWAKE_INSTANCE_CLOUDERY_ORGANIZATION_OFFER",""],["--twake-instance-cozy-url","DM_TWAKE_INSTANCE_COZY_URL",""],["--twake-instance-cozy-user","DM_TWAKE_INSTANCE_COZY_USER","admin"],["--twake-instance-cozy-passphrase","DM_TWAKE_INSTANCE_COZY_PASSPHRASE",""],["--twake-instance-cozy-domain","DM_TWAKE_INSTANCE_COZY_DOMAIN",""],["--twake-instance-cozy-context","DM_TWAKE_INSTANCE_COZY_CONTEXT","default"],["--twake-instance-cozy-apps","DM_TWAKE_INSTANCE_COZY_APPS","home,drive,settings,notes,dataproxy"],["--twake-instance-cozy-org-id","DM_TWAKE_INSTANCE_COZY_ORG_ID",""],["--twake-instance-cozy-org-domain","DM_TWAKE_INSTANCE_COZY_ORG_DOMAIN",""],["--twake-instance-auth-exchange","DM_TWAKE_INSTANCE_AUTH_EXCHANGE","auth"],["--twake-instance-b2b-exchange","DM_TWAKE_INSTANCE_B2B_EXCHANGE","b2b"],["--twake-instance-user-created-key","DM_TWAKE_INSTANCE_USER_CREATED_KEY","user.created"],["--twake-instance-organization-created-key","DM_TWAKE_INSTANCE_ORGANIZATION_CREATED_KEY","organization.created"],["--twake-instance-workplace-created-key","DM_TWAKE_INSTANCE_WORKPLACE_CREATED_KEY","workplace.created"],["--twake-instance-queue","DM_TWAKE_INSTANCE_QUEUE","workplace.created.ldap-rest"],["--twake-instance-organization-base","DM_TWAKE_INSTANCE_ORGANIZATION_BASE",""],["--twake-instance-organization-id-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_ID_ATTRIBUTE","ou"],["--twake-instance-organization-domain-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_DOMAIN_ATTRIBUTE","twakeDomain"],["--twake-instance-organization-name-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_NAME_ATTRIBUTE","description"],["--twake-instance-organization-fqdn-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_FQDN_ATTRIBUTE",""],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes","identifiers"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes","identifiers"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--llng-username-header","DM_LLNG_USERNAME_HEADER",""],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-fake-user","DM_AUTH_FAKE_USER",""],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps","phrases"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs","phrases"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array",null,"identifiers"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-transit-branch","DM_AUTHZ_TRANSIT_BRANCH",""],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-bypass","DM_AUTHZ_DYNAMIC_BYPASS",[],"array",null,"identifiers"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-username-claim","DM_OIDC_USERNAME_CLAIM","sub"],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies","identifiers"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes","identifiers"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes","identifiers"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
|
package/dist/lib/authz/base.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../../abstract/plugin.js";import{assertIdentityMode as e,identityFor as r}from"../auth/base.js";import{AmbiguousIdentityError as s,ForbiddenError as i}from"../errors.js";import{organizationLink as n,getParentDn as a,normalizeDn as o,escapeLdapFilter as h,isDnInBranch as c}from"../utils.js";import{authzFor as u,servesRequest as l}from"./composition.js";class AuthzBase extends t{constructor(t){super(t),this.roles=["authz"],this.resolutionCache=new Map,this.resolutionCacheFullWarned=!1,this.fallbackWarned=!1,this.hooks={ldaprenamedone:()=>{this.resolutionCache.clear()},ldapdeletedone:()=>{this.resolutionCache.clear()},ldapmodifyrequest:async([t,e,r,s])=>{if(this.shouldSkipAuthorization(s))return[t,e,r,s];const i=await this.resolveCaller(s);if(!i)return[t,e,r,s];const a=this.config.ldap_organization_link_attribute;if(a&&e.replace?.[a]){let r,o=this.extractBranchDn(t),h=!1;try{const e=(await this.server.ldap.search({paged:!1,scope:"base",attributes:[a]},t)).searchEntries[0];e&&(h=!0,r=n(e[a]),r&&(o=r))}catch{}const c=e.replace[a],u=Array.isArray(c)?String(c[0]):String(c),l=this.filtersAttachedEntries(),d=l&&h&&this.inTransit(r),f=l&&this.inTransit(u);if(!d){const t=f?"write":"read";if(!(await this.getUserPermissions(i,o))[t])throw new Error(`[authz-forbidden] User ${s.user} does not have ${t} permission for source branch ${o}`)}if(f){if(d){if(0===(await this.getAuthorizedBranches(i)).length)throw new Error(`[authz-forbidden] User ${s.user} administers no branch`)}}else{if(!(await this.getUserPermissions(i,u)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for destination branch ${u}`)}}else{const e=await this.effectiveBranch(t);if(!(await this.getUserPermissions(i,e)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for branch ${e}`)}return[t,e,r,s]},ldapaddrequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];let i;const n=this.config.ldap_organization_link_attribute;if(n&&e[n]){const t=e[n];i=Array.isArray(t)?String(t[0]):String(t)}else i=this.extractBranchDn(t);if(!(await this.getUserPermissions(s,i)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for branch ${i}`);return[t,e,r]},ldapsearchrequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];if(t===this.config.ldap_top_organization&&"base"===e.scope)return[t,e,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(e,i)){const t=e.attributes,r=Array.isArray(t)?t.map(String):[String(t)];e={...e,attributes:[...r,i]},AuthzBase.widenedForLink.add(e)}return[t,e,r]}if(!(await this.getUserPermissions(s,t)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for branch ${t}`);return[t,e,r]},ldapsearchfilter:async([t,e,r])=>{const s=[t,e,r];if(!this.filtersAttachedEntries())return s;const i=this.config.ldap_organization_link_attribute;if(!i||this.shouldSkipAuthorization(e))return s;if(!t?.searchEntries?.length)return s;const n=await this.resolveCaller(e);if(!n)return s;const a=await this.getAuthorizedBranches(n);if(0===a.length)return s;const within=t=>a.some(e=>c(t,e)),o=this.transitKey();if(t.searchEntries=t.searchEntries.filter(t=>{const r=t[i];if(null==r)return!0;const s=(Array.isArray(r)?r:[r]).map(String);return 0===s.length||(!!s.some(within)||(!!s.some(t=>this.inTransit(t,o))||(this.logger.debug(`${this.name}: hiding ${String(t.dn)} from ${e.user}, attached to ${s[0]}`),!1)))}),r&&AuthzBase.widenedForLink.has(r))for(const e of t.searchEntries)delete e[i];return[t,e,r]},getOrganisationTop:async([t,e])=>{if(this.shouldSkipAuthorization(t))return[t,e];const r=await this.resolveCaller(t);if(!r)return[t,e];const s=await this.getAuthorizedBranches(r);if(s.length>0){const e=[];for(const r of s)try{const s=await this.server.ldap.search({paged:!1,scope:"base"},r,t);1===s.searchEntries.length&&e.push(s.searchEntries[0])}catch(t){this.logger.warn(`Failed to fetch authorized branch ${r}: ${t}`)}if(1===e.length)return[t,e[0]];if(e.length>1)return[t,e[0]]}return[t,e]},ldaprenamerequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];const i=this.extractBranchDn(t),n=this.extractBranchDn(e);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[t,e,r]},ldapdeleterequest:async([t,e])=>{if(this.shouldSkipAuthorization(e))return[t,e];const r=await this.resolveCaller(e);if(!r)return[t,e];const s=Array.isArray(t)?t:[t];for(const t of s){const s=await this.effectiveBranch(t);if(!(await this.getUserPermissions(r,s)).delete)throw new Error(`[authz-forbidden] User ${e.user} does not have delete permission for branch ${s}`)}return[t,e]}},e(this.config,this.constructor.name),u(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(", ")}.`);this.config.authz_transit_branch&&!this.filtersAttachedEntries()&&this.logger.warn(`${this.constructor.name}: --authz-transit-branch is ignored, entries are not judged by their attachment here (--authz-filter-attached-entries)`)}static wantsAttribute(t,e){const r=t?.attributes;if(void 0===r)return!0;const s=(Array.isArray(r)?r:[r]).map(t=>String(t).toLowerCase());return 0===s.length||(!!s.includes("*")||s.includes(e.toLowerCase()))}extractBranchDn(t){return a(t)}filtersAttachedEntries(){return Boolean(this.config.authz_filter_attached_entries)}async effectiveBranch(t){const e=this.config.ldap_organization_link_attribute;if(!e||!this.filtersAttachedEntries())return this.extractBranchDn(t);try{const r=await this.server.ldap.search({paged:!1,scope:"base",attributes:[e]},t),s=n(r.searchEntries?.[0]?.[e]);if(s)return s}catch{}return this.extractBranchDn(t)}transitBranch(){if(!this.filtersAttachedEntries())return;const t=String(this.config.authz_transit_branch??"").trim();return t.length>0?t:void 0}inTransit(t,e=this.transitKey()){return void 0===t||void 0!==e&&o(t)===e}transitKey(){const t=this.transitBranch();return void 0===t?void 0:o(t)}async findUserDn(t){const e=`(${this.config.ldap_user_main_attribute||"uid"}=${h(t)})`,r=(await this.server.ldap.search({paged:!1,filter:e,attributes:["dn"],scope:"sub"},this.server.ldap.base)).searchEntries??[];if(r.length>1)throw new s(t,r.length);if(0===r.length)return null;const i=r[0].dn;return"string"==typeof i?i:String(i)}shouldSkipAuthorization(t){return!t?.user||!l(t,this.config)}async resolveCaller(t){const{value:e,fellBack:s}=r(t,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=e,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[t,e]of this.resolutionCache)a-e.at>=this.cacheTTL&&this.resolutionCache.delete(t);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 t from"../../abstract/plugin.js";import{roleAttribute as e}from"../../config/schema.js";import{assertIdentityMode as r,identityFor as s}from"../auth/base.js";import{AmbiguousIdentityError as i,ForbiddenError as n}from"../errors.js";import{organizationLink as a,getParentDn as o,normalizeDn as h,escapeLdapFilter as c,isDnInBranch as u}from"../utils.js";import{authzFor as l,servesRequest as d}from"./composition.js";class AuthzBase extends t{constructor(t){super(t),this.roles=["authz"],this.resolutionCache=new Map,this.resolutionCacheFullWarned=!1,this.fallbackWarned=!1,this.hooks={ldaprenamedone:()=>{this.resolutionCache.clear()},ldapdeletedone:()=>{this.resolutionCache.clear()},ldapmodifyrequest:async([t,e,r,s])=>{if(this.shouldSkipAuthorization(s))return[t,e,r,s];const i=await this.resolveCaller(s);if(!i)return[t,e,r,s];const n=this.config.ldap_organization_link_attribute;if(n&&e.replace?.[n]){let r,o=this.extractBranchDn(t),h=!1;try{const e=(await this.server.ldap.search({paged:!1,scope:"base",attributes:[n]},t)).searchEntries[0];e&&(h=!0,r=a(e[n]),r&&(o=r))}catch{}const c=e.replace[n],u=Array.isArray(c)?String(c[0]):String(c),l=this.filtersAttachedEntries(),d=l&&h&&this.inTransit(r),f=l&&this.inTransit(u);if(!d){const t=f?"write":"read";if(!(await this.getUserPermissions(i,o))[t])throw new Error(`[authz-forbidden] User ${s.user} does not have ${t} permission for source branch ${o}`)}if(f){if(d){if(0===(await this.getAuthorizedBranches(i)).length)throw new Error(`[authz-forbidden] User ${s.user} administers no branch`)}}else{if(!(await this.getUserPermissions(i,u)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for destination branch ${u}`)}}else{const e=await this.effectiveBranch(t);if(!(await this.getUserPermissions(i,e)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for branch ${e}`)}const o=this.domainLinkAttribute();return o&&await this.guardDomainLinks(i,s,t,[...AuthzBase.valuesOf(e.add,o),...AuthzBase.valuesOf(e.replace,o)],o,!1),[t,e,r,s]},ldapaddrequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];let i;const n=this.config.ldap_organization_link_attribute;if(n&&e[n]){const t=e[n];i=Array.isArray(t)?String(t[0]):String(t)}else i=this.extractBranchDn(t);if(!(await this.getUserPermissions(s,i)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for branch ${i}`);const a=this.domainLinkAttribute();return a&&await this.guardDomainLinks(s,r,t,AuthzBase.valuesOf(e,a),a,!0),[t,e,r]},ldapsearchrequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];if(t===this.config.ldap_top_organization&&"base"===e.scope)return[t,e,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(e,i)){const t=e.attributes,r=Array.isArray(t)?t.map(String):[String(t)];e={...e,attributes:[...r,i]},AuthzBase.widenedForLink.add(e)}return[t,e,r]}if(!(await this.getUserPermissions(s,t)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for branch ${t}`);return[t,e,r]},ldapsearchfilter:async([t,e,r])=>{const s=[t,e,r];if(!this.filtersAttachedEntries())return s;const i=this.config.ldap_organization_link_attribute;if(!i||this.shouldSkipAuthorization(e))return s;if(!t?.searchEntries?.length)return s;const n=await this.resolveCaller(e);if(!n)return s;const a=await this.getAuthorizedBranches(n);if(0===a.length)return s;const within=t=>a.some(e=>u(t,e)),o=this.transitKey();if(t.searchEntries=t.searchEntries.filter(t=>{const r=t[i];if(null==r)return!0;const s=(Array.isArray(r)?r:[r]).map(String);return 0===s.length||(!!s.some(within)||(!!s.some(t=>this.inTransit(t,o))||(this.logger.debug(`${this.name}: hiding ${String(t.dn)} from ${e.user}, attached to ${s[0]}`),!1)))}),r&&AuthzBase.widenedForLink.has(r))for(const e of t.searchEntries)delete e[i];return[t,e,r]},getOrganisationTop:async([t,e])=>{if(this.shouldSkipAuthorization(t))return[t,e];const r=await this.resolveCaller(t);if(!r)return[t,e];const s=await this.getAuthorizedBranches(r);if(s.length>0){const e=[];for(const r of s)try{const s=await this.server.ldap.search({paged:!1,scope:"base"},r,t);1===s.searchEntries.length&&e.push(s.searchEntries[0])}catch(t){this.logger.warn(`Failed to fetch authorized branch ${r}: ${t}`)}if(1===e.length)return[t,e[0]];if(e.length>1)return[t,e[0]]}return[t,e]},ldaprenamerequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];const i=this.extractBranchDn(t),n=this.extractBranchDn(e);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[t,e,r]},ldapdeleterequest:async([t,e])=>{if(this.shouldSkipAuthorization(e))return[t,e];const r=await this.resolveCaller(e);if(!r)return[t,e];const s=Array.isArray(t)?t:[t];for(const t of s){const s=await this.effectiveBranch(t);if(!(await this.getUserPermissions(r,s)).delete)throw new Error(`[authz-forbidden] User ${e.user} does not have delete permission for branch ${s}`)}return[t,e]}},r(this.config,this.constructor.name),l(this.config,this.constructor.name);const e=this.config.authz_unresolved_user??"deny";if(!AuthzBase.UNRESOLVED_POLICIES.includes(e))throw new Error(`${this.constructor.name}: unknown --authz-unresolved-user "${e}". Known: ${AuthzBase.UNRESOLVED_POLICIES.join(", ")}.`);this.config.authz_transit_branch&&!this.filtersAttachedEntries()&&this.logger.warn(`${this.constructor.name}: --authz-transit-branch is ignored, entries are not judged by their attachment here (--authz-filter-attached-entries)`)}static wantsAttribute(t,e){const r=t?.attributes;if(void 0===r)return!0;const s=(Array.isArray(r)?r:[r]).map(t=>String(t).toLowerCase());return 0===s.length||(!!s.includes("*")||s.includes(e.toLowerCase()))}extractBranchDn(t){return o(t)}filtersAttachedEntries(){return Boolean(this.config.authz_filter_attached_entries)}async effectiveBranch(t){const e=this.config.ldap_organization_link_attribute;if(!e||!this.filtersAttachedEntries())return this.extractBranchDn(t);try{const r=await this.server.ldap.search({paged:!1,scope:"base",attributes:[e]},t),s=a(r.searchEntries?.[0]?.[e]);if(s)return s}catch{}return this.extractBranchDn(t)}transitBranch(){if(!this.filtersAttachedEntries())return;const t=String(this.config.authz_transit_branch??"").trim();return t.length>0?t:void 0}inTransit(t,e=this.transitKey()){return void 0===t||void 0!==e&&h(t)===e}transitKey(){const t=this.transitBranch();return void 0===t?void 0:h(t)}async findUserDn(t){const e=`(${this.config.ldap_user_main_attribute||"uid"}=${c(t)})`,r=(await this.server.ldap.search({paged:!1,filter:e,attributes:["dn"],scope:"sub"},this.server.ldap.base)).searchEntries??[];if(r.length>1)throw new i(t,r.length);if(0===r.length)return null;const s=r[0].dn;return"string"==typeof s?s:String(s)}shouldSkipAuthorization(t){return!t?.user||!d(t,this.config)}async resolveCaller(t){const{value:e,fellBack:r}=s(t,this.config);r&&!this.fallbackWarned&&(this.fallbackWarned=!0,this.logger.warn(`${this.name}: --authz-identity asks for req.userName and the authenticator published none, so permissions are read for req.user instead. Rules written on logins will not match`));const i=e,a=Date.now(),o=this.resolutionCache.get(i);let h;if(o&&a-o.at<this.cacheTTL)h=o.user;else{if(h=await this.resolveUser(i),this.resolutionCache.size>=AuthzBase.RESOLUTION_CACHE_MAX)for(const[t,e]of this.resolutionCache)a-e.at>=this.cacheTTL&&this.resolutionCache.delete(t);this.resolutionCache.size<AuthzBase.RESOLUTION_CACHE_MAX?this.resolutionCache.set(i,{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 ${i} could not be resolved by ${this.name}; allowed by --authz-unresolved-user allow`),null;throw new n(`[authz-forbidden] User ${i} could not be resolved by ${this.name}, so no permission can be read for them`)}domainLinkAttribute(){const t=this.server.loadedPlugins.ldapOrganizations;return e(t?.schema,"domainLink")||this.config.enterprise_domain_link_attribute||void 0}static valuesOf(t,e){if(!t)return[];const r=e.toLowerCase(),s=[];for(const[e,i]of Object.entries(t))if(e.split(";")[0].toLowerCase()===r&&null!=i)for(const t of Array.isArray(i)?i:[i])s.push(String(t));return s}async storedValues(t,e){try{const r=await this.server.ldap.search({paged:!1,scope:"base",attributes:[e]},t);return AuthzBase.valuesOf(r.searchEntries?.[0],e)}catch{return[]}}async guardDomainLinks(t,e,r,s,i,n){if(0===s.length)return;const a=new Set((n?[]:await this.storedValues(r,i)).map(h)),c=s.filter(t=>!a.has(h(t)));if(0===c.length)return;const l=this.config.ldap_top_organization,d=h(l||this.server.ldap.base),f=new Set;let g=o(r);const w=new Set;for(;g&&!w.has(h(g));){const t=h(g);if(w.add(t),t!==d&&!u(g,d))break;for(const t of await this.storedValues(g,i))f.add(h(t));if(t===d)break;const e=o(g);if(e===g)break;g=e}const p=c.filter(t=>!f.has(h(t)));if(!(0===p.length||l&&(await this.getUserPermissions(t,l)).write))throw new Error(`[authz-forbidden] User ${e.user} may not link ${r} to domain ${p[0]}: no organization above it is linked to it`)}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 r from"../../abstract/plugin.js";import{HttpError as e}from"../../lib/errors.js";import{launchHooksChained as s,launchHooks as a}from"../../lib/utils.js";class ExternalUsersInGroups extends r{constructor(r){super(r),this.name="externalUsersInGroups",this.roles=["consistency"],this.dependencies={ldapGroups:"core/ldap/groups"},this.hooks={ldapgroupvalidatemembers:async([r,
|
|
1
|
+
import r from"../../abstract/plugin.js";import{HttpError as e}from"../../lib/errors.js";import{launchHooksChained as s,launchHooks as t,parseDn as a,normalizeDn as n,unescapeDnValue as i}from"../../lib/utils.js";class ExternalUsersInGroups extends r{constructor(r){super(r),this.name="externalUsersInGroups",this.roles=["consistency","configurable"],this.dependencies={ldapGroups:"core/ldap/groups"},this.hooks={ldapgroupvalidatemembers:async([r,a])=>(await Promise.all(a.map(r=>this.server.ldap.queryLimit(async()=>{const a=this.externalAddress(r);if(null===a)return!1;try{return await this.ldap.search({paged:!1,scope:"base"},r),!0}catch{if(!a)throw new Error(`Malformed member ${r}`);const n=a.split("@")[1]?.toLowerCase();if(n&&this.managedDomains().includes(n))throw new Error(`Cannot create external user with managed domain: ${a}`);let i={objectClass:this.config.external_branch_class||this.config.user_class,mail:a,[this.config.ldap_groups_main_attribute]:a,sn:a};[r,i]=await s(this.registeredHooks.externaluserentry,[r,i]);try{return await this.ldap.add(r,i),t(this.registeredHooks.externaluseradded,r,i),!0}catch(s){if(s instanceof e)throw s;throw new Error(`Unable to insert ${r}: ${s instanceof Error?s.message:String(s)}`)}}}))),[r,a])},this.ldap=r.ldap}externalAddress(r){const e=this.config.external_members_branch;if(!e)return null;const[s,...t]=a(r);if(!s||n(t.join(","))!==n(e))return null;const o=s.indexOf("=");if(o<0||"mail"!==s.slice(0,o).trim().toLowerCase())return null;const l=s.slice(o+1).trim();return/(^|[^\\])(\\\\)*\+/.test(l)?null:i(l)}managedDomains(){const r=this.config.mail_domain;return Array.isArray(r)?r.map(r=>String(r).toLowerCase()):[]}getConfigApiData(){return{enabled:!0,branch:this.config.external_members_branch,rdnAttribute:"mail",managedDomains:this.managedDomains()}}}export{ExternalUsersInGroups as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"../../abstract/plugin.js";import{wantJson as t,jsonBody as r,tryMethod as i,ok as s}from"../../lib/expressFormatedResponses.js";import{extractLdapCode as a}from"../../lib/ldapCodes.js";import{loadSchemaFileAsync as o,schemaUrl as n}from"../../lib/schemaFile.js";import{asyncHandler as h,isDummyMemberDn as c,escapeDnValue as d,validateDnValue as p,launchHooksChained as m,launchHooks as u,escapeLdapFilter as l,organizationLink as f,substringSearchFilter as g}from"../../lib/utils.js";import{NotFoundError as b,BadRequestError as w,HttpError as y}from"../../lib/errors.js";import{assertClientMaySet as $,modifiedAttributeNames as v,invalidValueMessage as _,missingRequiredAttribute as E,checkDnValues as A,matchesPattern as G}from"../../config/schema.js";class LdapGroups extends e{constructor(e){if(super(e),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=e.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");if(this.config.ldap_group_base&&e.ldap.declareObjectClasses(this.base,this.config.group_class||[]),this.config.group_schema){const e=this.config.group_schema;o(e,{config:this.config}).then(e=>{this.schema=this.adaptSchema(e),this.logger.debug("Group schema loaded")}).catch(t=>this.logger.error(`Failed to load group schema from ${e}: ${t}`))}}adaptSchema(e){return e}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,h(async(e,r)=>{if(!t(e,r))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new w("Invalid match query");if("string"==typeof e.query.attribute&&e.query.attribute)i.filter=g(e.query.match,e.query.attribute);else if(/=/.test(e.query.match)){if(!/^[\w*=()&|, -]+$/.test(e.query.match))throw new w("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=l(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const a=await this.listGroups({...i,req:e});return s(r,this.hideNeverReturn(a))})),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 s=decodeURIComponent(e.params.cn);if(!s)throw new w("cn is required");const a=r(e,t,"member");if(!a)return;if((Array.isArray(a.member)?a.member:[a.member]).some(e=>"string"!=typeof e))throw new w("member must be a string or string array");await i(t,this.addMember.bind(this),s,a.member)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,h(async(e,r)=>{if(!t(e,r))return;const s=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!s||!a)throw new w("cn and member are required");await i(r,this.deleteMember.bind(this),s,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,s)=>{if(!t(e,s))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new w("cn is required");const o=r(e,s,"targetOrgDn");if(o){if("string"!=typeof o.targetOrgDn)throw new w("targetOrgDn must be a string");await i(s,this.moveGroup.bind(this),a,o.targetOrgDn,e)}})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,h(async(e,s)=>{if(!t(e,s))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new w("cn is required");const o=r(e,s,"newCn");if(o){if("string"!=typeof o.newCn)throw new w("newCn must be a string");await i(s,this.renameGroup.bind(this),a,o.newCn,e)}}))}isDummyMember(e){return c(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,r){if(!t(e,r))return;const i=decodeURIComponent(e.params.cn);try{const t=/,/.test(i)?i:`${this.cn}=${d(i)},${this.base}`,s=await this.ldap.search({paged:!1,scope:"base"},t,e);if(0===s.searchEntries.length)throw new b("Group not found");r.json(this.hideNeverReturn(this.withoutDummyMember(s.searchEntries[0])))}catch(e){if(e.code&&32===e.code)throw new b("Group not found");throw e}}async apiAdd(e,t,...s){const a=r(e,t,...s);if(!a)return;$(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 i(t,this.addGroup.bind(this),o,n,h,e)}async apiDelete(e,r){if(!t(e,r))return;const s=decodeURIComponent(e.params.cn);if(!s)throw new w("cn is required");await i(r,this.deleteGroup.bind(this),s,e)}async apiModify(e,t){const s=r(e,t);if(!s)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new w("cn is required");$(this.schema,v(s));const o=Object.fromEntries(Object.entries(s).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await i(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}=${d(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 y)throw e;throw new Error(`Failed to add group ${s}: ${e}`)}return u(this.registeredHooks.ldapgroupadddone,[s,n]),o}async modifyGroup(e,t,r){let i=/,/.test(e)?e:`${this.cn}=${d(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 u(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}=${d(e)},${this.base}`,s=/,/.test(t)?t:`${this.cn}=${d(t)},${this.base}`;[i,s]=await m(this.registeredHooks.ldapgrouprename,[i,s]);const a=await this.ldap.rename(i,s,r);return u(this.registeredHooks.ldapgrouprenamedone,[i,s]),a}async deleteGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;r=await m(this.registeredHooks.ldapgroupdelete,r);const i=await this.ldap.delete(r,t);return u(this.registeredHooks.ldapgroupdeletedone,r),i}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${d(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 y)throw e;throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;if([e,t]=await m(this.registeredHooks.ldapgroupdeletemember,[e,t]),this.isDummyMember(t))throw new w("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{if(e instanceof y)throw e;throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${l(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===a(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=a(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}=${d(e)},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0],h=f(n[i]);if(!h)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(h===t)throw new Error("Group is already in the target organization");let c;try{c=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===c.searchEntries.length)throw new Error(`Target organization ${t} not found`);const p=c.searchEntries[0][s];if(!p)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:p}},r),this.logger.info(`Group ${a} moved from ${h} 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=l(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}=${d(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){const r=a(t);if(32===r)throw new w(`Member ${e} not found`);if(34===r)throw new w(`Member ${e} is not a valid DN`);throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){if(e instanceof y)throw 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 w(_(e,this.schema.attributes[e]));const r=E(this.schema,t,this.server.loadedPlugins);if(r)throw new w(`Attribute "${r}" is required`);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))if(!await this._validateOneChange(e,r))throw new w(_(e,this.schema.attributes[e]));if(t.replace)for(const[e,r]of Object.entries(t.replace))if(!await this._validateOneChange(e,r))throw new w(_(e,this.schema.attributes[e]));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 w(`Attribute "${e}" is not allowed`);return!0}if(null==t||Array.isArray(t)&&0===t.length){if(r.required)throw new w(`Attribute "${e}" is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new w(`Attribute "${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 w(`Attribute "${e}" must be of type ${r.items.type}`)}else if("pointer"!==r.type&&typeof t!==r.type)return!1;if(await A(e,r,t,async e=>(await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},e)).searchEntries.length>0),!G(r,t))throw new w(_(e,r));return!0}getConfigApiData(){const e=this.config.api_prefix||"/api",t=this.server.loadedPlugins.static&&this.config.group_schema?n(this.config,this.config.group_schema):void 0;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"../../abstract/plugin.js";import{wantJson as t,jsonBody as r,tryMethod as i,ok as s}from"../../lib/expressFormatedResponses.js";import{extractLdapCode as a}from"../../lib/ldapCodes.js";import{loadSchemaFileAsync as o,schemaUrl as n}from"../../lib/schemaFile.js";import{asyncHandler as h,isDummyMemberDn as c,escapeDnValue as d,validateDnValue as p,launchHooksChained as m,launchHooks as u,escapeLdapFilter as l,organizationLink as f,isAuthzRefusal as g,substringSearchFilter as b}from"../../lib/utils.js";import{NotFoundError as w,BadRequestError as y,HttpError as $}from"../../lib/errors.js";import{assertClientMaySet as v,modifiedAttributeNames as _,invalidValueMessage as E,missingRequiredAttribute as A,checkDnValues as G,matchesPattern as q}from"../../config/schema.js";class LdapGroups extends e{constructor(e){if(super(e),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=e.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");if(this.config.ldap_group_base&&e.ldap.declareObjectClasses(this.base,this.config.group_class||[]),this.config.group_schema){const e=this.config.group_schema;o(e,{config:this.config}).then(e=>{this.schema=this.adaptSchema(e),this.logger.debug("Group schema loaded")}).catch(t=>this.logger.error(`Failed to load group schema from ${e}: ${t}`))}}adaptSchema(e){return e}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,h(async(e,r)=>{if(!t(e,r))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new y("Invalid match query");if("string"==typeof e.query.attribute&&e.query.attribute)i.filter=b(e.query.match,e.query.attribute);else if(/=/.test(e.query.match)){if(!/^[\w*=()&|, -]+$/.test(e.query.match))throw new y("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=l(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const a=await this.listGroups({...i,req:e});return s(r,this.hideNeverReturn(a))})),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 s=decodeURIComponent(e.params.cn);if(!s)throw new y("cn is required");const a=r(e,t,"member");if(!a)return;if((Array.isArray(a.member)?a.member:[a.member]).some(e=>"string"!=typeof e))throw new y("member must be a string or string array");await i(t,this.addMember.bind(this),s,a.member,e)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,h(async(e,r)=>{if(!t(e,r))return;const s=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!s||!a)throw new y("cn and member are required");await i(r,this.deleteMember.bind(this),s,a,e)})),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,s)=>{if(!t(e,s))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new y("cn is required");const o=r(e,s,"targetOrgDn");if(o){if("string"!=typeof o.targetOrgDn)throw new y("targetOrgDn must be a string");await i(s,this.moveGroup.bind(this),a,o.targetOrgDn,e)}})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,h(async(e,s)=>{if(!t(e,s))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new y("cn is required");const o=r(e,s,"newCn");if(o){if("string"!=typeof o.newCn)throw new y("newCn must be a string");await i(s,this.renameGroup.bind(this),a,o.newCn,e)}}))}isDummyMember(e){return c(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,r){if(!t(e,r))return;const i=decodeURIComponent(e.params.cn);try{const t=/,/.test(i)?i:`${this.cn}=${d(i)},${this.base}`,s=await this.ldap.search({paged:!1,scope:"base"},t,e);if(0===s.searchEntries.length)throw new w("Group not found");r.json(this.hideNeverReturn(this.withoutDummyMember(s.searchEntries[0])))}catch(e){if(e.code&&32===e.code)throw new w("Group not found");throw e}}async apiAdd(e,t,...s){const a=r(e,t,...s);if(!a)return;v(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 i(t,this.addGroup.bind(this),o,n,h,e)}async apiDelete(e,r){if(!t(e,r))return;const s=decodeURIComponent(e.params.cn);if(!s)throw new y("cn is required");await i(r,this.deleteGroup.bind(this),s,e)}async apiModify(e,t){const s=r(e,t);if(!s)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new y("cn is required");v(this.schema,_(s));const o=Object.fromEntries(Object.entries(s).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await i(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}=${d(e)},${this.base}`),await this.validateMembers(s,t,i),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 $)throw e;throw new Error(`Failed to add group ${s}: ${e}`)}return u(this.registeredHooks.ldapgroupadddone,[s,n]),o}async modifyGroup(e,t,r){let i=/,/.test(e)?e:`${this.cn}=${d(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 u(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}=${d(e)},${this.base}`,s=/,/.test(t)?t:`${this.cn}=${d(t)},${this.base}`;[i,s]=await m(this.registeredHooks.ldapgrouprename,[i,s]);const a=await this.ldap.rename(i,s,r);return u(this.registeredHooks.ldapgrouprenamedone,[i,s]),a}async deleteGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;r=await m(this.registeredHooks.ldapgroupdelete,r);const i=await this.ldap.delete(r,t);return u(this.registeredHooks.ldapgroupdeletedone,r),i}async addMember(e,t,r){const i=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await m(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(i,t,r),await this.ldap.modify(i,{add:{member:t}},r).catch(e=>{if(e instanceof $)throw e;throw new Error(`Failed to add member(s) to ${i}: ${e}`)})}async deleteMember(e,t,r){const i=/,/.test(e)?e:`${this.cn}=${d(e)},${this.base}`;if([e,t]=await m(this.registeredHooks.ldapgroupdeletemember,[e,t]),this.isDummyMember(t))throw new y("Cannot delete dummy member from group");return await this.ldap.modify(i,{delete:{member:t}},r).catch(e=>{if(e instanceof $)throw e;throw new Error(`Failed to delete member ${t} from ${i}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${l(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===a(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=a(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}=${d(e)},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0],h=f(n[i]);if(!h)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(h===t)throw new Error("Group is already in the target organization");let c;try{c=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===c.searchEntries.length)throw new Error(`Target organization ${t} not found`);const p=c.searchEntries[0][s];if(!p)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:p}},r),this.logger.info(`Group ${a} moved from ${h} 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=l(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}=${d(e)},${this.base}`)}async validateMembers(e,t,r){[e,t]=await m(this.server.hooks.ldapgroupvalidatemembers,[e,t]);const i=Boolean(this.config.groups_allow_unexistent_members);if((!i||r)&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{let t;try{t=await this.ldap.search({paged:!1,scope:"base"},e,r)}catch(t){if(g(t))throw t;if(i)return;const r=a(t);if(32===r)throw new y(`Member ${e} not found`);if(34===r)throw new y(`Member ${e} is not a valid DN`);throw new Error(`Member ${e} not found: ${t}`)}if(r&&!t.searchEntries?.length)throw new y(`Member ${e} not found`)})))}catch(e){if(e instanceof $||g(e))throw 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 y(E(e,this.schema.attributes[e]));const r=A(this.schema,t,this.server.loadedPlugins);if(r)throw new y(`Attribute "${r}" is required`);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))if(!await this._validateOneChange(e,r))throw new y(E(e,this.schema.attributes[e]));if(t.replace)for(const[e,r]of Object.entries(t.replace))if(!await this._validateOneChange(e,r))throw new y(E(e,this.schema.attributes[e]));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 y(`Attribute "${e}" is not allowed`);return!0}if(null==t||Array.isArray(t)&&0===t.length){if(r.required)throw new y(`Attribute "${e}" is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new y(`Attribute "${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 y(`Attribute "${e}" must be of type ${r.items.type}`)}else if("pointer"!==r.type&&typeof t!==r.type)return!1;if(await G(e,r,t,async e=>(await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},e)).searchEntries.length>0),!q(r,t))throw new y(E(e,r));return!0}getConfigApiData(){const e=this.config.api_prefix||"/api",t=this.server.loadedPlugins.static&&this.config.group_schema?n(this.config,this.config.group_schema):void 0;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 e from"../../abstract/plugin.js";import{asyncHandler as i,escapeDnValue as s}from"../../lib/utils.js";import{NotFoundError as t,BadRequestError as a}from"../../lib/errors.js";const r=["pwdChangedTime","pwdAccountLockedTime","pwdFailureTime","pwdGraceUseTime","pwdReset","pwdPolicySubentry"],o=["pwdMaxAge","pwdMinAge","pwdInHistory","pwdCheckQuality","pwdMinLength","pwdMaxFailure","pwdLockout","pwdLockoutDuration","pwdGraceAuthNLimit","pwdExpireWarning","pwdMustChange","pwdAllowUserChange"],n=/[!@#$%^&*()_+\-=[\]{}|;:'",.<>?/]/;class PasswordPolicy extends e{constructor(){super(...arguments),this.name="ldapPasswordPolicy",this.roles=["api","configurable"],this.policyCache=null,this.policyCacheTime=0,this.POLICY_CACHE_TTL=6e4}api(e){const s=`${this.config.api_prefix}/v1`;e.get(`${s}/password-policy`,i(async(e,i)=>{const s=await this.readPolicyConfig();i.json(s)})),e.get(`${s}/users/:id/password-status`,i(async(e,i)=>{const s=Array.isArray(e.params.id)?e.params.id[0]:e.params.id,t=await this.getPasswordStatus(s);i.json(t)})),e.post(`${s}/users/:id/unlock`,i(async(e,i)=>{const s=Array.isArray(e.params.id)?e.params.id[0]:e.params.id;await this.unlockAccount(s),i.json({success:!0,message:"Account unlocked"})})),e.get(`${s}/password-policy/expiring-soon`,i(async(e,i)=>{const s=parseInt(e.query.days)||this.config.ppolicy_warn_days||14,t=await this.findExpiringPasswords(s);i.json({warningDays:s,users:t})})),e.get(`${s}/password-policy/locked-accounts`,i(async(e,i)=>{const s=await this.findLockedAccounts();i.json({accounts:s})})),this.config.ppolicy_validate_complexity&&e.post(`${s}/password/validate`,i(async(e,i)=>{const{password:s}=e.body;if(!s)throw new a("password required");const t=this.validateComplexity(s);i.json(t)}))}async getPasswordStatus(e){const
|
|
1
|
+
import e from"../../abstract/plugin.js";import{asyncHandler as i,escapeDnValue as s}from"../../lib/utils.js";import{NotFoundError as t,BadRequestError as a}from"../../lib/errors.js";const r=["pwdChangedTime","pwdAccountLockedTime","pwdFailureTime","pwdGraceUseTime","pwdReset","pwdPolicySubentry"],o=["pwdMaxAge","pwdMinAge","pwdInHistory","pwdCheckQuality","pwdMinLength","pwdMaxFailure","pwdLockout","pwdLockoutDuration","pwdGraceAuthNLimit","pwdExpireWarning","pwdMustChange","pwdAllowUserChange"],n=/[!@#$%^&*()_+\-=[\]{}|;:'",.<>?/]/;class PasswordPolicy extends e{constructor(){super(...arguments),this.name="ldapPasswordPolicy",this.roles=["api","configurable"],this.policyCache=null,this.policyCacheTime=0,this.POLICY_CACHE_TTL=6e4}api(e){const s=`${this.config.api_prefix}/v1`;e.get(`${s}/password-policy`,i(async(e,i)=>{const s=await this.readPolicyConfig();i.json(s)})),e.get(`${s}/users/:id/password-status`,i(async(e,i)=>{const s=Array.isArray(e.params.id)?e.params.id[0]:e.params.id,t=await this.getPasswordStatus(s,e);i.json(t)})),e.post(`${s}/users/:id/unlock`,i(async(e,i)=>{const s=Array.isArray(e.params.id)?e.params.id[0]:e.params.id;await this.unlockAccount(s,e),i.json({success:!0,message:"Account unlocked"})})),e.get(`${s}/password-policy/expiring-soon`,i(async(e,i)=>{const s=parseInt(e.query.days)||this.config.ppolicy_warn_days||14,t=await this.findExpiringPasswords(s,e);i.json({warningDays:s,users:t})})),e.get(`${s}/password-policy/locked-accounts`,i(async(e,i)=>{const s=await this.findLockedAccounts(e);i.json({accounts:s})})),this.config.ppolicy_validate_complexity&&e.post(`${s}/password/validate`,i(async(e,i)=>{const{password:s}=e.body;if(!s)throw new a("password required");const t=this.validateComplexity(s);i.json(t)}))}async getPasswordStatus(e,i){const s=this.resolveUserDn(e);try{const a=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:["*",...r],paged:!1},s,i);if(!a.searchEntries||0===a.searchEntries.length)throw new t(`User not found: ${e}`);const o=a.searchEntries[0],n=await this.readPolicyConfig();return this.calculateStatus(s,o,n)}catch(i){if(i instanceof Error&&("NoSuchObjectError"===i.name||i.message.includes("0x20")||i.message.includes("Code: 32")))throw new t(`User not found: ${e}`);throw i}}calculateStatus(e,i,s){const t=Date.now(),a=this.parseGeneralizedTime(i.pwdChangedTime);let r=null,o=!1,n=null;s.pwdMaxAge&&a&&(r=new Date(a.getTime()+1e3*s.pwdMaxAge),o=t>r.getTime(),n=Math.ceil((r.getTime()-t)/864e5));let c=!1;const p=this.config.ppolicy_warn_days||14;null!==n&&n>0&&(c=n<=p);const d=i.pwdAccountLockedTime;let l=!!d;if(l&&!("000001010000Z"===d)&&s.pwdLockoutDuration){const e=this.parseGeneralizedTime(d);if(e){t>new Date(e.getTime()+1e3*s.pwdLockoutDuration).getTime()&&(l=!1)}}const u=i.pwdFailureTime,h=Array.isArray(u)?u.length:0,w=i.pwdGraceUseTime,g=Array.isArray(w)?w.length:0,y="TRUE"===i.pwdReset;return{dn:e,passwordSet:!!i.userPassword,lastChanged:a?.toISOString()||null,expiresAt:r?.toISOString()||null,daysUntilExpiration:n,isExpired:o,isExpiringSoon:c,mustChange:y,isLocked:l,lockedAt:d&&this.parseGeneralizedTime(d)?.toISOString()||null,failureCount:h,graceLoginsUsed:g}}async unlockAccount(e,i){const s=this.resolveUserDn(e),a=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:["pwdAccountLockedTime","pwdFailureTime"],paged:!1},s,i);if(!a.searchEntries||0===a.searchEntries.length)throw new t(`User not found: ${e}`);const r=a.searchEntries[0],o={};r.pwdAccountLockedTime&&(o.pwdAccountLockedTime=[]),r.pwdFailureTime&&(o.pwdFailureTime=[]),Object.keys(o).length>0?(await this.server.ldap.modify(s,{delete:o},i),this.logger.info(`Account unlocked: ${s}`)):this.logger.info(`Account was not locked: ${s}`)}async findExpiringPasswords(e,i){const s=await this.readPolicyConfig();if(!s.pwdMaxAge)return[];const t=this.config.ldap_users_base||this.config.ldap_base,a=await this.server.ldap.search({filter:"(pwdChangedTime=*)",scope:"sub",attributes:["uid","cn","displayName","mail","pwdChangedTime"],paged:!1},t,i),r=Date.now(),o=24*e*60*60*1e3,n=[];for(const e of a.searchEntries){const i=e,t=this.parseGeneralizedTime(i.pwdChangedTime);if(!t)continue;const a=new Date(t.getTime()+1e3*s.pwdMaxAge),c=a.getTime()-r;c>0&&c<o&&n.push({dn:i.dn,uid:i.uid,displayName:i.displayName||i.cn,mail:i.mail,expiresAt:a.toISOString(),daysUntilExpiration:Math.ceil(c/864e5)})}return n.sort((e,i)=>e.daysUntilExpiration-i.daysUntilExpiration)}async findLockedAccounts(e){const i=this.config.ldap_users_base||this.config.ldap_base;return(await this.server.ldap.search({filter:"(pwdAccountLockedTime=*)",scope:"sub",attributes:["uid","cn","displayName","mail","pwdAccountLockedTime","pwdFailureTime"],paged:!1},i,e)).searchEntries.map(e=>{const i=e;return{dn:i.dn,uid:i.uid,displayName:i.displayName||i.cn,mail:i.mail,lockedAt:this.parseGeneralizedTime(i.pwdAccountLockedTime)?.toISOString(),failureCount:Array.isArray(i.pwdFailureTime)?i.pwdFailureTime.length:0}})}async readPolicyConfig(){if(this.policyCache&&Date.now()-this.policyCacheTime<this.POLICY_CACHE_TTL)return this.policyCache;const e=this.config.ppolicy_default_dn;if(e)try{const i=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:o,paged:!1},e);if(i.searchEntries.length>0)return this.policyCache=this.parsePolicyEntry(i.searchEntries[0],e),this.policyCacheTime=Date.now(),this.policyCache}catch(i){this.logger.warn(`Failed to read ppolicy from ${e}: ${i}`)}try{const e=await this.server.ldap.search({filter:"(objectClass=pwdPolicy)",scope:"sub",attributes:o,paged:!1},this.config.ldap_base);if(e.searchEntries.length>0){const i=e.searchEntries[0];return this.policyCache=this.parsePolicyEntry(i,i.dn),this.policyCacheTime=Date.now(),this.policyCache}}catch(e){this.logger.debug(`No ppolicy found: ${e}`)}return this.policyCache={},this.policyCacheTime=Date.now(),this.policyCache}parsePolicyEntry(e,i){return{dn:i,pwdMaxAge:this.parseNumber(e.pwdMaxAge),pwdMinAge:this.parseNumber(e.pwdMinAge),pwdInHistory:this.parseNumber(e.pwdInHistory),pwdCheckQuality:this.parseNumber(e.pwdCheckQuality),pwdMinLength:this.parseNumber(e.pwdMinLength),pwdMaxFailure:this.parseNumber(e.pwdMaxFailure),pwdLockout:"TRUE"===e.pwdLockout,pwdLockoutDuration:this.parseNumber(e.pwdLockoutDuration),pwdGraceAuthNLimit:this.parseNumber(e.pwdGraceAuthNLimit),pwdExpireWarning:this.parseNumber(e.pwdExpireWarning),pwdMustChange:"TRUE"===e.pwdMustChange,pwdAllowUserChange:void 0===e.pwdAllowUserChange||"TRUE"===e.pwdAllowUserChange}}parseNumber(e){if(void 0===e)return;const i=parseInt(e,10);return isNaN(i)?void 0:i}parseGeneralizedTime(e){if(!e)return null;const i=e.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);if(!i)return null;const[,s,t,a,r,o,n]=i;return new Date(Date.UTC(+s,+t-1,+a,+r,+o,+n))}validateComplexity(e){const i=[],s=this.config,t=s.ppolicy_min_length||12;return e.length<t&&i.push(`Minimum ${t} characters required`),s.ppolicy_require_uppercase&&!/[A-Z]/.test(e)&&i.push("At least one uppercase letter required"),s.ppolicy_require_lowercase&&!/[a-z]/.test(e)&&i.push("At least one lowercase letter required"),s.ppolicy_require_digit&&!/\d/.test(e)&&i.push("At least one digit required"),s.ppolicy_require_special&&!n.test(e)&&i.push("At least one special character required"),{valid:0===i.length,errors:i}}resolveUserDn(e){if(e.includes("="))return e;const i=this.config.ldap_users_base||this.config.ldap_base;return`${this.config.ldap_user_main_attribute||"uid"}=${s(e)},${i}`}getConfigApiData(){const e=this.config.api_prefix||"/api",i=!!this.config.ppolicy_validate_complexity;return{name:this.name,enabled:!0,endpoints:{getPolicy:`${e}/v1/password-policy`,getUserStatus:`${e}/v1/users/:id/password-status`,unlockUser:`${e}/v1/users/:id/unlock`,getExpiringSoon:`${e}/v1/password-policy/expiring-soon`,getLockedAccounts:`${e}/v1/password-policy/locked-accounts`,validatePassword:i?`${e}/v1/password/validate`:void 0},config:{warnDays:this.config.ppolicy_warn_days||14,validateComplexity:i,...i&&{complexityRules:{minLength:this.config.ppolicy_min_length||12,requireUppercase:this.config.ppolicy_require_uppercase??!0,requireLowercase:this.config.ppolicy_require_lowercase??!0,requireDigit:this.config.ppolicy_require_digit??!0,requireSpecial:this.config.ppolicy_require_special??!0}}},ldapPolicy:this.policyCache||void 0}}}export{PasswordPolicy as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node-fetch";import{TwakePlugin as e}from"../../abstract/twakePlugin.js";import{asyncHandler as a,escapeLdapFilter as i,isDummyMemberDn as s,normalizeDn as r}from"../../lib/utils.js";import{wantJson as n}from"../../lib/expressFormatedResponses.js";const o=[400,404],members=t=>{const e=t?.member;return void 0===e?[]:(Array.isArray(e)?e:[e]).map(String)},memberKey=t=>{try{return r(t)}catch{return t.toLowerCase()}},l=["groupofnames","groupofuniquenames","groupofurls"],isGroup=t=>(Array.isArray(t)?t:t?[t]:[]).some(t=>l.includes(String(t).toLowerCase()));class James extends e{constructor(t){super(t,"james_webadmin_url","james_webadmin_token","james_concurrency"),this.name="james",this.roles=["consistency","api"],this.dependencies={onLdapChange:"core/ldap/onChange",ldapGroups:"core/ldap/groups"},this.hooks={ldapadddone:async t=>{const[e,a]=t,i=a[this.mailAttr],s=a[this.quotaAttr],r=a[this.aliasAttr];if(!i||isGroup(a.objectClass))return;const n=Array.isArray(i)?String(i[0]):String(i);if(this.initDelay>0&&await new Promise(t=>setTimeout(t,this.initDelay)),s){const t=Array.isArray(s)?Number(s[0]):Number(s);!isNaN(t)&&t>0&&await this.callWebAdminApi("ldapadddone:quota",`${this.webadminUrl}/quota/users/${n}/size`,"PUT",e,t.toString(),{mail:n,quota:t},o)}if(r){const t=this.getAliases(r);await Promise.all(t.map(t=>this.callWebAdminApi("ldapadddone:alias",`${this.webadminUrl}/address/aliases/${n}/sources/${t}`,"PUT",e,null,{mail:n,alias:t},o)))}const l=this.getDisplayNameFromAttributes(a);l&&await this.updateJamesIdentity(e,n,l)},onLdapMailChange:async(t,e,a)=>{const i=e?Array.isArray(e)?String(e[0]):String(e):null,s=a?Array.isArray(a)?String(a[0]):String(a):null;if(i)if(s){if(!await this.isGroupDn(t)){await this.callWebAdminApi("onLdapMailChange",`${this.webadminUrl}/users/${i}/rename/${s}?action=rename&force`,"POST",t,null,{oldmail:i,newmail:s},o);try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.aliasAttr]},t);if(e.searchEntries&&e.searchEntries.length>0){const a=this.getAliases(e.searchEntries[0][this.aliasAttr]);a.length>0&&await Promise.all([...a.map(e=>this.callWebAdminApi("onLdapMailChange-delete",`${this.webadminUrl}/address/aliases/${i}/sources/${e}`,"DELETE",t,null,{oldmail:i,alias:e})),...a.map(e=>this.callWebAdminApi("onLdapMailChange-create",`${this.webadminUrl}/address/aliases/${s}/sources/${e}`,"PUT",t,null,{newmail:s,alias:e},o))])}}catch(t){this.logger.debug("Could not fetch aliases for mail change:",t)}}}else this.logger.debug(`Skipping mail rename for ${t}: newmail is empty (mail attribute was deleted, not changed)`);else this.logger.debug(`Skipping mail rename for ${t}: oldmail is empty (mail attribute was added, not changed)`)},onLdapAliasChange:async(t,e,a,i)=>{if(await this.isGroupDn(t))return;const s=a.map(t=>this.normalizeAlias(t)),r=i.map(t=>this.normalizeAlias(t)),n=s.filter(t=>!r.includes(t)),l=r.filter(t=>!s.includes(t));await Promise.all([...n.map(a=>this.callWebAdminApi("onLdapAliasChange-delete",`${this.webadminUrl}/address/aliases/${e}/sources/${a}`,"DELETE",t,null,{mail:e,alias:a,action:"delete"})),...l.map(a=>this.callWebAdminApi("onLdapAliasChange-add",`${this.webadminUrl}/address/aliases/${e}/sources/${a}`,"PUT",t,null,{mail:e,alias:a,action:"add"},o))])},onLdapQuotaChange:async(t,e,a,i)=>{if(!await this.isGroupDn(t))return this.callWebAdminApi("onLdapQuotaChange",`${this.webadminUrl}/quota/users/${e}/size`,"PUT",t,i.toString(),{oldQuota:a,newQuota:i},o)},onLdapForwardChange:async(t,e,a,i)=>{if(await this.isGroupDn(t))return;const s=this.extractMailDomain(e);if(!s)return void this.logger.error(`Cannot extract domain from mail ${e} for forward management`);const r=a.filter(t=>!i.includes(t)),n=i.filter(t=>!a.includes(t));await Promise.all([...r.map(a=>this.callWebAdminApi("onLdapForwardChange-delete",`${this.webadminUrl}/address/forwards/${e}/targets/${a}`,"DELETE",t,null,{mail:e,forward:a,domain:s,action:"delete"})),...n.map(a=>this.callWebAdminApi("onLdapForwardChange-add",`${this.webadminUrl}/address/forwards/${e}/targets/${a}`,"PUT",t,null,{mail:e,forward:a,domain:s,action:"add"},o))])},onLdapChange:async(t,e)=>{this.config.delegation_attribute&&e[this.config.delegation_attribute]&&!await this.isGroupDn(t)&&await this._handleDelegationChange(t,e)},onLdapDisplayNameChange:async t=>{const e=[this.mailAttr,this.displayNameAttr,"cn","givenName","sn","objectClass"],a=await this.ldapGetAttributes(t,e);if(!a)return void this.logger.warn(`Cannot update James identity: entry not found for ${t}`);if(isGroup(a.objectClass))return;const i=this.attributeToString(a[this.mailAttr]);if(!i)return void this.logger.warn(`Cannot update James identity: no mail found for ${t}`);const s=this.getDisplayNameFromAttributes(a);if(s)return this.updateJamesIdentity(t,i,s);this.logger.warn(`Cannot update James identity: no display name found for ${t}`)},onLdapEntryChange:(t,e,a)=>this.followTeamMailbox(e,a)},this.teamMailboxQueue=new Map,this.identityQueue=new Map,this.quotaAttr=this.config.quota_attribute,this.aliasAttr=this.config.alias_attribute||"mailAlternateAddress",this.mailboxTypeAttr=this.config.james_mailbox_type_attribute||"twakeMailboxType",this.initDelay="number"==typeof this.config.james_init_delay?this.config.james_init_delay:1e3,this.retryAttempts="number"==typeof this.config.james_retry_attempts?Math.max(1,this.config.james_retry_attempts):5,this.retryDelay="number"==typeof this.config.james_retry_delay?Math.max(0,this.config.james_retry_delay):1e3}api(t){const e=this.config.api_prefix||"/api";t.get(`${e}/v1/users/:user/quota-usage`,a((t,e)=>this.getUserQuota(t,e))),this.logger.info(`James API registered at ${e}/v1/users/:user/quota-usage`)}async getUserQuota(e,a){if(!n(e,a))return;const s=e.params.user;try{const e=i(s),r=await this.server.ldap.search({scope:"sub",filter:`(uid=${e})`,paged:!1,attributes:[this.mailAttr]},this.config.ldap_base||""),n=r.searchEntries?.[0];if(!n)return void a.status(404).json({error:`User ${s} not found`});const o=n[this.mailAttr];if(!o)return void a.status(400).json({error:`User ${s} has no mail attribute`});const l=Array.isArray(o)?String(o[0]):String(o),u=`${this.webadminUrl}/quota/users/${l}`,m=await this.requestLimit(()=>t(u,{method:"GET",headers:this.createHeaders()}));if(!m.ok)return void(404===m.status?a.status(404).json({error:`Quota not found for user ${s}`}):a.status(502).json({error:`Failed to retrieve quota from James: ${m.statusText}`}));const d=await m.json();a.json(d)}catch(t){this.logger.error({plugin:this.name,event:"getUserQuota",username:s,error:t instanceof Error?t.message:String(t)}),a.status(500).json({error:"Internal server error while retrieving quota"})}}teamMailboxOf(t){if(!t||"teamMailbox"!==this.getMailboxType(t))return null;const e=this.attributeToString(t.mail);if(!e)return null;const a=String(t.dn);return{mail:e,refused:(this.config.james_mailing_list_branch||[]).some(t=>a===t||a.endsWith(","+t))}}async followTeamMailbox(t,e){const a=this.teamMailboxOf(t),i=this.teamMailboxOf(e);if(!a&&!i)return;const s=String((e??t).dn);i?.refused&&!a?.refused&&this.logger.error({plugin:this.name,event:"teamMailbox",dn:s,error:`Team mailbox cannot be in mailing list branches: ${(this.config.james_mailing_list_branch||[]).join(", ")}`});const r=a&&!a.refused?a.mail:null,n=i&&!i.refused?i.mail:null;if(!r&&!n)return;const o=memberKey(s),follow=async()=>{if(r&&n&&r.toLowerCase()===n.toLowerCase()){const a=new Map(members(t).map(t=>[memberKey(t),t])),i=[];for(const t of members(e))a.delete(memberKey(t))||i.push(t);return void await Promise.all([this.teamMailboxMembers(s,n,"PUT",i),this.teamMailboxMembers(s,n,"DELETE",[...a.values()])])}r&&await this.teamMailboxMembers(s,r,"DELETE",members(t)),n&&await this.createTeamMailbox(s,e,n)},l=(this.teamMailboxQueue.get(o)??Promise.resolve()).then(follow,follow);this.teamMailboxQueue.set(o,l);try{await l}finally{this.teamMailboxQueue.get(o)===l&&this.teamMailboxQueue.delete(o)}}async teamMailboxMembers(t,e,a,i){const s=this.extractMailDomain(e);if(!s||!i.length)return;const r=await this.getMemberEmails(i);await Promise.all(r.map(i=>this.callWebAdminApi("teamMailbox:"+("PUT"===a?"addMember":"removeMember"),`${this.webadminUrl}/domains/${s}/team-mailboxes/${e}/members/${i}`,a,t,null,{domain:s,teamMailbox:e,memberMail:i})))}getDisplayNameFromAttributes(t){const e=this.attributeToString(t[this.displayNameAttr]);if(e)return e;const a=this.attributeToString(t.cn);if(a)return a;const i=this.attributeToString(t.givenName),s=this.attributeToString(t.sn);if(i||s){const t=[];return i&&t.push(i),s&&t.push(s),t.join(" ")}const r=this.attributeToString(t[this.mailAttr]);return r||null}async isGroupDn(t){const e=await this.ldapGetAttributes(t,["objectClass"]);return isGroup(e?.objectClass)}async getMailFromDN(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}async getDisplayNameFromDN(t){const e=[this.displayNameAttr,"cn","givenName","sn",this.mailAttr],a=await this.ldapGetAttributes(t,e);return a?this.getDisplayNameFromAttributes(a):null}async generateSignature(t){const e=this.config.james_signature_template;if(!e)return null;const a=await this.ldapGetAttributes(t);if(!a)return null;let i=e;return i=i.replace(/\{(\w+)\}/g,(t,e)=>"string"==typeof e&&a&&"object"==typeof a&&Object.prototype.hasOwnProperty.call(a,e)&&this.attributeToString(a[e])||""),i}async updateJamesIdentity(t,e,a){const i=(this.identityQueue.get(e)??Promise.resolve()).then(()=>this.doUpdateJamesIdentity(t,e,a));this.identityQueue.set(e,i);try{await i}finally{this.identityQueue.get(e)===i&&this.identityQueue.delete(e)}}async doUpdateJamesIdentity(e,a,i){const s={plugin:this.name,event:"onLdapDisplayNameChange",result:"error",dn:e,mail:a,displayName:i};for(let r=1;;r++){try{const n=`${this.webadminUrl}/users/${a}/identities`,l=await this.requestLimit(()=>t(`${n}?default=true`,{method:"GET",headers:this.createHeaders()}));let u;if(l.ok){const t=await l.json();u=(Array.isArray(t)?t:[t]).filter(t=>t&&t.id&&!1!==t.mayDelete).sort((t,e)=>(t.sortOrder??0)-(e.sortOrder??0))[0]}else if(404!==l.status){const t="get_default_identity";if(this.shouldRetry(l.status,o,r)){await this.warnAndWait(s,t,r,l);continue}return void this.logger.error({...s,step:t,...r>1?{attempts:r}:{},http_status:l.status,http_status_text:l.statusText})}const m=await this.generateSignature(e),d=u?.id?{id:u.id,email:u.email,name:i}:{email:a,name:i,sortOrder:0};m&&(d.htmlSignature=m);const h=u?.id?"update_identity":"create_identity",c=await this.requestLimit(()=>t(u?.id?`${n}/${u.id}`:n,{method:u?.id?"PUT":"POST",headers:this.createHeaders("application/json"),body:JSON.stringify(d)}));if(c.ok)this.logger.info({...s,step:h,result:"success",http_status:c.status});else{if(this.shouldRetry(c.status,o,r)){await this.warnAndWait(s,h,r,c);continue}this.logger.error({...s,step:h,...r>1?{attempts:r}:{},http_status:c.status,http_status_text:c.statusText})}}catch(t){this.logger.error({...s,error:`${t}`})}return}}async warnAndWait(t,e,a,i){this.logger.warn({...t,step:e,result:"retry",attempt:a,http_status:i.status,http_status_text:i.statusText}),await this.waitBeforeRetry(a)}async deleteUserData(e,a){const i={plugin:this.name,event:"deleteUserData",mail:e,fromStep:a};try{const s=new URL(`${this.webadminUrl}/users/${e}`);s.searchParams.set("action","deleteData"),a&&s.searchParams.set("fromStep",a);const r=await this.requestLimit(()=>t(s.toString(),{method:"POST",headers:this.createHeaders()}));if(!r.ok)return this.logger.error({...i,http_status:r.status,http_status_text:r.statusText}),null;const n=await r.json();return this.logger.info({...i,http_status:r.status,taskId:n.taskId}),n}catch(t){return this.logger.error({...i,error:`${t}`}),null}}getMailboxType(t){const e=t[this.mailboxTypeAttr];if(!e)return null;const a=(Array.isArray(e)?String(e[0]):String(e)).match(/^cn=([^,]+)/);if(!a)return null;const i=a[1];return"group"===i||"mailingList"===i||"teamMailbox"===i?i:null}async createTeamMailbox(t,e,a){const i=this.extractMailDomain(a);i?(await this.callWebAdminApi("teamMailbox:create",`${this.webadminUrl}/domains/${i}/team-mailboxes/${a}`,"PUT",t,null,{domain:i,teamMailbox:a}),await this.teamMailboxMembers(t,a,"PUT",members(e))):this.logger.error({plugin:this.name,event:"createTeamMailbox",dn:t,mail:a,error:"Cannot extract domain from mail address"})}async getMemberEmails(t){const e=t.filter(t=>!s(t,this.config.group_dummy_user)).map(t=>this.server.ldap.queryLimit(async()=>{const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}));return(await Promise.all(e)).filter(t=>null!==t)}async getGroupMail(t){const e=await this.ldapGetAttributes(t,["mail"]);return e?this.attributeToString(e.mail):null}async getGroupMailboxType(t){try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.mailboxTypeAttr]},t);if(e.searchEntries&&e.searchEntries.length>0)return this.getMailboxType(e.searchEntries[0])}catch(e){const a=e instanceof Error?e.message:String(e);this.logger.debug(`Could not get mailbox type for group ${t}: ${a}`)}return null}async _handleDelegationChange(t,e){const a=await this.ldapGetAttributes(t,[this.mailAttr]);if(!a)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:t,message:"Could not find user entry to get mail attribute"});const i=a[this.mailAttr];if(!i||"string"!=typeof i)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:t,message:"User has no mail attribute, cannot manage delegation"});const s=this.config.delegation_attribute;if(!s)return;const[r,n]=e[s]||[],l=this._normalizeToArray(r),u=this._normalizeToArray(n),m=u.filter(t=>!l.includes(t)),d=l.filter(t=>!u.includes(t)),h=m.map(t=>this.server.ldap.queryLimit(async()=>{const e=await this._getDelegateEmail(t);return{dn:t,email:e}})),c=d.map(t=>this.server.ldap.queryLimit(async()=>{const e=await this._getDelegateEmail(t);return{dn:t,email:e}})),[g,p]=await Promise.all([Promise.all(h),Promise.all(c)]),b=[];for(const{dn:e,email:a}of g)a&&b.push(this.callWebAdminApi("onLdapChange:addDelegation",`${this.webadminUrl}/users/${i}/authorizedUsers/${a}`,"PUT",t,null,{userMail:i,delegateEmail:a,delegateDN:e,action:"add"},o));for(const{dn:e,email:a}of p)a&&b.push(this.callWebAdminApi("onLdapChange:removeDelegation",`${this.webadminUrl}/users/${i}/authorizedUsers/${a}`,"DELETE",t,null,{userMail:i,delegateEmail:a,delegateDN:e,action:"remove"}));await Promise.all(b)}async _getDelegateEmail(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):(this.logger.warn({plugin:this.name,event:"getDelegateEmail",dn:t,message:"Could not resolve delegate DN to email"}),null)}_normalizeToArray(t){return t?Array.isArray(t)?t:[t]:[]}}export{James as default};
|
|
1
|
+
import t from"node-fetch";import{TwakePlugin as e}from"../../abstract/twakePlugin.js";import{asyncHandler as a,escapeLdapFilter as i,isDummyMemberDn as s,normalizeDn as r}from"../../lib/utils.js";import{wantJson as n}from"../../lib/expressFormatedResponses.js";const o=[400,404],pickDefaultIdentity=t=>(Array.isArray(t)?t:[t]).filter(t=>t&&t.id&&!1!==t.mayDelete).sort((t,e)=>(t.sortOrder??0)-(e.sortOrder??0))[0],members=t=>{const e=t?.member;return void 0===e?[]:(Array.isArray(e)?e:[e]).map(String)},memberKey=t=>{try{return r(t)}catch{return t.toLowerCase()}},l=["groupofnames","groupofuniquenames","groupofurls"],isGroup=t=>(Array.isArray(t)?t:t?[t]:[]).some(t=>l.includes(String(t).toLowerCase()));class James extends e{constructor(t){super(t,"james_webadmin_url","james_webadmin_token","james_concurrency"),this.name="james",this.roles=["consistency","api"],this.dependencies={onLdapChange:"core/ldap/onChange",ldapGroups:"core/ldap/groups"},this.hooks={ldapadddone:([t,e])=>this.provisionAccount(t,e),onLdapMailChange:async(t,e,a)=>{const i=e?Array.isArray(e)?String(e[0]):String(e):null,s=a?Array.isArray(a)?String(a[0]):String(a):null;if(!i)return void this.logger.debug(`Skipping mail rename for ${t}: oldmail is empty (mail attribute was added, not changed)`);if(!s)return void this.logger.debug(`Skipping mail rename for ${t}: newmail is empty (mail attribute was deleted, not changed)`);if(await this.isGroupDn(t))return;const r=await this.defaultSignatures(i),n=await this.ldapGetAttributes(t,[this.forwardAttr,this.displayNameAttr,"cn","givenName","sn",this.mailAttr]);await this.dropStaleForwards(t,s,this.ownForwards(n?.[this.forwardAttr])),await this.callWebAdminApi("onLdapMailChange",`${this.webadminUrl}/users/${i}/rename/${s}?action=rename&force`,"POST",t,null,{oldmail:i,newmail:s},o);try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.aliasAttr]},t);if(e.searchEntries&&e.searchEntries.length>0){const a=this.getAliases(e.searchEntries[0][this.aliasAttr]);a.length>0&&await Promise.all([...a.map(e=>this.callWebAdminApi("onLdapMailChange-delete",`${this.webadminUrl}/address/aliases/${i}/sources/${e}`,"DELETE",t,null,{oldmail:i,alias:e})),...a.map(e=>this.callWebAdminApi("onLdapMailChange-create",`${this.webadminUrl}/address/aliases/${s}/sources/${e}`,"PUT",t,null,{newmail:s,alias:e},o))])}}catch(t){this.logger.debug("Could not fetch aliases for mail change:",t)}const l=n&&this.getDisplayNameFromAttributes(n);l&&await this.updateJamesIdentity(t,s,l,r)},onLdapAliasChange:async(t,e,a,i)=>{if(await this.isGroupDn(t))return;const s=a.map(t=>this.normalizeAlias(t)),r=i.map(t=>this.normalizeAlias(t)),n=s.filter(t=>!r.includes(t)),l=r.filter(t=>!s.includes(t));await Promise.all([...n.map(a=>this.callWebAdminApi("onLdapAliasChange-delete",`${this.webadminUrl}/address/aliases/${e}/sources/${a}`,"DELETE",t,null,{mail:e,alias:a,action:"delete"})),...l.map(async a=>{await this.dropStaleForwards(t,a,[]),await this.callWebAdminApi("onLdapAliasChange-add",`${this.webadminUrl}/address/aliases/${e}/sources/${a}`,"PUT",t,null,{mail:e,alias:a,action:"add"},o)})])},onLdapQuotaChange:async(t,e,a,i)=>{if(!await this.isGroupDn(t))return this.callWebAdminApi("onLdapQuotaChange",`${this.webadminUrl}/quota/users/${e}/size`,"PUT",t,i.toString(),{oldQuota:a,newQuota:i},o)},onLdapForwardChange:async(t,e,a,i)=>{if(!this.config.james_sync_forwards)return;if(await this.isGroupDn(t))return;const s=this.extractMailDomain(e);if(!s)return void this.logger.error(`Cannot extract domain from mail ${e} for forward management`);const r=a.filter(t=>!i.includes(t)),n=i.filter(t=>!a.includes(t));await Promise.all([...r.map(a=>this.callWebAdminApi("onLdapForwardChange-delete",`${this.webadminUrl}/address/forwards/${e}/targets/${a}`,"DELETE",t,null,{mail:e,forward:a,domain:s,action:"delete"})),...n.map(a=>this.callWebAdminApi("onLdapForwardChange-add",`${this.webadminUrl}/address/forwards/${e}/targets/${a}`,"PUT",t,null,{mail:e,forward:a,domain:s,action:"add"},o))])},onLdapChange:async(t,e)=>{this.config.delegation_attribute&&e[this.config.delegation_attribute]&&!await this.isGroupDn(t)&&await this._handleDelegationChange(t,e)},onLdapDisplayNameChange:async t=>{const e=[this.mailAttr,this.displayNameAttr,"cn","givenName","sn","objectClass"],a=await this.ldapGetAttributes(t,e);if(!a)return void this.logger.warn(`Cannot update James identity: entry not found for ${t}`);if(isGroup(a.objectClass))return;const i=this.attributeToString(a[this.mailAttr]);if(!i)return void this.logger.warn(`Cannot update James identity: no mail found for ${t}`);const s=this.getDisplayNameFromAttributes(a);if(s)return this.updateJamesIdentity(t,i,s);this.logger.warn(`Cannot update James identity: no display name found for ${t}`)},onLdapEntryChange:async(t,e,a)=>{e&&a&&!e[this.mailAttr]&&a[this.mailAttr]&&await this.provisionAccount(t,a),await this.followTeamMailbox(e,a)}},this.teamMailboxQueue=new Map,this.identityQueue=new Map,this.quotaAttr=this.config.quota_attribute,this.aliasAttr=this.config.alias_attribute||"mailAlternateAddress",this.mailboxTypeAttr=this.config.james_mailbox_type_attribute||"twakeMailboxType",this.forwardAttr=this.config.forward_attribute||"mailForwardingAddress",this.initDelay="number"==typeof this.config.james_init_delay?this.config.james_init_delay:1e3,this.retryAttempts="number"==typeof this.config.james_retry_attempts?Math.max(1,this.config.james_retry_attempts):5,this.retryDelay="number"==typeof this.config.james_retry_delay?Math.max(0,this.config.james_retry_delay):1e3}api(t){const e=this.config.api_prefix||"/api";t.get(`${e}/v1/users/:user/quota-usage`,a((t,e)=>this.getUserQuota(t,e))),this.logger.info(`James API registered at ${e}/v1/users/:user/quota-usage`)}async getUserQuota(e,a){if(!n(e,a))return;const s=e.params.user;try{const e=i(s),r=await this.server.ldap.search({scope:"sub",filter:`(uid=${e})`,paged:!1,attributes:[this.mailAttr]},this.config.ldap_base||""),n=r.searchEntries?.[0];if(!n)return void a.status(404).json({error:`User ${s} not found`});const o=n[this.mailAttr];if(!o)return void a.status(400).json({error:`User ${s} has no mail attribute`});const l=Array.isArray(o)?String(o[0]):String(o),d=`${this.webadminUrl}/quota/users/${l}`,u=await this.requestLimit(()=>t(d,{method:"GET",headers:this.createHeaders()}));if(!u.ok)return void(404===u.status?a.status(404).json({error:`Quota not found for user ${s}`}):a.status(502).json({error:`Failed to retrieve quota from James: ${u.statusText}`}));const m=await u.json();a.json(m)}catch(t){this.logger.error({plugin:this.name,event:"getUserQuota",username:s,error:t instanceof Error?t.message:String(t)}),a.status(500).json({error:"Internal server error while retrieving quota"})}}teamMailboxOf(t){if(!t||"teamMailbox"!==this.getMailboxType(t))return null;const e=this.attributeToString(t.mail);if(!e)return null;const a=String(t.dn),i=this.config.james_mailing_list_branch||[];if(i.some(t=>a===t||a.endsWith(","+t)))return{mail:e,refused:`Team mailbox cannot be in mailing list branches: ${i.join(", ")}`};const s=e.lastIndexOf("@");return/^[#a-zA-Z0-9_-]{1,255}$/.test(e.slice(0,s))&&this.extractMailDomain(e)?{mail:e,refused:null}:{mail:e,refused:"A team mailbox address is a name of letters, digits, #, - and _ at a domain"}}async provisionAccount(t,e){const a=e[this.mailAttr],i=e[this.quotaAttr],s=e[this.aliasAttr];if(!a||isGroup(e.objectClass))return;const r=Array.isArray(a)?String(a[0]):String(a);if(this.initDelay>0&&await new Promise(t=>setTimeout(t,this.initDelay)),i){const e=Array.isArray(i)?Number(i[0]):Number(i);!isNaN(e)&&e>0&&await this.callWebAdminApi("ldapadddone:quota",`${this.webadminUrl}/quota/users/${r}/size`,"PUT",t,e.toString(),{mail:r,quota:e},o)}const n=this.getAliases(s);await Promise.all([this.dropStaleForwards(t,r,this.ownForwards(e[this.forwardAttr])),...n.map(e=>this.dropStaleForwards(t,e,[]))]),n.length>0&&await Promise.all(n.map(e=>this.callWebAdminApi("ldapadddone:alias",`${this.webadminUrl}/address/aliases/${r}/sources/${e}`,"PUT",t,null,{mail:r,alias:e},o)));const l=this.getDisplayNameFromAttributes(e);l&&await this.updateJamesIdentity(t,r,l)}async followTeamMailbox(t,e){const a=this.teamMailboxOf(t),i=this.teamMailboxOf(e);if(!a&&!i)return;const s=String((e??t).dn);i?.refused&&i.refused!==a?.refused&&this.logger.error({plugin:this.name,event:"teamMailbox",dn:s,mail:i.mail,error:i.refused});const r=a&&!a.refused?a.mail:null,n=i&&!i.refused?i.mail:null;if(!r&&!n)return;const o=memberKey(s),follow=async()=>{if(r&&n&&r.toLowerCase()===n.toLowerCase()){const a=new Map(members(t).map(t=>[memberKey(t),t])),i=[];for(const t of members(e))a.delete(memberKey(t))||i.push(t);return void await Promise.all([this.teamMailboxMembers(s,n,"PUT",i),this.teamMailboxMembers(s,n,"DELETE",[...a.values()])])}r&&await this.teamMailboxMembers(s,r,"DELETE",members(t)),n&&await this.createTeamMailbox(s,e,n)},l=(this.teamMailboxQueue.get(o)??Promise.resolve()).then(follow,follow);this.teamMailboxQueue.set(o,l);try{await l}finally{this.teamMailboxQueue.get(o)===l&&this.teamMailboxQueue.delete(o)}}teamMailboxUrl(t){const e=t.toLowerCase(),a=e.lastIndexOf("@");return`${this.webadminUrl}/domains/${e.slice(a+1)}/team-mailboxes/${encodeURIComponent(e.slice(0,a))}`}async teamMailboxMembers(t,e,a,i){if(!i.length)return;const s=this.teamMailboxUrl(e),r=await this.getMemberEmails(i);await Promise.all(r.map(i=>this.callWebAdminApi("teamMailbox:"+("PUT"===a?"addMember":"removeMember"),`${s}/members/${i}`,a,t,null,{teamMailbox:e,memberMail:i})))}getDisplayNameFromAttributes(t){const e=this.attributeToString(t[this.displayNameAttr]);if(e)return e;const a=this.attributeToString(t.cn);if(a)return a;const i=this.attributeToString(t.givenName),s=this.attributeToString(t.sn);if(i||s){const t=[];return i&&t.push(i),s&&t.push(s),t.join(" ")}const r=this.attributeToString(t[this.mailAttr]);return r||null}async isGroupDn(t){const e=await this.ldapGetAttributes(t,["objectClass"]);return isGroup(e?.objectClass)}async getMailFromDN(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}async getDisplayNameFromDN(t){const e=[this.displayNameAttr,"cn","givenName","sn",this.mailAttr],a=await this.ldapGetAttributes(t,e);return a?this.getDisplayNameFromAttributes(a):null}async generateSignature(t){const e=this.config.james_signature_template;if(!e)return null;const a=await this.ldapGetAttributes(t);if(!a)return null;let i=e;return i=i.replace(/\{(\w+)\}/g,(t,e)=>"string"==typeof e&&a&&"object"==typeof a&&Object.prototype.hasOwnProperty.call(a,e)&&this.attributeToString(a[e])||""),i}async updateJamesIdentity(t,e,a,i={}){const s=(this.identityQueue.get(e)??Promise.resolve()).then(()=>this.doUpdateJamesIdentity(t,e,a,i));this.identityQueue.set(e,s);try{await s}finally{this.identityQueue.get(e)===s&&this.identityQueue.delete(e)}}async doUpdateJamesIdentity(e,a,i,s){const r={plugin:this.name,event:"onLdapDisplayNameChange",result:"error",dn:e,mail:a,displayName:i};for(let n=1;;n++){try{const l=`${this.webadminUrl}/users/${a}/identities`,d=await this.requestLimit(()=>t(`${l}?default=true`,{method:"GET",headers:this.createHeaders()}));let u;if(d.ok)u=pickDefaultIdentity(await d.json());else if(404!==d.status){const t="get_default_identity";if(this.shouldRetry(d.status,o,n)){await this.warnAndWait(r,t,n,d);continue}return void this.logger.error({...r,step:t,...n>1?{attempts:n}:{},http_status:d.status,http_status_text:d.statusText})}const m=await this.generateSignature(e),h=u?.id?{id:u.id,email:u.email,name:i}:{email:a,name:i,sortOrder:0};m?h.htmlSignature=m:Object.assign(h,s);const c=u?.id?"update_identity":"create_identity",g=await this.requestLimit(()=>t(u?.id?`${l}/${u.id}`:l,{method:u?.id?"PUT":"POST",headers:this.createHeaders("application/json"),body:JSON.stringify(h)}));if(g.ok)this.logger.info({...r,step:c,result:"success",http_status:g.status});else{if(this.shouldRetry(g.status,o,n)){await this.warnAndWait(r,c,n,g);continue}this.logger.error({...r,step:c,...n>1?{attempts:n}:{},http_status:g.status,http_status_text:g.statusText})}}catch(t){this.logger.error({...r,error:`${t}`})}return}}async defaultSignatures(e){try{const a=await this.requestLimit(()=>t(`${this.webadminUrl}/users/${e}/identities?default=true`,{method:"GET",headers:this.createHeaders()}));if(!a.ok)return{};const i=pickDefaultIdentity(await a.json()),s={};return i?.htmlSignature&&(s.htmlSignature=i.htmlSignature),i?.textSignature&&(s.textSignature=i.textSignature),s}catch(t){return this.logger.debug(`Could not read the identities of ${e}: ${t}`),{}}}ownForwards(t){return this.config.james_sync_forwards?this.getAliases(t):[]}async dropStaleForwards(e,a,i){let s;try{const r=await this.requestLimit(()=>t(`${this.webadminUrl}/address/forwards/${a}`,{method:"GET",headers:this.createHeaders()}));if(!r.ok)return void(404!==r.status&&this.logger.warn({plugin:this.name,event:"dropStaleForward",result:"error",dn:e,address:a,step:"list_forwards",http_status:r.status,http_status_text:r.statusText}));const n=i.map(t=>t.toLowerCase());s=(await r.json()).map(t=>t.mailAddress).filter(t=>t&&!n.includes(t.toLowerCase()))}catch(t){return void this.logger.debug(`Could not read the forwards of ${a}: ${t}`)}await Promise.all(s.map(t=>this.callWebAdminApi("dropStaleForward",`${this.webadminUrl}/address/forwards/${a}/targets/${t}`,"DELETE",e,null,{address:a,forward:t})))}async warnAndWait(t,e,a,i){this.logger.warn({...t,step:e,result:"retry",attempt:a,http_status:i.status,http_status_text:i.statusText}),await this.waitBeforeRetry(a)}async deleteUserData(e,a){const i={plugin:this.name,event:"deleteUserData",mail:e,fromStep:a};try{const s=new URL(`${this.webadminUrl}/users/${e}`);s.searchParams.set("action","deleteData"),a&&s.searchParams.set("fromStep",a);const r=await this.requestLimit(()=>t(s.toString(),{method:"POST",headers:this.createHeaders()}));if(!r.ok)return this.logger.error({...i,http_status:r.status,http_status_text:r.statusText}),null;const n=await r.json();return this.logger.info({...i,http_status:r.status,taskId:n.taskId}),n}catch(t){return this.logger.error({...i,error:`${t}`}),null}}getMailboxType(t){const e=t[this.mailboxTypeAttr];if(!e)return null;const a=(Array.isArray(e)?String(e[0]):String(e)).match(/^cn=([^,]+)/);if(!a)return null;const i=a[1];return"group"===i||"mailingList"===i||"teamMailbox"===i?i:null}async createTeamMailbox(t,e,a){const i=this.teamMailboxUrl(a);await this.callWebAdminApi("teamMailbox:create",i,"PUT",t,null,{teamMailbox:a}),await this.teamMailboxMembers(t,a,"PUT",members(e))}async getMemberEmails(t){const e=t.filter(t=>!s(t,this.config.group_dummy_user)).map(t=>this.server.ldap.queryLimit(async()=>{const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}));return(await Promise.all(e)).filter(t=>null!==t)}async getGroupMail(t){const e=await this.ldapGetAttributes(t,["mail"]);return e?this.attributeToString(e.mail):null}async getGroupMailboxType(t){try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.mailboxTypeAttr]},t);if(e.searchEntries&&e.searchEntries.length>0)return this.getMailboxType(e.searchEntries[0])}catch(e){const a=e instanceof Error?e.message:String(e);this.logger.debug(`Could not get mailbox type for group ${t}: ${a}`)}return null}async _handleDelegationChange(t,e){const a=await this.ldapGetAttributes(t,[this.mailAttr]);if(!a)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:t,message:"Could not find user entry to get mail attribute"});const i=a[this.mailAttr];if(!i||"string"!=typeof i)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:t,message:"User has no mail attribute, cannot manage delegation"});const s=this.config.delegation_attribute;if(!s)return;const[r,n]=e[s]||[],l=this._normalizeToArray(r),d=this._normalizeToArray(n),u=d.filter(t=>!l.includes(t)),m=l.filter(t=>!d.includes(t)),h=u.map(t=>this.server.ldap.queryLimit(async()=>{const e=await this._getDelegateEmail(t);return{dn:t,email:e}})),c=m.map(t=>this.server.ldap.queryLimit(async()=>{const e=await this._getDelegateEmail(t);return{dn:t,email:e}})),[g,p]=await Promise.all([Promise.all(h),Promise.all(c)]),f=[];for(const{dn:e,email:a}of g)a&&f.push(this.callWebAdminApi("onLdapChange:addDelegation",`${this.webadminUrl}/users/${i}/authorizedUsers/${a}`,"PUT",t,null,{userMail:i,delegateEmail:a,delegateDN:e,action:"add"},o));for(const{dn:e,email:a}of p)a&&f.push(this.callWebAdminApi("onLdapChange:removeDelegation",`${this.webadminUrl}/users/${i}/authorizedUsers/${a}`,"DELETE",t,null,{userMail:i,delegateEmail:a,delegateDN:e,action:"remove"}));await Promise.all(f)}async _getDelegateEmail(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):(this.logger.warn({plugin:this.name,event:"getDelegateEmail",dn:t,message:"Could not resolve delegate DN to email"}),null)}_normalizeToArray(t){return t?Array.isArray(t)?t:[t]:[]}}export{James as default};
|
|
@@ -170,6 +170,7 @@ export interface Config {
|
|
|
170
170
|
james_retry_delay?: number;
|
|
171
171
|
james_mailing_list_branch?: string[];
|
|
172
172
|
james_mailbox_type_attribute?: string;
|
|
173
|
+
james_sync_forwards?: boolean;
|
|
173
174
|
twake_drive_webadmin_url?: string;
|
|
174
175
|
twake_drive_webadmin_token?: string;
|
|
175
176
|
twake_drive_concurrency?: number;
|
|
@@ -198,6 +198,48 @@ export default abstract class AuthzBase extends DmPlugin {
|
|
|
198
198
|
/** Said once: a rule keyed on a value the authenticator did not publish. */
|
|
199
199
|
private fallbackWarned;
|
|
200
200
|
protected resolveCaller(req: DmRequest): Promise<string | null>;
|
|
201
|
+
/**
|
|
202
|
+
* The attribute linking an organization to the mail domains its entries
|
|
203
|
+
* may use: the organization schema's `domainLink` role, or
|
|
204
|
+
* `--enterprise-domain-link-attribute`, as `enterpriseRules` reads it.
|
|
205
|
+
*
|
|
206
|
+
* @returns the attribute name, or undefined when no domain is linked
|
|
207
|
+
*/
|
|
208
|
+
protected domainLinkAttribute(): string | undefined;
|
|
209
|
+
/**
|
|
210
|
+
* The values a write gives an attribute, whatever case its name is
|
|
211
|
+
* spelled in.
|
|
212
|
+
*/
|
|
213
|
+
private static valuesOf;
|
|
214
|
+
/**
|
|
215
|
+
* The values of an attribute on stored entries, read as the server, not
|
|
216
|
+
* as the caller.
|
|
217
|
+
*/
|
|
218
|
+
private storedValues;
|
|
219
|
+
/**
|
|
220
|
+
* Refuse a delegated administrator a mail domain their branch was not
|
|
221
|
+
* given.
|
|
222
|
+
*
|
|
223
|
+
* The domains an organization's entries may use are those it links and
|
|
224
|
+
* those every organization above it links (`enterpriseRules`). Writing an
|
|
225
|
+
* organization only asks for write on its parent, so an administrator
|
|
226
|
+
* could create a sub-organization of their branch linked to any domain of
|
|
227
|
+
* the directory — another customer's — and create addresses in it (#325).
|
|
228
|
+
*
|
|
229
|
+
* A domain newly linked to an organization must therefore be one an
|
|
230
|
+
* organization above it already links. Only a caller holding write on the
|
|
231
|
+
* top organization — who creates the organizations domains are given to —
|
|
232
|
+
* links another. Removing a link narrows nothing above the caller's own
|
|
233
|
+
* branch and is judged as any other write.
|
|
234
|
+
*
|
|
235
|
+
* @param user the resolved caller
|
|
236
|
+
* @param req the request, for the refusal's wording
|
|
237
|
+
* @param dn the organization being written
|
|
238
|
+
* @param submitted the domain DNs the write gives it
|
|
239
|
+
* @param attr the domain link attribute
|
|
240
|
+
* @param creating whether the organization is being created, so holds none
|
|
241
|
+
*/
|
|
242
|
+
private guardDomainLinks;
|
|
201
243
|
/** Forget what was resolved, for a test that changes the directory. */
|
|
202
244
|
protected clearResolutionCache(): void;
|
|
203
245
|
/**
|
|
@@ -18,4 +18,15 @@ export default class ExternalUsersInGroups extends DmPlugin {
|
|
|
18
18
|
ldap: ldapActions;
|
|
19
19
|
constructor(server: DM);
|
|
20
20
|
hooks: Hooks;
|
|
21
|
+
/**
|
|
22
|
+
* The address of an external member, unescaped: the value of a `mail` RDN
|
|
23
|
+
* right under the branch. Null for any other member.
|
|
24
|
+
*/
|
|
25
|
+
externalAddress(member: string): string | null;
|
|
26
|
+
/** `--mail-domain`, lower-cased: the domains no external member may use */
|
|
27
|
+
managedDomains(): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Where a client sends an external member: `mail=<escaped address>,<branch>`
|
|
30
|
+
*/
|
|
31
|
+
getConfigApiData(): Record<string, unknown>;
|
|
21
32
|
}
|
|
@@ -147,8 +147,29 @@ export default class LdapGroups extends DmPlugin {
|
|
|
147
147
|
}, req?: Request): Promise<boolean>;
|
|
148
148
|
renameGroup(cn: string, newCn: string, req?: Request): Promise<boolean>;
|
|
149
149
|
deleteGroup(cn: string, req?: Request): Promise<boolean>;
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Add members to a group.
|
|
152
|
+
*
|
|
153
|
+
* With `req`, the change is judged as a modification of the group, and
|
|
154
|
+
* each member must be an entry the caller can read: the routes used to
|
|
155
|
+
* call this without it, which every authorization plugin reads as an
|
|
156
|
+
* internal call and lets through — anyone authenticated could join any
|
|
157
|
+
* group, a team mailbox included (#325).
|
|
158
|
+
*
|
|
159
|
+
* @param cn group cn or DN
|
|
160
|
+
* @param member member DN(s)
|
|
161
|
+
* @param req the request, to authorize the change against its caller
|
|
162
|
+
*/
|
|
163
|
+
addMember(cn: string, member: string | string[], req?: Request): Promise<boolean>;
|
|
164
|
+
/**
|
|
165
|
+
* Remove a member from a group; with `req`, judged as a modification of
|
|
166
|
+
* the group, as {@link addMember} is.
|
|
167
|
+
*
|
|
168
|
+
* @param cn group cn or DN
|
|
169
|
+
* @param member member DN
|
|
170
|
+
* @param req the request, to authorize the change against its caller
|
|
171
|
+
*/
|
|
172
|
+
deleteMember(cn: string, member: string, req?: Request): Promise<boolean>;
|
|
152
173
|
deleteMemberFromAll(memberDn: string): Promise<void>;
|
|
153
174
|
/**
|
|
154
175
|
* Move a group to a different organization by updating organization link and path attributes
|
|
@@ -189,11 +210,21 @@ export default class LdapGroups extends DmPlugin {
|
|
|
189
210
|
* It calls ldapgroupvalidatemembers hook before validation
|
|
190
211
|
* so that plugins can modify the members list and/or the group DN
|
|
191
212
|
* and/or create missing members on the fly
|
|
213
|
+
*
|
|
214
|
+
* With `req`, each member is looked up as the caller sees the directory:
|
|
215
|
+
* an entry an authorization plugin hides from them is refused as one that
|
|
216
|
+
* does not exist, so that an administrator cannot put an account of
|
|
217
|
+
* another branch in a group of theirs. `--group-allow-unexistent-members`
|
|
218
|
+
* still accepts a member that does not exist, but not one that does and
|
|
219
|
+
* is hidden — which tells the caller it exists, a smaller leak than the
|
|
220
|
+
* membership it would otherwise grant.
|
|
221
|
+
*
|
|
192
222
|
* @param dn Group DN (given to hooks)
|
|
193
223
|
* @param members Array of member DNs to check
|
|
224
|
+
* @param req the request, whose caller must be able to read each member
|
|
194
225
|
* @returns nothing, throw if error
|
|
195
226
|
*/
|
|
196
|
-
validateMembers(dn: string, members: string[]): Promise<void>;
|
|
227
|
+
validateMembers(dn: string, members: string[], req?: Request): Promise<void>;
|
|
197
228
|
validateNewGroup(dn: string, entry: AttributesList): Promise<boolean>;
|
|
198
229
|
validateChanges(dn: string, changes: ModifyRequest): Promise<boolean>;
|
|
199
230
|
_validateOneChange(field: string, value: AttributeValue | null): Promise<boolean>;
|
|
@@ -198,6 +198,11 @@ export default class PasswordPolicy extends DmPlugin {
|
|
|
198
198
|
api(app: Express): void;
|
|
199
199
|
/**
|
|
200
200
|
* Get password status for a user
|
|
201
|
+
*
|
|
202
|
+
* Every lookup of this plugin carries the request, so an authorization
|
|
203
|
+
* plugin answers for the caller: without it, a route read as an internal
|
|
204
|
+
* call, and a delegated administrator could read or unlock any account of
|
|
205
|
+
* the directory (#325).
|
|
201
206
|
*/
|
|
202
207
|
private getPasswordStatus;
|
|
203
208
|
/**
|