ldap-rest 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/bin/index.js +1 -1
  2. package/dist/config/args.js +1 -1
  3. package/dist/lib/authz/composition.js +1 -1
  4. package/dist/lib/instanceProviders.js +1 -0
  5. package/dist/lib/ldapActions.js +1 -1
  6. package/dist/plugins/auth/openidconnect.js +1 -1
  7. package/dist/plugins/configApi.js +1 -1
  8. package/dist/plugins/scim/groups.js +1 -1
  9. package/dist/plugins/scim/mapping.js +1 -1
  10. package/dist/plugins/scim/patch.js +1 -1
  11. package/dist/plugins/scim/users.js +1 -1
  12. package/dist/plugins/twake/instances.js +1 -0
  13. package/dist/plugins/twake/lifecycleAttributes.js +1 -0
  14. package/dist/plugins/twake/lifecycleEvents.js +1 -0
  15. package/dist/plugins/twake/tombstone.js +1 -0
  16. package/dist/src/abstract/plugin.d.ts +7 -4
  17. package/dist/src/config/args.d.ts +48 -0
  18. package/dist/src/hooks.d.ts +2 -0
  19. package/dist/src/lib/authz/composition.d.ts +1 -1
  20. package/dist/src/lib/instanceProviders.d.ts +86 -0
  21. package/dist/src/lib/ldapActions.d.ts +10 -2
  22. package/dist/src/plugins/auth/openidconnect.d.ts +24 -0
  23. package/dist/src/plugins/scim/mapping.d.ts +4 -1
  24. package/dist/src/plugins/scim/patch.d.ts +7 -0
  25. package/dist/src/plugins/twake/instances.d.ts +88 -0
  26. package/dist/src/plugins/twake/lifecycleAttributes.d.ts +29 -0
  27. package/dist/src/plugins/twake/lifecycleEvents.d.ts +45 -0
  28. package/dist/src/plugins/twake/tombstone.d.ts +86 -0
  29. package/docs/client-development/api/rest-api.md +2 -0
  30. package/docs/plugin-development/hooks.md +24 -0
  31. package/docs/usage/configuration.md +75 -0
  32. package/docs/usage/plugins/README.md +9 -6
  33. package/docs/usage/plugins/auth/oidc.md +27 -0
  34. package/docs/usage/plugins/integrations/README.md +11 -4
  35. package/docs/usage/plugins/integrations/instances.md +122 -0
  36. package/docs/usage/plugins/integrations/lifecycle-events.md +200 -0
  37. package/docs/usage/plugins/integrations/scim.md +11 -0
  38. package/docs/usage/plugins/integrations/tombstone.md +115 -0
  39. package/docs/usage/upgrading.md +58 -0
  40. package/package.json +17 -1
package/dist/bin/index.js CHANGED
@@ -1 +1 @@
1
- import{fileURLToPath as e}from"url";import{join as t,dirname as r}from"path";import i from"express";import s from"body-parser";import{parseConfig as o}from"../lib/parseConfig.js";import n from"../config/args.js";import a from"../lib/ldapActions.js";import{buildLogger as h}from"../logger/winston.js";import{setLogger as l}from"../lib/expressFormatedResponses.js";import{prefixCoversPath as u}from"../lib/auth/base.js";import{assertAuthzComposition as d}from"../lib/authz/composition.js";import{recordHookOwner as g}from"../lib/utils.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,hookOwner,isChildOf,isDnInBranch,isDummyMemberDn,launchHooks,launchHooksChained,normalizeDn,organizationLink,parseDn,rdnValue,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";import c from"../plugins/priority.json.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this.authenticators=[],this._authState={dispatcherMounted:!1},this._errorMiddlewareSetup=!1,this._composed=!1,this.config=o(n),this.app=i(),this.app.use(s.json()),this.app.use(s.urlencoded({extended:!0})),this.logger=h(this.config),this.ldap=new a(this),l(this.logger),this.operationSequence=0;const e=[];if(this.config.plugin){const t=this.config.plugin.find(e=>e.includes("configApi"));let r=this.config.plugin.filter(e=>!e.includes("configApi"));const i=this.logger,s=(async()=>{for(const e of c){const t=this.resolveModule(DM.moduleOf(e)),s=r.filter(e=>this.resolveModule(DM.moduleOf(e))===t);if(0!==s.length){r=r.filter(e=>!s.includes(e));for(const e of s)i.debug(`Loading ${e} in the priority pass`),await this.loadPlugin(e)}}})();e.push(s),e.push(s.then(async()=>{const e=r.map(e=>(i.debug(`Loading ${e} in the parallel batch`),this.loadPlugin(e)));await Promise.all(e)})),t&&e.push(Promise.all(e).then(()=>this.loadPlugin(t)))}this.ready=new Promise((t,r)=>{e.length>0?Promise.all(e).then(()=>{this.setupErrorMiddleware(),this.warnUnauthenticatedRoutes(),d(this),this._composed=!0,this.afterLoad(),t()}).catch(e=>r(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),this._composed=!0,t())})}routerStack(){const e=this.app;return(e.router??e._router)?.stack}withConfig(e){return Object.assign(Object.create(Object.getPrototypeOf(this)),this,{config:e})}mountAuthDispatcher(e){const t=e.roles||[];t.includes("protect")||t.includes("logging")||this.mountAuthDispatcherNow()}mountAuthDispatcherNow(){this._authState.dispatcherMounted||(this._authState.dispatcherMounted=!0,this.app.use((e,t,r)=>{this.dispatchAuth(e,t,r)}))}registerAuthenticator(e){this.mountAuthDispatcherNow(),this.authenticators.includes(e)||this.authenticators.push(e)}dispatchAuth(e,t,r){let i=-1,s=[];for(const t of this.authenticators)for(const r of t.pathPrefixes)u(r,e.path)&&(r.length>i?(i=r.length,s=[t]):r.length!==i||s.includes(t)||s.push(t));if(i<0&&(s=this.authenticators.filter(e=>0===e.pathPrefixes.length)),0===s.length)return r();const finish=i=>{if(i>=s.length)return r();const o=s[i];if(!o.afterChain)return finish(i+1);o.afterChain(e,t,()=>finish(i+1))},o=[];e.authenticators=o;const run=r=>{if(r>=s.length)return finish(0);const i=s[r];i.authenticate(e,t,()=>{i.vouchedFor(e)&&o.push(i.name),run(r+1)})};run(0)}afterLoad(){for(const e of Object.values(this.loadedPlugins))try{e.afterLoad?.()}catch(t){this.logger.error(`Plugin ${e.name}: afterLoad failed: ${String(t)}`)}}warnUnauthenticatedRoutes(){const e=Object.values(this.loadedPlugins).filter(e=>e.roles?.includes("auth"));if(0===e.length)return[];const t=[];for(const r of e){if(!r.pathPrefixes||0===r.pathPrefixes.length)return[];t.push(...r.pathPrefixes)}const r=this.routerStack(),i=new Set;for(const e of r||[]){const t=e.route?.path;"string"==typeof t?i.add(t):Array.isArray(t)&&t.forEach(e=>"string"==typeof e&&i.add(e))}const s=[...i].filter(e=>!t.some(t=>e===t||e.startsWith(`${t}/`)));return s.length>0&&this.logger.warn(`Authentication is restricted to ${t.join(", ")}, so these routes are served without authentication: ${s.sort().join(", ")}`),s.sort()}setupErrorMiddleware(){if(this._errorMiddlewareSetup&&this._errorMiddleware){const e=this.routerStack();if(e){const t=e.findIndex(e=>e.handle===this._errorMiddleware);-1!==t&&e.splice(t,1)}}this._errorMiddleware=(e,t,r,i)=>{let s="statusCode"in e?e.statusCode:500,o=e.message;if(500===s&&/\[authz-forbidden\]/.test(e.message)?(s=403,o="Token does not have permission on this branch"):403===s&&/\[authz-forbidden\]/.test(e.message)&&(o="Token does not have permission on this branch"),s>=400&&s<500)return this.logger.warn(`Client error ${s} in request ${t.method} ${t.path}: ${o}`),void(r.headersSent||r.status(s).json({error:o}));this.logger.error(`Server error ${s} in request ${t.method} ${t.path}: ${e.message}`,{stack:e.stack,url:t.url,method:t.method}),r.headersSent||r.status(s).json({error:"Internal Server Error",message:this.config.debug?e.message:"An error occurred"})},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}run(){return process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack})}),process.on("unhandledRejection",(e,t)=>{this.logger.error(`Unhandled promise rejection: ${e}`,{reason:e,promise:t})}),new Promise((e,t)=>{this.server=this.app.listen(this.config.port,r=>{r?(this.logger.error(`Error starting server: ${r}`),t(r)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}static moduleOf(e){const t=e.indexOf(":");return-1===t?e:e.substring(0,t)}resolveModule(i){return i.startsWith("core/")?i.replace("core/",t(r(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js"):i.startsWith(".")?e(new URL(i,import.meta.url)):i}loadPlugin(e){let t,r;if(/:/.test(e)){let i=e.substring(e.indexOf(":")+1);if(e=e.substring(0,e.indexOf(":")),/:/.test(i)){t=i.substring(0,i.indexOf(":")),t||(t=void 0),i=i.substring(i.indexOf(":")+1);try{r=JSON.parse(i),"object"!=typeof r?(this.logger.error(`Overrides for plugin ${e} are not valid: ${i}`),r=void 0):this.logger.debug(`Overrides for plugin ${t||e}: ${i}`)}catch(t){this.logger.error(`Failed to parse overrides for plugin ${e}: ${t}, using ${i}`),r=void 0}}else t=i,t||(t=void 0)}else t=void 0;return this.logger.debug(`Loading plugin ${e}`),e.startsWith("core/")&&(e=this.resolveModule(e)),new Promise((i,s)=>{import(e).then(async o=>{let n;if(o&&o.default&&(o=o.default),r){const e={...this.config,...r};n=new o(this.withConfig(e))}else n=new o(this);if(!n)return s(new Error(`Unable to load ${e}`));i(await this.registerPlugin(e,n,t)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(t=>s(new Error(`Failed to load plugin ${e}: ${t}`)))})}admitToComposition(e){this.loadedPlugins[e.name]=e;try{d(this,e)}finally{delete this.loadedPlugins[e.name]}}async registerPlugin(e,t,r){if(t.name||(t.name=e),r&&(t.name=r),this.loadedPlugins[t.name])return this.logger.warn(`Plugin ${e} not registered: the name "${t.name}" is already taken. This instance is dropped and its routes will not exist. To load the same plugin twice, give each one a name: --plugin '${e}:${t.name}2:{…}'`),!1;if(this.logger.debug(`Registering plugin ${e} as ${t.name}`),t.dependencies)for(const e in t.dependencies)this.loadedPlugins[e]||(this.logger.debug(`Plugin ${t.name} depends on ${e}, loading it first`),await this.loadPlugin(t.dependencies[e]));if(this._composed&&this.admitToComposition(t),t.api&&(this.mountAuthDispatcher(t),this.logger.debug(`Plugin ${t.name} has API, registering it`),await t.api(this.app),this._errorMiddlewareSetup&&this.setupErrorMiddleware()),t.hooks)for(const e in t.hooks){this.logger.debug(`Plugin ${t.name} has hook ${e}, registering it`);const r=t.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!r||"function"!=typeof r)throw new Error(`Plugin ${t.name}: hook ${e} is invalid`);this.hooks[e].push(r),g(r,t.name,e)}return this.loadedPlugins[t.name]=t,!0}}export{DM,g as recordHookOwner};
1
+ import{fileURLToPath as e}from"url";import{join as t,dirname as r}from"path";import i from"express";import s from"body-parser";import{parseConfig as o}from"../lib/parseConfig.js";import n from"../config/args.js";import a from"../lib/ldapActions.js";import{buildLogger as h}from"../logger/winston.js";import{setLogger as l}from"../lib/expressFormatedResponses.js";import{prefixCoversPath as u}from"../lib/auth/base.js";import{assertAuthzComposition as d}from"../lib/authz/composition.js";import{recordHookOwner as g}from"../lib/utils.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,hookOwner,isChildOf,isDnInBranch,isDummyMemberDn,launchHooks,launchHooksChained,normalizeDn,organizationLink,parseDn,rdnValue,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";import c from"../plugins/priority.json.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this.authenticators=[],this._authState={dispatcherMounted:!1},this._errorMiddlewareSetup=!1,this._composed=!1,this.config=o(n),this.app=i(),this.app.use(s.json()),this.app.use(s.urlencoded({extended:!0})),this.logger=h(this.config),this.ldap=new a(this),l(this.logger),this.operationSequence=0;const e=[];if(this.config.plugin){const t=this.config.plugin.find(e=>e.includes("configApi"));let r=this.config.plugin.filter(e=>!e.includes("configApi"));const i=this.logger,s=(async()=>{for(const e of c){const t=this.resolveModule(DM.moduleOf(e)),s=r.filter(e=>this.resolveModule(DM.moduleOf(e))===t);if(0!==s.length){r=r.filter(e=>!s.includes(e));for(const e of s)i.debug(`Loading ${e} in the priority pass`),await this.loadPlugin(e)}}})();e.push(s),e.push(s.then(async()=>{const e=r.map(e=>(i.debug(`Loading ${e} in the parallel batch`),this.loadPlugin(e)));await Promise.all(e)})),t&&e.push(Promise.all(e).then(()=>this.loadPlugin(t)))}this.ready=new Promise((t,r)=>{e.length>0?Promise.all(e).then(async()=>{this.setupErrorMiddleware(),this.warnUnauthenticatedRoutes(),await d(this),this._composed=!0,this.afterLoad(),t()}).catch(e=>r(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),this._composed=!0,t())})}routerStack(){const e=this.app;return(e.router??e._router)?.stack}withConfig(e){return Object.assign(Object.create(Object.getPrototypeOf(this)),this,{config:e})}mountAuthDispatcher(e){const t=e.roles||[];t.includes("protect")||t.includes("logging")||this.mountAuthDispatcherNow()}mountAuthDispatcherNow(){this._authState.dispatcherMounted||(this._authState.dispatcherMounted=!0,this.app.use((e,t,r)=>{this.dispatchAuth(e,t,r)}))}registerAuthenticator(e){this.mountAuthDispatcherNow(),this.authenticators.includes(e)||this.authenticators.push(e)}dispatchAuth(e,t,r){let i=-1,s=[];for(const t of this.authenticators)for(const r of t.pathPrefixes)u(r,e.path)&&(r.length>i?(i=r.length,s=[t]):r.length!==i||s.includes(t)||s.push(t));if(i<0&&(s=this.authenticators.filter(e=>0===e.pathPrefixes.length)),0===s.length)return r();const finish=i=>{if(i>=s.length)return r();const o=s[i];if(!o.afterChain)return finish(i+1);o.afterChain(e,t,()=>finish(i+1))},o=[];e.authenticators=o;const run=r=>{if(r>=s.length)return finish(0);const i=s[r];i.authenticate(e,t,()=>{i.vouchedFor(e)&&o.push(i.name),run(r+1)})};run(0)}afterLoad(){for(const e of Object.values(this.loadedPlugins))try{e.afterLoad?.()}catch(t){this.logger.error(`Plugin ${e.name}: afterLoad failed: ${String(t)}`)}}warnUnauthenticatedRoutes(){const e=Object.values(this.loadedPlugins).filter(e=>e.roles?.includes("auth"));if(0===e.length)return[];const t=[];for(const r of e){if(!r.pathPrefixes||0===r.pathPrefixes.length)return[];t.push(...r.pathPrefixes)}const r=this.routerStack(),i=new Set;for(const e of r||[]){const t=e.route?.path;"string"==typeof t?i.add(t):Array.isArray(t)&&t.forEach(e=>"string"==typeof e&&i.add(e))}const s=[...i].filter(e=>!t.some(t=>e===t||e.startsWith(`${t}/`)));return s.length>0&&this.logger.warn(`Authentication is restricted to ${t.join(", ")}, so these routes are served without authentication: ${s.sort().join(", ")}`),s.sort()}setupErrorMiddleware(){if(this._errorMiddlewareSetup&&this._errorMiddleware){const e=this.routerStack();if(e){const t=e.findIndex(e=>e.handle===this._errorMiddleware);-1!==t&&e.splice(t,1)}}this._errorMiddleware=(e,t,r,i)=>{let s="statusCode"in e?e.statusCode:500,o=e.message;if(500===s&&/\[authz-forbidden\]/.test(e.message)?(s=403,o="Token does not have permission on this branch"):403===s&&/\[authz-forbidden\]/.test(e.message)&&(o="Token does not have permission on this branch"),s>=400&&s<500)return this.logger.warn(`Client error ${s} in request ${t.method} ${t.path}: ${o}`),void(r.headersSent||r.status(s).json({error:o}));this.logger.error(`Server error ${s} in request ${t.method} ${t.path}: ${e.message}`,{stack:e.stack,url:t.url,method:t.method}),r.headersSent||r.status(s).json({error:"Internal Server Error",message:this.config.debug?e.message:"An error occurred"})},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}run(){return process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack})}),process.on("unhandledRejection",(e,t)=>{this.logger.error(`Unhandled promise rejection: ${e}`,{reason:e,promise:t})}),new Promise((e,t)=>{this.server=this.app.listen(this.config.port,r=>{r?(this.logger.error(`Error starting server: ${r}`),t(r)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}static moduleOf(e){const t=e.indexOf(":");return-1===t?e:e.substring(0,t)}resolveModule(i){return i.startsWith("core/")?i.replace("core/",t(r(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js"):i.startsWith(".")?e(new URL(i,import.meta.url)):i}loadPlugin(e){let t,r;if(/:/.test(e)){let i=e.substring(e.indexOf(":")+1);if(e=e.substring(0,e.indexOf(":")),/:/.test(i)){t=i.substring(0,i.indexOf(":")),t||(t=void 0),i=i.substring(i.indexOf(":")+1);try{r=JSON.parse(i),"object"!=typeof r?(this.logger.error(`Overrides for plugin ${e} are not valid: ${i}`),r=void 0):this.logger.debug(`Overrides for plugin ${t||e}: ${i}`)}catch(t){this.logger.error(`Failed to parse overrides for plugin ${e}: ${t}, using ${i}`),r=void 0}}else t=i,t||(t=void 0)}else t=void 0;return this.logger.debug(`Loading plugin ${e}`),e.startsWith("core/")&&(e=this.resolveModule(e)),new Promise((i,s)=>{import(e).then(async o=>{let n;if(o&&o.default&&(o=o.default),r){const e={...this.config,...r};n=new o(this.withConfig(e))}else n=new o(this);if(!n)return s(new Error(`Unable to load ${e}`));i(await this.registerPlugin(e,n,t)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(t=>s(new Error(`Failed to load plugin ${e}: ${t}`)))})}async admitToComposition(e){this.loadedPlugins[e.name]=e;try{await d(this,e)}finally{delete this.loadedPlugins[e.name]}}async registerPlugin(e,t,r){if(t.name||(t.name=e),r&&(t.name=r),this.loadedPlugins[t.name])return this.logger.warn(`Plugin ${e} not registered: the name "${t.name}" is already taken. This instance is dropped and its routes will not exist. To load the same plugin twice, give each one a name: --plugin '${e}:${t.name}2:{…}'`),!1;if(this.logger.debug(`Registering plugin ${e} as ${t.name}`),t.dependencies)for(const e in t.dependencies)this.loadedPlugins[e]||(this.logger.debug(`Plugin ${t.name} depends on ${e}, loading it first`),await this.loadPlugin(t.dependencies[e]));if(this._composed&&await this.admitToComposition(t),t.api&&(this.mountAuthDispatcher(t),this.logger.debug(`Plugin ${t.name} has API, registering it`),await t.api(this.app),this._errorMiddlewareSetup&&this.setupErrorMiddleware()),t.hooks)for(const e in t.hooks){this.logger.debug(`Plugin ${t.name} has hook ${e}, registering it`);const r=t.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!r||"function"!=typeof r)throw new Error(`Plugin ${t.name}: hook ${e} is invalid`);this.hooks[e].push(r),g(r,t.name,e)}return this.loadedPlugins[t.name]=t,!0}}export{DM,g as recordHookOwner};
@@ -1 +1 @@
1
- import{fileURLToPath as _}from"url";import{join as a,dirname as e}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",a(e(_(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--organization-schema","DM_ORGANIZATION_SCHEMA",""],["--enterprise-domain-name-attribute","DM_ENTERPRISE_DOMAIN_NAME_ATTRIBUTE","associatedDomain"],["--enterprise-domain-link-attribute","DM_ENTERPRISE_DOMAIN_LINK_ATTRIBUTE",""],["--storage-backend","DM_STORAGE_BACKEND",""],["--storage-sweep-interval","DM_STORAGE_SWEEP_INTERVAL",600,"number"],["--storage-ldap-base","DM_STORAGE_LDAP_BASE",""],["--storage-ldap-object-class","DM_STORAGE_LDAP_OBJECT_CLASS","applicationProcess"],["--storage-file-directory","DM_STORAGE_FILE_DIRECTORY",""],["--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"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",a(e(_(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",a(e(_(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-flat-auto-repair","DM_LDAP_FLAT_AUTO_REPAIR",!0,"boolean"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--llng-username-header","DM_LLNG_USERNAME_HEADER",""],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-fake-user","DM_AUTH_FAKE_USER",""],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-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"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-username-claim","DM_OIDC_USERNAME_CLAIM","sub"],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
1
+ import{fileURLToPath as _}from"url";import{join as e,dirname as a}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",e(a(_(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--organization-schema","DM_ORGANIZATION_SCHEMA",""],["--enterprise-domain-name-attribute","DM_ENTERPRISE_DOMAIN_NAME_ATTRIBUTE","associatedDomain"],["--enterprise-domain-link-attribute","DM_ENTERPRISE_DOMAIN_LINK_ATTRIBUTE",""],["--storage-backend","DM_STORAGE_BACKEND",""],["--storage-sweep-interval","DM_STORAGE_SWEEP_INTERVAL",600,"number"],["--storage-ldap-base","DM_STORAGE_LDAP_BASE",""],["--storage-ldap-object-class","DM_STORAGE_LDAP_OBJECT_CLASS","applicationProcess"],["--storage-file-directory","DM_STORAGE_FILE_DIRECTORY",""],["--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"],["--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",e(a(_(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",e(a(_(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"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--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-tombstone-dn","DM_TWAKE_TOMBSTONE_DN",[],"array"],["--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"],["--twake-tombstone-clear-attributes","DM_TWAKE_TOMBSTONE_CLEAR_ATTRIBUTES",[],"array"],["--twake-tombstone-erase-min-age","DM_TWAKE_TOMBSTONE_ERASE_MIN_AGE",2592e3,"number"],["--twake-tombstone-group-bases","DM_TWAKE_TOMBSTONE_GROUP_BASES",[],"array"],["--twake-instance-dn","DM_TWAKE_INSTANCE_DN",[],"array"],["--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-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"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--llng-username-header","DM_LLNG_USERNAME_HEADER",""],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-fake-user","DM_AUTH_FAKE_USER",""],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-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"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-username-claim","DM_OIDC_USERNAME_CLAIM","sub"],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
@@ -1 +1 @@
1
- const e=["ldapsearchrequest","ldapaddrequest","ldapmodifyrequest","ldapdeleterequest","ldaprenamerequest","ldapsearchfilter"],t="everyone",n=new WeakMap;function authzFor(e,t){if(n.has(e))return n.get(e);const o=function(e,t){const n=e.authz_for;if(null==n||""===n)return;const o=Array.isArray(n)?n:[n],s=[];for(const e of o){if("string"!=typeof e||""===e.trim())throw new Error(`${t}: authz_for must list authentication plugin names, got `+JSON.stringify(e));s.push(e.trim())}return s.length>0?s:void 0}(e,t);return n.set(e,o),o}function servesRequest(e,t){const n=authzFor(t,"authz_for");if(!n)return!0;const o=e?.authenticators;return!o||0===o.length||o.some(e=>n.includes(e))}function runTogether(e,t){return 0===e.pathPrefixes.length&&0===t.pathPrefixes.length||e.pathPrefixes.some(e=>t.pathPrefixes.includes(e))}function ldapJudges(n){const o=[];for(const s of Object.values(n.loadedPlugins)){if(!s.roles?.includes("authz"))continue;const n=e.filter(e=>Boolean(s.hooks?.[e]));if(0===n.length)continue;const r=s.roles.includes("auth")?[s.name]:authzFor(s.config,s.name)??t;o.push({plugin:s,hooks:n,population:r})}return o}function overlap(e,n,o){if(e===t||n===t)return"same";if(e.some(e=>n.includes(e)))return"same";const byName=e=>o.find(t=>t.name===e);for(const t of e)for(const e of n){const n=byName(t),o=byName(e);if(n&&o&&runTogether(n,o))return"together"}}function describePopulation(e,n){return e!==t?`requests authenticated by ${e.join(", ")}`:n.length>0?`every authenticated request (${n.map(e=>e.name).join(", ")})`:"every authenticated request"}function assertAuthzComposition(e,t){const n=[...e.authenticators];var o;t&&(o=t,Boolean(o.roles?.includes("auth"))&&Array.isArray(o.pathPrefixes))&&!n.some(e=>e.name===t.name)&&n.push(t);const s=n.map(e=>e.name),concerns=e=>!t||e===t,r=Object.values(e.loadedPlugins).filter(concerns);for(const n of r){if(!n.roles?.includes("authz"))continue;if(n.roles.includes("auth")){const t=n.config.authz_for,o=e.config.authz_for;void 0!==t&&JSON.stringify(t)!==JSON.stringify(o)&&"[]"!==JSON.stringify(t)&&e.logger.warn(`${n.name}: authz_for is ignored here. This plugin judges the requests it authenticates itself, and no others`);continue}const o=(authzFor(n.config,n.name)??[]).filter(e=>!s.includes(e));if(o.length>0)throw new Error(`${n.name}: authz_for names ${o.join(", ")}, which `+(o.length>1?"are not loaded authentication plugins":"is not a loaded authentication plugin")+` (loaded: ${s.join(", ")||"none"}). It would judge no request of theirs.`+(t?" Register the authentication plugins before the authorization plugins that name them.":""))}const i=t?void 0:authzFor(e.config,"--authz-for");i&&!Object.values(e.loadedPlugins).some(t=>t.roles?.includes("authz")&&!t.roles.includes("auth")&&t.config.authz_for===e.config.authz_for)&&e.logger.warn(`--authz-for names ${i.join(", ")}, and no loaded plugin reads it: it scopes nothing. authzDynamic judges the requests it authenticates itself; the option is for authzPerBranch, authzLinid1 and authzPerRoute`);const a=ldapJudges(e);for(const t of a)concerns(t.plugin)&&e.logger.info(`${t.plugin.name} judges ${t.hooks.join(", ")} for `+describePopulation(t.population,n));const u=Boolean(e.config.authz_combine),h=[];for(let t=0;t<a.length;t++)for(let o=t+1;o<a.length;o++){const s=a[t],r=a[o];if(!concerns(s.plugin)&&!concerns(r.plugin))continue;const i=overlap(s.population,r.population,n);if(!i)continue;const l=`${s.plugin.name} and ${r.plugin.name}`;"together"===i?e.logger.warn(`${l} judge the requests of authenticators that run on the same paths: a request carrying both credentials is judged by both, and the first refusal wins`):u?e.logger.warn(`${l} both judge the LDAP operations of the same requests, and compose as an AND (--authz-combine): the first refusal wins`):h.push(l)}if(h.length>0)throw new Error(`${h.join("; ")} both judge the LDAP operations of the same requests, and would compose as an AND: the first refusal wins, and the 403 does not say whose it was. Give each an authz_for naming the authentication plugins it serves, so that no request is judged by both, or set --authz-combine to make the AND deliberate.`);for(const e of r)e.assertComposition?.()}export{t as EVERYONE,e as LDAP_JUDGING_HOOKS,assertAuthzComposition,authzFor,describePopulation,ldapJudges,overlap,runTogether,servesRequest};
1
+ const e=["ldapsearchrequest","ldapaddrequest","ldapmodifyrequest","ldapdeleterequest","ldaprenamerequest","ldapsearchfilter"],t="everyone",n=new WeakMap;function authzFor(e,t){if(n.has(e))return n.get(e);const o=function(e,t){const n=e.authz_for;if(null==n||""===n)return;const o=Array.isArray(n)?n:[n],s=[];for(const e of o){if("string"!=typeof e||""===e.trim())throw new Error(`${t}: authz_for must list authentication plugin names, got `+JSON.stringify(e));s.push(e.trim())}return s.length>0?s:void 0}(e,t);return n.set(e,o),o}function servesRequest(e,t){const n=authzFor(t,"authz_for");if(!n)return!0;const o=e?.authenticators;return!o||0===o.length||o.some(e=>n.includes(e))}function runTogether(e,t){return 0===e.pathPrefixes.length&&0===t.pathPrefixes.length||e.pathPrefixes.some(e=>t.pathPrefixes.includes(e))}function ldapJudges(n){const o=[];for(const s of Object.values(n.loadedPlugins)){if(!s.roles?.includes("authz"))continue;const n=e.filter(e=>Boolean(s.hooks?.[e]));if(0===n.length)continue;const r=s.roles.includes("auth")?[s.name]:authzFor(s.config,s.name)??t;o.push({plugin:s,hooks:n,population:r})}return o}function overlap(e,n,o){if(e===t||n===t)return"same";if(e.some(e=>n.includes(e)))return"same";const byName=e=>o.find(t=>t.name===e);for(const t of e)for(const e of n){const n=byName(t),o=byName(e);if(n&&o&&runTogether(n,o))return"together"}}function describePopulation(e,n){return e!==t?`requests authenticated by ${e.join(", ")}`:n.length>0?`every authenticated request (${n.map(e=>e.name).join(", ")})`:"every authenticated request"}function assertAuthzComposition(e,t){const n=[...e.authenticators];var o;t&&(o=t,Boolean(o.roles?.includes("auth"))&&Array.isArray(o.pathPrefixes))&&!n.some(e=>e.name===t.name)&&n.push(t);const s=n.map(e=>e.name),concerns=e=>!t||e===t,r=Object.values(e.loadedPlugins).filter(concerns);for(const n of r){if(!n.roles?.includes("authz"))continue;if(n.roles.includes("auth")){const t=n.config.authz_for,o=e.config.authz_for;void 0!==t&&JSON.stringify(t)!==JSON.stringify(o)&&"[]"!==JSON.stringify(t)&&e.logger.warn(`${n.name}: authz_for is ignored here. This plugin judges the requests it authenticates itself, and no others`);continue}const o=(authzFor(n.config,n.name)??[]).filter(e=>!s.includes(e));if(o.length>0)throw new Error(`${n.name}: authz_for names ${o.join(", ")}, which `+(o.length>1?"are not loaded authentication plugins":"is not a loaded authentication plugin")+` (loaded: ${s.join(", ")||"none"}). It would judge no request of theirs.`+(t?" Register the authentication plugins before the authorization plugins that name them.":""))}const a=t?void 0:authzFor(e.config,"--authz-for");a&&!Object.values(e.loadedPlugins).some(t=>t.roles?.includes("authz")&&!t.roles.includes("auth")&&t.config.authz_for===e.config.authz_for)&&e.logger.warn(`--authz-for names ${a.join(", ")}, and no loaded plugin reads it: it scopes nothing. authzDynamic judges the requests it authenticates itself; the option is for authzPerBranch, authzLinid1 and authzPerRoute`);const i=ldapJudges(e);for(const t of i)concerns(t.plugin)&&e.logger.info(`${t.plugin.name} judges ${t.hooks.join(", ")} for `+describePopulation(t.population,n));const u=Boolean(e.config.authz_combine),h=[];for(let t=0;t<i.length;t++)for(let o=t+1;o<i.length;o++){const s=i[t],r=i[o];if(!concerns(s.plugin)&&!concerns(r.plugin))continue;const a=overlap(s.population,r.population,n);if(!a)continue;const l=`${s.plugin.name} and ${r.plugin.name}`;"together"===a?e.logger.warn(`${l} judge the requests of authenticators that run on the same paths: a request carrying both credentials is judged by both, and the first refusal wins`):u?e.logger.warn(`${l} both judge the LDAP operations of the same requests, and compose as an AND (--authz-combine): the first refusal wins`):h.push(l)}if(h.length>0)throw new Error(`${h.join("; ")} both judge the LDAP operations of the same requests, and would compose as an AND: the first refusal wins, and the 403 does not say whose it was. Give each an authz_for naming the authentication plugins it serves, so that no request is judged by both, or set --authz-combine to make the AND deliberate.`);return(async()=>{for(const e of r)await(e.assertComposition?.())})()}export{t as EVERYONE,e as LDAP_JUDGING_HOOKS,assertAuthzComposition,authzFor,describePopulation,ldapJudges,overlap,runTogether,servesRequest};
@@ -0,0 +1 @@
1
+ import t from"node-fetch";import{ConflictError as e,GatewayTimeoutError as i,BadGatewayError as a}from"./errors.js";async function refused(t,e){const i=(await e.text().catch(()=>"")).slice(0,500);return new a(`${t}: HTTP ${e.status}${i?` ${i}`:""}`)}class HttpProvider{constructor(t,e){this.domain=t,this.timeout=e}address(t){return`${t.id}.${this.domain}`.toLowerCase()}async call(e,i={}){try{return await t(e,{...i,signal:AbortSignal.timeout(this.timeout)})}catch(t){throw this.timedOut(t,e)}}async json(t){try{return await t.json()}catch(e){throw this.timedOut(e,t.url)}}timedOut(t,e){return["AbortError","TimeoutError"].includes(t.name)?new i(`${new URL(e).host} did not answer within ${this.timeout} ms`):t}}class ClouderyProvider extends HttpProvider{constructor(t,e,i,a,s){super(i,s),this.url=t,this.token=e,this.offer=a}headers(){return{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json"}}async create(t){const e=await this.call(`${this.url}/api/v1/instances`,{method:"POST",headers:this.headers(),body:JSON.stringify({email:t.email,internal_email:t.email,publicName:t.publicName,public_name:t.publicName,locale:t.locale,oidc:t.id,slug:t.id,phone:t.phone||"",offer:t.offer||this.offer,domain:this.domain,skip_email_validation:!0,...t.orgId?{org_id:t.orgId}:{},...t.orgDomain?{org_domain:t.orgDomain}:{}})});if(!e.ok)throw await refused("Cloudery refused the instance",e)}async find(t,i){const a=(i??this.address(t)).toLowerCase(),s=new URL(`${this.url}/api/v2/instances`);s.searchParams.set("fqdn",a),s.searchParams.set("limit","1");const r=await this.call(s.toString(),{headers:this.headers()});if(!r.ok)throw await refused("Cloudery instance search failed",r);const{items:o}=await this.json(r),n=o?.[0];if(n){if(n.internal_email&&n.internal_email.toLowerCase()!==t.email.toLowerCase()||n.oidc&&n.oidc!==t.id)throw new e(`${a} already exists and belongs to another user`);return{fqdn:n.fqdn,ready:Boolean(n.instantiated_at)}}}async createOrganization(t){const e=await this.call(`${this.url}/api/v2/organizations`,{method:"POST",headers:this.headers(),body:JSON.stringify({ldap_branch:t.id,name:t.name,custom_domain:t.domain})});if(!e.ok&&409!==e.status)throw await refused("Cloudery refused the organization",e)}}class CozyStackProvider extends HttpProvider{constructor(t,e,i,a,s,r,o){super(a,o),this.url=t,this.context=s,this.apps=r,this.auth=`Basic ${Buffer.from(`${e}:${i}`).toString("base64")}`}headers(){return{Authorization:this.auth,Accept:"application/json"}}async create(t){const i=this.address(t),a=new URLSearchParams({Domain:i,Email:t.email,PublicName:t.publicName,Locale:t.locale,OIDCID:t.oidc??t.id});t.phone&&a.set("Phone",t.phone),t.orgId&&a.set("OrgID",t.orgId),t.orgDomain&&a.set("OrgDomain",t.orgDomain),this.context&&a.set("ContextName",this.context),this.apps&&a.set("Apps",this.apps);const s=await this.call(`${this.url}/instances?${a.toString()}`,{method:"POST",headers:this.headers()});if(409===s.status){if(!await this.find(t))throw new e(`${i} already exists`);return i}if(!s.ok)throw await refused("cozy-stack refused the instance",s);return await this.finishOnboarding(i),i}async find(t,i){const a=(i??this.address(t)).toLowerCase(),s=await this.read(a);if(s){if(s.email?.toLowerCase()!==t.email.toLowerCase()||s.oidc_id!==(t.oidc??t.id))throw new e(`${a} already exists and belongs to another user`);return s.onboarding_finished||await this.finishOnboarding(a),{fqdn:a,ready:!0}}}async destroy(t,i){const a=this.address({id:t}),s=await this.read(a);if(!s)return;if(s.oidc_id!==i)throw new e(`${a} belongs to another user, left in place`);const r=await this.call(`${this.url}/instances/${encodeURIComponent(a)}`,{method:"DELETE",headers:this.headers()});if(!r.ok&&404!==r.status)throw await refused("cozy-stack refused to destroy the instance",r)}async read(t){const e=await this.call(`${this.url}/instances/${encodeURIComponent(t)}`,{headers:this.headers()});if(404===e.status)return;if(!e.ok)throw await refused("cozy-stack instance read failed",e);const{data:i}=await this.json(e);return i?.attributes??{}}async finishOnboarding(t){const e=await this.call(`${this.url}/instances/${encodeURIComponent(t)}?OnboardingFinished=true`,{method:"PATCH",headers:this.headers()});if(!e.ok)throw await refused("cozy-stack refused to finish onboarding",e)}}export{ClouderyProvider,CozyStackProvider};
@@ -1 +1 @@
1
- import{createHash as e}from"node:crypto";import{Client as t,Attribute as i,Change as s}from"ldapts";import{LRUCache as n}from"lru-cache";import a from"p-limit";import{isDnInBranch as r,normalizeDn as o,launchHooksChained as c,launchHooks as l,escapeDnValue as h}from"./utils.js";import{changeContext as d}from"./changeContext.js";import{NotFoundError as u,ConflictError as p}from"./errors.js";import{SchemaIndex as f,parseSchema as g}from"./ldapSchema.js";const y={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}};function ldapError(e,t){const i=new Error(`${e}: ${String(t)}`),s=t?.code;return"number"==typeof s&&(i.code=s),i}const digestKey=t=>e("sha256").update(t).digest("hex").slice(0,12),foldDn=e=>e.trim().toLowerCase().split(",").map(e=>e.trim()).join(","),cloneSearchResult=e=>({searchEntries:e.searchEntries.map(e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?[...t]:t]))),searchReferences:[...e.searchReferences]});class ldapActions{constructor(e){this.cacheGeneration=0,this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.declaredClasses=[],this.schemaUnavailable=!1,this.classWarnings=new n({max:1e4}),this.parent=e,this.logger=e.logger,this.config=e.config,this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=a(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const i="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,s=this.config.ldap_cache_ttl,r="string"==typeof s?parseInt(s,10)||0:s??0,o=1e3*Math.max(r,0);if(this.cacheEnabled=o>0,this.searchCache=new n({max:i,ttl:this.cacheEnabled?o:void 0,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(this.cacheEnabled?`LDAP search cache initialized: max=${i}, ttl=${o/1e3}s`:"LDAP search cache disabled (--ldap-cache-ttl=0)"),this.attrSignatureCache=new n({max:1e3}),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP DN is not defined");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined");if(!e.config.ldap_base)throw new Error("LDAP base is not defined, please set --ldap-base (or DM_LDAP_BASE)");this.base=e.config.ldap_base,this.ldapUrls=e.config.ldap_url,this.logger.info(`LDAP failover configured with ${this.ldapUrls.length} URL(s): ${this.ldapUrls.join(", ")}`),this.options={url:this.ldapUrls[0],timeout:0,connectTimeout:0,strictDN:!1},this.ldapUrls[0].startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const e=[];for(let i=0;i<this.ldapUrls.length;i++){const s=(this.currentUrlIndex+i)%this.ldapUrls.length,n=this.ldapUrls[s];try{this.logger.debug(`Attempting connection to ${n}`);const e={...this.options,url:n,tlsOptions:n.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},i=new t(e);return await i.bind(this.dn,this.pwd),s!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${n}`),this.currentUrlIndex=s),i}catch(t){this.logger.warn(`Failed to connect to ${n}: ${String(t)}`),e.push(t)}}throw this.logger.error(`LDAP connection failed for all ${this.ldapUrls.length} URL(s)`),new Error(`LDAP connection failed for all URLs: ${e.map(e=>e.message).join(", ")}`)}cleanupExpiredConnections(){if(!this.isCleaningUp){this.isCleaningUp=!0;try{const e=Date.now(),t=[];for(let i=this.availableConnections.length-1;i>=0;i--){const s=this.availableConnections[i];if(e-s.createdAt>this.connectionTtl){t.push(s),this.availableConnections.splice(i,1);const e=this.connectionPool.indexOf(s);-1!==e&&this.connectionPool.splice(e,1)}}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}finally{this.isCleaningUp=!1}}}async acquireConnection(){if(this.cleanupExpiredConnections(),this.availableConnections.length>0){const e=this.availableConnections.pop();return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e}if(this.connectionPool.length<this.poolSize){const e={client:await this.createConnection(),createdAt:Date.now(),inUse:!0};return this.connectionPool.push(e),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),e}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((e,t)=>{const i=globalThis.setTimeout(()=>{const e=this.waitingResolvers.indexOf(resolveWrapper);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),resolveWrapper=t=>{globalThis.clearTimeout(i),e(t)};this.waitingResolvers.push(resolveWrapper)})}releaseConnection(e){if(Date.now()-e.createdAt>this.connectionTtl){const t=this.connectionPool.indexOf(e);return-1!==t&&this.connectionPool.splice(t,1),e.client.unbind().catch(()=>{}),void this.logger.debug("Released expired LDAP connection (discarded)")}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t(e)}e.inUse=!1,this.availableConnections.push(e),this.logger.debug("Released LDAP connection back to pool")}getAttributeSignature(e){if(!e||0===e.length)return"*";const t=e.join("|");let i=this.attrSignatureCache.get(t);return i||(i=[...e].sort().join(","),this.attrSignatureCache.set(t,i)),i}getCacheKey(e,t){const i=this.getAttributeSignature(t.attributes),s=t.explicitBufferAttributes?.length?[...t.explicitBufferAttributes].sort().join(","):"-",n="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return[e,t.scope||"sub",n,i,s,!1===t.returnAttributeValues?"novalues":"values",t.derefAliases??"-",String(t.sizeLimit??"-")].join("\0")}invalidateCache(e){if(!this.cacheEnabled)return;this.cacheGeneration++;const t=foldDn(e),i=`,${t}`,s=[];for(const e of this.searchCache.keys()){const n=foldDn(e.split("\0")[0]);(n===t||n.endsWith(i))&&s.push(e)}for(const e of s)this.searchCache.delete(e)}get system(){return this}forRequest(e){return{search:(t,i)=>this.search(t,i??this.base,e),add:(t,i)=>this.add(t,i,e),modify:(t,i)=>this.modify(t,i,e),rename:(t,i)=>this.rename(t,i,e),delete:t=>this.delete(t,e)}}declareObjectClasses(e,t,i=!1){const s=(Array.isArray(t)?t:[t]).filter(e=>"string"==typeof e&&""!==e);e&&0!==s.length&&this.declaredClasses.push({base:e,classes:s,includeBase:i})}declaredFor(e){let t=0,i=[];for(const s of this.declaredClasses){if(!r(e,s.base))continue;const n=o(s.base);if(!s.includeBase&&o(e)===n)continue;const a=n.split(",").length;a>t&&(t=a,i=[]),a===t&&i.push(...s.classes)}const s=new Set;return i.filter(e=>{const t=e.toLowerCase();return!s.has(t)&&(s.add(t),!0)})}warnOnce(e){this.classWarnings.has(e)||(this.classWarnings.set(e,!0),this.logger.warn(e))}async classRepair(e,t){if(!1===this.config.ldap_flat_auto_repair)return[];const i=this.declaredFor(e);if(0===i.length)return[];const s=Array.isArray(t.delete)?t.delete:Object.entries(t.delete||{}).filter(([,e])=>!e||Array.isArray(e)&&!e.length).map(([e])=>e);if([...Object.keys(t.add||{}),...Object.keys(t.replace||{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete||{})].some(e=>"objectclass"===e.toLowerCase()))return[];let n;try{n=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){return this.schemaUnavailable||this.logger.warn(`Object classes are not repaired: the directory schema could not be read (${String(e)})`),this.schemaUnavailable=!0,[]}const attributeKey=e=>n.getAttributeType(e)?.oid??e.toLowerCase(),allowedBy=e=>{const{must:t,may:i}=n.resolveAttributes(e);return new Set([...t,...i].map(attributeKey))},valued=e=>Object.entries(e||{}).filter(([,e])=>Array.isArray(e)?e.length>0:null!=e&&""!==e).map(([e])=>e),a=[...valued(t.add),...valued(t.replace)],r=i.filter(e=>{const t=n.getObjectClass(e);if("AUXILIARY"!==t?.kind)return!1;const i=allowedBy([e]);return a.some(e=>i.has(attributeKey(e)))});if(0===r.length)return[];let o;try{o=(await this.system.search({paged:!1,scope:"base",attributes:["objectClass",...n.resolveAttributes(r).must]},e)).searchEntries[0]}catch{return[]}if(!o)return[];const c=o.objectClass??o.objectclass,l=(Array.isArray(c)?c:c?[c]:[]).map(String),h=allowedBy(l);let d=a.filter(e=>n.getAttributeType(e)&&!h.has(attributeKey(e)));if(0===d.length)return[];const u=new Set(s.map(attributeKey)),p=new Set([...valued(o).filter(e=>!u.has(attributeKey(e))),...a].map(attributeKey)),{auxiliary:f,structural:g}=n.missingClasses(i,l),y=[];for(const t of f){const i=t.names[0]??t.oid,s=allowedBy([i]),a=d.filter(e=>s.has(attributeKey(e)));if(0===a.length)continue;const r=n.resolveAttributes([i]).must.filter(e=>!p.has(attributeKey(e)));r.length>0?this.warnOnce(`${e} needs ${i} for ${a.join(", ")}, and cannot be given it without ${r.join(", ")}`):(y.push(i),d=d.filter(e=>!a.includes(e)))}const m=g.filter(e=>{const t=allowedBy([e]);return d.some(e=>t.has(attributeKey(e)))});return m.length>0&&this.warnOnce(`${e} cannot hold ${d.join(", ")}: its schema declares ${m.join(", ")}, a structural class an existing entry cannot be given`),y.length>0&&this.logger.info(`${e}: adding ${y.join(", ")}, which the change needs and its schema declares`),y}async subschemaDn(){try{const e=await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["subschemaSubentry"]},""),t=e.searchEntries[0]?.subschemaSubentry,i=Array.isArray(t)?t[0]:t;if(i)return String(i)}catch(e){this.logger.warn(`Unable to read subschemaSubentry from root DSE: ${String(e)}`)}return"cn=Subschema"}async schemaIndex(){const e=1e3*(this.config.ldap_raw_schema_cache_ttl??3600);if(this.schemaCache&&Date.now()-this.schemaCache.fetchedAt<e)return this.schemaCache.index;if(this.schemaLoading)return this.schemaLoading;this.schemaLoading=(async()=>{const e=await this.subschemaDn(),t=(await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["objectClasses","attributeTypes","ldapSyntaxes","matchingRules"]},e).catch(t=>{if(32===t?.code)throw new u(`Subschema entry ${e} not found`);throw t})).searchEntries[0];if(!t)throw new u(`Subschema entry ${e} not found`);const list=e=>{if(void 0===e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString("utf8"):e)},i=new f(g({objectClasses:list(t.objectClasses),attributeTypes:list(t.attributeTypes),ldapSyntaxes:list(t.ldapSyntaxes),matchingRules:list(t.matchingRules)}));return this.schemaCache={index:i,fetchedAt:Date.now()},this.logger.info(`LDAP schema loaded from ${e}: ${i.schema.objectClasses.length} object classes, ${i.schema.attributeTypes.length} attribute types`),i})();try{return await this.schemaLoading}finally{this.schemaLoading=void 0}}filterForCaller(e,t,i){if(!this.parent.hooks.ldapsearchfilter)return e;const s=this.parent.hooks.ldapsearchfilter,one=async e=>{const[n]=await c(s,[e,t,i]);return n??e};if("function"==typeof e[Symbol.asyncIterator]){const t=e;return async function*(){for await(const e of t)yield await one(e)}()}return one(e)}async search(e,t=this.base,i){let s={...y,...e};s=await c(this.parent.hooks.ldapsearchopts,s),[t,s]=await c(this.parent.hooks.ldapsearchrequest,[t,s,i]);const n=this.cacheEnabled&&!s.paged&&"base"===s.scope,a=n?this.getCacheKey(t,s):"",r=this.cacheGeneration;if(n){const e=this.searchCache.get(a);if(e)return this.logger.debug(`LDAP search cache hit: ${digestKey(a)}`),this.filterForCaller(cloneSearchResult(e),i,s)}const o=await this.acquireConnection();try{let e=s.paged?o.client.searchPaginated(t,s):o.client.search(t,s);if(e=await c(this.parent.hooks.ldapsearchresult,e),n){const t=e;return r===this.cacheGeneration?(this.searchCache.set(a,cloneSearchResult(t)),this.logger.debug(`LDAP search cached: ${digestKey(a)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${digestKey(a)}`),this.filterForCaller(t,i,s)}return s.paged?this.filterForCaller(this.wrapPaginatedSearch(e,o),i,s):this.filterForCaller(e,i,s)}finally{s.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,t,s){e=this.setDn(e),t.objectClass&&0!==t.objectClass.length||!this.config.user_class||(t.objectClass=this.config.user_class);const n={};for(const[e,i]of Object.entries(t))Buffer.isBuffer(i)?n[e]=i.toString():Array.isArray(i)&&i.length>0&&Buffer.isBuffer(i[0])?n[e]=i.map(e=>e.toString()):n[e]=i;[e,t]=await c(this.parent.hooks.ldapaddrequest,[e,n,s]);const a=[];for(const[e,t]of Object.entries(n)){const s=Array.isArray(t)?t:[t];a.push(new i({type:e,values:s}))}const r=await this.acquireConnection();try{return this.invalidateCache(e),await r.client.add(e,a),this.invalidateCache(e),l(this.parent.hooks.ldapadddone,[e,t],d(s)),!0}catch(t){throw function(e,t){const i=ldapError("LDAP add error",t);if(68!==i.code)return i;const s=new p(`Entry ${e} already exists`);return s.code=68,s}(e,t)}finally{this.releaseConnection(r)}}async modify(e,t,n){e=this.setDn(e);const a=[],r=this.opNumber();if([e,t]=await c(this.parent.hooks.ldapmodifyrequest,[e,t,r,n]),t.add)for(const[e,n]of Object.entries(t.add))a.push(new s({operation:"add",modification:new i({type:e,values:Array.isArray(n)?n:[n]})}));if(t.replace)for(const[e,n]of Object.entries(t.replace))a.push(new s({operation:"replace",modification:new i({type:e,values:Array.isArray(n)?n:[n]})}));if(t.delete)if(Array.isArray(t.delete))for(const e of t.delete)e&&a.push(new s({operation:"delete",modification:new i({type:e,values:[]})}));else for(const[e,n]of Object.entries(t.delete)){const t=new s({operation:"delete",modification:new i(n?{type:e,values:Array.isArray(n)?n:[n]}:{type:e})});a.push(t)}if(0===a.length){return Object.keys(t.add||{}).length>0||Object.keys(t.replace||{}).length>0||(Array.isArray(t.delete)?t.delete.length>0:Object.keys(t.delete||{}).length>0)?this.logger.warn(`Modify on ${e} asked for changes but emitted none; nothing was written`):this.logger.debug(`Modify on ${e} had nothing to apply`),l(this.parent.hooks.ldapmodifydone,[e,{},r],d(n)),!1}{const o=await this.classRepair(e,t),c=o.length>0?new s({operation:"add",modification:new i({type:"objectClass",values:o})}):void 0;c&&a.push(c);const h=await this.acquireConnection();try{this.invalidateCache(e);try{await h.client.modify(e,a)}catch(t){if(!c||20!==t?.code)throw t;a.splice(a.indexOf(c),1),await h.client.modify(e,a)}return this.invalidateCache(e),l(this.parent.hooks.ldapmodifydone,[e,t,r],d(n)),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(a)}`),function(e,t){const i=ldapError("LDAP modify error",t);if(65!==i.code)return i;const s=new p(`Entry ${e} cannot hold this change: ${String(t)}`);return s.code=65,s}(e,t)}finally{this.releaseConnection(h)}}}async rename(e,t,i){e=this.setDn(e),t=this.setDn(t),[e,t]=await c(this.parent.hooks.ldaprenamerequest,[e,t,i]);const s=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await s.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),l(this.parent.hooks.ldaprenamedone,[e,t],d(i)),!0}catch(e){throw ldapError("LDAP rename error",e)}finally{this.releaseConnection(s)}}async move(e,t,i){e=this.setDn(e),t=this.setDn(t);const s=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await s.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),l(this.parent.hooks.ldaprenamedone,[e,t],d(i)),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(e){throw ldapError("LDAP move error",e)}finally{this.releaseConnection(s)}}async delete(e,t){e=Array.isArray(e)?e.map(e=>this.setDn(e)):this.setDn(e),Array.isArray(e)||(e=[e]),[e]=await c(this.parent?.hooks.ldapdeleterequest,[e,t]);const i=await this.acquireConnection();try{for(const s of e){try{this.invalidateCache(s),await i.client.del(s),this.invalidateCache(s)}catch(e){throw ldapError("LDAP delete error",e)}l(this.parent.hooks.ldapdeletedone,s,d(t))}return!0}finally{this.releaseConnection(i)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${h(e)},${this.base}`,e}opNumber(){return this.parent.operationSequence++}}export{ldapActions as default};
1
+ import{createHash as e}from"node:crypto";import{Client as t,Attribute as i,Change as s,Control as n}from"ldapts";import{LRUCache as a}from"lru-cache";import r from"p-limit";import{isDnInBranch as o,normalizeDn as c,launchHooksChained as l,launchHooks as h,escapeDnValue as d}from"./utils.js";import{changeContext as p}from"./changeContext.js";import{NotFoundError as u,ConflictError as f}from"./errors.js";import{SchemaIndex as g,parseSchema as y}from"./ldapSchema.js";const m={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}};function ldapError(e,t){const i=new Error(`${e}: ${String(t)}`),s=t?.code;return"number"==typeof s&&(i.code=s),i}const digestKey=t=>e("sha256").update(t).digest("hex").slice(0,12),foldDn=e=>e.trim().toLowerCase().split(",").map(e=>e.trim()).join(","),cloneSearchResult=e=>({searchEntries:e.searchEntries.map(e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?[...t]:t]))),searchReferences:[...e.searchReferences]});class ldapActions{constructor(e){this.cacheGeneration=0,this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.declaredClasses=[],this.schemaUnavailable=!1,this.classWarnings=new a({max:1e4}),this.parent=e,this.logger=e.logger,this.config=e.config,this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=r(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const i="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,s=this.config.ldap_cache_ttl,n="string"==typeof s?parseInt(s,10)||0:s??0,o=1e3*Math.max(n,0);if(this.cacheEnabled=o>0,this.searchCache=new a({max:i,ttl:this.cacheEnabled?o:void 0,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(this.cacheEnabled?`LDAP search cache initialized: max=${i}, ttl=${o/1e3}s`:"LDAP search cache disabled (--ldap-cache-ttl=0)"),this.attrSignatureCache=new a({max:1e3}),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP DN is not defined");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined");if(!e.config.ldap_base)throw new Error("LDAP base is not defined, please set --ldap-base (or DM_LDAP_BASE)");this.base=e.config.ldap_base,this.ldapUrls=e.config.ldap_url,this.logger.info(`LDAP failover configured with ${this.ldapUrls.length} URL(s): ${this.ldapUrls.join(", ")}`),this.options={url:this.ldapUrls[0],timeout:0,connectTimeout:0,strictDN:!1},this.ldapUrls[0].startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const e=[];for(let i=0;i<this.ldapUrls.length;i++){const s=(this.currentUrlIndex+i)%this.ldapUrls.length,n=this.ldapUrls[s];try{this.logger.debug(`Attempting connection to ${n}`);const e={...this.options,url:n,tlsOptions:n.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},i=new t(e);return await i.bind(this.dn,this.pwd),s!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${n}`),this.currentUrlIndex=s),i}catch(t){this.logger.warn(`Failed to connect to ${n}: ${String(t)}`),e.push(t)}}throw this.logger.error(`LDAP connection failed for all ${this.ldapUrls.length} URL(s)`),new Error(`LDAP connection failed for all URLs: ${e.map(e=>e.message).join(", ")}`)}cleanupExpiredConnections(){if(!this.isCleaningUp){this.isCleaningUp=!0;try{const e=Date.now(),t=[];for(let i=this.availableConnections.length-1;i>=0;i--){const s=this.availableConnections[i];if(e-s.createdAt>this.connectionTtl){t.push(s),this.availableConnections.splice(i,1);const e=this.connectionPool.indexOf(s);-1!==e&&this.connectionPool.splice(e,1)}}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}finally{this.isCleaningUp=!1}}}async acquireConnection(){if(this.cleanupExpiredConnections(),this.availableConnections.length>0){const e=this.availableConnections.pop();return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e}if(this.connectionPool.length<this.poolSize){const e={client:await this.createConnection(),createdAt:Date.now(),inUse:!0};return this.connectionPool.push(e),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),e}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((e,t)=>{const i=globalThis.setTimeout(()=>{const e=this.waitingResolvers.indexOf(resolveWrapper);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),resolveWrapper=t=>{globalThis.clearTimeout(i),e(t)};this.waitingResolvers.push(resolveWrapper)})}releaseConnection(e){if(Date.now()-e.createdAt>this.connectionTtl){const t=this.connectionPool.indexOf(e);return-1!==t&&this.connectionPool.splice(t,1),e.client.unbind().catch(()=>{}),void this.logger.debug("Released expired LDAP connection (discarded)")}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t(e)}e.inUse=!1,this.availableConnections.push(e),this.logger.debug("Released LDAP connection back to pool")}getAttributeSignature(e){if(!e||0===e.length)return"*";const t=e.join("|");let i=this.attrSignatureCache.get(t);return i||(i=[...e].sort().join(","),this.attrSignatureCache.set(t,i)),i}getCacheKey(e,t){const i=this.getAttributeSignature(t.attributes),s=t.explicitBufferAttributes?.length?[...t.explicitBufferAttributes].sort().join(","):"-",n="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return[e,t.scope||"sub",n,i,s,!1===t.returnAttributeValues?"novalues":"values",t.derefAliases??"-",String(t.sizeLimit??"-")].join("\0")}invalidateCache(e){if(!this.cacheEnabled)return;this.cacheGeneration++;const t=foldDn(e),i=`,${t}`,s=[];for(const e of this.searchCache.keys()){const n=foldDn(e.split("\0")[0]);(n===t||n.endsWith(i))&&s.push(e)}for(const e of s)this.searchCache.delete(e)}get system(){return this}forRequest(e){return{search:(t,i)=>this.search(t,i??this.base,e),add:(t,i)=>this.add(t,i,e),modify:(t,i,s)=>this.modify(t,i,e,s),rename:(t,i)=>this.rename(t,i,e),delete:t=>this.delete(t,e)}}declareObjectClasses(e,t,i=!1){const s=(Array.isArray(t)?t:[t]).filter(e=>"string"==typeof e&&""!==e);e&&0!==s.length&&this.declaredClasses.push({base:e,classes:s,includeBase:i})}declaredFor(e){let t=0,i=[];for(const s of this.declaredClasses){if(!o(e,s.base))continue;const n=c(s.base);if(!s.includeBase&&c(e)===n)continue;const a=n.split(",").length;a>t&&(t=a,i=[]),a===t&&i.push(...s.classes)}const s=new Set;return i.filter(e=>{const t=e.toLowerCase();return!s.has(t)&&(s.add(t),!0)})}warnOnce(e){this.classWarnings.has(e)||(this.classWarnings.set(e,!0),this.logger.warn(e))}async classRepair(e,t){if(!1===this.config.ldap_flat_auto_repair)return[];const i=this.declaredFor(e);if(0===i.length)return[];const s=Array.isArray(t.delete)?t.delete:Object.entries(t.delete||{}).filter(([,e])=>!e||Array.isArray(e)&&!e.length).map(([e])=>e);if([...Object.keys(t.add||{}),...Object.keys(t.replace||{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete||{})].some(e=>"objectclass"===e.toLowerCase()))return[];let n;try{n=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){return this.schemaUnavailable||this.logger.warn(`Object classes are not repaired: the directory schema could not be read (${String(e)})`),this.schemaUnavailable=!0,[]}const attributeKey=e=>n.getAttributeType(e)?.oid??e.toLowerCase(),allowedBy=e=>{const{must:t,may:i}=n.resolveAttributes(e);return new Set([...t,...i].map(attributeKey))},valued=e=>Object.entries(e||{}).filter(([,e])=>Array.isArray(e)?e.length>0:null!=e&&""!==e).map(([e])=>e),a=[...valued(t.add),...valued(t.replace)],r=i.filter(e=>{const t=n.getObjectClass(e);if("AUXILIARY"!==t?.kind)return!1;const i=allowedBy([e]);return a.some(e=>i.has(attributeKey(e)))});if(0===r.length)return[];let o;try{o=(await this.system.search({paged:!1,scope:"base",attributes:["objectClass",...n.resolveAttributes(r).must]},e)).searchEntries[0]}catch{return[]}if(!o)return[];const c=o.objectClass??o.objectclass,l=(Array.isArray(c)?c:c?[c]:[]).map(String),h=allowedBy(l);let d=a.filter(e=>n.getAttributeType(e)&&!h.has(attributeKey(e)));if(0===d.length)return[];const p=new Set(s.map(attributeKey)),u=new Set([...valued(o).filter(e=>!p.has(attributeKey(e))),...a].map(attributeKey)),{auxiliary:f,structural:g}=n.missingClasses(i,l),y=[];for(const t of f){const i=t.names[0]??t.oid,s=allowedBy([i]),a=d.filter(e=>s.has(attributeKey(e)));if(0===a.length)continue;const r=n.resolveAttributes([i]).must.filter(e=>!u.has(attributeKey(e)));r.length>0?this.warnOnce(`${e} needs ${i} for ${a.join(", ")}, and cannot be given it without ${r.join(", ")}`):(y.push(i),d=d.filter(e=>!a.includes(e)))}const m=g.filter(e=>{const t=allowedBy([e]);return d.some(e=>t.has(attributeKey(e)))});return m.length>0&&this.warnOnce(`${e} cannot hold ${d.join(", ")}: its schema declares ${m.join(", ")}, a structural class an existing entry cannot be given`),y.length>0&&this.logger.info(`${e}: adding ${y.join(", ")}, which the change needs and its schema declares`),y}async subschemaDn(){try{const e=await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["subschemaSubentry"]},""),t=e.searchEntries[0]?.subschemaSubentry,i=Array.isArray(t)?t[0]:t;if(i)return String(i)}catch(e){this.logger.warn(`Unable to read subschemaSubentry from root DSE: ${String(e)}`)}return"cn=Subschema"}async schemaIndex(){const e=1e3*(this.config.ldap_raw_schema_cache_ttl??3600);if(this.schemaCache&&Date.now()-this.schemaCache.fetchedAt<e)return this.schemaCache.index;if(this.schemaLoading)return this.schemaLoading;this.schemaLoading=(async()=>{const e=await this.subschemaDn(),t=(await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["objectClasses","attributeTypes","ldapSyntaxes","matchingRules"]},e).catch(t=>{if(32===t?.code)throw new u(`Subschema entry ${e} not found`);throw t})).searchEntries[0];if(!t)throw new u(`Subschema entry ${e} not found`);const list=e=>{if(void 0===e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString("utf8"):e)},i=new g(y({objectClasses:list(t.objectClasses),attributeTypes:list(t.attributeTypes),ldapSyntaxes:list(t.ldapSyntaxes),matchingRules:list(t.matchingRules)}));return this.schemaCache={index:i,fetchedAt:Date.now()},this.logger.info(`LDAP schema loaded from ${e}: ${i.schema.objectClasses.length} object classes, ${i.schema.attributeTypes.length} attribute types`),i})();try{return await this.schemaLoading}finally{this.schemaLoading=void 0}}filterForCaller(e,t,i){if(!this.parent.hooks.ldapsearchfilter)return e;const s=this.parent.hooks.ldapsearchfilter,one=async e=>{const[n]=await l(s,[e,t,i]);return n??e};if("function"==typeof e[Symbol.asyncIterator]){const t=e;return async function*(){for await(const e of t)yield await one(e)}()}return one(e)}async search(e,t=this.base,i){let s={...m,...e};s=await l(this.parent.hooks.ldapsearchopts,s),[t,s]=await l(this.parent.hooks.ldapsearchrequest,[t,s,i]);const n=this.cacheEnabled&&!s.paged&&"base"===s.scope,a=n?this.getCacheKey(t,s):"",r=this.cacheGeneration;if(n){const e=this.searchCache.get(a);if(e)return this.logger.debug(`LDAP search cache hit: ${digestKey(a)}`),this.filterForCaller(cloneSearchResult(e),i,s)}const o=await this.acquireConnection();try{let e=s.paged?o.client.searchPaginated(t,s):o.client.search(t,s);if(e=await l(this.parent.hooks.ldapsearchresult,e),n){const t=e;return r===this.cacheGeneration?(this.searchCache.set(a,cloneSearchResult(t)),this.logger.debug(`LDAP search cached: ${digestKey(a)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${digestKey(a)}`),this.filterForCaller(t,i,s)}return s.paged?this.filterForCaller(this.wrapPaginatedSearch(e,o),i,s):this.filterForCaller(e,i,s)}finally{s.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,t,s){e=this.setDn(e),t.objectClass&&0!==t.objectClass.length||!this.config.user_class||(t.objectClass=this.config.user_class);const n={};for(const[e,i]of Object.entries(t))Buffer.isBuffer(i)?n[e]=i.toString():Array.isArray(i)&&i.length>0&&Buffer.isBuffer(i[0])?n[e]=i.map(e=>e.toString()):n[e]=i;[e,t]=await l(this.parent.hooks.ldapaddrequest,[e,n,s]);const a=[];for(const[e,t]of Object.entries(n)){const s=Array.isArray(t)?t:[t];a.push(new i({type:e,values:s}))}const r=await this.acquireConnection();try{this.invalidateCache(e),await r.client.add(e,a),this.invalidateCache(e),h(this.parent.hooks.ldapadddone,[e,t],p(s))}catch(t){throw function(e,t){const i=ldapError("LDAP add error",t);if(68!==i.code)return i;const s=new f(`Entry ${e} already exists`);return s.code=68,s}(e,t)}finally{this.releaseConnection(r)}return await h(this.parent.hooks.ldapaddafter,[e,t],p(s)),!0}async modify(e,t,a,r={}){e=this.setDn(e);const o=[],c=this.opNumber();if([e,t]=await l(this.parent.hooks.ldapmodifyrequest,[e,t,c,a]),t.add)for(const[e,n]of Object.entries(t.add))o.push(new s({operation:"add",modification:new i({type:e,values:Array.isArray(n)?n:[n]})}));if(t.replace)for(const[e,n]of Object.entries(t.replace))o.push(new s({operation:"replace",modification:new i({type:e,values:Array.isArray(n)?n:[n]})}));if(t.delete)if(Array.isArray(t.delete))for(const e of t.delete)e&&o.push(new s({operation:"delete",modification:new i({type:e,values:[]})}));else for(const[e,n]of Object.entries(t.delete)){const t=new s({operation:"delete",modification:new i(n?{type:e,values:Array.isArray(n)?n:[n]}:{type:e})});o.push(t)}if(0===o.length){return Object.keys(t.add||{}).length>0||Object.keys(t.replace||{}).length>0||(Array.isArray(t.delete)?t.delete.length>0:Object.keys(t.delete||{}).length>0)?this.logger.warn(`Modify on ${e} asked for changes but emitted none; nothing was written`):this.logger.debug(`Modify on ${e} had nothing to apply`),h(this.parent.hooks.ldapmodifydone,[e,{},c],p(a)),!1}{const l=await this.classRepair(e,t),d=l.length>0?new s({operation:"add",modification:new i({type:"objectClass",values:l})}):void 0;d&&o.push(d);const u=await this.acquireConnection();try{this.invalidateCache(e);const i=r.permissive?[new n("1.2.840.113556.1.4.1413",{critical:!1})]:void 0;try{await u.client.modify(e,o,i)}catch(t){if(!d||20!==t?.code)throw t;o.splice(o.indexOf(d),1),await u.client.modify(e,o,i)}return this.invalidateCache(e),h(this.parent.hooks.ldapmodifydone,[e,t,c],p(a)),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(o)}`),function(e,t){const i=ldapError("LDAP modify error",t);if(65!==i.code)return i;const s=new f(`Entry ${e} cannot hold this change: ${String(t)}`);return s.code=65,s}(e,t)}finally{this.releaseConnection(u)}}}async rename(e,t,i){e=this.setDn(e),t=this.setDn(t),[e,t]=await l(this.parent.hooks.ldaprenamerequest,[e,t,i]);const s=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await s.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),h(this.parent.hooks.ldaprenamedone,[e,t],p(i)),!0}catch(e){throw ldapError("LDAP rename error",e)}finally{this.releaseConnection(s)}}async move(e,t,i){e=this.setDn(e),t=this.setDn(t);const s=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await s.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),h(this.parent.hooks.ldaprenamedone,[e,t],p(i)),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(e){throw ldapError("LDAP move error",e)}finally{this.releaseConnection(s)}}async delete(e,t){e=Array.isArray(e)?e.map(e=>this.setDn(e)):this.setDn(e),Array.isArray(e)||(e=[e]),[e]=await l(this.parent?.hooks.ldapdeleterequest,[e,t]);const i=await this.acquireConnection();try{for(const s of e){try{this.invalidateCache(s),await i.client.del(s),this.invalidateCache(s)}catch(e){throw ldapError("LDAP delete error",e)}h(this.parent.hooks.ldapdeletedone,s,p(t))}return!0}finally{this.releaseConnection(i)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${d(e)},${this.base}`,e}opNumber(){return this.parent.operationSequence++}}export{ldapActions as default};
@@ -1 +1 @@
1
- import e from"express-openid-connect";import o from"../../lib/auth/base.js";import{launchHooksChained as i}from"../../lib/utils.js";import{serverError as t}from"../../lib/expressFormatedResponses.js";const{auth:s,requiresAuth:r}=e,n=["/login","/logout","/callback","/backchannel-logout"],walkSubscribers=async(e,o)=>{let i;for(const t of e??[])if(t)try{await t(o)}catch(e){i??(i=e instanceof Error?e:new Error(String(e)))}if(i)throw i};class OpenIDConnect extends o{constructor(e){super(e),this.name="openidconnect",this.roles=["auth"];for(const e of["oidc_server","oidc_client_id","oidc_client_secret","base_url"])if(!this.config[e])throw new Error(`Missing config parameter ${e}`)}buildConfig(){return{authRequired:!1,issuerBaseURL:this.config.oidc_server,clientID:this.config.oidc_client_id,secret:this.config.oidc_client_secret,baseURL:this.config.base_url,clientSecret:this.config.oidc_client_secret,authorizationParams:{response_type:"code",scope:"openid profile email"},backchannelLogout:{onLogoutToken:async e=>{this.server.hooks.oidclogouttoken?await walkSubscribers(this.server.hooks.oidclogouttoken,e):this.logger.warn("openidconnect: a logout token arrived but no Back-Channel Logout plugin is loaded, so nothing records it")},onLogin:async e=>{if(!this.server.hooks.oidclogin)return;const o=e.oidc?.idTokenClaims;if(o)try{await walkSubscribers(this.server.hooks.oidclogin,o)}catch(o){const i=e.appSession;if(i)for(const e of Object.keys(i))delete i[e];throw o}},isLoggedOut:async e=>{if(!this.server.hooks.oidcsessionvalid)return!1;const o=e.oidc.idTokenClaims,[,t]=await i(this.server.hooks.oidcsessionvalid,[o,!0]);return!t}}}}identitySource(){const e=this.config.oidc_username_claim||"sub";return`req.user: the OIDC sub, an opaque provider identifier; req.userName: the "${e}" claim`+("sub"===e?" — the same value, until --oidc-username-claim names another":"")}get pathPrefixes(){const e=super.pathPrefixes;return 0===e.length?e:[...e,...n]}api(e){this.router=s(this.buildConfig()),super.api(e)}authMethod(e,o,i){if(!this.router)return t(o,new Error(`${this.name}: api() has not built the router`));this.router(e,o,s=>{if(s)return t(o,s);r()(e,o,()=>{const o=e.oidc.user,t=this.config.oidc_username_claim||"sub",s=o[t];"sub"!==t&&"string"!=typeof s&&this.logger.warn(`${this.name}: no "${t}" claim on this session, so req.userName falls back to the sub. Check --oidc-username-claim against the scopes the provider is asked for`),this.publishIdentity(e,String(o.sub),"string"==typeof s?s:String(o.sub)),i()})})}}export{OpenIDConnect as default};
1
+ import e from"express-openid-connect";import o from"../../lib/auth/base.js";import{launchHooksChained as i}from"../../lib/utils.js";import{serverError as t}from"../../lib/expressFormatedResponses.js";const{auth:s,requiresAuth:n}=e,r=["/login","/logout","/callback","/backchannel-logout"],a=/^end_session_endpoint must be configured/,c=/^\/logout\/?$/i,walkSubscribers=async(e,o)=>{let i;for(const t of e??[])if(t)try{await t(o)}catch(e){i??(i=e instanceof Error?e:new Error(String(e)))}if(i)throw i};class OpenIDConnect extends o{constructor(e){super(e),this.name="openidconnect",this.configApiKey="openidconnect",this.roles=["auth","configurable"],this.warnedNoEndSession=!1;for(const e of["oidc_server","oidc_client_id","oidc_client_secret","base_url"])if(!this.config[e])throw new Error(`Missing config parameter ${e}`)}buildConfig(){return{authRequired:!1,issuerBaseURL:this.config.oidc_server,clientID:this.config.oidc_client_id,secret:this.config.oidc_client_secret,baseURL:this.config.base_url,clientSecret:this.config.oidc_client_secret,authorizationParams:{response_type:"code",scope:"openid profile email"},idpLogout:!0,routes:{logout:!1},backchannelLogout:{onLogoutToken:async e=>{this.server.hooks.oidclogouttoken?await walkSubscribers(this.server.hooks.oidclogouttoken,e):this.logger.warn("openidconnect: a logout token arrived but no Back-Channel Logout plugin is loaded, so nothing records it")},onLogin:async e=>{if(!this.server.hooks.oidclogin)return;const o=e.oidc?.idTokenClaims;if(o)try{await walkSubscribers(this.server.hooks.oidclogin,o)}catch(o){const i=e.appSession;if(i)for(const e of Object.keys(i))delete i[e];throw o}},isLoggedOut:async e=>{if(!this.server.hooks.oidcsessionvalid)return!1;const o=e.oidc.idTokenClaims,[,t]=await i(this.server.hooks.oidcsessionvalid,[o,!0]);return!t}}}}identitySource(){const e=this.config.oidc_username_claim||"sub";return`req.user: the OIDC sub, an opaque provider identifier; req.userName: the "${e}" claim`+("sub"===e?" — the same value, until --oidc-username-claim names another":"")}getConfigApiData(){return{enabled:!0,endpoints:{logout:"/logout"}}}get pathPrefixes(){const e=super.pathPrefixes;return 0===e.length?e:[...e,...r]}api(e){this.router=s(this.buildConfig()),super.api(e)}authMethod(e,o,i){if(!this.router)return t(o,new Error(`${this.name}: api() has not built the router`));this.router(e,o,s=>s&&c.test(e.path)&&this.noEndSession(s)?o.redirect(this.config.base_url):s?t(o,s):void("GET"!==e.method&&"HEAD"!==e.method||!c.test(e.path)?n()(e,o,()=>{const o=e.oidc.user,t=this.config.oidc_username_claim||"sub",s=o[t];"sub"!==t&&"string"!=typeof s&&this.logger.warn(`${this.name}: no "${t}" claim on this session, so req.userName falls back to the sub. Check --oidc-username-claim against the scopes the provider is asked for`),this.publishIdentity(e,String(o.sub),"string"==typeof s?s:String(o.sub)),i()}):o.oidc.logout({logoutParams:{post_logout_redirect_uri:void 0}})))}noEndSession(e){return!!(e instanceof Error&&a.test(e.message))&&(this.warnedNoEndSession||(this.warnedNoEndSession=!0,this.logger.warn(`${this.name}: the provider publishes no end_session_endpoint, so /logout ends the session here only, and the provider may log the caller back in`)),!0)}}export{OpenIDConnect as default};
@@ -1 +1 @@
1
- import i from"../abstract/plugin.js";import{wantJson as o}from"../lib/expressFormatedResponses.js";class ConfigApi extends i{constructor(){super(...arguments),this.name="configApi",this.roles=["api"]}api(i){const t=this.config.api_prefix||"/api";i.get(`${t}/v1/config`,(i,e)=>{if(!o(i,e))return;const s={apiPrefix:t,ldapBase:this.config.ldap_base||"",features:this.collectPluginConfigs()};e.json(s)}),this.logger.info(`Configuration API registered at ${t}/v1/config`)}collectPluginConfigs(){const i={};for(const[o,t]of Object.entries(this.server.loadedPlugins))if("configApi"!==o&&t.roles?.includes("configurable")&&t.getConfigApiData)try{const e=t.getConfigApiData();e&&(i[o]=e)}catch(i){this.logger.warn(`Failed to get config from plugin ${o}: ${String(i)}`)}return i}}export{ConfigApi as default};
1
+ import i from"../abstract/plugin.js";import{wantJson as o}from"../lib/expressFormatedResponses.js";class ConfigApi extends i{constructor(){super(...arguments),this.name="configApi",this.roles=["api"]}api(i){const t=this.config.api_prefix||"/api";i.get(`${t}/v1/config`,(i,e)=>{if(!o(i,e))return;const s={apiPrefix:t,ldapBase:this.config.ldap_base||"",features:this.collectPluginConfigs()};e.json(s)}),this.logger.info(`Configuration API registered at ${t}/v1/config`)}collectPluginConfigs(){const i={};for(const[o,t]of Object.entries(this.server.loadedPlugins))if("configApi"!==o&&t.roles?.includes("configurable")&&t.getConfigApiData)try{const e=t.getConfigApiData();e&&(i[t.configApiKey??o]=e)}catch(i){this.logger.warn(`Failed to get config from plugin ${o}: ${String(i)}`)}return i}}export{ConfigApi as default};
@@ -1 +1 @@
1
- import{escapeDnValue as t,isDummyMemberDn as e,launchHooksChained as s,validateDnValue as r,launchHooks as i,isChildOf as o,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as n}from"./types.js";import{withExternalId as c,mergeMapping as h,loadMappingFile as u,DEFAULT_GROUP_MAPPING as p,requiredLdapAttributes as m,ldapToScimGroup as l,scimGroupToLdap as d}from"./mapping.js";import{scimFilterToLdap as f}from"./filter.js";import{pagedSearch as g}from"./list.js";import{patchToModifyRequest as b}from"./patch.js";import{scimNotFound as w,ScimError as y,scimInvalidValue as R,scimUniqueness as _}from"./errors.js";import{extractLdapCode as $}from"../../lib/ldapCodes.js";class ScimGroups{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_group_rdn_attribute||"cn",this.objectClass=this.config.scim_group_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=this.config.scim_group_mapping||"";this.mapping=c(h(p,e?u(e):void 0),this.config.scim_group_external_id_attribute||"")}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"Group",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:"",lockValue:""}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const s=this.baseResolver.userBase(t),r=this.baseResolver.groupBase(t),i=this.scimPrefix,o=this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),a=this.config.scim_user_rdn_attribute||"uid",n=this.rdnAttribute,c=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(e(t,c))return;const h=/^([^=]+)=((?:\\.|[^,])+)/.exec(t);if(!h)return;const u=h[2].replace(/\\(.)/g,"$1"),p=t.toLowerCase();let m;s&&p.endsWith(s.toLowerCase())?m="User":r&&p.endsWith(r.toLowerCase())?m="Group":h[1]===a?m="User":h[1]===n&&(m="Group");const l={value:u};if(m&&(l.type=m,o)){const t="User"===m?"Users":"Groups";l.$ref=`${o.replace(/\/$/,"")}${i}/${t}/${encodeURIComponent(u)}`}return l}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:[...m(this.mapping),"member"]},s)}catch(t){if(32===$(t))throw w(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw w(`Group ${e} not found`);return l(r.searchEntries[0],this.mapping,this.ctx(t),this.buildMemberResolver(t))}async list(t,e){const s=this.baseResolver.groupBase(t),r=Math.max(1,e.startIndex||1),i=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let o,a=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=f(e.filter,this.mapping);t.idEquals?o=t.idEquals:a=`(&${a}${t.ldapFilter})`}if(o)try{const e=await this.get(t,o);return{schemas:[n],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof y&&404===t.statusCode)return{schemas:[n],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:c,totalResults:h}=await g({directory:this.ldap.forRequest(t),base:s,filter:a,attributes:[...m(this.mapping),"member"],startIndex:r,count:i,maxScanned:this.maxScanned}),u=this.buildMemberResolver(t),p=c.map(e=>l(e,this.mapping,this.ctx(t),u));return{schemas:[n],totalResults:h,startIndex:r,itemsPerPage:p.length,Resources:p}}async create(e,o){if(!o.displayName)throw R("displayName is required");const a=(await s(this.hooks.scimgroupcreate,[o,e]))[0],{rdn:n,attributes:c}=d(a,this.mapping,this.objectClass);if(!n)throw R("displayName is required");r(n,this.rdnAttribute),c[this.rdnAttribute]=n;const h=[];if(a.members&&Array.isArray(a.members))for(const t of a.members){if(!t||!t.value)continue;const s=await this.users.resolveRef(e,t.value);s&&h.push(s)}if(0===h.length){const t=this.config.group_dummy_user||"cn=fakeuser";h.push(t)}c.member=h;const u=this.baseResolver.groupBase(e),p=`${this.rdnAttribute}=${t(n)},${u}`;try{await this.ldap.forRequest(e).add(p,c)}catch(t){if(68===$(t))throw _(`Group ${n} already exists`);throw t}const m=await this.get(e,n);return i(this.hooks.scimgroupcreatedone,m),m}async replace(t,e,r){await this.get(t,e);const o=(await s(this.hooks.scimgroupupdate,[e,r,t]))[1],{attributes:a}=d(o,this.mapping,this.objectClass),n=[];if(o.members&&Array.isArray(o.members))for(const e of o.members){if(!e||!e.value)continue;const s=await this.users.resolveRef(t,e.value);s&&n.push(s)}if(0===n.length){const t=this.config.group_dummy_user||"cn=fakeuser";n.push(t)}const c=this.dnForId(e,t),h={replace:{}};for(const[t,e]of Object.entries(a))"objectClass"!==t&&t!==this.rdnAttribute&&(h.replace[t]=e);h.replace.member=n,await this.ldap.forRequest(t).modify(c,h);const u=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,u),u}async patch(t,e,s){const r=this.dnForId(e,t),o=await this.currentEntry(t,e),a=await b(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e),current:o});if(a.delete&&!Array.isArray(a.delete)&&"member"in a.delete){const t=this.config.group_dummy_user||"cn=fakeuser";delete a.delete.member,0===Object.keys(a.delete).length&&delete a.delete,a.replace||(a.replace={}),a.replace.member=[t]}await this.ldap.forRequest(t).modify(r,a);const n=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,n),n}async currentEntry(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:[...m(this.mapping),"member"]},s)}catch(t){if(32===$(t))throw w(`Group ${e} not found`);throw t}if(!r.searchEntries?.length)throw w(`Group ${e} not found`);return r.searchEntries[0]}async delete(t,e){await this.get(t,e);const r=(await s(this.hooks.scimgroupdelete,[e,t]))[0],o=this.dnForId(r,t);await this.ldap.forRequest(t).delete(o),i(this.hooks.scimgroupdeletedone,r)}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e);if(s.includes("=")&&s.includes(","))return o(s,r)?s:void 0;const i=`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==$(t))throw t}try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},r);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==$(t))throw t}}get groupMapping(){return this.mapping}}export{ScimGroups};
1
+ import{escapeDnValue as t,isDummyMemberDn as e,launchHooksChained as s,validateDnValue as r,launchHooks as i,isChildOf as o,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as n}from"./types.js";import{withExternalId as c,mergeMapping as h,loadMappingFile as u,DEFAULT_GROUP_MAPPING as m,requiredLdapAttributes as d,ldapToScimGroup as p,scimGroupToLdap as l}from"./mapping.js";import{scimFilterToLdap as f}from"./filter.js";import{pagedSearch as g}from"./list.js";import{writePatch as b,patchToModifyRequest as w}from"./patch.js";import{scimNotFound as y,ScimError as R,scimInvalidValue as _,scimUniqueness as $}from"./errors.js";import{extractLdapCode as v}from"../../lib/ldapCodes.js";class ScimGroups{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_group_rdn_attribute||"cn",this.objectClass=this.config.scim_group_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=this.config.scim_group_mapping||"";this.mapping=c(h(m,e?u(e):void 0),this.config.scim_group_external_id_attribute||"")}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"Group",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:"",lockValue:""}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const s=this.baseResolver.userBase(t),r=this.baseResolver.groupBase(t),i=this.scimPrefix,o=this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),a=this.config.scim_user_rdn_attribute||"uid",n=this.rdnAttribute,c=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(e(t,c))return;const h=/^([^=]+)=((?:\\.|[^,])+)/.exec(t);if(!h)return;const u=h[2].replace(/\\(.)/g,"$1"),m=t.toLowerCase();let d;s&&m.endsWith(s.toLowerCase())?d="User":r&&m.endsWith(r.toLowerCase())?d="Group":h[1]===a?d="User":h[1]===n&&(d="Group");const p={value:u};if(d&&(p.type=d,o)){const t="User"===d?"Users":"Groups";p.$ref=`${o.replace(/\/$/,"")}${i}/${t}/${encodeURIComponent(u)}`}return p}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:[...d(this.mapping),"member"]},s)}catch(t){if(32===v(t))throw y(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw y(`Group ${e} not found`);return p(r.searchEntries[0],this.mapping,this.ctx(t),this.buildMemberResolver(t))}async list(t,e){const s=this.baseResolver.groupBase(t),r=Math.max(1,e.startIndex||1),i=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let o,a=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=f(e.filter,this.mapping);t.idEquals?o=t.idEquals:a=`(&${a}${t.ldapFilter})`}if(o)try{const e=await this.get(t,o);return{schemas:[n],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof R&&404===t.statusCode)return{schemas:[n],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:c,totalResults:h}=await g({directory:this.ldap.forRequest(t),base:s,filter:a,attributes:[...d(this.mapping),"member"],startIndex:r,count:i,maxScanned:this.maxScanned}),u=this.buildMemberResolver(t),m=c.map(e=>p(e,this.mapping,this.ctx(t),u));return{schemas:[n],totalResults:h,startIndex:r,itemsPerPage:m.length,Resources:m}}async create(e,o){if(!o.displayName)throw _("displayName is required");const a=(await s(this.hooks.scimgroupcreate,[o,e]))[0],{rdn:n,attributes:c}=l(a,this.mapping,this.objectClass);if(!n)throw _("displayName is required");r(n,this.rdnAttribute),c[this.rdnAttribute]=n;const h=[];if(a.members&&Array.isArray(a.members))for(const t of a.members){if(!t||!t.value)continue;const s=await this.users.resolveRef(e,t.value);s&&h.push(s)}if(0===h.length){const t=this.config.group_dummy_user||"cn=fakeuser";h.push(t)}c.member=h;const u=this.baseResolver.groupBase(e),m=`${this.rdnAttribute}=${t(n)},${u}`;try{await this.ldap.forRequest(e).add(m,c)}catch(t){if(68===v(t))throw $(`Group ${n} already exists`);throw t}const d=await this.get(e,n);return i(this.hooks.scimgroupcreatedone,d),d}async replace(t,e,r){await this.get(t,e);const o=(await s(this.hooks.scimgroupupdate,[e,r,t]))[1],{attributes:a}=l(o,this.mapping,this.objectClass),n=[];if(o.members&&Array.isArray(o.members))for(const e of o.members){if(!e||!e.value)continue;const s=await this.users.resolveRef(t,e.value);s&&n.push(s)}if(0===n.length){const t=this.config.group_dummy_user||"cn=fakeuser";n.push(t)}const c=this.dnForId(e,t),h={replace:{}};for(const[t,e]of Object.entries(a))"objectClass"!==t&&t!==this.rdnAttribute&&(h.replace[t]=e);h.replace.member=n,await this.ldap.forRequest(t).modify(c,h);const u=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,u),u}async patch(t,e,s){const r=this.dnForId(e,t);await b(r,async()=>{const i=await this.currentEntry(t,e),o=await w(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e),current:i}),a=o.delete&&!Array.isArray(o.delete)?o.delete.member:void 0;if(void 0!==a){const t=this.config.group_dummy_user||"cn=fakeuser";if(""===a)delete o.delete.member,0===Object.keys(o.delete).length&&delete o.delete,o.replace||(o.replace={}),o.replace.member=[t];else{const e=[].concat(a);0===[].concat(i.member??[],o.add?.member??[]).map(String).filter(t=>!e.includes(t)).length&&(o.add||(o.add={}),o.add.member=t)}}await this.ldap.forRequest(t).modify(r,o,{permissive:!0})});const o=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,o),o}async currentEntry(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:[...d(this.mapping),"member"]},s)}catch(t){if(32===v(t))throw y(`Group ${e} not found`);throw t}if(!r.searchEntries?.length)throw y(`Group ${e} not found`);return r.searchEntries[0]}async delete(t,e){await this.get(t,e);const r=(await s(this.hooks.scimgroupdelete,[e,t]))[0],o=this.dnForId(r,t);await this.ldap.forRequest(t).delete(o),i(this.hooks.scimgroupdeletedone,r)}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e);if(s.includes("=")&&s.includes(","))return o(s,r)?s:void 0;const i=`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==v(t))throw t}try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},r);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==v(t))throw t}}get groupMapping(){return this.mapping}}export{ScimGroups};
@@ -1 +1 @@
1
- import e from"fs";import{scimInvalidValue as r}from"./errors.js";import{SCHEMA_GROUP as t,SCHEMA_USER as i}from"./types.js";const a=["createTimestamp","modifyTimestamp","entryUUID"],n={resourceType:"User",schemas:[i],entries:[{scim:"userName",ldap:"uid"},{scim:"externalId",ldap:"employeeNumber"},{scim:"name",sub:{familyName:"sn",givenName:"givenName",formatted:"cn",middleName:"initials"}},{scim:"displayName",ldap:"displayName"},{scim:"nickName",ldap:"displayName"},{scim:"title",ldap:"title"},{scim:"preferredLanguage",ldap:"preferredLanguage"},{scim:"emails",ldapPrimary:"mail",ldapSecondary:"mailAlternateAddress",multi:"array"},{scim:"phoneNumbers",ldapPrimary:"telephoneNumber",ldapSecondary:"mobile",multi:"array"}]},o={resourceType:"Group",schemas:[t],entries:[{scim:"displayName",ldap:"cn"}]};function withExternalId(e,r){const t=(r||"").trim();return t?e.entries.some(e=>"externalId"===e.scim)?e:{...e,entries:[...e.entries,{scim:"externalId",ldap:t}]}:e}function asString(e){if(null!=e){if(Array.isArray(e)){const r=e[0];return null==r?void 0:String(r)}return Buffer.isBuffer(e)?e.toString():String(e)}}function ldapTimeToIso(e){if(!e)return;const r=/^(\d{4})(\d{2})(\d{2})(\d{2})(?:(\d{2})(?:(\d{2}))?)?(?:[.,](\d+))?(Z|[+-]\d{2}(?:\d{2})?)?$/.exec(e);if(!r)return e;const[,t,i,a,n,o,s,c,l]=r;let d=0;if(c){const e=s?1e3:o?6e4:36e5;d=Math.round(Number(`0.${c}`)*e)}let u=1e3*(60*(60*Number(n)+Number(o||0))+Number(s||0))+d;const m=Math.floor(u/36e5);u-=36e5*m;const p=Math.floor(u/6e4);u-=6e4*p;const f=Math.floor(u/1e3),y=u-1e3*f;let b="Z";l&&"Z"!==l&&(b=`${l[0]}${l.slice(1,3)}:${l.slice(3,5)||"00"}`);const pad=e=>String(e).padStart(2,"0"),g=`${t}-${i}-${a}T${pad(m)}:${pad(p)}:${pad(f)}${y?`.${String(y).padStart(3,"0")}`:""}${b}`;return Number.isNaN(Date.parse(g))?e:g}function asArray(e){return null==e?[]:Array.isArray(e)?e.map(e=>Buffer.isBuffer(e)?e.toString():String(e)):Buffer.isBuffer(e)?[e.toString()]:[String(e)]}function loadMappingFile(r){if(!r)return;const t=e.readFileSync(r,"utf8");return JSON.parse(t)}function mergeMapping(e,r){if(!r)return e;const t=new Map;for(const r of e.entries)t.set(r.scim,r);for(const e of r.entries)t.set(e.scim,e);return{...e,entries:Array.from(t.values())}}const s="pwdAccountLockedTime",c="000001010000Z",l=/^\d{10,14}(\.\d+)?(Z|[+-]\d{2,4})$/;let warn=e=>{console.warn(`WARNING: ${e}`)};function setLockConfigWarn(e){const r=warn;return warn=e,r}const d=/^([A-Za-z][A-Za-z0-9-]*|\d+(\.\d+)+)$/;function resolveLockConfig(e,r){const t=(e||"").trim()||s;if(!d.test(t))throw new Error(`--scim-user-lock-attribute must be an LDAP attribute name, got '${t}'`);const i=t.toLowerCase()===s.toLowerCase(),a=(r||"").trim();if(!a){if(i)return{attribute:t,value:c};throw new Error(`--scim-user-lock-attribute is '${t}', so --scim-user-lock-value must say what marks an account locked (for nsAccountLock on 389-ds, 'TRUE'). The default '${c}' only means anything to ${s}.`)}return i||a!==c||warn(`--scim-user-lock-value is the ppolicy convention '${c}' but --scim-user-lock-attribute is '${t}', not ${s}. Unless '${t}' is a GeneralizedTime your directory reads as a lock, every deactivation will answer 200 and read back as active:false while the account keeps binding. Verify a deactivation actually prevents a bind.`),i&&!l.test(a)&&warn(`--scim-user-lock-value '${a}' is not a GeneralizedTime, and ${s} holds one. The directory will refuse the write, or — where the attribute was redefined locally — store a value it does not read as a lock. The ppolicy convention is '${c}'.`),{attribute:t,value:a}}function readActive(e){if(void 0!==e){if("boolean"==typeof e)return e;if("string"==typeof e){const r=e.trim().toLowerCase();if("true"===r)return!0;if("false"===r)return!1}throw r(`Cannot read ${JSON.stringify(e)} as active`)}}function isLocked(e,r){const t=e[r.lockAttribute];return null!=t&&(Array.isArray(t)?t.length>0:String(t).length>0)}function resolveScimId(e,r){if("rdn"===r.idAttribute)return asString(e[r.rdnAttribute]);return asString(e[r.idAttribute])??asString(e[r.rdnAttribute])}function buildLocation(e,r){if(!r.baseUrl)return;const t="User"===r.resourceType?"Users":"Groups";return`${r.baseUrl.replace(/\/$/,"")}${r.scimPrefix}/${t}/${encodeURIComponent(e)}`}function ldapToScimUser(e,r,t){const a={schemas:[i]},n=resolveScimId(e,t);n&&(a.id=n);for(const t of r.entries){if(t.sub){const r={};for(const[i,a]of Object.entries(t.sub)){const t=asString(e[a]);null!=t&&(r[i]=t)}Object.keys(r).length>0&&(a[t.scim]=r);continue}if(t.ldapPrimary||t.ldapSecondary){const r=[];if(t.ldapPrimary){const i=asString(e[t.ldapPrimary]);null!=i&&r.push({value:i,primary:!0})}if(t.ldapSecondary)for(const i of asArray(e[t.ldapSecondary]))r.push({value:i});r.length>0&&(a[t.scim]=r);continue}if(t.ldap)if("array"===t.multi){const r=asArray(e[t.ldap]);r.length>0&&(a[t.scim]=r)}else{const r=asString(e[t.ldap]);null!=r&&(a[t.scim]=r)}}return a.active=!isLocked(e,t),n&&(a.meta={resourceType:"User",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(n,t)}),a}function scimUserToLdap(e,r,t,i){const a={objectClass:i},n=e.userName||e.id||"";for(const t of r.entries){if(t.readOnly||t.operational)continue;const r=e[t.scim];if(null!=r)if(t.sub&&"object"==typeof r)for(const[e,i]of Object.entries(t.sub)){const t=r[e];"string"==typeof t&&t.length>0&&(a[i]=t)}else{if(t.ldapPrimary||t.ldapSecondary){if(!Array.isArray(r))continue;const e=r,i=e.find(e=>!0===e.primary)||e[0],n=e.filter(e=>e!==i);t.ldapPrimary&&i&&i.value&&(a[t.ldapPrimary]=i.value),t.ldapSecondary&&n.length>0&&(a[t.ldapSecondary]=n.map(e=>e.value).filter(Boolean));continue}t.ldap&&(Array.isArray(r)?a[t.ldap]=r.map(e=>String(e)):"string"!=typeof r&&"number"!=typeof r||(a[t.ldap]=String(r)))}}const o=readActive(e.active);if(!1===o&&(a[t.lockAttribute]=t.lockValue||c),!a.cn&&n&&(a.cn=n),!a.sn){const r=e.name?.familyName||e.displayName||n;r&&(a.sn=r)}return{rdn:n,attributes:a,active:o}}function ldapToScimGroup(e,r,i,a){const n={schemas:[t]},o=resolveScimId(e,i);o&&(n.id=o);for(const t of r.entries){if(!t.ldap)continue;const r=e[t.ldap];if(null!=r)if("array"===t.multi){const e=asArray(r);e.length>0&&(n[t.scim]=e)}else{const e=asString(r);null!=e&&(n[t.scim]=e)}}const s=asArray(e.member),c=[];for(const e of s)if(e)if(a){const r=a(e);r&&c.push(r)}else c.push({value:e,type:"User"});return c.length>0&&(n.members=c),o&&(n.meta={resourceType:"Group",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(o,i)}),n}function scimGroupToLdap(e,r,t){const i={objectClass:t},a=e.displayName||e.id||"";for(const t of r.entries){if(t.readOnly||t.operational)continue;if(!t.ldap)continue;const r=e[t.scim];null!=r&&(Array.isArray(r)?i[t.ldap]=r.map(e=>"string"==typeof e?e:JSON.stringify(e)):"string"!=typeof r&&"number"!=typeof r||(i[t.ldap]=String(r)))}return{rdn:a,attributes:i}}function requiredLdapAttributes(e,r=[]){const t=new Set(["objectClass",...a,...r]);for(const r of e.entries)if(r.ldap&&t.add(r.ldap),r.ldapPrimary&&t.add(r.ldapPrimary),r.ldapSecondary&&t.add(r.ldapSecondary),r.sub)for(const e of Object.values(r.sub))t.add(e);return Array.from(t)}function scimPathToLdapAttribute(e,r){const t=e.split(".")[0],i=e.includes(".")?e.split(".").slice(1).join("."):"";for(const e of r.entries)if(e.scim===t){if(e.sub&&i)return e.sub[i];if((e.ldapPrimary||e.ldapSecondary)&&("value"===i||!i))return e.ldapPrimary||e.ldapSecondary;if(e.ldap)return e.ldap}}export{o as DEFAULT_GROUP_MAPPING,s as DEFAULT_LOCK_ATTRIBUTE,c as DEFAULT_LOCK_VALUE,n as DEFAULT_USER_MAPPING,buildLocation,isLocked,ldapTimeToIso,ldapToScimGroup,ldapToScimUser,loadMappingFile,mergeMapping,readActive,requiredLdapAttributes,resolveLockConfig,resolveScimId,scimGroupToLdap,scimPathToLdapAttribute,scimUserToLdap,setLockConfigWarn,warn,withExternalId};
1
+ import e from"fs";import{scimInvalidValue as r}from"./errors.js";import{SCHEMA_GROUP as t,SCHEMA_USER as i}from"./types.js";const a=["createTimestamp","modifyTimestamp","entryUUID"],n={resourceType:"User",schemas:[i],entries:[{scim:"userName",ldap:"uid"},{scim:"externalId",ldap:"employeeNumber"},{scim:"name",sub:{familyName:"sn",givenName:"givenName",formatted:"cn",middleName:"initials"}},{scim:"displayName",ldap:"displayName"},{scim:"nickName",ldap:"displayName"},{scim:"title",ldap:"title"},{scim:"preferredLanguage",ldap:"preferredLanguage"},{scim:"emails",ldapPrimary:"mail",ldapSecondary:"mailAlternateAddress",multi:"array"},{scim:"phoneNumbers",ldapPrimary:"telephoneNumber",ldapSecondary:"mobile",multi:"array"}]},o={resourceType:"Group",schemas:[t],entries:[{scim:"displayName",ldap:"cn"}]};function withExternalId(e,r){const t=(r||"").trim();return t?e.entries.some(e=>"externalId"===e.scim)?e:{...e,entries:[...e.entries,{scim:"externalId",ldap:t}]}:e}function asString(e){if(null!=e){if(Array.isArray(e)){const r=e[0];return null==r?void 0:String(r)}return Buffer.isBuffer(e)?e.toString():String(e)}}function ldapTimeToIso(e){if(!e)return;const r=/^(\d{4})(\d{2})(\d{2})(\d{2})(?:(\d{2})(?:(\d{2}))?)?(?:[.,](\d+))?(Z|[+-]\d{2}(?:\d{2})?)?$/.exec(e);if(!r)return e;const[,t,i,a,n,o,s,c,l]=r;let d=0;if(c){const e=s?1e3:o?6e4:36e5;d=Math.round(Number(`0.${c}`)*e)}let u=1e3*(60*(60*Number(n)+Number(o||0))+Number(s||0))+d;const m=Math.floor(u/36e5);u-=36e5*m;const p=Math.floor(u/6e4);u-=6e4*p;const f=Math.floor(u/1e3),y=u-1e3*f;let b="Z";l&&"Z"!==l&&(b=`${l[0]}${l.slice(1,3)}:${l.slice(3,5)||"00"}`);const pad=e=>String(e).padStart(2,"0"),g=`${t}-${i}-${a}T${pad(m)}:${pad(p)}:${pad(f)}${y?`.${String(y).padStart(3,"0")}`:""}${b}`;return Number.isNaN(Date.parse(g))?e:g}function asArray(e){return null==e?[]:Array.isArray(e)?e.map(e=>Buffer.isBuffer(e)?e.toString():String(e)):Buffer.isBuffer(e)?[e.toString()]:[String(e)]}function loadMappingFile(r){if(!r)return;const t=e.readFileSync(r,"utf8");return JSON.parse(t)}function mergeMapping(e,r){if(!r)return e;const t=new Map;for(const r of e.entries)t.set(r.scim,r);for(const e of r.entries)t.set(e.scim,e);return{...e,entries:Array.from(t.values())}}const s="pwdAccountLockedTime",c="000001010000Z",l=/^\d{10,14}(\.\d+)?(Z|[+-]\d{2,4})$/;let warn=e=>{console.warn(`WARNING: ${e}`)};function setLockConfigWarn(e){const r=warn;return warn=e,r}const d=/^([A-Za-z][A-Za-z0-9-]*|\d+(\.\d+)+)$/;function resolveLockConfig(e,r,t={attribute:"--scim-user-lock-attribute",value:"--scim-user-lock-value"}){const i=(e||"").trim()||s;if(!d.test(i))throw new Error(`${t.attribute} must be an LDAP attribute name, got '${i}'`);const a=i.toLowerCase()===s.toLowerCase(),n=(r||"").trim();if(!n){if(a)return{attribute:i,value:c};throw new Error(`${t.attribute} is '${i}', so ${t.value} must say what marks an account locked (for nsAccountLock on 389-ds, 'TRUE'). The default '${c}' only means anything to ${s}.`)}return a||n!==c||warn(`${t.value} is the ppolicy convention '${c}' but ${t.attribute} is '${i}', not ${s}. Unless '${i}' is a GeneralizedTime your directory reads as a lock, every deactivation will answer 200 and read back as active:false while the account keeps binding. Verify a deactivation actually prevents a bind.`),a&&!l.test(n)&&warn(`${t.value} '${n}' is not a GeneralizedTime, and ${s} holds one. The directory will refuse the write, or — where the attribute was redefined locally — store a value it does not read as a lock. The ppolicy convention is '${c}'.`),{attribute:i,value:n}}function readActive(e){if(void 0!==e){if("boolean"==typeof e)return e;if("string"==typeof e){const r=e.trim().toLowerCase();if("true"===r)return!0;if("false"===r)return!1}throw r(`Cannot read ${JSON.stringify(e)} as active`)}}function isLocked(e,r){const t=e[r.lockAttribute];return null!=t&&(Array.isArray(t)?t.length>0:String(t).length>0)}function resolveScimId(e,r){if("rdn"===r.idAttribute)return asString(e[r.rdnAttribute]);return asString(e[r.idAttribute])??asString(e[r.rdnAttribute])}function buildLocation(e,r){if(!r.baseUrl)return;const t="User"===r.resourceType?"Users":"Groups";return`${r.baseUrl.replace(/\/$/,"")}${r.scimPrefix}/${t}/${encodeURIComponent(e)}`}function ldapToScimUser(e,r,t){const a={schemas:[i]},n=resolveScimId(e,t);n&&(a.id=n);for(const t of r.entries){if(t.sub){const r={};for(const[i,a]of Object.entries(t.sub)){const t=asString(e[a]);null!=t&&(r[i]=t)}Object.keys(r).length>0&&(a[t.scim]=r);continue}if(t.ldapPrimary||t.ldapSecondary){const r=[];if(t.ldapPrimary){const i=asString(e[t.ldapPrimary]);null!=i&&r.push({value:i,primary:!0})}if(t.ldapSecondary)for(const i of asArray(e[t.ldapSecondary]))r.push({value:i});r.length>0&&(a[t.scim]=r);continue}if(t.ldap)if("array"===t.multi){const r=asArray(e[t.ldap]);r.length>0&&(a[t.scim]=r)}else{const r=asString(e[t.ldap]);null!=r&&(a[t.scim]=r)}}return a.active=!isLocked(e,t),n&&(a.meta={resourceType:"User",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(n,t)}),a}function scimUserToLdap(e,r,t,i){const a={objectClass:i},n=e.userName||e.id||"";for(const t of r.entries){if(t.readOnly||t.operational)continue;const r=e[t.scim];if(null!=r)if(t.sub&&"object"==typeof r)for(const[e,i]of Object.entries(t.sub)){const t=r[e];"string"==typeof t&&t.length>0&&(a[i]=t)}else{if(t.ldapPrimary||t.ldapSecondary){if(!Array.isArray(r))continue;const e=r,i=e.find(e=>!0===e.primary)||e[0],n=e.filter(e=>e!==i);t.ldapPrimary&&i&&i.value&&(a[t.ldapPrimary]=i.value),t.ldapSecondary&&n.length>0&&(a[t.ldapSecondary]=n.map(e=>e.value).filter(Boolean));continue}t.ldap&&(Array.isArray(r)?a[t.ldap]=r.map(e=>String(e)):"string"!=typeof r&&"number"!=typeof r||(a[t.ldap]=String(r)))}}const o=readActive(e.active);if(!1===o&&(a[t.lockAttribute]=t.lockValue||c),!a.cn&&n&&(a.cn=n),!a.sn){const r=e.name?.familyName||e.displayName||n;r&&(a.sn=r)}return{rdn:n,attributes:a,active:o}}function ldapToScimGroup(e,r,i,a){const n={schemas:[t]},o=resolveScimId(e,i);o&&(n.id=o);for(const t of r.entries){if(!t.ldap)continue;const r=e[t.ldap];if(null!=r)if("array"===t.multi){const e=asArray(r);e.length>0&&(n[t.scim]=e)}else{const e=asString(r);null!=e&&(n[t.scim]=e)}}const s=asArray(e.member),c=[];for(const e of s)if(e)if(a){const r=a(e);r&&c.push(r)}else c.push({value:e,type:"User"});return c.length>0&&(n.members=c),o&&(n.meta={resourceType:"Group",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(o,i)}),n}function scimGroupToLdap(e,r,t){const i={objectClass:t},a=e.displayName||e.id||"";for(const t of r.entries){if(t.readOnly||t.operational)continue;if(!t.ldap)continue;const r=e[t.scim];null!=r&&(Array.isArray(r)?i[t.ldap]=r.map(e=>"string"==typeof e?e:JSON.stringify(e)):"string"!=typeof r&&"number"!=typeof r||(i[t.ldap]=String(r)))}return{rdn:a,attributes:i}}function requiredLdapAttributes(e,r=[]){const t=new Set(["objectClass",...a,...r]);for(const r of e.entries)if(r.ldap&&t.add(r.ldap),r.ldapPrimary&&t.add(r.ldapPrimary),r.ldapSecondary&&t.add(r.ldapSecondary),r.sub)for(const e of Object.values(r.sub))t.add(e);return Array.from(t)}function scimPathToLdapAttribute(e,r){const t=e.split(".")[0],i=e.includes(".")?e.split(".").slice(1).join("."):"";for(const e of r.entries)if(e.scim===t){if(e.sub&&i)return e.sub[i];if((e.ldapPrimary||e.ldapSecondary)&&("value"===i||!i))return e.ldapPrimary||e.ldapSecondary;if(e.ldap)return e.ldap}}export{o as DEFAULT_GROUP_MAPPING,s as DEFAULT_LOCK_ATTRIBUTE,c as DEFAULT_LOCK_VALUE,n as DEFAULT_USER_MAPPING,buildLocation,isLocked,ldapTimeToIso,ldapToScimGroup,ldapToScimUser,loadMappingFile,mergeMapping,readActive,requiredLdapAttributes,resolveLockConfig,resolveScimId,scimGroupToLdap,scimPathToLdapAttribute,scimUserToLdap,setLockConfigWarn,warn,withExternalId};
@@ -1 +1 @@
1
- import{scimInvalidValue as e,scimInvalidPath as t,scimNoTarget as r}from"./errors.js";import{DEFAULT_LOCK_ATTRIBUTE as o,DEFAULT_LOCK_VALUE as n,scimPathToLdapAttribute as a,readActive as i}from"./mapping.js";function asValues(e){if(null==e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString():String(e)).filter(e=>e.length>0)}class WorkingEntry{constructor(e){this.current=e,this.values=new Map,this.touched=new Set}get(e){return this.values.has(e)||this.values.set(e,asValues(this.current[e])),this.values.get(e)}set(e,t){this.touched.add(e),this.values.set(e,t)}append(e,t){const r=this.get(e);this.set(e,[...r,...t.filter(e=>!r.includes(e))])}drop(e,t){if(!t)return this.set(e,[]);this.set(e,this.get(e).filter(e=>!t.includes(e)))}diff(){const e={};for(const t of this.touched){const r=asValues(this.current[t]),o=this.get(t),n=o.filter(e=>!r.includes(e)),a=r.filter(e=>!o.includes(e));if(0!==n.length||0!==a.length)if(0!==o.length)0===a.length&&r.length>0?(e.add||(e.add={}),e.add[t]=1===n.length?n[0]:n):(e.replace||(e.replace={}),e.replace[t]=1===o.length?o[0]:o);else{if(0===r.length)continue;e.delete||(e.delete={}),e.delete[t]=""}}return e}}function normalizeOp(t){const r=t.toLowerCase();if("add"===r||"remove"===r||"replace"===r)return r;throw e(`Unknown patch op '${t}'`)}const s=new Set(["__proto__","constructor","prototype"]),l=/^[A-Za-z_$:][\w$:]*$/;function assertSafeKey(e){if(s.has(e))throw t(`Forbidden attribute name '${e}'`)}function parsePath(e){if("string"!=typeof e||0===e.length||e.length>512)throw t(`Malformed path '${String(e)}'`);let r,o,n;const a=e.indexOf("[");if(a>=0){const i=e.lastIndexOf("]");if(i<=a)throw t(`Malformed path '${e}'`);r=e.slice(0,a),o=e.slice(a+1,i);const s=e.slice(i+1);if(s.length>0){if("."!==s[0])throw t(`Malformed path '${e}'`);n=s.slice(1)}}else{const t=e.indexOf(".");t>=0?(r=e.slice(0,t),n=e.slice(t+1)):r=e}if(!l.test(r))throw t(`Malformed path '${e}'`);if(assertSafeKey(r),null!=n){if(!l.test(n))throw t(`Malformed sub-attribute in '${e}'`);assertSafeKey(n)}return{top:r,filter:o,sub:n}}async function applyOperation(s,l,u){const f=normalizeOp(s.op),p=u.memberAttribute||"member";if(!s.path){if("remove"===f)throw r("PATCH 'remove' requires a path");if(null==s.value||"object"!=typeof s.value||Array.isArray(s.value))throw r("PATCH without path requires an object value");const e=s.value;for(const[t,r]of Object.entries(e))await applyOperation({op:s.op,path:t,value:r},l,u);return}const{top:c,sub:h,filter:d}=parsePath(s.path);if("members"===c){const e=u.resolveMemberRef;if(!e)throw r("Member operations require a member resolver");const asMemberValues=async t=>{const r=Array.isArray(t)?t:[t],o=[];for(const t of r){const r="string"==typeof t?t:"object"==typeof t&&null!=t&&"value"in t?String(t.value):"";if(!r)continue;const n=await e(r);n&&o.push(n)}return o};if("add"===f)return void l.append(p,await asMemberValues(s.value));if("replace"===f){const e=await asMemberValues(s.value);if(0===e.length)return;return void l.set(p,e)}if(!d&&null==s.value)return void l.drop(p);const t=d?/value\s+eq\s+"([^"]+)"/i.exec(d)?.[1]??"":"",o=[...t?await asMemberValues(t):[],...null!=s.value?await asMemberValues(s.value):[]];return void l.drop(p,o)}if("active"===c&&u.supportsActive){if(h||d)throw t(`'active' has no sub-attribute (got '${s.path}')`);const r=u.lockAttribute||o,a="remove"===f||function(t){const r=i(t);if(void 0===r)throw e("PATCH on 'active' requires a value");return r}(s.value);return void(a?l.drop(r):l.set(r,[u.lockValue||n]))}if(d)throw t(`Complex multi-valued filters are only supported on 'members' (got '${s.path}')`);const v=a(h?`${c}.${h}`:c,u.mapping);if(!v)throw t(`Unknown SCIM attribute path '${s.path}'`);if("remove"===f)return void l.drop(v);const y=function(t){if(null!=t)return"string"==typeof t?t:"number"==typeof t||"boolean"==typeof t?String(t):Array.isArray(t)?t.map(t=>{if("string"==typeof t)return t;if("number"==typeof t||"boolean"==typeof t)return String(t);if(t&&"object"==typeof t&&!Array.isArray(t)&&"value"in t&&("string"==typeof t.value||"number"==typeof t.value||"boolean"==typeof t.value))return String(t.value);throw e(`Unsupported array element in PATCH value: ${JSON.stringify(t)}`)}):void 0}(s.value);if(null==y)throw e(`PATCH ${s.op} ${s.path} missing value`);const m=Array.isArray(y)?y:[y];"add"===f?l.append(v,m):l.set(v,m)}async function patchToModifyRequest(t,r){if(!t.Operations||!Array.isArray(t.Operations))throw e("Missing Operations array");const o=new WorkingEntry(r.current);for(const e of t.Operations)await applyOperation(e,o,r);return o.diff()}function applyPatchToResource(e,t){const r={...e};for(const e of t.Operations){const t=normalizeOp(e.op);if(!e.path){if(e.value&&"object"==typeof e.value&&!Array.isArray(e.value))for(const[t,o]of Object.entries(e.value))assertSafeKey(t),r[t]=o;continue}const{top:o,sub:n}=parsePath(e.path);if("remove"!==t)if(n){const t=r[o],a=t&&"object"==typeof t&&!Array.isArray(t)?{...t}:Object.create(null);a[n]=e.value,r[o]=a}else r[o]=e.value;else if(n){const e=r[o];e&&"object"==typeof e&&delete e[n]}else delete r[o]}return r}export{applyPatchToResource,patchToModifyRequest};
1
+ import{extractLdapCode as e}from"../../lib/ldapCodes.js";import{scimInvalidValue as t,ScimError as r,scimInvalidPath as o,scimNoTarget as i}from"./errors.js";import{DEFAULT_LOCK_ATTRIBUTE as n,DEFAULT_LOCK_VALUE as a,scimPathToLdapAttribute as s,readActive as l}from"./mapping.js";function asValues(e){if(null==e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString():String(e)).filter(e=>e.length>0)}class WorkingEntry{constructor(e){this.current=e,this.values=new Map,this.touched=new Set,this.whole=new Set}get(e){return this.values.has(e)||this.values.set(e,asValues(this.current[e])),this.values.get(e)}set(e,t){this.whole.add(e),this.write(e,t)}append(e,t){const r=this.get(e);this.write(e,[...r,...t.filter(e=>!r.includes(e))])}drop(e,t){if(!t)return this.set(e,[]);this.write(e,this.get(e).filter(e=>!t.includes(e)))}write(e,t){this.touched.add(e),this.values.set(e,t)}diff(){const e={};for(const t of this.touched){const r=asValues(this.current[t]),o=this.get(t),i=o.filter(e=>!r.includes(e)),n=r.filter(e=>!o.includes(e));if(0!==i.length||0!==n.length)if(this.whole.has(t))if(0!==o.length)0===n.length&&r.length>0?(e.add||(e.add={}),e.add[t]=1===i.length?i[0]:i):(e.replace||(e.replace={}),e.replace[t]=1===o.length?o[0]:o);else{if(0===r.length)continue;e.delete||(e.delete={}),e.delete[t]=""}else i.length>0&&(e.add||(e.add={}),e.add[t]=1===i.length?i[0]:i),n.length>0&&(e.delete||(e.delete={}),e.delete[t]=1===n.length?n[0]:n)}return e}}function normalizeOp(e){const r=e.toLowerCase();if("add"===r||"remove"===r||"replace"===r)return r;throw t(`Unknown patch op '${e}'`)}const u=new Set(["__proto__","constructor","prototype"]),f=/^[A-Za-z_$:][\w$:]*$/;function assertSafeKey(e){if(u.has(e))throw o(`Forbidden attribute name '${e}'`)}function parsePath(e){if("string"!=typeof e||0===e.length||e.length>512)throw o(`Malformed path '${String(e)}'`);let t,r,i;const n=e.indexOf("[");if(n>=0){const a=e.lastIndexOf("]");if(a<=n)throw o(`Malformed path '${e}'`);t=e.slice(0,n),r=e.slice(n+1,a);const s=e.slice(a+1);if(s.length>0){if("."!==s[0])throw o(`Malformed path '${e}'`);i=s.slice(1)}}else{const r=e.indexOf(".");r>=0?(t=e.slice(0,r),i=e.slice(r+1)):t=e}if(!f.test(t))throw o(`Malformed path '${e}'`);if(assertSafeKey(t),null!=i){if(!f.test(i))throw o(`Malformed sub-attribute in '${e}'`);assertSafeKey(i)}return{top:t,filter:r,sub:i}}async function applyOperation(e,r,u){const f=normalizeOp(e.op),p=u.memberAttribute||"member";if(!e.path){if("remove"===f)throw i("PATCH 'remove' requires a path");if(null==e.value||"object"!=typeof e.value||Array.isArray(e.value))throw i("PATCH without path requires an object value");const t=e.value;for(const[o,i]of Object.entries(t))await applyOperation({op:e.op,path:o,value:i},r,u);return}const{top:c,sub:h,filter:d}=parsePath(e.path);if("members"===c){const t=u.resolveMemberRef;if(!t)throw i("Member operations require a member resolver");const asMemberValues=async e=>{const r=Array.isArray(e)?e:[e],o=[];for(const e of r){const r="string"==typeof e?e:"object"==typeof e&&null!=e&&"value"in e?String(e.value):"";if(!r)continue;const i=await t(r);i&&o.push(i)}return o};if("add"===f)return void r.append(p,await asMemberValues(e.value));if("replace"===f){const t=await asMemberValues(e.value);if(0===t.length)return;return void r.set(p,t)}if(!d&&null==e.value)return void r.drop(p);const o=d?/value\s+eq\s+"([^"]+)"/i.exec(d)?.[1]??"":"",n=[...o?await asMemberValues(o):[],...null!=e.value?await asMemberValues(e.value):[]];return void r.drop(p,n)}if("active"===c&&u.supportsActive){if(h||d)throw o(`'active' has no sub-attribute (got '${e.path}')`);const i=u.lockAttribute||n,s="remove"===f||function(e){const r=l(e);if(void 0===r)throw t("PATCH on 'active' requires a value");return r}(e.value);return void(s?r.drop(i):r.set(i,[u.lockValue||a]))}if(d)throw o(`Complex multi-valued filters are only supported on 'members' (got '${e.path}')`);const y=s(h?`${c}.${h}`:c,u.mapping);if(!y)throw o(`Unknown SCIM attribute path '${e.path}'`);if("remove"===f)return void r.drop(y);const v=function(e){if(null!=e)return"string"==typeof e?e:"number"==typeof e||"boolean"==typeof e?String(e):Array.isArray(e)?e.map(e=>{if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);if(e&&"object"==typeof e&&!Array.isArray(e)&&"value"in e&&("string"==typeof e.value||"number"==typeof e.value||"boolean"==typeof e.value))return String(e.value);throw t(`Unsupported array element in PATCH value: ${JSON.stringify(e)}`)}):void 0}(e.value);if(null==v)throw t(`PATCH ${e.op} ${e.path} missing value`);const w=Array.isArray(v)?v:[v];"add"===f?r.append(y,w):r.set(y,w)}async function patchToModifyRequest(e,r){if(!e.Operations||!Array.isArray(e.Operations))throw t("Missing Operations array");const o=new WorkingEntry(r.current);for(const t of e.Operations)await applyOperation(t,o,r);return o.diff()}async function writePatch(t,o){for(let i=1;;i++)try{return await o()}catch(o){if(16!==e(o))throw o;if(2===i)throw new r(409,`${t} changed while this PATCH was being applied; nothing was written. Retry the request.`)}}function applyPatchToResource(e,t){const r={...e};for(const e of t.Operations){const t=normalizeOp(e.op);if(!e.path){if(e.value&&"object"==typeof e.value&&!Array.isArray(e.value))for(const[t,o]of Object.entries(e.value))assertSafeKey(t),r[t]=o;continue}const{top:o,sub:i}=parsePath(e.path);if("remove"!==t)if(i){const t=r[o],n=t&&"object"==typeof t&&!Array.isArray(t)?{...t}:Object.create(null);n[i]=e.value,r[o]=n}else r[o]=e.value;else if(i){const e=r[o];e&&"object"==typeof e&&delete e[i]}else delete r[o]}return r}export{applyPatchToResource,patchToModifyRequest,writePatch};
@@ -1 +1 @@
1
- import{escapeDnValue as t,launchHooksChained as e,validateDnValue as s,launchHooks as i,isChildOf as r,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as o}from"./types.js";import{resolveLockConfig as c,mergeMapping as n,loadMappingFile as h,DEFAULT_USER_MAPPING as u,requiredLdapAttributes as l,ldapToScimUser as d,scimUserToLdap as p}from"./mapping.js";import{scimFilterToLdap as m}from"./filter.js";import{pagedSearch as f}from"./list.js";import{patchToModifyRequest as b}from"./patch.js";import{scimNotFound as g,ScimError as w,scimInvalidValue as k,scimUniqueness as A}from"./errors.js";import{extractLdapCode as y}from"../../lib/ldapCodes.js";class ScimUsers{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_user_rdn_attribute||"uid",this.objectClass=this.config.scim_user_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=c(this.config.scim_user_lock_attribute||"",this.config.scim_user_lock_value||"");this.lockAttribute=e.attribute,this.lockValue=e.value;const s=this.config.scim_user_mapping||"";this.mapping=n(u,s?h(s):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"User",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:this.lockAttribute,lockValue:this.lockValue}}ldapAttributes(){return l(this.mapping,[this.lockAttribute])}dnForId(e,s){const i=this.baseResolver.userBase(s);return`${this.rdnAttribute}=${t(e)},${i}`}async get(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===y(t))throw g(`User ${e} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw g(`User ${e} not found`);return d(i.searchEntries[0],this.mapping,this.ctx(t))}async list(t,e){const s=this.baseResolver.userBase(t),i=Math.max(1,e.startIndex||1),r=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let a,c=`(${this.objectClass.includes("inetOrgPerson")?"objectClass=inetOrgPerson":`objectClass=${this.objectClass[0]}`})`;if(e.filter){const t=m(e.filter,this.mapping,{lockAttribute:this.lockAttribute,supportsActive:!0});t.idEquals?a=t.idEquals:c=`(&${c}${t.ldapFilter})`}if(a)try{const e=await this.get(t,a);return{schemas:[o],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof w&&404===t.statusCode)return{schemas:[o],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:n,totalResults:h}=await f({directory:this.ldap.forRequest(t),base:s,filter:c,attributes:this.ldapAttributes(),startIndex:i,count:r,maxScanned:this.maxScanned});e.sortBy,e.sortOrder;const u=n.map(e=>d(e,this.mapping,this.ctx(t)));return{schemas:[o],totalResults:h,startIndex:i,itemsPerPage:u.length,Resources:u}}async create(r,a){if(!a.userName)throw k("userName is required");const o=(await e(this.hooks.scimusercreate,[a,r]))[0],{rdn:c,attributes:n}=p(o,this.mapping,this.ctx(r),this.objectClass);if(!c)throw k("userName is required");s(c,this.rdnAttribute),n[this.rdnAttribute]=c;const h=this.baseResolver.userBase(r),u=`${this.rdnAttribute}=${t(c)},${h}`;try{await this.ldap.forRequest(r).add(u,n)}catch(t){if(68===y(t))throw A(`User ${c} already exists`);if(void 0!==n[this.lockAttribute])throw this.lockSchemaError(t);throw t}const l=await this.get(r,c);return i(this.hooks.scimusercreatedone,l),l}async replace(t,s,r){const a=this.dnForId(s,t),o=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},a);if(!o.searchEntries?.length)throw g(`User ${s} not found`);const c=o.searchEntries[0],n=(await e(this.hooks.scimuserupdate,[s,r,t]))[1],{attributes:h,active:u}=p(n,this.mapping,this.ctx(t),this.objectClass),l={replace:{},delete:[]},d=new Set(["objectClass","entryUUID","createTimestamp","modifyTimestamp",this.rdnAttribute,this.lockAttribute]),hasAttrValue=t=>null!=t&&("string"==typeof t?t.length>0:!Array.isArray(t)||t.length>0&&t.some(t=>null!=t));for(const t of this.ldapAttributes())d.has(t)||(null!=h[t]?l.replace[t]=h[t]:hasAttrValue(c[t])&&l.delete.push(t));if(!1===u?l.replace[this.lockAttribute]=this.lockValue:!0===u&&hasAttrValue(c[this.lockAttribute])&&l.delete.push(this.lockAttribute),0===Object.keys(l.replace||{}).length&&delete l.replace,0===l.delete.length&&delete l.delete,l.replace||l.delete)try{await this.ldap.forRequest(t).modify(a,l)}catch(t){if(void 0!==u)throw this.lockSchemaError(t);if(16!==y(t))throw t;throw new w(409,`${a} changed while this PUT was being applied; nothing was written. Retry the request.`)}const m=await this.get(t,s);return i(this.hooks.scimuserupdatedone,s,m),m}async patch(t,e,s){const r=this.dnForId(e,t),a=await this.currentEntry(t,e),o=await b(s,{mapping:this.mapping,lockAttribute:this.lockAttribute,lockValue:this.lockValue,supportsActive:!0,current:a});try{await this.ldap.forRequest(t).modify(r,o)}catch(t){if(function(t,e){if(t.replace&&e in t.replace)return!0;if(t.add&&e in t.add)return!0;const s=t.delete;return Array.isArray(s)?s.includes(e):!(!s||"object"!=typeof s)&&e in s}(o,this.lockAttribute))throw this.lockSchemaError(t);throw t}const c=await this.get(t,e);return i(this.hooks.scimuserupdatedone,e,c),c}lockSchemaError(t){const e=y(t);if(17!==e&&65!==e)return t;const s=t instanceof Error?t.message:String(t);return k(`The directory rejected '${this.lockAttribute}', the attribute backing 'active': ${s}. Point --scim-user-lock-attribute and --scim-user-lock-value at an attribute this directory defines (nsAccountLock / TRUE on 389-ds); the default needs the ppolicy overlay.`)}async currentEntry(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===y(t))throw g(`User ${e} not found`);throw t}if(!i.searchEntries?.length)throw g(`User ${e} not found`);return i.searchEntries[0]}async delete(t,s){await this.get(t,s);const r=(await e(this.hooks.scimuserdelete,[s,t]))[0],a=this.dnForId(r,t);await this.ldap.forRequest(t).delete(a),i(this.hooks.scimuserdeletedone,r)}async resolveRef(e,s){if(!s)return;const i=this.baseResolver.userBase(e);if(s.includes("=")&&s.includes(","))return r(s,i)?s:void 0;const o=`${this.rdnAttribute}=${t(s)},${i}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},o);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==y(t))throw t}try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==y(t))throw t}}get userMapping(){return this.mapping}}export{ScimUsers};
1
+ import{escapeDnValue as t,launchHooksChained as e,validateDnValue as s,launchHooks as i,isChildOf as r,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as o}from"./types.js";import{resolveLockConfig as c,mergeMapping as n,loadMappingFile as h,DEFAULT_USER_MAPPING as u,requiredLdapAttributes as l,ldapToScimUser as d,scimUserToLdap as p}from"./mapping.js";import{scimFilterToLdap as m}from"./filter.js";import{pagedSearch as f}from"./list.js";import{writePatch as b,patchToModifyRequest as g}from"./patch.js";import{scimNotFound as w,ScimError as k,scimInvalidValue as A,scimUniqueness as y}from"./errors.js";import{extractLdapCode as R}from"../../lib/ldapCodes.js";class ScimUsers{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_user_rdn_attribute||"uid",this.objectClass=this.config.scim_user_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=c(this.config.scim_user_lock_attribute||"",this.config.scim_user_lock_value||"");this.lockAttribute=e.attribute,this.lockValue=e.value;const s=this.config.scim_user_mapping||"";this.mapping=n(u,s?h(s):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"User",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:this.lockAttribute,lockValue:this.lockValue}}ldapAttributes(){return l(this.mapping,[this.lockAttribute])}dnForId(e,s){const i=this.baseResolver.userBase(s);return`${this.rdnAttribute}=${t(e)},${i}`}async get(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===R(t))throw w(`User ${e} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw w(`User ${e} not found`);return d(i.searchEntries[0],this.mapping,this.ctx(t))}async list(t,e){const s=this.baseResolver.userBase(t),i=Math.max(1,e.startIndex||1),r=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let a,c=`(${this.objectClass.includes("inetOrgPerson")?"objectClass=inetOrgPerson":`objectClass=${this.objectClass[0]}`})`;if(e.filter){const t=m(e.filter,this.mapping,{lockAttribute:this.lockAttribute,supportsActive:!0});t.idEquals?a=t.idEquals:c=`(&${c}${t.ldapFilter})`}if(a)try{const e=await this.get(t,a);return{schemas:[o],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof k&&404===t.statusCode)return{schemas:[o],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:n,totalResults:h}=await f({directory:this.ldap.forRequest(t),base:s,filter:c,attributes:this.ldapAttributes(),startIndex:i,count:r,maxScanned:this.maxScanned});e.sortBy,e.sortOrder;const u=n.map(e=>d(e,this.mapping,this.ctx(t)));return{schemas:[o],totalResults:h,startIndex:i,itemsPerPage:u.length,Resources:u}}async create(r,a){if(!a.userName)throw A("userName is required");const o=(await e(this.hooks.scimusercreate,[a,r]))[0],{rdn:c,attributes:n}=p(o,this.mapping,this.ctx(r),this.objectClass);if(!c)throw A("userName is required");s(c,this.rdnAttribute),n[this.rdnAttribute]=c;const h=this.baseResolver.userBase(r),u=`${this.rdnAttribute}=${t(c)},${h}`;try{await this.ldap.forRequest(r).add(u,n)}catch(t){if(68===R(t))throw y(`User ${c} already exists`);if(void 0!==n[this.lockAttribute])throw this.lockSchemaError(t);throw t}const l=await this.get(r,c);return i(this.hooks.scimusercreatedone,l),l}async replace(t,s,r){const a=this.dnForId(s,t),o=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},a);if(!o.searchEntries?.length)throw w(`User ${s} not found`);const c=o.searchEntries[0],n=(await e(this.hooks.scimuserupdate,[s,r,t]))[1],{attributes:h,active:u}=p(n,this.mapping,this.ctx(t),this.objectClass),l={replace:{},delete:[]},d=new Set(["objectClass","entryUUID","createTimestamp","modifyTimestamp",this.rdnAttribute,this.lockAttribute]),hasAttrValue=t=>null!=t&&("string"==typeof t?t.length>0:!Array.isArray(t)||t.length>0&&t.some(t=>null!=t));for(const t of this.ldapAttributes())d.has(t)||(null!=h[t]?l.replace[t]=h[t]:hasAttrValue(c[t])&&l.delete.push(t));if(!1===u?l.replace[this.lockAttribute]=this.lockValue:!0===u&&hasAttrValue(c[this.lockAttribute])&&l.delete.push(this.lockAttribute),0===Object.keys(l.replace||{}).length&&delete l.replace,0===l.delete.length&&delete l.delete,l.replace||l.delete)try{await this.ldap.forRequest(t).modify(a,l)}catch(t){if(void 0!==u)throw this.lockSchemaError(t);if(16!==R(t))throw t;throw new k(409,`${a} changed while this PUT was being applied; nothing was written. Retry the request.`)}const m=await this.get(t,s);return i(this.hooks.scimuserupdatedone,s,m),m}async patch(t,e,s){const r=this.dnForId(e,t);await b(r,async()=>{const i=await this.currentEntry(t,e),a=await g(s,{mapping:this.mapping,lockAttribute:this.lockAttribute,lockValue:this.lockValue,supportsActive:!0,current:i});try{await this.ldap.forRequest(t).modify(r,a,{permissive:!0})}catch(t){if(function(t,e){if(t.replace&&e in t.replace)return!0;if(t.add&&e in t.add)return!0;const s=t.delete;return Array.isArray(s)?s.includes(e):!(!s||"object"!=typeof s)&&e in s}(a,this.lockAttribute))throw this.lockSchemaError(t);throw t}});const a=await this.get(t,e);return i(this.hooks.scimuserupdatedone,e,a),a}lockSchemaError(t){const e=R(t);if(17!==e&&65!==e)return t;const s=t instanceof Error?t.message:String(t);return A(`The directory rejected '${this.lockAttribute}', the attribute backing 'active': ${s}. Point --scim-user-lock-attribute and --scim-user-lock-value at an attribute this directory defines (nsAccountLock / TRUE on 389-ds); the default needs the ppolicy overlay.`)}async currentEntry(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===R(t))throw w(`User ${e} not found`);throw t}if(!i.searchEntries?.length)throw w(`User ${e} not found`);return i.searchEntries[0]}async delete(t,s){await this.get(t,s);const r=(await e(this.hooks.scimuserdelete,[s,t]))[0],a=this.dnForId(r,t);await this.ldap.forRequest(t).delete(a),i(this.hooks.scimuserdeletedone,r)}async resolveRef(e,s){if(!s)return;const i=this.baseResolver.userBase(e);if(s.includes("=")&&s.includes(","))return r(s,i)?s:void 0;const o=`${this.rdnAttribute}=${t(s)},${i}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},o);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==R(t))throw t}try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==R(t))throw t}}get userMapping(){return this.mapping}}export{ScimUsers};
@@ -0,0 +1 @@
1
+ import t from"../../abstract/plugin.js";import{NotFoundError as e,BadRequestError as i}from"../../lib/errors.js";import{jsonBody as a}from"../../lib/expressFormatedResponses.js";import{CozyStackProvider as n,ClouderyProvider as r}from"../../lib/instanceProviders.js";import{extractLdapCode as s}from"../../lib/ldapCodes.js";import{rdnValue as o,asyncHandler as c,escapeLdapFilter as d}from"../../lib/utils.js";import{lifecycleAttributes as h,isTombstone as u}from"./lifecycleAttributes.js";const first=t=>{const e=Array.isArray(t)?t[0]:t;return(null==e?"":e.toString())||void 0},l=/(^|\/)twake\/(cozy|cloudery)Provision(\.js)?$/;class TwakeInstances extends t{constructor(t){super(t),this.name="twakeInstances",this.roles=["consistency","api"],this.dependencies={rabbitmq:"core/rabbitmq"},this.queues=new Map,this.hooks={ldapaddrequest:([t,e,i])=>{if(this.patterns.some(e=>e.test(t))||this.isOrganization(t)){const t=[this.fqdnAttribute,this.sentAttribute,this.organizationFqdn].map(t=>t.toLowerCase());for(const i of Object.keys(e))t.includes(i.toLowerCase())&&delete e[i]}return[t,e,i]},ldapaddafter:async([t,e])=>{const i=this.account(t,e);i&&await this.serialized(t,async()=>{try{await this.provide(t,e,i)}catch(e){this.logger.error({plugin:this.name,event:"create",dn:t,error:String(e)})}})},ldapdeletedone:async t=>{const e=this.provider;if(e instanceof n)for(const i of Array.isArray(t)?t:[t]){const t=this.match(i),a=/^uid=/i.test(i)?o(i):"";t&&a&&await this.serialized(i,async()=>{try{await e.destroy(this.id(a,t),a)}catch(t){this.logger.error({plugin:this.name,event:"destroy",dn:i,error:String(t)})}})}}};const e=this.config,i=(e.plugin||[]).find(t=>l.test(t));if(i)throw new Error(`${this.name}: ${i} gives accounts their instance too; load one of them`);this.patterns=(e.twake_instance_dn||[]).map(t=>new RegExp(t,"i"));const a=new Set(["uid"]);for(const t of this.patterns)for(const[,e]of t.source.matchAll(/\(\?<(\w+)>/g))a.add(e);for(const[,t]of(e.twake_instance_id||"{uid}").matchAll(/\{(\w+)\}/g))if(!a.has(t))throw new Error(`${this.name}: --twake-instance-id names {${t}}, which no --twake-instance-dn group captures`);this.lifecycle=h(e),this.fqdnAttribute=e.twake_instance_fqdn_attribute||"twakeWorkspaceUrl",this.sentAttribute=e.twake_instance_sent_attribute||"twakeCreatedEventAt";const s=e.twake_instance_timeout||3e4;if("cozy-stack"===e.twake_instance_provider){if(!e.twake_instance_cozy_url||!e.twake_instance_cozy_domain)throw new Error(`${this.name}: --twake-instance-cozy-url and --twake-instance-cozy-domain are required`);this.provider=new n(e.twake_instance_cozy_url.replace(/\/$/,""),e.twake_instance_cozy_user||"admin",e.twake_instance_cozy_passphrase||"",e.twake_instance_cozy_domain,e.twake_instance_cozy_context||"",e.twake_instance_cozy_apps||"",s),this.locale=e.twake_instance_locale||"fr"}else{if("cloudery"!==e.twake_instance_provider)throw new Error(`${this.name}: --twake-instance-provider must be cloudery or cozy-stack`);if(!e.twake_instance_cloudery_url||!e.twake_instance_cloudery_domain)throw new Error(`${this.name}: --twake-instance-cloudery-url and --twake-instance-cloudery-domain are required`);this.provider=new r(e.twake_instance_cloudery_url.replace(/\/$/,""),e.twake_instance_cloudery_token||"",e.twake_instance_cloudery_domain,e.twake_instance_cloudery_offer||"",s),this.locale=e.twake_instance_locale||"en"}}async api(t){const n=this.requirePlugin("rabbitmq");if(!n||!await n.getRawClient())throw new Error(`${this.name}: RabbitMQ is required and unreachable`);const s=await this.server.ldap.schemaIndex(),o=[this.fqdnAttribute,this.sentAttribute,...this.config.twake_instance_organization_base?[this.organizationDomain,this.organizationFqdn]:[]].filter(t=>t&&!s.getAttributeType(t));if(o.length)throw new Error(`${this.name}: the directory schema defines no ${o.join(", ")}; load a schema that does, or name other attributes`);t.post(`${this.config.api_prefix}/v1/twake/instances/ensure`,c(async(t,n)=>{const r=a(t,n);if(!1===r)return;let s=r?.dn;if(void 0===s&&"string"==typeof r?.mail&&r.mail){const i=await this.findByMail(r.mail,t);if(!i)throw new e(`No account for ${r.mail}`);s=i.dn}if("string"!=typeof s||!s)throw new i("Give the account as dn or mail");n.json({state:await this.ensureInstance(s,t)})})),this.provider instanceof r&&await n.subscribe(this.config.twake_instance_auth_exchange||"auth",this.config.twake_instance_workplace_created_key||"workplace.created",this.config.twake_instance_queue||"workplace.created.ldap-rest",t=>this.onWorkplaceCreated(t))}async ensureInstance(t,a){return this.serialized(t,async()=>{const n=await this.read(t,a);if(!n)throw new e(`No entry at ${t}`);const r=this.account(t,n);if(!r)throw new i(`${t} gets no instance`);if(first(this.value(n,this.sentAttribute)))return"ready";a&&await this.server.ldap.modify(t,{replace:{[this.sentAttribute]:[]}},a);const s=first(this.value(n,this.fqdnAttribute));if(s){const e=s.toLowerCase()===this.provider.address(r)?{fqdn:s,ready:!0}:await this.provider.find(r,s);if(e?.ready)return await this.ready(t,n,r,e.fqdn,a),"ready"}return this.provide(t,n,r,a)})}async ensureOrganization(t){return this.serialized(`organization:${t.id}`,async()=>{const i=await this.findOrganization(t.id);if(!i)throw new e(`No organization ${t.id}`);if(first(this.value(i,this.sentAttribute)))return"ready";const a=this.organizationFqdn&&first(this.value(i,this.organizationFqdn)),n={id:t.id,email:this.organizationMail(t.id,t.domain),publicName:t.name,locale:this.locale,orgId:t.id,orgDomain:t.domain,offer:this.config.twake_instance_cloudery_organization_offer},s=a?{fqdn:a,ready:!0}:await this.provider.find(n);let o=s?.ready&&s.fqdn;return s||(this.provider instanceof r&&await this.provider.createOrganization(t),o=await this.provider.create(n)),o?(await this.organizationReady(i,o),"ready"):"pending"})}async onWorkplaceCreated(t){const{internalEmail:e,workplaceFqdn:i}=t;if(!e||!i)return;const a=t.twakeId&&await this.findOrganization(t.twakeId);if(a){const n=t.twakeId.toLowerCase(),r=first(this.value(a,this.organizationDomain)),s=e.toLowerCase();if(r?this.organizationMail(n,r)===s:s.startsWith(`${n}@`))return this.serialized(`organization:${t.twakeId}`,async()=>{const t=await this.read(a.dn);t&&await this.organizationReady(t,i)})}const n=await this.findByMail(e);if(!n)return void this.logger.debug(`${this.name}: no account here for ${e}`);const r=n.dn;await this.serialized(r,async()=>{const t=await this.read(r),e=t&&this.account(r,t);t&&e&&(first(this.value(t,this.sentAttribute))||await this.ready(r,t,e,i))})}async provide(t,e,i,a){const n=await this.provider.find(i),r=n?n.ready&&n.fqdn:await this.provider.create(i);return r?(await this.ready(t,e,i,r,a),"ready"):"pending"}async ready(t,e,i,a,n){first(this.value(e,this.fqdnAttribute))!==a&&await this.server.ldap.modify(t,{replace:{[this.fqdnAttribute]:a}},n),first(this.value(e,this.sentAttribute))||(await this.announce(this.config.twake_instance_auth_exchange||"auth",this.config.twake_instance_user_created_key||"user.created",{twakeId:i.twakeId,internalEmail:i.email,workplaceFqdn:a,...i.orgId?{organizationId:i.orgId}:{},...i.orgDomain?{domain:i.orgDomain,organizationDomain:i.orgDomain}:{},...i.phone?{mobile:i.phone}:{}}),await this.server.ldap.modify(t,{replace:{[this.sentAttribute]:(new Date).toISOString()}},n))}async organizationReady(t,e){const i=t.dn;if(first(this.value(t,this.sentAttribute)))return;this.organizationFqdn&&!first(this.value(t,this.organizationFqdn))&&await this.server.ldap.modify(i,{replace:{[this.organizationFqdn]:e}});const a=first(this.value(t,this.organizationId)),n=first(this.value(t,this.organizationDomain));await this.announce(this.config.twake_instance_b2b_exchange||"b2b",this.config.twake_instance_organization_created_key||"organization.created",{organizationId:a,workplaceFqdn:e,...n?{domain:n}:{}}),await this.server.ldap.modify(i,{replace:{[this.sentAttribute]:(new Date).toISOString()}})}async serialized(t,e){const i=t.toLowerCase(),a=(this.queues.get(i)??Promise.resolve()).catch(()=>{}).then(e),n=a.catch(()=>{});return this.queues.set(i,n),n.then(()=>{this.queues.get(i)===n&&this.queues.delete(i)}),a}get organizationId(){return this.config.twake_instance_organization_id_attribute||"ou"}get organizationDomain(){return this.config.twake_instance_organization_domain_attribute||"twakeDomain"}get organizationFqdn(){return this.config.twake_instance_organization_fqdn_attribute||""}organizationMail(t,e){return`${t}@${e}`.toLowerCase()}isOrganization(t){const e=this.config.twake_instance_organization_base;if(!e)return!1;return t.slice(t.indexOf(",")+1).toLowerCase()===e.toLowerCase()}async findOrganization(t){const e=this.config.twake_instance_organization_base;if(!e)return;return(await this.server.ldap.search({paged:!1,scope:"one",filter:`(${this.organizationId}=${d(t)})`},e)).searchEntries[0]}async read(t,e){try{return(await this.server.ldap.search({paged:!1,scope:"base"},t,e)).searchEntries[0]}catch(e){const a=s(e);if(32===a)return;if(34===a)throw new i(`Invalid DN: ${t}`);throw e}}async announce(t,e,i){const a=this.requirePlugin("rabbitmq");if(!a||!await a.getRawClient())throw new Error(`${this.name}: RabbitMQ unreachable, ${e} not sent`);await a.publish(t,e,i),this.logger.info({plugin:this.name,event:e,exchange:t})}account(t,e){const i=this.match(t);if(!i)return;if(u(e,this.lifecycle))return;const a=this.config.twake_instance_skip_attribute;if(a&&first(this.value(e,a))?.toLowerCase()===(this.config.twake_instance_skip_value||"").toLowerCase())return;const r=first(this.value(e,"mail")),s=first(this.value(e,"uid"));if(!r||!s)return void this.logger.warn(`${this.name}: ${t} has no mail or uid, no instance`);const o=this.provider instanceof n,c=i.groups?.org||(o?this.config.twake_instance_cozy_org_id:"")||void 0,d=i.groups?.org?r.split("@")[1]:o&&this.config.twake_instance_cozy_org_domain||void 0;return{id:this.id(s,i),...o?{oidc:s}:{},twakeId:s,email:r,publicName:first(this.value(e,"displayName"))||first(this.value(e,"cn"))||s,locale:first(this.value(e,"preferredLanguage"))||this.locale,phone:first(this.value(e,"mobile")),...c?{orgId:c}:{},...d?{orgDomain:d}:{}}}match(t){return this.patterns.map(e=>e.exec(t)).find(t=>null!==t)}id(t,e){const i={uid:t,...e.groups};return(this.config.twake_instance_id||"{uid}").replace(/\{(\w+)\}/g,(t,e)=>i[e]??"").replace(/\./g,"").toLowerCase()}value(t,e){const i=e.toLowerCase(),a=Object.keys(t).find(t=>t.toLowerCase()===i);return a?t[a]:void 0}async findByMail(t,e){return(await this.server.ldap.search({paged:!1,scope:"sub",filter:`(mail=${d(t)})`},this.config.ldap_base,e)).searchEntries.find(t=>this.patterns.some(e=>e.test(t.dn)))}}export{TwakeInstances as default};
@@ -0,0 +1 @@
1
+ import{DEFAULT_LOCK_ATTRIBUTE as e,resolveLockConfig as t}from"../scim/mapping.js";function lifecycleAttributes(r){const l=r.twake_lifecycle_deleted_at_format||"iso8601";if("iso8601"!==l&&"generalizedTime"!==l)throw new Error(`--twake-lifecycle-deleted-at-format must be iso8601 or generalizedTime, got '${l}'`);const o=r.twake_lifecycle_lock_attribute?.trim(),i=r.scim_user_lock_attribute?.trim()||e,a=t(o||i,r.twake_lifecycle_lock_value?.trim()||(o&&o.toLowerCase()!==i.toLowerCase()?"":r.scim_user_lock_value||""),{attribute:"--twake-lifecycle-lock-attribute",value:"--twake-lifecycle-lock-value"});return{role:r.twake_lifecycle_role_attribute||"",lock:a.attribute,lockValue:a.value,deleted:r.twake_lifecycle_deleted_attribute||"",deletedValue:r.twake_lifecycle_deleted_value||"TRUE",deletedAt:r.twake_lifecycle_deleted_at_attribute||"",deletedAtFormat:l,reason:r.twake_lifecycle_reason_attribute||""}}function valueOf(e,t){if(!t)return;if(t in e)return e[t];const r=t.toLowerCase();for(const t of Object.keys(e))if(t.toLowerCase()===r)return e[t]}function first(e){const t=Array.isArray(e)?e[0]:e;if(null==t)return;const r=Buffer.isBuffer(t)?t.toString():String(t);return""===r?void 0:r}function holds(e,t,r){const l=valueOf(e,t);if(void 0===l)return!1;const o=r.toLowerCase();return(Array.isArray(l)?l:[l]).some(e=>String(e).toLowerCase()===o)}const isTombstone=(e,t)=>holds(e,t.deleted,t.deletedValue),isLocked=(e,t)=>holds(e,t.lock,t.lockValue);function formatDeletedAt(e,t){const r=e.toISOString();return"iso8601"===t?r:r.replace(/[-:T]/g,"").replace(/\.\d+Z$/,"Z")}function parseDeletedAt(e,t){if("iso8601"===t){const t=new Date(e);return isNaN(t.getTime())?void 0:t}const r=/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})?(?:[.,](\d+))?Z$/.exec(e);if(r)return new Date(Date.UTC(+r[1],+r[2]-1,+r[3],+r[4],+r[5],+(r[6]||0),Math.round(1e3*+`0.${r[7]||0}`)))}export{first,formatDeletedAt,isLocked,isTombstone,lifecycleAttributes,parseDeletedAt,valueOf};
@@ -0,0 +1 @@
1
+ import{randomUUID as e}from"node:crypto";import t from"node:fs";import r from"../../abstract/plugin.js";import{unescapeDnValue as s,parseDn as i}from"../../lib/utils.js";import{DEFAULT_LOCK_ATTRIBUTE as n,DEFAULT_LOCK_VALUE as o}from"../scim/mapping.js";import{lifecycleAttributes as a,isTombstone as l,isLocked as c,valueOf as h,first as u,parseDeletedAt as f}from"./lifecycleAttributes.js";const d=["created","roleChanged","disabled","enabled","deleted"];const isObject=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function optionalString(e,t,r){if(void 0!==e&&"string"!=typeof e)throw new Error(`${t}: "${r}" must be a string`)}function optionalPayload(e,t,r){if(void 0!==e){if(!isObject(e)||Object.values(e).some(e=>"string"!=typeof e))throw new Error(`${t}: "${r}" must be an object of strings`);return e}}function parseRules(e){if(!e.trim())return[];const r=/^\s*[[{]/.test(e)?e:t.readFileSync(e,"utf8"),s=JSON.parse(r);if(!Array.isArray(s))throw new Error("--twake-lifecycle-rules must be a JSON array of rules");return s.map(e=>{if(!isObject(e))throw new Error("Every lifecycle rule must be an object");if("string"!=typeof e.dn)throw new Error('Every lifecycle rule needs a "dn" pattern');const{dn:t}=e,r=`Lifecycle rule ${t}`;optionalString(e.exchange,r,"exchange");const s=optionalPayload(e.payload,r,"payload");if(void 0!==e.events&&!isObject(e.events))throw new Error(`${r}: "events" must be an object`);const i={};for(const[r,n]of Object.entries(e.events||{})){if(!d.includes(r))throw new Error(`Unknown lifecycle event "${r}"; known: ${d.join(", ")}`);const o=`Lifecycle event "${r}" of ${t}`;i[r]=(Array.isArray(n)?n:[n]).map(t=>{if("string"!=typeof t&&!isObject(t))throw new Error(`${o}: a target is a routing key or an object`);const r="string"==typeof t?{routingKey:t}:t;optionalString(r.routingKey,o,"routingKey"),optionalString(r.exchange,o,"exchange");const i=r.exchange||e.exchange,n=r.routingKey;if(!i||!n)throw new Error(`${o} needs an exchange and a routing key`);return{exchange:i,routingKey:n,payload:optionalPayload(r.payload,o,"payload")||s||{},when:optionalPayload(r.when,o,"when")||{}}})}return{dn:new RegExp(t,"i"),targets:i}})}class TwakeLifecycleEvents extends r{constructor(e){if(super(e),this.name="twakeLifecycleEvents",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange",rabbitmq:"core/rabbitmq"},this.hooks={onLdapEntryChange:(e,t,r,s)=>this.onEntryChange(e,t,r,s),twakedeletionreplay:(e,t,r)=>this.replay(e,t,r)},this.attrs=a(this.config),this.followedOperationalAttributes=[this.attrs.lock],this.rules=parseRules(this.config.twake_lifecycle_rules||""),0===this.rules.length)this.logger.warn(`${this.name}: --twake-lifecycle-rules is empty, nothing will be published`);else if(!this.config.rabbitmq_url)throw new Error(`${this.name}: --twake-lifecycle-rules needs --rabbitmq-url`)}async assertComposition(){if(0!==this.rules.length&&!await(this.requirePlugin("rabbitmq")?.getRawClient()))throw new Error(`${this.name}: RabbitMQ at --rabbitmq-url cannot be reached`)}afterLoad(){const e=this.server.loadedPlugins.scim?.config;if(!e)return;const t=e.scim_user_lock_attribute?.trim()||n,r=e.scim_user_lock_value?.trim()||o,{lock:s,lockValue:i}=this.attrs;t.toLowerCase()===s.toLowerCase()&&r.toLowerCase()===i.toLowerCase()||this.logger.warn(`${this.name}: SCIM locks an account with ${t}: ${r}, these events read ${s}: ${i}, so a SCIM deactivation publishes no disabled`)}async replay(e,t,r){const s=this.match(e);s&&l(t,this.attrs)&&await this.publish(s.rule,"deleted",{before:t,after:t,dn:s.groups,change:r})}match(e){const t=function(e){return i(e).map(e=>{const t=e.indexOf("=");if(-1===t)return e;const r=e.slice(t+1).trim();return`${e.slice(0,t).trim()}=${r.replace(/\\,/g,"\\2C")}`}).join(",")}(e);for(const e of this.rules){const r=e.dn.exec(t);if(!r)continue;const i={};for(const[e,t]of Object.entries(r.groups||{}))void 0!==t&&(i[e]=s(t));return{rule:e,groups:i}}}async onEntryChange(e,t,r,s){const i=this.match(e);if(!i||t&&l(t,this.attrs))return;const{rule:n}=i,o={before:t||{},after:r||t||{},dn:i.groups,change:s};if(!r)return this.publish(n,"deleted",o);if(l(r,this.attrs))return t?this.publish(n,"deleted",o):void 0;if(!t)return await this.publish(n,"created",o),void(c(r,this.attrs)&&await this.publish(n,"disabled",o));this.attrs.role&&!function(e,t){const set=e=>new Set((void 0===e?[]:Array.isArray(e)?e:[e]).map(e=>e.toString().toLowerCase())),r=set(e),s=set(t);return r.size===s.size&&[...r].every(e=>s.has(e))}(h(t,this.attrs.role),h(r,this.attrs.role))&&await this.publish(n,"roleChanged",o);const a=c(r,this.attrs);a!==c(t,this.attrs)&&await this.publish(n,a?"disabled":"enabled",o)}resolve(e,t){if(!e.startsWith("$"))return e;if("$now"===e)return(new Date).toISOString();let r=e.slice(1);const s=r.endsWith("|domain");let i;s&&(r=r.slice(0,-7));let n=r;return r.startsWith("dn.")?(i=t.dn[r.slice(3)],n=""):r.startsWith("context.")?(i=t.change[r.slice(8)],n=""):r.startsWith("previous.")?(n=r.slice(9),i=u(h(t.before,n))):i=u(h(t.after,n)),void 0!==i&&this.attrs.deletedAt&&n.toLowerCase()===this.attrs.deletedAt.toLowerCase()&&(i=f(i,this.attrs.deletedAtFormat)?.toISOString()),s&&(i=i?.split("@")[1]),i}async publish(t,r,s){const i=t.targets[r];if(!i?.length)return;const n=this.requirePlugin("rabbitmq");if(n)for(const t of i){if(!Object.entries(t.when).every(([e,t])=>{const r=this.resolve(e,s);return t.startsWith("!")?r!==t.slice(1):r===t}))continue;const i={};for(const[e,r]of Object.entries(t.payload)){const t=this.resolve(r,s);void 0!==t&&(i[e]=t)}const o=e(),a={plugin:this.name,event:r,exchange:t.exchange,routingKey:t.routingKey,messageId:o};if(await n.getRawClient())try{await n.publish(t.exchange,t.routingKey,i,{messageId:o}),this.logger.info({...a,result:"published"})}catch(e){this.logger.error({...a,result:"error",error:String(e)})}else this.logger.error({...a,result:"no broker"})}}}export{d as LIFECYCLE_EVENTS,TwakeLifecycleEvents as default,parseRules};
@@ -0,0 +1 @@
1
+ import e from"../../abstract/plugin.js";import{BadRequestError as t,NotFoundError as s,ConflictError as r}from"../../lib/errors.js";import{jsonBody as a}from"../../lib/expressFormatedResponses.js";import{changeContext as o}from"../../lib/changeContext.js";import{extractLdapCode as i}from"../../lib/ldapCodes.js";import{escapeLdapFilter as n,asyncHandler as h,launchHooks as l}from"../../lib/utils.js";import{isTombstone as d,lifecycleAttributes as c,first as f,valueOf as m,parseDeletedAt as p,formatDeletedAt as u}from"./lifecycleAttributes.js";const w=/^[A-Za-z0-9_.-]{1,64}$/;class TwakeTombstone extends e{constructor(e){super(e),this.name="twakeTombstone",this.roles=["consistency","api"],this.reasonFor=new Map,this.erasing=new Set,this.hooks={ldapdeleterequest:async([e,t])=>{const s=[];for(const r of Array.isArray(e)?e:[e]){if(!this.erasing.has(r)&&this.matches(r)){const e=await this.read(r);if(e){await this.writeTombstone(r,e,this.reasonOf(r,t),t);continue}}s.push(r)}return[s,t]},ldapaddrequest:async([e,t,s])=>{if(this.isScim(s)&&this.matches(e)){const t=await this.read(e);t&&d(t,this.attrs)&&await this.replace(e,t,s)}return[e,t,s]},ldapsearchrequest:([e,t,s])=>{if(!this.isScim(s))return[e,t,s];const r=t.filter||"(objectClass=*)",a=`(!(${this.attrs.deleted}=${n(this.attrs.deletedValue)}))`;return[e,{...t,filter:`(&${String(r).startsWith("(")?String(r):`(${String(r)})`}${a})`},s]}};const t=this.config;if(this.attrs=c(t),!this.attrs.deleted)throw new Error(`${this.name}: --twake-lifecycle-deleted-attribute is required`);this.patterns=(t.twake_tombstone_dn||[]).map(e=>new RegExp(e,"i")),this.reasons=t.twake_tombstone_reasons||[],this.defaultReason=this.checkReason(t.twake_tombstone_default_reason||"deleted"),this.reasonHeader=(t.twake_tombstone_reason_header||"x-deletion-reason").toLowerCase(),this.clearAttributes=t.twake_tombstone_clear_attributes||[],this.eraseMinAge=t.twake_tombstone_erase_min_age??2592e3,this.groupBases=t.twake_tombstone_group_bases?.length?t.twake_tombstone_group_bases:[t.ldap_group_base||t.ldap_base||""],this.scimPrefix=t.scim_prefix||"/scim/v2",0===this.patterns.length&&this.logger.warn(`${this.name}: --twake-tombstone-dn is empty, every delete stays a delete`)}afterLoad(){for(const e of["ldapdeleterequest","ldapaddrequest"]){const t=this.server.hooks[e],s=this.hooks[e],r=t&&s?t.indexOf(s):-1;r<0||(t.splice(r,1),t.push(s))}}api(e){e.post(`${this.config.api_prefix}/v1/twake/tombstones/erase`,h(async(e,s)=>{const r=a(e,s,"dn");if(r){if("string"!=typeof r.dn)throw new t("Field dn must be a string");await this.erase(r.dn,{force:!0===r.force,req:e}),s.json({success:!0})}}))}async tombstone(e,s,r){if(!this.matches(e))throw new t(`${e} is not an entry that becomes a tombstone`);this.reasonFor.set(e,this.checkReason(s));try{await this.server.ldap.delete(e,r)}finally{this.reasonFor.delete(e)}}async erase(e,{force:t=!1,req:a}={}){const o=await this.read(e);if(!o||!d(o,this.attrs))throw new s(`No tombstone at ${e}`);if(!t){const t=f(m(o,this.attrs.deletedAt)),s=t&&p(t,this.attrs.deletedAtFormat);if(!s)throw new r(`${e} holds no readable deletion date (--twake-lifecycle-deleted-at-attribute); force it to erase now`);if(Date.now()-s.getTime()<1e3*this.eraseMinAge)throw new r(`${e} was deleted less than ${this.eraseMinAge} seconds ago; force it to erase now`)}this.erasing.add(e);try{await this.server.ldap.delete(e,a)}finally{this.erasing.delete(e)}await this.leaveGroups(e),this.logger.info({plugin:this.name,event:"erase",dn:e,force:t})}assertComposition(){if(!this.server.loadedPlugins.trash)return;const e=String(this.config.trash_watched_bases||"").split(",").map(e=>e.trim().toLowerCase()).filter(Boolean),t=this.patterns.filter(t=>{const s=t.source.toLowerCase().replace(/\$$/,"");return 0===e.length||e.some(e=>s===e||s.endsWith(`,${e}`))});if(t.length)throw new Error(`${this.name}: core/ldap/trash watches the branch of ${t.map(e=>e.source).join(", ")}, and would move those entries instead of leaving a tombstone. Leave that branch out of --trash-watched-bases, or load one of the two plugins only`)}async replace(e,t,s){const r=await this.groupsOf(e),a=this.config.group_dummy_user,o=new Set(a?await this.groupsOf(a):[]);await this.erase(e,{force:!0,req:s});const i=s.res;if(!i)return;const n=i.end.bind(i);i.end=(...s)=>{i.end=n;const a=n(...s);return this.restore(e,t,r,o).catch(t=>this.logger.error({plugin:this.name,event:"restore",dn:e,error:String(t)})),a}}async restore(e,t,s,r){if(await this.read(e))return;const a=Object.fromEntries(Object.entries(t).filter(([e,t])=>"dn"!==e&&!(Array.isArray(t)&&0===t.length)));await this.server.ldap.add(e,a);const o=this.config.group_dummy_user;for(const t of s)await this.server.ldap.modify(t,{add:{member:e}}),o&&!r.has(t)&&await this.server.ldap.modify(t,{delete:{member:o}}).catch(e=>{if(16!==i(e))throw e});this.logger.warn({plugin:this.name,event:"restore",dn:e})}matches(e){return this.patterns.some(t=>t.test(e))}isScim(e){const t=e?.originalUrl??e?.url;return"string"==typeof t&&t.startsWith(this.scimPrefix)}checkReason(e){if(!(this.reasons.length?this.reasons.includes(e):w.test(e)))throw new t(`Unknown deletion reason "${e}"${this.reasons.length?`; known: ${this.reasons.join(", ")}`:""}`);return e}reasonOf(e,t){const s=this.reasonFor.get(e);if(s)return s;const r=t?.headers?.[this.reasonHeader],a=Array.isArray(r)?r[0]:r;return a?this.checkReason(a):this.defaultReason}async read(e){try{return(await this.server.ldap.search({paged:!1,scope:"base",attributes:["*",this.attrs.lock]},e)).searchEntries[0]}catch(e){if(32===i(e))return;throw e}}async writeTombstone(e,t,s,r){const{attrs:a}=this;if(d(t,a))return l(this.server.hooks.twakedeletionreplay,e,t,o(r)),void this.logger.info({plugin:this.name,event:"replay",dn:e});const i={[a.deleted]:a.deletedValue,[a.lock]:a.lockValue};a.deletedAt&&(i[a.deletedAt]=u(new Date,a.deletedAtFormat)),a.reason&&(i[a.reason]=s);const n=this.clearAttributes.filter(e=>void 0!==m(t,e));await this.server.ldap.modify(e,n.length?{replace:i,delete:n}:{replace:i}),this.logger.info({plugin:this.name,event:"tombstone",dn:e,reason:s})}async groupsOf(e){const t=[];for(const s of this.groupBases.filter(Boolean))try{const r=await this.server.ldap.search({paged:!1,scope:"sub",filter:`(member=${n(e)})`,attributes:["dn"]},s);t.push(...r.searchEntries.map(e=>e.dn))}catch(e){if(32!==i(e))throw e}return t}async leaveGroups(e){for(const t of await this.groupsOf(e))await this.leaveGroup(t,e)}async leaveGroup(e,t){const s=this.config.group_dummy_user;let r=!1;for(let a=0;;a++)try{return void await this.server.ldap.modify(e,r?{add:{member:s},delete:{member:t}}:{delete:{member:t}})}catch(e){const t=i(e);if(16===t)return;if(a>=2)throw e;if(65===t&&s)r=!0;else{if(20!==t)throw e;r=!1}}}}export{TwakeTombstone as default};
@@ -33,6 +33,8 @@ export default abstract class DmPlugin {
33
33
  followedOperationalAttributes?: string[];
34
34
  api?(app: Express): MaybePromise<void>;
35
35
  getConfigApiData?(): Record<string, unknown> | undefined;
36
+ /** Key of that configuration under `features`; the instance name otherwise */
37
+ configApiKey?: string;
36
38
  /**
37
39
  * Called once every plugin is loaded, before the first request.
38
40
  *
@@ -48,11 +50,12 @@ export default abstract class DmPlugin {
48
50
  * Called once every plugin is loaded, before `afterLoad`: refuse a
49
51
  * configuration this plugin cannot serve.
50
52
  *
51
- * `afterLoad` is an opinion, logged and served anyway. Throwing here stops
52
- * the server from starting — for a setting that would otherwise make the
53
- * plugin answer on behalf of something that is not there.
53
+ * `afterLoad` is an opinion, logged and served anyway. Throwing here, or
54
+ * rejecting, stops the server from starting — for a setting that would
55
+ * otherwise make the plugin answer on behalf of something that is not
56
+ * there, or a service it cannot work without.
54
57
  */
55
- assertComposition?(): void;
58
+ assertComposition?(): MaybePromise<void>;
56
59
  abstract name: string;
57
60
  /**
58
61
  * Constructor