ldap-rest 1.0.0 → 1.1.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.
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{keepRawBody as n}from"../lib/rawBody.js";import a from"../config/args.js";import h from"../lib/ldapActions.js";import{buildLogger as l}from"../logger/winston.js";import{setLogger as u}from"../lib/expressFormatedResponses.js";import{prefixCoversPath as d}from"../lib/auth/base.js";import{assertAuthzComposition as g,assertEnforceable as c,LDAP_JUDGING_HOOKS as p}from"../lib/authz/composition.js";import{errorBody as f,AUTHZ_REFUSED as m}from"../lib/errors.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";import{registerAttributeTypes as w,recordHookOwner as b}from"../lib/utils.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,hookOwner,isAuthzRefusal,isChildOf,isDnInBranch,isDnSyntax,isDummyMemberDn,knownAttributeName,launchHooks,launchHooksChained,movedToTrash,normalizeDn,organizationLink,parseDn,rdnTypes,rdnValue,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";import{runForRequest as $}from"../lib/requestContext.js";import y from"../plugins/priority.json.js";const P=p.filter(e=>"ldapsearchfilter"!==e);class DM{constructor(){this.hooks={},this.loadedPlugins={},this.authenticators=[],this._authState={dispatcherMounted:!1},this._errorMiddlewareSetup=!1,this.bodyParsers=i.Router(),this._composed=!1,this.pluginLoads=new Map,this.authzHooks=new WeakSet,this.config=o(a),this.app=i(),this.app.use(this.bodyParsers),this.app.use(s.json({verify:n})),this.app.use(s.urlencoded({extended:!0,verify:n})),this.logger=l(this.config),this.ldap=new h(this),u(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 y){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 g(this),c(this),await this.learnAttributeTypes(),this._composed=!0,this.afterLoad(),t()}).catch(e=>r(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),this._composed=!0,t())})}async learnAttributeTypes(){if(Object.values(this.loadedPlugins).some(e=>e.roles?.includes("authz")))try{w((await this.ldap.schemaIndex()).schema.attributeTypes)}catch(e){this.logger.warn(`Could not read the directory schema, DNs are compared with the usual attribute type names only: ${String(e)}`)}}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)}registerBodyParser(e,t){this.bodyParsers.use(e,t)}dispatchAuth(e,t,r){let i=-1,s=[];for(const t of this.authenticators)for(const r of t.pathPrefixes)d(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 $(e,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}/`))),o=Object.values(this.loadedPlugins).some(e=>e.roles?.includes("authz"));return s.length>0&&this.logger.warn(`Authentication is restricted to ${t.join(", ")}, so these routes are served without authentication`+(o?" nor authorization":"")+`: ${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=m):403===s&&/\[authz-forbidden\]/.test(e.message)&&(o=m),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(f(e,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;const i=r?void 0:`${this.resolveModule(e)}\0${t??""}`,s=void 0===i?void 0:this.pluginLoads.get(i);if(s)return this.logger.debug(`Plugin ${e} already loaded, skipping`),s;const o=this.importPlugin(e,t,r);if(void 0!==i){this.pluginLoads.set(i,o);const forget=()=>{this.pluginLoads.get(i)===o&&this.pluginLoads.delete(i)};o.then(e=>e||forget(),forget)}return o}importPlugin(e,t,r){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 g(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`);{const i=this.hooks[e];if(t.roles?.includes("authz")&&P.includes(e)){const e=i.findIndex(e=>!this.authzHooks.has(e));i.splice(-1===e?i.length:e,0,r),this.authzHooks.add(r)}else i.push(r);b(r,t.name,e)}}return this.loadedPlugins[t.name]=t,!0}}export{DM,b as recordHookOwner,w as registerAttributeTypes};
1
+ import{fileURLToPath as e}from"url";import{join as t,dirname as r}from"path";import s from"express";import i from"body-parser";import{parseConfig as o}from"../lib/parseConfig.js";import{keepRawBody as n}from"../lib/rawBody.js";import a from"../config/args.js";import h from"../lib/ldapActions.js";import{buildLogger as l}from"../logger/winston.js";import{setLogger as u}from"../lib/expressFormatedResponses.js";import{prefixCoversPath as d}from"../lib/auth/base.js";import{assertAuthzComposition as g,assertEnforceable as p,LDAP_JUDGING_HOOKS as c}from"../lib/authz/composition.js";import{errorBody as f,AUTHZ_REFUSED as m}from"../lib/errors.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";import{registerAttributeTypes as w,recordHookOwner as b}from"../lib/utils.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,hookOwner,isAuthzRefusal,isChildOf,isDnInBranch,isDnSyntax,isDummyMemberDn,knownAttributeName,launchHooks,launchHooksChained,movedToTrash,normalizeDn,organizationLink,parseDn,rdnTypes,rdnValue,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";import{runForRequest as $}from"../lib/requestContext.js";import y from"../plugins/priority.json.js";const P=c.filter(e=>"ldapsearchfilter"!==e);class DM{constructor(){this.hooks={},this.loadedPlugins={},this.authenticators=[],this._authState={dispatcherMounted:!1},this._errorMiddlewareSetup=!1,this.bodyParsers=s.Router(),this._composed=!1,this.pluginLoads=new Map,this.authzHooks=new WeakSet,this.config=o(a),this.app=s(),this.app.disable("x-powered-by"),this.app.use((e,t,r)=>{t.setHeader("X-Content-Type-Options","nosniff"),r()}),this.app.use(this.bodyParsers),this.app.use(i.json({verify:n})),this.app.use(i.urlencoded({extended:!0,verify:n})),this.logger=l(this.config),this.ldap=new h(this),u(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 s=this.logger,i=(async()=>{for(const e of y){const t=this.resolveModule(DM.moduleOf(e)),i=r.filter(e=>this.resolveModule(DM.moduleOf(e))===t);if(0!==i.length){r=r.filter(e=>!i.includes(e));for(const e of i)s.debug(`Loading ${e} in the priority pass`),await this.loadPlugin(e)}}})();e.push(i),e.push(i.then(async()=>{const e=r.map(e=>(s.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 g(this),p(this),await this.learnAttributeTypes(),this._composed=!0,this.afterLoad(),t()}).catch(e=>r(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),this._composed=!0,t())})}async learnAttributeTypes(){if(Object.values(this.loadedPlugins).some(e=>e.roles?.includes("authz")))try{w((await this.ldap.schemaIndex()).schema.attributeTypes)}catch(e){this.logger.warn(`Could not read the directory schema, DNs are compared with the usual attribute type names only: ${String(e)}`)}}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)}registerBodyParser(e,t){this.bodyParsers.use(e,t)}dispatchAuth(e,t,r){let s=-1,i=[];for(const t of this.authenticators)for(const r of t.pathPrefixes)d(r,e.path)&&(r.length>s?(s=r.length,i=[t]):r.length!==s||i.includes(t)||i.push(t));if(s<0&&(i=this.authenticators.filter(e=>0===e.pathPrefixes.length)),0===i.length)return r();const finish=s=>{if(s>=i.length)return $(e,r);const o=i[s];if(!o.afterChain)return finish(s+1);o.afterChain(e,t,()=>finish(s+1))},o=[];e.authenticators=o;const run=r=>{if(r>=i.length)return finish(0);const s=i[r];s.authenticate(e,t,()=>{s.vouchedFor(e)&&o.push(s.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(),s=new Set;for(const e of r||[]){const t=e.route?.path;"string"==typeof t?s.add(t):Array.isArray(t)&&t.forEach(e=>"string"==typeof e&&s.add(e))}const i=[...s].filter(e=>!t.some(t=>e===t||e.startsWith(`${t}/`))),o=Object.values(this.loadedPlugins).some(e=>e.roles?.includes("authz"));return i.length>0&&this.logger.warn(`Authentication is restricted to ${t.join(", ")}, so these routes are served without authentication`+(o?" nor authorization":"")+`: ${i.sort().join(", ")}`),i.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,s)=>{let i="statusCode"in e?e.statusCode:500,o=e.message;if(500===i&&/\[authz-forbidden\]/.test(e.message)?(i=403,o=m):403===i&&/\[authz-forbidden\]/.test(e.message)&&(o=m),i>=400&&i<500)return this.logger.warn(`Client error ${i} in request ${t.method} ${t.path}: ${o}`),void(r.headersSent||r.status(i).json(f(e,o)));this.logger.error(`Server error ${i} in request ${t.method} ${t.path}: ${e.message}`,{stack:e.stack,url:t.url,method:t.method}),r.headersSent||r.status(i).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(s){return s.startsWith("core/")?s.replace("core/",t(r(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js"):s.startsWith(".")?e(new URL(s,import.meta.url)):s}loadPlugin(e){let t,r;if(/:/.test(e)){let s=e.substring(e.indexOf(":")+1);if(e=e.substring(0,e.indexOf(":")),/:/.test(s)){t=s.substring(0,s.indexOf(":")),t||(t=void 0),s=s.substring(s.indexOf(":")+1);try{r=JSON.parse(s),"object"!=typeof r?(this.logger.error(`Overrides for plugin ${e} are not valid: ${s}`),r=void 0):this.logger.debug(`Overrides for plugin ${t||e}: ${s}`)}catch(t){this.logger.error(`Failed to parse overrides for plugin ${e}: ${t}, using ${s}`),r=void 0}}else t=s,t||(t=void 0)}else t=void 0;const s=r?void 0:`${this.resolveModule(e)}\0${t??""}`,i=void 0===s?void 0:this.pluginLoads.get(s);if(i)return this.logger.debug(`Plugin ${e} already loaded, skipping`),i;const o=this.importPlugin(e,t,r);if(void 0!==s){this.pluginLoads.set(s,o);const forget=()=>{this.pluginLoads.get(s)===o&&this.pluginLoads.delete(s)};o.then(e=>e||forget(),forget)}return o}importPlugin(e,t,r){return this.logger.debug(`Loading plugin ${e}`),e.startsWith("core/")&&(e=this.resolveModule(e)),new Promise((s,i)=>{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 i(new Error(`Unable to load ${e}`));s(await this.registerPlugin(e,n,t)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(t=>i(new Error(`Failed to load plugin ${e}: ${t}`)))})}async admitToComposition(e){this.loadedPlugins[e.name]=e;try{await g(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`);{const s=this.hooks[e];if(t.roles?.includes("authz")&&P.includes(e)){const e=s.findIndex(e=>!this.authzHooks.has(e));s.splice(-1===e?s.length:e,0,r),this.authzHooks.add(r)}else s.push(r);b(r,t.name,e)}}return this.loadedPlugins[t.name]=t,!0}}export{DM,b as recordHookOwner,w as registerAttributeTypes};
@@ -1 +1 @@
1
- import{fileURLToPath as e}from"url";import{join as _,dirname as a}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins","identifiers"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains","identifiers"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls","identifiers",!0],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",_(a(e(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuotaSize"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes","identifiers"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes","identifiers"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--organization-schema","DM_ORGANIZATION_SCHEMA",""],["--enterprise-domain-name-attribute","DM_ENTERPRISE_DOMAIN_NAME_ATTRIBUTE","associatedDomain"],["--enterprise-domain-link-attribute","DM_ENTERPRISE_DOMAIN_LINK_ATTRIBUTE",""],["--storage-backend","DM_STORAGE_BACKEND",""],["--storage-sweep-interval","DM_STORAGE_SWEEP_INTERVAL",600,"number"],["--storage-ldap-base","DM_STORAGE_LDAP_BASE",""],["--storage-ldap-object-class","DM_STORAGE_LDAP_OBJECT_CLASS","applicationProcess"],["--storage-file-directory","DM_STORAGE_FILE_DIRECTORY",""],["--storage-postgres-url","DM_STORAGE_POSTGRES_URL",""],["--storage-postgres-table","DM_STORAGE_POSTGRES_TABLE","ldap_rest_storage"],["--storage-valkey-url","DM_STORAGE_VALKEY_URL",""],["--storage-valkey-prefix","DM_STORAGE_VALKEY_PREFIX","ldap-rest:"],["--bcl-retention","DM_BCL_RETENTION",604800,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes","identifiers"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",_(a(e(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes","identifiers"],["--static-path","DM_STATIC_PATH",_(a(e(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-flat-auto-repair","DM_LDAP_FLAT_AUTO_REPAIR",!0,"boolean"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases","dns"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes","identifiers"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-retry-attempts","DM_JAMES_RETRY_ATTEMPTS",5,"number"],["--james-retry-delay","DM_JAMES_RETRY_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches","dns"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--james-sync-forwards","DM_JAMES_SYNC_FORWARDS",!1,"boolean"],["--james-team-mailbox-any-domain","DM_JAMES_TEAM_MAILBOX_ANY_DOMAIN",!1,"boolean"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--twake-lifecycle-role-attribute","DM_TWAKE_LIFECYCLE_ROLE_ATTRIBUTE",""],["--twake-lifecycle-lock-attribute","DM_TWAKE_LIFECYCLE_LOCK_ATTRIBUTE",""],["--twake-lifecycle-lock-value","DM_TWAKE_LIFECYCLE_LOCK_VALUE",""],["--twake-lifecycle-deleted-attribute","DM_TWAKE_LIFECYCLE_DELETED_ATTRIBUTE",""],["--twake-lifecycle-deleted-value","DM_TWAKE_LIFECYCLE_DELETED_VALUE","TRUE"],["--twake-lifecycle-deleted-at-attribute","DM_TWAKE_LIFECYCLE_DELETED_AT_ATTRIBUTE",""],["--twake-lifecycle-deleted-at-format","DM_TWAKE_LIFECYCLE_DELETED_AT_FORMAT","iso8601"],["--twake-lifecycle-reason-attribute","DM_TWAKE_LIFECYCLE_REASON_ATTRIBUTE",""],["--twake-lifecycle-rules","DM_TWAKE_LIFECYCLE_RULES",""],["--twake-lifecycle-member-attribute","DM_TWAKE_LIFECYCLE_MEMBER_ATTRIBUTE","member"],["--twake-group-base","DM_TWAKE_GROUP_BASE",""],["--twake-group-user-base","DM_TWAKE_GROUP_USER_BASE",""],["--twake-group-organization-dn","DM_TWAKE_GROUP_ORGANIZATION_DN",""],["--twake-group-organization-status-attribute","DM_TWAKE_GROUP_ORGANIZATION_STATUS_ATTRIBUTE","twakeOrgStatus"],["--twake-group-organization-deleted-value","DM_TWAKE_GROUP_ORGANIZATION_DELETED_VALUE","deleted"],["--twake-group-display-name-attribute","DM_TWAKE_GROUP_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-group-color-attribute","DM_TWAKE_GROUP_COLOR_ATTRIBUTE","twakeGroupColor"],["--twake-group-created-at-attribute","DM_TWAKE_GROUP_CREATED_AT_ATTRIBUTE","twakeCreatedAt"],["--twake-group-max-page-limit","DM_TWAKE_GROUP_MAX_PAGE_LIMIT",1e3,"number"],["--twake-group-member-fields","DM_TWAKE_GROUP_MEMBER_FIELDS",{},"json"],["--twake-space-base","DM_TWAKE_SPACE_BASE",""],["--twake-space-class","DM_TWAKE_SPACE_CLASS",["top","twakeSpace"],"array",null,"identifiers"],["--twake-space-display-name-attribute","DM_TWAKE_SPACE_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-space-admin-attribute","DM_TWAKE_SPACE_ADMIN_ATTRIBUTE","twakeSpaceAdmin"],["--twake-space-editor-attribute","DM_TWAKE_SPACE_EDITOR_ATTRIBUTE","twakeSpaceEditor"],["--twake-space-viewer-attribute","DM_TWAKE_SPACE_VIEWER_ATTRIBUTE","twakeSpaceViewer"],["--twake-space-user-role-attribute","DM_TWAKE_SPACE_USER_ROLE_ATTRIBUTE",""],["--twake-space-exchange","DM_TWAKE_SPACE_EXCHANGE","space"],["--twake-space-sync-queue","DM_TWAKE_SPACE_SYNC_QUEUE","twake.space.sync.requested.ldap-rest"],["--twake-tombstone-dn","DM_TWAKE_TOMBSTONE_DN",[],"array",null,"dns"],["--twake-tombstone-default-reason","DM_TWAKE_TOMBSTONE_DEFAULT_REASON","deleted"],["--twake-tombstone-reason-header","DM_TWAKE_TOMBSTONE_REASON_HEADER","x-deletion-reason"],["--twake-tombstone-reasons","DM_TWAKE_TOMBSTONE_REASONS",[],"array",null,"identifiers"],["--twake-tombstone-clear-attributes","DM_TWAKE_TOMBSTONE_CLEAR_ATTRIBUTES",[],"array",null,"identifiers"],["--twake-tombstone-erase-min-age","DM_TWAKE_TOMBSTONE_ERASE_MIN_AGE",2592e3,"number"],["--twake-tombstone-group-bases","DM_TWAKE_TOMBSTONE_GROUP_BASES",[],"array",null,"dns"],["--twake-instance-dn","DM_TWAKE_INSTANCE_DN",[],"array",null,"dns"],["--twake-instance-skip-attribute","DM_TWAKE_INSTANCE_SKIP_ATTRIBUTE",""],["--twake-instance-skip-value","DM_TWAKE_INSTANCE_SKIP_VALUE",""],["--twake-instance-provider","DM_TWAKE_INSTANCE_PROVIDER","cloudery"],["--twake-instance-id","DM_TWAKE_INSTANCE_ID","{uid}"],["--twake-instance-fqdn-attribute","DM_TWAKE_INSTANCE_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--twake-instance-sent-attribute","DM_TWAKE_INSTANCE_SENT_ATTRIBUTE","twakeCreatedEventAt"],["--twake-instance-locale","DM_TWAKE_INSTANCE_LOCALE",""],["--twake-instance-timeout","DM_TWAKE_INSTANCE_TIMEOUT",3e4,"number"],["--twake-instance-cloudery-url","DM_TWAKE_INSTANCE_CLOUDERY_URL",""],["--twake-instance-cloudery-token","DM_TWAKE_INSTANCE_CLOUDERY_TOKEN",""],["--twake-instance-cloudery-offer","DM_TWAKE_INSTANCE_CLOUDERY_OFFER","b2b_twake_default"],["--twake-instance-cloudery-domain","DM_TWAKE_INSTANCE_CLOUDERY_DOMAIN",""],["--twake-instance-cloudery-organization-offer","DM_TWAKE_INSTANCE_CLOUDERY_ORGANIZATION_OFFER",""],["--twake-instance-cozy-url","DM_TWAKE_INSTANCE_COZY_URL",""],["--twake-instance-cozy-user","DM_TWAKE_INSTANCE_COZY_USER","admin"],["--twake-instance-cozy-passphrase","DM_TWAKE_INSTANCE_COZY_PASSPHRASE",""],["--twake-instance-cozy-domain","DM_TWAKE_INSTANCE_COZY_DOMAIN",""],["--twake-instance-cozy-context","DM_TWAKE_INSTANCE_COZY_CONTEXT","default"],["--twake-instance-cozy-apps","DM_TWAKE_INSTANCE_COZY_APPS","home,drive,settings,notes,dataproxy"],["--twake-instance-cozy-org-id","DM_TWAKE_INSTANCE_COZY_ORG_ID",""],["--twake-instance-cozy-org-domain","DM_TWAKE_INSTANCE_COZY_ORG_DOMAIN",""],["--twake-instance-auth-exchange","DM_TWAKE_INSTANCE_AUTH_EXCHANGE","auth"],["--twake-instance-b2b-exchange","DM_TWAKE_INSTANCE_B2B_EXCHANGE","b2b"],["--twake-instance-user-created-key","DM_TWAKE_INSTANCE_USER_CREATED_KEY","user.created"],["--twake-instance-organization-created-key","DM_TWAKE_INSTANCE_ORGANIZATION_CREATED_KEY","organization.created"],["--twake-instance-workplace-created-key","DM_TWAKE_INSTANCE_WORKPLACE_CREATED_KEY","workplace.created"],["--twake-instance-queue","DM_TWAKE_INSTANCE_QUEUE","workplace.created.ldap-rest"],["--twake-instance-organization-base","DM_TWAKE_INSTANCE_ORGANIZATION_BASE",""],["--twake-instance-organization-id-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_ID_ATTRIBUTE","ou"],["--twake-instance-organization-domain-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_DOMAIN_ATTRIBUTE","twakeDomain"],["--twake-instance-organization-name-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_NAME_ATTRIBUTE","description"],["--twake-instance-organization-fqdn-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_FQDN_ATTRIBUTE",""],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--app-accounts-user-attribute","DM_APP_ACCOUNTS_USER_ATTRIBUTE",""],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes","identifiers"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes","identifiers"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--llng-username-header","DM_LLNG_USERNAME_HEADER",""],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-fake-user","DM_AUTH_FAKE_USER",""],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps","phrases"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs","phrases"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array",null,"identifiers"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-transit-branch","DM_AUTHZ_TRANSIT_BRANCH",""],["--authz-transit-origin-attribute","DM_AUTHZ_TRANSIT_ORIGIN_ATTRIBUTE",""],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-bypass","DM_AUTHZ_DYNAMIC_BYPASS",[],"array",null,"identifiers"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--authz-global-branch","DM_AUTHZ_GLOBAL_BRANCHES",[],"array","--authz-global-branches","dns"],["--authz-global-write-branch","DM_AUTHZ_GLOBAL_WRITE_BRANCHES",[],"array","--authz-global-write-branches","dns"],["--authz-global-organization","DM_AUTHZ_GLOBAL_ORGANIZATION",""],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-username-claim","DM_OIDC_USERNAME_CLAIM","sub"],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies","identifiers"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-base","DM_PPOLICY_BASE",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes","identifiers"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes","identifiers"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
1
+ import{fileURLToPath as e}from"url";import{join as _,dirname as a}from"path";const t="frame-ancestors 'self'",E="SAMEORIGIN",r=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins","identifiers"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains","identifiers"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls","identifiers",!0],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",_(a(e(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuotaSize"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes","identifiers"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes","identifiers"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--organization-schema","DM_ORGANIZATION_SCHEMA",""],["--enterprise-domain-name-attribute","DM_ENTERPRISE_DOMAIN_NAME_ATTRIBUTE","associatedDomain"],["--enterprise-domain-link-attribute","DM_ENTERPRISE_DOMAIN_LINK_ATTRIBUTE",""],["--storage-backend","DM_STORAGE_BACKEND",""],["--storage-sweep-interval","DM_STORAGE_SWEEP_INTERVAL",600,"number"],["--storage-ldap-base","DM_STORAGE_LDAP_BASE",""],["--storage-ldap-object-class","DM_STORAGE_LDAP_OBJECT_CLASS","applicationProcess"],["--storage-file-directory","DM_STORAGE_FILE_DIRECTORY",""],["--storage-postgres-url","DM_STORAGE_POSTGRES_URL",""],["--storage-postgres-table","DM_STORAGE_POSTGRES_TABLE","ldap_rest_storage"],["--storage-valkey-url","DM_STORAGE_VALKEY_URL",""],["--storage-valkey-prefix","DM_STORAGE_VALKEY_PREFIX","ldap-rest:"],["--bcl-retention","DM_BCL_RETENTION",604800,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes","identifiers"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",_(a(e(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes","identifiers"],["--static-path","DM_STATIC_PATH",_(a(e(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--static-csp","DM_STATIC_CSP",t],["--static-frame-options","DM_STATIC_FRAME_OPTIONS",E],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-flat-auto-repair","DM_LDAP_FLAT_AUTO_REPAIR",!0,"boolean"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases","dns"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes","identifiers"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-retry-attempts","DM_JAMES_RETRY_ATTEMPTS",5,"number"],["--james-retry-delay","DM_JAMES_RETRY_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches","dns"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--james-sync-forwards","DM_JAMES_SYNC_FORWARDS",!1,"boolean"],["--james-team-mailbox-any-domain","DM_JAMES_TEAM_MAILBOX_ANY_DOMAIN",!1,"boolean"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--twake-lifecycle-role-attribute","DM_TWAKE_LIFECYCLE_ROLE_ATTRIBUTE",""],["--twake-lifecycle-lock-attribute","DM_TWAKE_LIFECYCLE_LOCK_ATTRIBUTE",""],["--twake-lifecycle-lock-value","DM_TWAKE_LIFECYCLE_LOCK_VALUE",""],["--twake-lifecycle-deleted-attribute","DM_TWAKE_LIFECYCLE_DELETED_ATTRIBUTE",""],["--twake-lifecycle-deleted-value","DM_TWAKE_LIFECYCLE_DELETED_VALUE","TRUE"],["--twake-lifecycle-deleted-at-attribute","DM_TWAKE_LIFECYCLE_DELETED_AT_ATTRIBUTE",""],["--twake-lifecycle-deleted-at-format","DM_TWAKE_LIFECYCLE_DELETED_AT_FORMAT","iso8601"],["--twake-lifecycle-reason-attribute","DM_TWAKE_LIFECYCLE_REASON_ATTRIBUTE",""],["--twake-lifecycle-rules","DM_TWAKE_LIFECYCLE_RULES",""],["--twake-lifecycle-member-attribute","DM_TWAKE_LIFECYCLE_MEMBER_ATTRIBUTE","member"],["--twake-group-base","DM_TWAKE_GROUP_BASE",""],["--twake-group-user-base","DM_TWAKE_GROUP_USER_BASE",""],["--twake-group-organization-dn","DM_TWAKE_GROUP_ORGANIZATION_DN",""],["--twake-group-organization-status-attribute","DM_TWAKE_GROUP_ORGANIZATION_STATUS_ATTRIBUTE","twakeOrgStatus"],["--twake-group-organization-deleted-value","DM_TWAKE_GROUP_ORGANIZATION_DELETED_VALUE","deleted"],["--twake-group-display-name-attribute","DM_TWAKE_GROUP_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-group-color-attribute","DM_TWAKE_GROUP_COLOR_ATTRIBUTE","twakeGroupColor"],["--twake-group-created-at-attribute","DM_TWAKE_GROUP_CREATED_AT_ATTRIBUTE","twakeCreatedAt"],["--twake-group-max-page-limit","DM_TWAKE_GROUP_MAX_PAGE_LIMIT",1e3,"number"],["--twake-group-member-fields","DM_TWAKE_GROUP_MEMBER_FIELDS",{},"json"],["--twake-space-base","DM_TWAKE_SPACE_BASE",""],["--twake-space-class","DM_TWAKE_SPACE_CLASS",["top","twakeSpace"],"array",null,"identifiers"],["--twake-space-display-name-attribute","DM_TWAKE_SPACE_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-space-admin-attribute","DM_TWAKE_SPACE_ADMIN_ATTRIBUTE","twakeSpaceAdmin"],["--twake-space-editor-attribute","DM_TWAKE_SPACE_EDITOR_ATTRIBUTE","twakeSpaceEditor"],["--twake-space-viewer-attribute","DM_TWAKE_SPACE_VIEWER_ATTRIBUTE","twakeSpaceViewer"],["--twake-space-user-role-attribute","DM_TWAKE_SPACE_USER_ROLE_ATTRIBUTE",""],["--twake-space-exchange","DM_TWAKE_SPACE_EXCHANGE","space"],["--twake-space-sync-queue","DM_TWAKE_SPACE_SYNC_QUEUE","twake.space.sync.requested.ldap-rest"],["--twake-tombstone-dn","DM_TWAKE_TOMBSTONE_DN",[],"array",null,"dns"],["--twake-tombstone-default-reason","DM_TWAKE_TOMBSTONE_DEFAULT_REASON","deleted"],["--twake-tombstone-reason-header","DM_TWAKE_TOMBSTONE_REASON_HEADER","x-deletion-reason"],["--twake-tombstone-reasons","DM_TWAKE_TOMBSTONE_REASONS",[],"array",null,"identifiers"],["--twake-tombstone-clear-attributes","DM_TWAKE_TOMBSTONE_CLEAR_ATTRIBUTES",[],"array",null,"identifiers"],["--twake-tombstone-erase-min-age","DM_TWAKE_TOMBSTONE_ERASE_MIN_AGE",2592e3,"number"],["--twake-tombstone-group-bases","DM_TWAKE_TOMBSTONE_GROUP_BASES",[],"array",null,"dns"],["--twake-instance-dn","DM_TWAKE_INSTANCE_DN",[],"array",null,"dns"],["--twake-instance-skip-attribute","DM_TWAKE_INSTANCE_SKIP_ATTRIBUTE",""],["--twake-instance-skip-value","DM_TWAKE_INSTANCE_SKIP_VALUE",""],["--twake-instance-provider","DM_TWAKE_INSTANCE_PROVIDER","cloudery"],["--twake-instance-id","DM_TWAKE_INSTANCE_ID","{uid}"],["--twake-instance-fqdn-attribute","DM_TWAKE_INSTANCE_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--twake-instance-sent-attribute","DM_TWAKE_INSTANCE_SENT_ATTRIBUTE","twakeCreatedEventAt"],["--twake-instance-locale","DM_TWAKE_INSTANCE_LOCALE",""],["--twake-instance-timeout","DM_TWAKE_INSTANCE_TIMEOUT",3e4,"number"],["--twake-instance-cloudery-url","DM_TWAKE_INSTANCE_CLOUDERY_URL",""],["--twake-instance-cloudery-token","DM_TWAKE_INSTANCE_CLOUDERY_TOKEN",""],["--twake-instance-cloudery-offer","DM_TWAKE_INSTANCE_CLOUDERY_OFFER","b2b_twake_default"],["--twake-instance-cloudery-domain","DM_TWAKE_INSTANCE_CLOUDERY_DOMAIN",""],["--twake-instance-cloudery-organization-offer","DM_TWAKE_INSTANCE_CLOUDERY_ORGANIZATION_OFFER",""],["--twake-instance-cozy-url","DM_TWAKE_INSTANCE_COZY_URL",""],["--twake-instance-cozy-user","DM_TWAKE_INSTANCE_COZY_USER","admin"],["--twake-instance-cozy-passphrase","DM_TWAKE_INSTANCE_COZY_PASSPHRASE",""],["--twake-instance-cozy-domain","DM_TWAKE_INSTANCE_COZY_DOMAIN",""],["--twake-instance-cozy-context","DM_TWAKE_INSTANCE_COZY_CONTEXT","default"],["--twake-instance-cozy-apps","DM_TWAKE_INSTANCE_COZY_APPS","home,drive,settings,notes,dataproxy"],["--twake-instance-cozy-org-id","DM_TWAKE_INSTANCE_COZY_ORG_ID",""],["--twake-instance-cozy-org-domain","DM_TWAKE_INSTANCE_COZY_ORG_DOMAIN",""],["--twake-instance-auth-exchange","DM_TWAKE_INSTANCE_AUTH_EXCHANGE","auth"],["--twake-instance-b2b-exchange","DM_TWAKE_INSTANCE_B2B_EXCHANGE","b2b"],["--twake-instance-user-created-key","DM_TWAKE_INSTANCE_USER_CREATED_KEY","user.created"],["--twake-instance-organization-created-key","DM_TWAKE_INSTANCE_ORGANIZATION_CREATED_KEY","organization.created"],["--twake-instance-workplace-created-key","DM_TWAKE_INSTANCE_WORKPLACE_CREATED_KEY","workplace.created"],["--twake-instance-queue","DM_TWAKE_INSTANCE_QUEUE","workplace.created.ldap-rest"],["--twake-instance-organization-base","DM_TWAKE_INSTANCE_ORGANIZATION_BASE",""],["--twake-instance-organization-id-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_ID_ATTRIBUTE","ou"],["--twake-instance-organization-domain-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_DOMAIN_ATTRIBUTE","twakeDomain"],["--twake-instance-organization-name-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_NAME_ATTRIBUTE","description"],["--twake-instance-organization-fqdn-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_FQDN_ATTRIBUTE",""],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--app-accounts-user-attribute","DM_APP_ACCOUNTS_USER_ATTRIBUTE",""],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes","identifiers"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes","identifiers"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--llng-username-header","DM_LLNG_USERNAME_HEADER",""],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-fake-user","DM_AUTH_FAKE_USER",""],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps","phrases"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs","phrases"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array",null,"identifiers"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-transit-branch","DM_AUTHZ_TRANSIT_BRANCH",""],["--authz-transit-origin-attribute","DM_AUTHZ_TRANSIT_ORIGIN_ATTRIBUTE",""],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-bypass","DM_AUTHZ_DYNAMIC_BYPASS",[],"array",null,"identifiers"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--authz-global-branch","DM_AUTHZ_GLOBAL_BRANCHES",[],"array","--authz-global-branches","dns"],["--authz-global-write-branch","DM_AUTHZ_GLOBAL_WRITE_BRANCHES",[],"array","--authz-global-write-branches","dns"],["--authz-global-organization","DM_AUTHZ_GLOBAL_ORGANIZATION",""],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-username-claim","DM_OIDC_USERNAME_CLAIM","sub"],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies","identifiers"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-base","DM_PPOLICY_BASE",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes","identifiers"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes","identifiers"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as STATIC_CSP,E as STATIC_FRAME_OPTIONS,r as default};
@@ -1 +1 @@
1
- import e from"fs";import{join as t,dirname as r,resolve as s,relative as a,isAbsolute as o}from"path";import{fileURLToPath as i}from"url";import n from"../config/args.js";import{transformSchemas as c}from"./utils.js";const h=t(r(i(import.meta.url)),"..","..","static","schemas"),l="ldap-rest:",isPlainObject=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function mergeSchema(e,t){if(!isPlainObject(t))return t;const r=isPlainObject(e)?{...e}:{};for(const[e,s]of Object.entries(t))null!==s?Object.defineProperty(r,e,{value:mergeSchema(Object.prototype.hasOwnProperty.call(r,e)?r[e]:void 0,s),enumerable:!0,writable:!0,configurable:!0}):delete r[e];return r}function resolveSchemaBase(e,t){if(!t.startsWith(l))return s(r(e),t);const i=s(h,t.slice(10)),n=a(h,i);if(!n||n.startsWith("..")||o(n))throw new Error(`${e}: "${t}" is not a shipped schema`);return i}class SchemaChain{constructor(e){this.options=e,this.files=[],this.realPaths=[],this.layers=[]}readError(e,t){return this.files.length?new Error(`${this.files[this.files.length-1]} extends ${e}, which cannot be read: ${t.message}`):t}add(e,t,r){if(this.realPaths.includes(t))throw new Error(`"extends" loop: ${[...this.files,e].join(" -> ")}`);this.files.push(e),this.realPaths.push(t);const s=this.options.config?c(r,this.options.config):r;let a;try{a=JSON.parse(s)}catch(t){throw new Error(`${e}: ${t.message}`)}if(!isPlainObject(a)||!("extends"in a))return void this.layers.push(a);const{extends:o,...i}=a;if("string"!=typeof o||!o)throw new Error(`${e}: "extends" must be the path of a schema`);return this.layers.push(i),resolveSchemaBase(e,o)}result(){return this.layers.reduceRight((e,t)=>mergeSchema(e,t))}}function loadSchemaFile(t,r={}){const s=new SchemaChain(r);for(let r=t;r;){let t;try{t=e.readFileSync(r,"utf8")}catch(e){throw s.readError(r,e)}r=s.add(r,e.realpathSync(r),t)}return s.result()}async function loadSchemaFileAsync(t,r={}){const s=new SchemaChain(r);for(let r=t;r;){let t;try{t=await e.promises.readFile(r,"utf8")}catch(e){throw s.readError(r,e)}r=s.add(r,await e.promises.realpath(r),t)}return s.result()}function schemaUrl(e,t){const r=t.indexOf("/schemas/");if(-1!==r)return`/${e.static_name||"static"}${t.substring(r)}`}const f=[["group_schema","--group-schema"],["organization_schema","--organization-schema"],["ldap_flat_schema","--ldap-flat-schema"]];function configuredSchemaFiles(e){const t=[];for(const[r,a]of f){const o=[n.find(e=>e[0]===a)?.[2]].flat().filter(e=>"string"==typeof e&&!!e).map(e=>s(e));for(const a of[e[r]].flat())"string"==typeof a&&a&&!o.includes(s(a))&&t.push(a)}return t}export{l as SHIPPED_SCHEMA_PREFIX,configuredSchemaFiles,loadSchemaFile,loadSchemaFileAsync,mergeSchema,resolveSchemaBase,schemaUrl,h as shippedSchemasPath};
1
+ import e from"fs";import{join as t,dirname as r,resolve as s,relative as a,isAbsolute as o}from"path";import{fileURLToPath as i}from"url";import n from"../config/args.js";import{transformSchemas as c}from"./utils.js";const h=t(r(i(import.meta.url)),"..","..","static","schemas"),l="ldap-rest:",isPlainObject=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function mergeSchema(e,t){if(!isPlainObject(t))return t;const r=isPlainObject(e)?{...e}:{};for(const[e,s]of Object.entries(t))null!==s?Object.defineProperty(r,e,{value:mergeSchema(Object.prototype.hasOwnProperty.call(r,e)?r[e]:void 0,s),enumerable:!0,writable:!0,configurable:!0}):delete r[e];return r}function resolveSchemaBase(e,t){if(!t.startsWith(l))return s(r(e),t);const i=s(h,t.slice(10)),n=a(h,i);if(!n||n.startsWith("..")||o(n))throw new Error(`${e}: "${t}" is not a shipped schema`);return i}class SchemaChain{constructor(e){this.options=e,this.files=[],this.realPaths=[],this.layers=[]}readError(e,t){return this.files.length?new Error(`${this.files[this.files.length-1]} extends ${e}, which cannot be read: ${t.message}`):t}add(e,t,r){if(this.realPaths.includes(t))throw new Error(`"extends" loop: ${[...this.files,e].join(" -> ")}`);this.files.push(e),this.realPaths.push(t);const s=this.options.config?c(r,this.options.config):r;let a;try{a=JSON.parse(s)}catch(t){throw new Error(`${e}: ${t.message}`)}if(!isPlainObject(a)||!("extends"in a))return void this.layers.push(a);const{extends:o,...i}=a;if("string"!=typeof o||!o)throw new Error(`${e}: "extends" must be the path of a schema`);return this.layers.push(i),resolveSchemaBase(e,o)}result(){return this.layers.reduceRight((e,t)=>mergeSchema(e,t))}}function loadSchemaFile(t,r={}){const s=new SchemaChain(r);for(let r=t;r;){let t;try{t=e.readFileSync(r,"utf8")}catch(e){throw s.readError(r,e)}r=s.add(r,e.realpathSync(r),t)}return s.result()}async function loadSchemaFileAsync(t,r={}){const s=new SchemaChain(r);for(let r=t;r;){let t;try{t=await e.promises.readFile(r,"utf8")}catch(e){throw s.readError(r,e)}r=s.add(r,await e.promises.realpath(r),t)}return s.result()}function staticName(e){const t=e.static_name,r=("string"==typeof t?t:"").trim();let s=0,a=r.length;for(;s<a&&"/"===r[s];)s++;for(;a>s&&"/"===r[a-1];)a--;return r.slice(s,a)||"static"}function schemaUrl(e,t){const r=t.indexOf("/schemas/");if(-1!==r)return`/${staticName(e)}${t.substring(r)}`}const f=[["group_schema","--group-schema"],["organization_schema","--organization-schema"],["ldap_flat_schema","--ldap-flat-schema"]];function configuredSchemaFiles(e){const t=[];for(const[r,a]of f){const o=[n.find(e=>e[0]===a)?.[2]].flat().filter(e=>"string"==typeof e&&!!e).map(e=>s(e));for(const a of[e[r]].flat())"string"==typeof a&&a&&!o.includes(s(a))&&t.push(a)}return t}export{l as SHIPPED_SCHEMA_PREFIX,configuredSchemaFiles,loadSchemaFile,loadSchemaFileAsync,mergeSchema,resolveSchemaBase,schemaUrl,h as shippedSchemasPath,staticName};
@@ -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,unauthorized as s}from"../../lib/expressFormatedResponses.js";const{auth:n,requiresAuth:r}=e,c=["/login","/logout","/callback","/backchannel-logout"],a=/^end_session_endpoint must be configured/,h=/^\/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,...c]}api(e){this.router=n(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,n=>{if(n&&h.test(e.path)&&this.noEndSession(n))return o.redirect(this.config.base_url);if(n)return t(o,n);if(("GET"===e.method||"HEAD"===e.method)&&h.test(e.path))return void o.oidc.logout({logoutParams:{post_logout_redirect_uri:void 0}});const guard=()=>{r()(e,o,t=>{if(t)return s(o);const n=e.oidc.user,r=this.config.oidc_username_claim||"sub",c=n[r];"sub"!==r&&"string"!=typeof c&&this.logger.warn(`${this.name}: no "${r}" 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(n.sub),"string"==typeof c?c:String(n.sub)),i()})},c=this.renewExpired(e);c?c.then(guard):guard()})}renewExpired(e){const o=e.oidc,i=o.accessToken;if(!i?.isExpired())return;const end=()=>{e.appSession=void 0};if(o.refreshToken)return i.refresh().then(()=>{},e=>{this.logger.info(`${this.name}: the provider refused to renew an access token, so the session ends: ${String(e)}`),end()});end()}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
+ import{randomUUID as e}from"crypto";import o from"express-openid-connect";import s from"../../lib/auth/base.js";import{launchHooksChained as t}from"../../lib/utils.js";import{serverError as i,unauthorized as n}from"../../lib/expressFormatedResponses.js";const{auth:r,requiresAuth:a}=o,c=["/login","/logout","/callback","/backchannel-logout"],d=/^end_session_endpoint must be configured/,h=/^\/logout\/?$/i,u="dmSession",sessionClaims=e=>{const{oidc:o,appSession:s}=e,t=o?.idTokenClaims;if(!t)return;const i=s?.[u];return"string"==typeof i?{...t,session:i}:t},subjectOf=e=>{try{const o=JSON.parse(Buffer.from(String(e).split(".")[1],"base64url").toString());return"string"==typeof o.sub?o.sub:void 0}catch{return}},walkSubscribers=async(e,o)=>{let s;for(const t of e??[])if(t)try{await t(o)}catch(e){s??(s=e instanceof Error?e:new Error(String(e)))}if(s)throw s};class OpenIDConnect extends s{constructor(e){super(e),this.name="openidconnect",this.configApiKey="openidconnect",this.roles=["auth","configurable"],this.warnedNoEndSession=!1,this.warnedNoRecord=!1,this.warnedUnnamed=!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},afterCallback:async(e,o,s)=>({...s,[u]:await this.sessionId(e,subjectOf(s.id_token))}),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 s=e.appSession;if(s)for(const e of Object.keys(s))delete s[e];throw o}},isLoggedOut:e=>this.loggedOut(e)}}}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,...c]}api(e){this.router=r(this.buildConfig()),super.api(e)}authMethod(e,o,s){if(!this.router)return i(o,new Error(`${this.name}: api() has not built the router`));this.router(e,o,t=>{if(t&&h.test(e.path)&&this.noEndSession(t))return o.redirect(this.config.base_url);if(t)return i(o,t);if(("GET"===e.method||"HEAD"===e.method)&&h.test(e.path))return void this.endHere(e).then(()=>o.oidc.logout({logoutParams:{post_logout_redirect_uri:void 0}}));const guard=()=>{a()(e,o,t=>{if(t)return n(o);const i=e.oidc.user,r=this.config.oidc_username_claim||"sub",a=i[r];"sub"!==r&&"string"!=typeof a&&this.logger.warn(`${this.name}: no "${r}" 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(i.sub),"string"==typeof a?a:String(i.sub)),s()})},r=this.renewExpired(e);r?r.then(guard):guard()})}renewExpired(e){const o=e.oidc,s=o.accessToken;if(!s?.isExpired())return;const end=()=>{e.appSession=void 0};if(o.refreshToken)return s.refresh().then(()=>{},e=>{this.logger.info(`${this.name}: the provider refused to renew an access token, so the session ends: ${String(e)}`),end()});end()}async loggedOut(e){if(!this.server.hooks.oidcsessionvalid)return!1;const o=sessionClaims(e),[,s]=await t(this.server.hooks.oidcsessionvalid,[o,!0]);return!s}async sessionId(o,s){const{oidc:t,appSession:i}=o,n=i?.[u];return"string"==typeof n&&void 0!==s&&t?.isAuthenticated()&&t.idTokenClaims?.sub===s&&!await this.loggedOut(o).catch(()=>!0)?n:e()}async endHere(e){if(!e.oidc?.isAuthenticated())return;const o=sessionClaims(e);if(!o)return;const{iss:s,sid:t,session:i}=o,n=i?{iss:s,session:i}:t?{iss:s,sid:t}:void 0;if(n)if(this.server.hooks.oidclogouttoken)try{await walkSubscribers(this.server.hooks.oidclogouttoken,n)}catch(e){this.logger.error(`${this.name}: /logout could not record the session it ends, so a copy of its cookie stays valid until its tokens expire: ${String(e)}`)}else this.warnedNoRecord||(this.warnedNoRecord=!0,this.logger.warn(`${this.name}: no Back-Channel Logout plugin is loaded, so /logout drops the browser's cookie and nothing else: a copy of it stays valid until its tokens expire. Load core/bcl to end the session here as well`));else this.warnedUnnamed||(this.warnedUnnamed=!0,this.logger.warn(`${this.name}: /logout ended a session nothing names — one established before this version, from a provider that sends no sid — so a copy of its cookie stays valid until its tokens expire`))}noEndSession(e){return!!(e instanceof Error&&d.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 t from"../abstract/plugin.js";const e="bcl";class Bcl extends t{constructor(t){super(t),this.name="bcl",this.dependencies={storage:"core/storage"},this.hooks={oidclogouttoken:async t=>{const s=Bcl.keys(t);if(0===s.length)return void this.logger.warn(`${this.name}: a logout token carried no sid nor sub`);const o=Date.now()+this.retention;await Promise.all(s.map(t=>this.store.set(e,t,"1",o)))},oidclogin:async t=>{await Promise.all(Bcl.keys(t).map(t=>this.store.delete(e,t)))},oidcsessionvalid:async([t,s])=>{if(!s)return[t,s];for(const s of Bcl.keys(t))if(null!==await this.store.get(e,s))return[t,!1];return[t,!0]}},this.retention=1e3*(t.config.bcl_retention||604800)}api(){const t=this.requirePlugin("storage");if(!t)throw new Error("bcl: needs core/storage loaded, and --storage-backend set. Without somewhere to keep what a logout killed, every session would outlive the logout that closed it and nothing would say so.");this.storage=t}get store(){if(!this.storage)throw new Error("bcl: the store was asked for before api() took hold of it. A plugin built outside registerPlugin has to call api() as it does.");return this.storage.store}static keys(t){const e=[];return t.sid&&e.push(`${t.iss}|sid|${t.sid}`),t.sub&&e.push(`${t.iss}|sub|${t.sub}`),e}}export{Bcl as default};
1
+ import s from"../abstract/plugin.js";const t="bcl";class Bcl extends s{constructor(s){super(s),this.name="bcl",this.dependencies={storage:"core/storage"},this.hooks={oidclogouttoken:async s=>{const e=Bcl.keys(s);if(0===e.length)return void this.logger.warn(`${this.name}: a logout token carried no sid nor sub`);const o=Date.now()+this.retention;await Promise.all(e.map(s=>this.store.set(t,s,"1",o)))},oidclogin:async s=>{await Promise.all(Bcl.keys(s).map(s=>this.store.delete(t,s)))},oidcsessionvalid:async([s,e])=>{if(!e)return[s,e];for(const e of Bcl.keys(s))if(null!==await this.store.get(t,e))return[s,!1];return[s,!0]}},this.retention=1e3*(s.config.bcl_retention||604800)}api(){const s=this.requirePlugin("storage");if(!s)throw new Error("bcl: needs core/storage loaded, and --storage-backend set. Without somewhere to keep what a logout killed, every session would outlive the logout that closed it and nothing would say so.");this.storage=s}get store(){if(!this.storage)throw new Error("bcl: the store was asked for before api() took hold of it. A plugin built outside registerPlugin has to call api() as it does.");return this.storage.store}static keys(s){const t=[];return s.sid&&t.push(`${s.iss}|sid|${s.sid}`),s.sub&&t.push(`${s.iss}|sub|${s.sub}`),s.session&&t.push(`${s.iss}|session|${s.session}`),t}}export{Bcl as default};
@@ -1 +1 @@
1
- import{fileURLToPath as s}from"url";import{join as e,dirname as i}from"path";import t from"../../abstract/plugin.js";import{setChangeSource as o}from"../../lib/changeContext.js";import{jsonBodyParser as r}from"../../lib/rawBody.js";import{BaseResolver as a}from"./baseResolver.js";import{ScimUsers as c}from"./users.js";import{ScimGroups as n}from"./groups.js";import{ScimBulk as p}from"./bulk.js";import{ScimDiscovery as m}from"./discovery.js";import{SCIM_CONTENT_TYPE as u,scimAsyncHandler as d,writeScimError as h}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";import{projectResource as l,assertProjection as g,projectList as f}from"./projection.js";function scimBodyParser(s){return r({type:["application/json","application/scim+json"],limit:`${s.scim_bulk_max_payload_size||1048576}b`})}class Scim extends t{constructor(t){super(t),this.name="scim",this.roles=["api","configurable"],this.ldap=t.ldap,this.scimPrefix=this.config.scim_prefix||"/scim/v2",this.baseResolver=new a(this.config),this.users=new c({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,hooks:this.registeredHooks}),this.groups=new n({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,users:this.users,hooks:this.registeredHooks}),this.bulk=new p({config:this.config,logger:this.logger,users:this.users,groups:this.groups,scimPrefix:this.scimPrefix,hooks:this.registeredHooks});const o=this.config.schemas_path||e(i(s(import.meta.url)),"..","..","..","static","schemas");this.discovery=new m({config:this.config,schemaDir:e(o,"scim"),scimPrefix:this.scimPrefix,loadedPlugins:t.loadedPlugins})}scimJson(s,e,i){s.status(e).type(u).json(i)}scimResource(s,e,i,t,o={}){const r=i.id?`${this.scimPrefix}/${t}/${encodeURIComponent(i.id)}`:void 0,a=(this.config.scim_base_url||"")&&i.meta?.location?i.meta.location:r;a&&s.set("Location",a),this.scimJson(s,e,l(i,o))}parseProjection(s){const toCsv=s=>{if("string"!=typeof s)return;const e=s.split(",").map(s=>s.trim()).filter(Boolean);return e.length>0?e:void 0},e={attributes:toCsv(s.query.attributes),excludedAttributes:toCsv(s.query.excludedAttributes)};return g(e),e}readScimBody(s){const e=String(s.headers["content-type"]||"");if(e.startsWith("application/scim+json")||e.startsWith("application/json"))return s.body}api(s){const e=this.scimPrefix;this.server.registerBodyParser(e,scimBodyParser(this.config)),s.use(e,(s,e,i)=>{o(s,"scim"),i()}),s.use(e,d((s,e,i)=>{this.parseProjection(s),i()})),s.get(`${e}/Users`,d(async(s,e)=>{const i=this.parseListQuery(s);g(i);const t=await this.users.list(s,i);this.scimJson(e,200,f(t,i))})),s.get(`${e}/Users/:id`,d(async(s,e)=>{const i=this.parseProjection(s),t=await this.users.get(s,decodeURIComponent(s.params.id));this.scimJson(e,200,l(t,i))})),s.post(`${e}/Users`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.users.create(s,i);this.scimResource(e,201,t,"Users",this.parseProjection(s))})),s.put(`${e}/Users/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.users.replace(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Users",this.parseProjection(s))})),s.patch(`${e}/Users/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.users.patch(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Users",this.parseProjection(s))})),s.delete(`${e}/Users/:id`,d(async(s,e)=>{await this.users.delete(s,decodeURIComponent(s.params.id)),e.status(204).end()})),s.get(`${e}/Groups`,d(async(s,e)=>{const i=this.parseListQuery(s);g(i);const t=await this.groups.list(s,i);this.scimJson(e,200,f(t,i))})),s.get(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.parseProjection(s),t=await this.groups.get(s,decodeURIComponent(s.params.id));this.scimJson(e,200,l(t,i))})),s.post(`${e}/Groups`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.groups.create(s,i);this.scimResource(e,201,t,"Groups",this.parseProjection(s))})),s.put(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.groups.replace(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Groups",this.parseProjection(s))})),s.patch(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.groups.patch(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Groups",this.parseProjection(s))})),s.delete(`${e}/Groups/:id`,d(async(s,e)=>{await this.groups.delete(s,decodeURIComponent(s.params.id)),e.status(204).end()})),s.post(`${e}/Bulk`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=this.config.scim_bulk_max_payload_size||1048576,o=JSON.stringify(i).length;if(o>t)return h(e,413,`Bulk payload too large: ${o} > ${t}`,"invalidValue");const r=await this.bulk.execute(s,i);this.scimJson(e,200,r)})),s.get(`${e}/ServiceProviderConfig`,d((s,e)=>{this.scimJson(e,200,this.discovery.serviceProviderConfig(s))})),s.get(`${e}/ResourceTypes`,d((s,e)=>{const i=this.discovery.resourceTypes(s);this.scimJson(e,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:i.length,Resources:i,startIndex:1,itemsPerPage:i.length})})),s.get(`${e}/ResourceTypes/:name`,d((s,e)=>{const i=this.discovery.resourceType(s.params.name,s);if(!i)return h(e,404,"ResourceType not found");this.scimJson(e,200,i)})),s.get(`${e}/Schemas`,d((s,e)=>{const i=this.discovery.schemas(s);this.scimJson(e,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:i.length,Resources:i,startIndex:1,itemsPerPage:i.length})})),s.get(`${e}/Schemas/:id`,d((s,e)=>{const i=this.discovery.schema(decodeURIComponent(s.params.id),s);if(!i)return h(e,404,"Schema not found");this.scimJson(e,200,i)}))}parseListQuery(s){const e=s.query,toInt=s=>{if("string"!=typeof s)return;const e=parseInt(s,10);return Number.isFinite(e)?e:void 0},toCsv=s=>{if("string"==typeof s)return s.split(",").map(s=>s.trim()).filter(Boolean)},i="string"==typeof e.sortOrder?e.sortOrder:void 0;return{filter:"string"==typeof e.filter?e.filter:void 0,startIndex:toInt(e.startIndex),count:toInt(e.count),attributes:toCsv(e.attributes),excludedAttributes:toCsv(e.excludedAttributes),sortBy:"string"==typeof e.sortBy?e.sortBy:void 0,sortOrder:"ascending"===i||"descending"===i?i:void 0}}getConfigApiData(){const s=this.scimPrefix;let e;if(this.server.loadedPlugins.static){const s=this.config.static_name||"static";e={user:`/${s}/schemas/scim/User.json`,group:`/${s}/schemas/scim/Group.json`,defaultMapping:`/${s}/schemas/scim/default-mapping.json`}}const i=this.users.userMapping,t=this.groups.groupMapping,o=i.entries.map(s=>s.scim).concat(["id","active"]),r=t.entries.map(s=>s.scim).concat(["id"]);return{enabled:!0,version:"2.0",prefix:s,endpoints:{users:`${s}/Users`,user:`${s}/Users/:id`,groups:`${s}/Groups`,group:`${s}/Groups/:id`,bulk:`${s}/Bulk`,serviceProviderConfig:`${s}/ServiceProviderConfig`,resourceTypes:`${s}/ResourceTypes`,schemas:`${s}/Schemas`},schemaUrls:e,capabilities:{patch:!0,bulk:!0,filter:!0,sort:!0,etag:Boolean(this.config.scim_etag),changePassword:!1,maxResults:this.config.scim_max_results||200,maxScanned:this.config.scim_max_scanned||1e4,bulkMaxOperations:this.config.scim_bulk_max_operations||100,bulkMaxPayloadSize:this.config.scim_bulk_max_payload_size||1048576},resourceTypes:[{id:"User",endpoint:`${s}/Users`,schema:"urn:ietf:params:scim:schemas:core:2.0:User",rdnAttribute:this.config.scim_user_rdn_attribute||"uid",objectClass:this.config.scim_user_object_class,filterableAttributes:o,mapping:i.entries},{id:"Group",endpoint:`${s}/Groups`,schema:"urn:ietf:params:scim:schemas:core:2.0:Group",rdnAttribute:this.config.scim_group_rdn_attribute||"cn",objectClass:this.config.scim_group_object_class,filterableAttributes:r,mapping:t.entries}],idStrategy:this.config.scim_id_attribute||"rdn",baseResolution:{userBaseTemplate:this.config.scim_user_base_template||void 0,groupBaseTemplate:this.config.scim_group_base_template||void 0,hasBaseMap:Boolean(this.config.scim_base_map)}}}}export{Scim as default,scimBodyParser};
1
+ import{fileURLToPath as s}from"url";import{join as e,dirname as i}from"path";import t from"../../abstract/plugin.js";import{setChangeSource as o}from"../../lib/changeContext.js";import{jsonBodyParser as r}from"../../lib/rawBody.js";import{BaseResolver as a}from"./baseResolver.js";import{ScimUsers as n}from"./users.js";import{ScimGroups as c}from"./groups.js";import{ScimBulk as p}from"./bulk.js";import{ScimDiscovery as m}from"./discovery.js";import{SCIM_CONTENT_TYPE as u,scimAsyncHandler as d,writeScimError as h}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";import{projectResource as l,assertProjection as g,projectList as f}from"./projection.js";const isStaticFiles=s=>"function"==typeof s.urlOf&&"function"==typeof s.servedPath;function scimBodyParser(s){return r({type:["application/json","application/scim+json"],limit:`${s.scim_bulk_max_payload_size||1048576}b`})}class Scim extends t{constructor(t){super(t),this.name="scim",this.roles=["api","configurable"],this.ldap=t.ldap,this.scimPrefix=this.config.scim_prefix||"/scim/v2",this.baseResolver=new a(this.config),this.users=new n({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,hooks:this.registeredHooks}),this.groups=new c({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,users:this.users,hooks:this.registeredHooks}),this.bulk=new p({config:this.config,logger:this.logger,users:this.users,groups:this.groups,scimPrefix:this.scimPrefix,hooks:this.registeredHooks});const o=this.config.schemas_path||e(i(s(import.meta.url)),"..","..","..","static","schemas");this.scimSchemaDir=e(o,"scim"),this.discovery=new m({config:this.config,schemaDir:this.scimSchemaDir,scimPrefix:this.scimPrefix,loadedPlugins:t.loadedPlugins})}scimJson(s,e,i){s.status(e).type(u).json(i)}scimResource(s,e,i,t,o={}){const r=i.id?`${this.scimPrefix}/${t}/${encodeURIComponent(i.id)}`:void 0,a=(this.config.scim_base_url||"")&&i.meta?.location?i.meta.location:r;a&&s.set("Location",a),this.scimJson(s,e,l(i,o))}parseProjection(s){const toCsv=s=>{if("string"!=typeof s)return;const e=s.split(",").map(s=>s.trim()).filter(Boolean);return e.length>0?e:void 0},e={attributes:toCsv(s.query.attributes),excludedAttributes:toCsv(s.query.excludedAttributes)};return g(e),e}readScimBody(s){const e=String(s.headers["content-type"]||"");if(e.startsWith("application/scim+json")||e.startsWith("application/json"))return s.body}api(s){const e=this.scimPrefix;this.server.registerBodyParser(e,scimBodyParser(this.config)),s.use(e,(s,e,i)=>{o(s,"scim"),i()}),s.use(e,d((s,e,i)=>{this.parseProjection(s),i()})),s.get(`${e}/Users`,d(async(s,e)=>{const i=this.parseListQuery(s);g(i);const t=await this.users.list(s,i);this.scimJson(e,200,f(t,i))})),s.get(`${e}/Users/:id`,d(async(s,e)=>{const i=this.parseProjection(s),t=await this.users.get(s,decodeURIComponent(s.params.id));this.scimJson(e,200,l(t,i))})),s.post(`${e}/Users`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.users.create(s,i);this.scimResource(e,201,t,"Users",this.parseProjection(s))})),s.put(`${e}/Users/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.users.replace(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Users",this.parseProjection(s))})),s.patch(`${e}/Users/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.users.patch(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Users",this.parseProjection(s))})),s.delete(`${e}/Users/:id`,d(async(s,e)=>{await this.users.delete(s,decodeURIComponent(s.params.id)),e.status(204).end()})),s.get(`${e}/Groups`,d(async(s,e)=>{const i=this.parseListQuery(s);g(i);const t=await this.groups.list(s,i);this.scimJson(e,200,f(t,i))})),s.get(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.parseProjection(s),t=await this.groups.get(s,decodeURIComponent(s.params.id));this.scimJson(e,200,l(t,i))})),s.post(`${e}/Groups`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.groups.create(s,i);this.scimResource(e,201,t,"Groups",this.parseProjection(s))})),s.put(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.groups.replace(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Groups",this.parseProjection(s))})),s.patch(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.groups.patch(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Groups",this.parseProjection(s))})),s.delete(`${e}/Groups/:id`,d(async(s,e)=>{await this.groups.delete(s,decodeURIComponent(s.params.id)),e.status(204).end()})),s.post(`${e}/Bulk`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=this.config.scim_bulk_max_payload_size||1048576,o=JSON.stringify(i).length;if(o>t)return h(e,413,`Bulk payload too large: ${o} > ${t}`,"invalidValue");const r=await this.bulk.execute(s,i);this.scimJson(e,200,r)})),s.get(`${e}/ServiceProviderConfig`,d((s,e)=>{this.scimJson(e,200,this.discovery.serviceProviderConfig(s))})),s.get(`${e}/ResourceTypes`,d((s,e)=>{const i=this.discovery.resourceTypes(s);this.scimJson(e,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:i.length,Resources:i,startIndex:1,itemsPerPage:i.length})})),s.get(`${e}/ResourceTypes/:name`,d((s,e)=>{const i=this.discovery.resourceType(s.params.name,s);if(!i)return h(e,404,"ResourceType not found");this.scimJson(e,200,i)})),s.get(`${e}/Schemas`,d((s,e)=>{const i=this.discovery.schemas(s);this.scimJson(e,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:i.length,Resources:i,startIndex:1,itemsPerPage:i.length})})),s.get(`${e}/Schemas/:id`,d((s,e)=>{const i=this.discovery.schema(decodeURIComponent(s.params.id),s);if(!i)return h(e,404,"Schema not found");this.scimJson(e,200,i)}))}parseListQuery(s){const e=s.query,toInt=s=>{if("string"!=typeof s)return;const e=parseInt(s,10);return Number.isFinite(e)?e:void 0},toCsv=s=>{if("string"==typeof s)return s.split(",").map(s=>s.trim()).filter(Boolean)},i="string"==typeof e.sortOrder?e.sortOrder:void 0;return{filter:"string"==typeof e.filter?e.filter:void 0,startIndex:toInt(e.startIndex),count:toInt(e.count),attributes:toCsv(e.attributes),excludedAttributes:toCsv(e.excludedAttributes),sortBy:"string"==typeof e.sortBy?e.sortBy:void 0,sortOrder:"ascending"===i||"descending"===i?i:void 0}}getConfigApiData(){const s=this.scimPrefix,i=["User.json","Group.json","default-mapping.json"].map(s=>e(this.scimSchemaDir,s)),t=new Set,o=[];for(const s of Object.values(this.server.loadedPlugins)){if(!isStaticFiles(s))continue;const e=s.servedPath().toLowerCase();t.has(e)||(t.add(e),o.push(s))}let r;o.sort((s,e)=>Number("static"===e.name)-Number("static"===s.name));for(const s of o){const[e,t,o]=i.map(e=>s.urlOf(e));if(e&&t){r={user:e,group:t,...o?{defaultMapping:o}:{}};break}}const a=this.users.userMapping,n=this.groups.groupMapping,c=a.entries.map(s=>s.scim).concat(["id","active"]),p=n.entries.map(s=>s.scim).concat(["id"]);return{enabled:!0,version:"2.0",prefix:s,endpoints:{users:`${s}/Users`,user:`${s}/Users/:id`,groups:`${s}/Groups`,group:`${s}/Groups/:id`,bulk:`${s}/Bulk`,serviceProviderConfig:`${s}/ServiceProviderConfig`,resourceTypes:`${s}/ResourceTypes`,schemas:`${s}/Schemas`},schemaUrls:r,capabilities:{patch:!0,bulk:!0,filter:!0,sort:!0,etag:Boolean(this.config.scim_etag),changePassword:!1,maxResults:this.config.scim_max_results||200,maxScanned:this.config.scim_max_scanned||1e4,bulkMaxOperations:this.config.scim_bulk_max_operations||100,bulkMaxPayloadSize:this.config.scim_bulk_max_payload_size||1048576},resourceTypes:[{id:"User",endpoint:`${s}/Users`,schema:"urn:ietf:params:scim:schemas:core:2.0:User",rdnAttribute:this.config.scim_user_rdn_attribute||"uid",objectClass:this.config.scim_user_object_class,filterableAttributes:c,mapping:a.entries},{id:"Group",endpoint:`${s}/Groups`,schema:"urn:ietf:params:scim:schemas:core:2.0:Group",rdnAttribute:this.config.scim_group_rdn_attribute||"cn",objectClass:this.config.scim_group_object_class,filterableAttributes:p,mapping:n.entries}],idStrategy:this.config.scim_id_attribute||"rdn",baseResolution:{userBaseTemplate:this.config.scim_user_base_template||void 0,groupBaseTemplate:this.config.scim_group_base_template||void 0,hasBaseMap:Boolean(this.config.scim_base_map)}}}}export{Scim as default,scimBodyParser};
@@ -1 +1 @@
1
- import t from"fs";import{resolve as s,join as e}from"path";import n from"express";import i from"../abstract/plugin.js";import{notFound as a,serverError as r}from"../lib/expressFormatedResponses.js";import{configuredSchemaFiles as c,schemaUrl as o,loadSchemaFileAsync as h}from"../lib/schemaFile.js";class Static extends i{constructor(){super(...arguments),this.name="static",this.roles=["api","configurable"]}api(s){const e=this.config.static_path;if(!e)throw new Error("--static-path is not defined");try{if(!t.statSync(e).isDirectory())throw new Error(`${e} isn't a directory`);t.accessSync(e,t.constants.R_OK)}catch(t){throw new Error(`Bad directory ${e}: ${t}`)}const i=this.schemaServer(e);s.get(`/${this.config.static_name}/schemas/:name`,(t,s,e)=>i([t.params.name],t,s,e)),s.get(`/${this.config.static_name}/schemas/:dir/:name`,(t,s,e)=>i([t.params.dir,t.params.name],t,s,e)),s.get(`/${this.config.static_name}/schemas/*path`,(t,s,e)=>i(t.params.path,t,s,e)),s.use(`/${this.config.static_name}`,n.static(e))}schemaServer(t){const n=this.configuredSchemas(t),i=s(e(t,"schemas"));return(t,a,r,c)=>{const o=n.get((t=>{try{return decodeURIComponent(t)}catch{return t}})(a.path));if(o)return this.sendSchema(o,r);if(!t[t.length-1].endsWith(".json"))return c();if(!t.every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t)))return void r.status(400).send("Invalid schema name");const h=s(e(i,...t));h.startsWith(i+"/")?this.sendSchema(h,r):r.status(403).send("Access denied")}}configuredSchemas(t){const n=new Map;for(const i of c(this.config)){const a=o(this.config,i);if(!a)continue;const r=e(t,a.substring(a.indexOf("/schemas/")));if(s(r)===s(i))continue;const c=n.get(a);c&&s(c)!==s(i)?this.logger.warn(`${i} is not served: ${c} already has its URL ${a}`):n.set(a,i)}return n}sendSchema(t,s){h(t,{config:this.config}).then(t=>s.type("json").send(JSON.stringify(t)),t=>{if(t.code)return a(s,"Schema not found");r(s,t)})}getConfigApiData(){const t=`/${this.config.static_name||"static"}`;return{enabled:!0,staticPath:t,endpoints:{schema:`${t}/schemas/:name`,schemaInSubdir:`${t}/schemas/:dir/:name`,files:`${t}/*`}}}}export{Static as default};
1
+ import t from"fs";import{resolve as e,join as s,relative as r,sep as n,isAbsolute as i}from"path";import{validateHeaderValue as o}from"http";import a from"express";import c from"../abstract/plugin.js";import{STATIC_CSP as h,STATIC_FRAME_OPTIONS as f}from"../config/args.js";import{notFound as m,serverError as p}from"../lib/expressFormatedResponses.js";import{staticName as u,configuredSchemaFiles as d,schemaUrl as g,loadSchemaFileAsync as l}from"../lib/schemaFile.js";const headerValue=(t,e,s,r)=>{if(!1===s)return;if(null!=s&&"string"!=typeof s)throw new Error(`${t}: a text is expected, or false for none`);const n=("string"==typeof s?s:"").replace(/\s+/g," ").trim();if(!n)return r;if("off"!==n.toLowerCase()){try{o(e,n)}catch{throw new Error(`${t}: "${n}" cannot be sent as ${e}`)}return n}};class Static extends c{constructor(){super(...arguments),this.name="static",this.roles=["api","configurable"],this.configured=new Map}api(e){const s=this.config.static_path;if(!s)throw new Error("--static-path is not defined");try{if(!t.statSync(s).isDirectory())throw new Error(`${s} isn't a directory`);t.accessSync(s,t.constants.R_OK)}catch(t){throw new Error(`Bad directory ${s}: ${t}`)}const r=this.config.static_name;if(null!=r&&"string"!=typeof r)throw new Error("--static-name: a text is expected, such as static");const n=u(this.config);if(n.split("/").some(t=>!t||/^\.+$/.test(t)||/[^\x21-\x7e]|[:*?+!()[\]{}\\#%"<>|^`]/.test(t)))throw new Error(`--static-name: "${n}" is not a path to serve under, such as static`);const i=`/${n}`,o=this.schemaServer(s),c=headerValue("--static-csp","Content-Security-Policy",this.config.static_csp,h),m=headerValue("--static-frame-options","X-Frame-Options",this.config.static_frame_options,f);e.use(i,(t,e,s)=>{c&&e.setHeader("Content-Security-Policy",c),m&&e.setHeader("X-Frame-Options",m),s()}),e.get(`${i}/schemas/:name`,(t,e,s)=>o([t.params.name],t,e,s)),e.get(`${i}/schemas/:dir/:name`,(t,e,s)=>o([t.params.dir,t.params.name],t,e,s)),e.get(`${i}/schemas/*path`,(t,e,s)=>o(t.params.path,t,e,s)),e.use(i,a.static(s))}schemaServer(t){const r=this.configured=this.configuredSchemas(t),n=e(s(t,"schemas"));return(t,i,o,a)=>{const c=r.get((t=>{try{return decodeURIComponent(t)}catch{return t}})(i.path));if(c)return this.sendSchema(c,o);if(!t[t.length-1].endsWith(".json"))return a();if(!t.every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t)))return void o.status(400).send("Invalid schema name");const h=e(s(n,...t));h.startsWith(n+"/")?this.sendSchema(h,o):o.status(403).send("Access denied")}}configuredSchemas(t){const r=new Map;for(const n of d(this.config)){const i=g(this.config,n);if(!i)continue;const o=s(t,i.substring(i.indexOf("/schemas/")));if(e(o)===e(n))continue;const a=r.get(i);a&&e(a)!==e(n)?this.logger.warn(`${n} is not served: ${a} already has its URL ${i}`):r.set(i,n)}return r}sendSchema(t,e){l(t,{config:this.config}).then(t=>e.type("json").send(JSON.stringify(t)),t=>{if(t.code)return m(e,"Schema not found");p(e,t)})}urlOf(s){const o=e(s);let a;try{if(!t.statSync(o).isFile())return;a=t.realpathSync(o)}catch{return}const same=s=>{if(e(s)===o)return!0;try{return t.realpathSync(s)===a}catch{return!1}},c=this.servedPath(),url=t=>`${c}/${t.map(encodeURIComponent).join("/")}`;for(const[t,e]of this.configured)if(same(e)){const e=t.slice(c.length+1).split("/");if(e.some(t=>["",".",".."].includes(t)))continue;return url(e)}const h=this.config.static_path;if(!h)return;let f;try{f=t.realpathSync(h)}catch{return}const m=[r(e(h),o),r(f,a)].find(t=>!!t&&".."!==t&&!t.startsWith(`..${n}`)&&!i(t));if(!m)return;const p=m.split(n);return p.some(t=>t.startsWith("."))||"schemas"===p[0].toLowerCase()&&"schemas"!==p[0]&&m.endsWith(".json")||"schemas"===p[0]&&m.endsWith(".json")&&!p.slice(1).every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t))||this.configured.has(`${c}/${p.join("/")}`)?void 0:url(p)}servedPath(){return`/${u(this.config)}`}getConfigApiData(){const t=this.servedPath();return{enabled:!0,staticPath:t,endpoints:{schema:`${t}/schemas/:name`,schemaInSubdir:`${t}/schemas/:dir/:name`,files:`${t}/*`}}}}export{Static as default};
@@ -1,5 +1,8 @@
1
1
  import type { AttributesList } from '../lib/ldapActions';
2
2
  import type { ConfigTemplate } from '../lib/parseConfig';
3
+ /** The headers `core/static` sends unless told otherwise: no framing by another site */
4
+ export declare const STATIC_CSP = "frame-ancestors 'self'";
5
+ export declare const STATIC_FRAME_OPTIONS = "SAMEORIGIN";
3
6
  export interface BranchPermissions {
4
7
  read?: boolean;
5
8
  write?: boolean;
@@ -89,6 +92,8 @@ export interface Config {
89
92
  external_branch_class?: string[];
90
93
  static_path?: string;
91
94
  static_name?: string;
95
+ static_csp?: string;
96
+ static_frame_options?: string;
92
97
  auth_path_prefix?: string | string[];
93
98
  llng_ini?: string;
94
99
  llng_username_header?: string;
@@ -13,13 +13,19 @@ export type ChainedHook<T> = (arg: T) => MaybePromise<T>;
13
13
  /**
14
14
  * What identifies an OpenID Connect session on both sides of a Back-Channel
15
15
  * Logout: the issuer, plus whichever of `sid` and `sub` the provider sends.
16
- * At least one of the two is present — the specification requires it, and
17
- * `express-openid-connect` refuses the token otherwise.
16
+ * At least one of the two is present in a logout token — the specification
17
+ * requires it, and `express-openid-connect` refuses the token otherwise.
18
+ *
19
+ * `session` is the id `core/auth/openidconnect` gave the session at login and
20
+ * keeps in its cookie. `/logout` ends a session by it: a provider that sends
21
+ * no `sid` leaves nothing else naming that one session, and a mark on the
22
+ * `sub` would end every session of that person.
18
23
  */
19
24
  export interface OidcSessionClaims {
20
25
  iss: string;
21
26
  sid?: string;
22
27
  sub?: string;
28
+ session?: string;
23
29
  }
24
30
  export type OidcLogoutToken = OidcSessionClaims;
25
31
  export type VoidHook<T extends unknown[]> = (...args: T) => MaybePromise<void>;
@@ -95,7 +101,10 @@ export interface Hooks {
95
101
  * already given is never turned back into an acceptance.
96
102
  */
97
103
  oidcsessionvalid?: ChainedHook<[OidcSessionClaims, boolean]>;
98
- /** A logout token arrived and verified; subscribers record what it kills. */
104
+ /**
105
+ * A logout token arrived and verified, or `/logout` ended a session here;
106
+ * subscribers record what it kills.
107
+ */
99
108
  oidclogouttoken?: VoidHook<[OidcLogoutToken]>;
100
109
  /**
101
110
  * A session was just established; subscribers forget what would kill it.
@@ -43,6 +43,16 @@ export declare function loadSchemaFile<T = unknown>(file: string, options?: Sche
43
43
  * @returns the parsed schema, merged with its bases
44
44
  */
45
45
  export declare function loadSchemaFileAsync<T = unknown>(file: string, options?: SchemaFileOptions): Promise<T>;
46
+ /**
47
+ * The path the static plugin serves under: --static-name without the
48
+ * slashes around it, and `static` when that leaves nothing, as a compose
49
+ * file's unset `${VAR:-}` gives. The plugin refuses at startup one that is
50
+ * not a path to serve under.
51
+ *
52
+ * @param config server configuration
53
+ * @returns the path, with no slash at either end
54
+ */
55
+ export declare function staticName(config: Config): string;
46
56
  /**
47
57
  * URL the static plugin serves a schema file at: its path from the first
48
58
  * `schemas` directory it is in, under --static-name
@@ -11,6 +11,10 @@ export default class OpenIDConnect extends AuthBase {
11
11
  private router?;
12
12
  /** Set once the missing `end_session_endpoint` has been reported. */
13
13
  private warnedNoEndSession;
14
+ /** Set once a `/logout` with nothing to record it has been reported. */
15
+ private warnedNoRecord;
16
+ /** Set once a `/logout` of a session nothing names has been reported. */
17
+ private warnedUnnamed;
14
18
  constructor(server: DM);
15
19
  /**
16
20
  * What is handed to `express-openid-connect`.
@@ -99,6 +103,49 @@ export default class OpenIDConnect extends AuthBase {
99
103
  * one ends the session. Undefined when there is nothing to wait for.
100
104
  */
101
105
  private renewExpired;
106
+ /**
107
+ * Whether the session in front of a request was ended — by a logout token,
108
+ * or by `/logout`.
109
+ *
110
+ * @param req the request, after the library has read its session
111
+ * @returns true when a subscriber of `oidcsessionvalid` refuses it
112
+ */
113
+ private loggedOut;
114
+ /**
115
+ * The id a session gets at login.
116
+ *
117
+ * The same person logging in again over a live session keeps its id: the
118
+ * library merges the new tokens into that session, and a copy of its
119
+ * cookie taken before would otherwise carry an id the next `/logout` no
120
+ * longer names. Anyone else, or a session already ended — the callback
121
+ * runs before the library asks — gets a new one.
122
+ *
123
+ * @param req the callback request, its session as the cookie brought it
124
+ * @param sub the `sub` of the id token just received
125
+ * @returns the id to keep in the session
126
+ */
127
+ private sessionId;
128
+ /**
129
+ * Record that the session in front of `/logout` is dead, as a logout token
130
+ * naming it would.
131
+ *
132
+ * Dropping the cookie ends the browser's copy only: the cookie holds the
133
+ * whole session, and a copy kept elsewhere — a shared computer, a backup
134
+ * of the browser profile, a stolen cookie — stayed valid until its tokens
135
+ * expired. The provider ends the sessions of the other applications
136
+ * through Back-Channel Logout, but does not tell the one that asked it to.
137
+ *
138
+ * The session is named by the id given at login, or by the provider's
139
+ * `sid` for a session from before that id existed — never by the `sub`,
140
+ * which would end every session of that person.
141
+ *
142
+ * A failure to record is logged and the logout goes on: the caller asked
143
+ * to leave, and refusing would keep the browser's copy alive as well.
144
+ *
145
+ * @param req the request, after the library's router
146
+ * @returns once the subscribers have run; never rejects
147
+ */
148
+ private endHere;
102
149
  /**
103
150
  * Whether `/logout` failed because the provider offers no RP-Initiated
104
151
  * Logout, rather than for a reason worth a 500.
@@ -54,7 +54,8 @@ export default class Bcl extends DmPlugin {
54
54
  * The keys a token kills.
55
55
  *
56
56
  * A provider sends `sid`, or `sub`, or both: `sid` ends one session, `sub`
57
- * ends every session of that person.
57
+ * ends every session of that person. `/logout` sends `session`, the id
58
+ * this server gave the one session it ends.
58
59
  */
59
60
  private static keys;
60
61
  hooks: {
@@ -411,6 +411,8 @@ export default class Scim extends DmPlugin {
411
411
  private readonly groups;
412
412
  private readonly bulk;
413
413
  private readonly discovery;
414
+ /** Where it reads its schema files, which the static plugins may serve */
415
+ private readonly scimSchemaDir;
414
416
  private readonly baseResolver;
415
417
  constructor(server: DM);
416
418
  private scimJson;
@@ -37,6 +37,8 @@ import DmPlugin, { type Role } from '../abstract/plugin';
37
37
  export default class Static extends DmPlugin {
38
38
  name: string;
39
39
  roles: Role[];
40
+ /** The configuration's schema files it serves outside its directory, by URL */
41
+ private configured;
40
42
  api(app: Express): void;
41
43
  /**
42
44
  * The handler of the schema routes
@@ -51,6 +53,20 @@ export default class Static extends DmPlugin {
51
53
  */
52
54
  private configuredSchemas;
53
55
  private sendSchema;
56
+ /**
57
+ * The URL it serves a file at, for another plugin to announce it: under
58
+ * its own name, which a plugin override may have changed, from its
59
+ * directory or among the configuration's schema files it serves. One its
60
+ * routes would not answer with that file is none: a hidden one, which
61
+ * express.static ignores, a schema whose path the schema route refuses,
62
+ * or one whose URL a configured schema holds.
63
+ *
64
+ * @param file the file, as the other plugin reads it
65
+ * @returns its URL, or undefined when this plugin does not serve it
66
+ */
67
+ urlOf(file: string): string | undefined;
68
+ /** The path it serves under, which an earlier plugin may have taken */
69
+ servedPath(): string;
54
70
  /**
55
71
  * Provide configuration for config API
56
72
  */
@@ -662,10 +662,14 @@ See [spaces](plugins/integrations/spaces.md).
662
662
 
663
663
  #### `core/static`
664
664
 
665
- | CLI | Env | Default | Description |
666
- | --------------- | ---------------- | -------- | ---------------------- |
667
- | `--static-path` | `DM_STATIC_PATH` | `static` | Static files directory |
668
- | `--static-name` | `DM_STATIC_NAME` | `static` | URL path prefix |
665
+ | CLI | Env | Default | Description |
666
+ | ------------------------ | ------------------------- | ------------------------ | ------------------------------------------------------ |
667
+ | `--static-path` | `DM_STATIC_PATH` | `static` | Static files directory |
668
+ | `--static-name` | `DM_STATIC_NAME` | `static` | URL path prefix |
669
+ | `--static-csp` | `DM_STATIC_CSP` | `frame-ancestors 'self'` | `Content-Security-Policy` of the files; `off` for none |
670
+ | `--static-frame-options` | `DM_STATIC_FRAME_OPTIONS` | `SAMEORIGIN` | `X-Frame-Options` of the files; `off` for none |
671
+
672
+ See [security headers](plugins/utilities/static.md#security-headers).
669
673
 
670
674
  #### `core/weblogs`
671
675
 
@@ -18,6 +18,29 @@ saying that one `sid`, or every session of one `sub`, is dead. Sessions stay
18
18
  in their cookie, and the storage holds only what died — which is what keeps
19
19
  it small.
20
20
 
21
+ ## The Server's Own `/logout`
22
+
23
+ A provider sends logout tokens to the other applications, not to the one
24
+ that asked it to log out. So `core/auth/openidconnect` records the session
25
+ its own `/logout` ends, as a logout token naming it would: by the id it gave
26
+ that session at login, kept in the cookie, which ends that one session and
27
+ leaves the person's others alone. The same person logging in again over a
28
+ live session keeps its id, so a copy taken before is ended with it.
29
+
30
+ A session from before that id existed is named by the provider's `sid`, when
31
+ it sent one. A provider that sends none (Dex) leaves such a session
32
+ unnamed: its copies keep working until its tokens expire, and the first
33
+ `/logout` of one logs a warning. That lasts until those sessions have
34
+ expired, a week at most.
35
+
36
+ Without `core/bcl`, `/logout` drops the browser's cookie and nothing else,
37
+ and a copy of it keeps working until its tokens expire.
38
+
39
+ A plugin subscribing to `oidclogouttoken` receives `{ iss, session }` from
40
+ `/logout`, with neither `sid` nor `sub`: `session` names one session of this
41
+ server, and is matched against the `session` that `oidcsessionvalid`
42
+ carries.
43
+
21
44
  ## Configuration
22
45
 
23
46
  ```bash
@@ -45,6 +68,11 @@ A tombstone has to outlive the session it kills, or a cookie older than the
45
68
  mark would be honoured again. The default matches `express-openid-connect`'s
46
69
  own seven-day session; shorten it only alongside the session lifetime.
47
70
 
71
+ A copy of a session `/logout` ended is refused, so no longer renewed: it
72
+ dies a day after its last use at the latest, the cookie's rolling lifetime.
73
+ While the store cannot answer, though, a copy is still renewed, up to the
74
+ week; keep the retention at the session lifetime.
75
+
48
76
  Expiry is enforced when the mark is read, not only when the sweeper passes,
49
77
  so a late sweeper never keeps out someone who has since logged in again.
50
78
 
@@ -121,6 +121,14 @@ A provider whose discovery document names no `end_session_endpoint` gets a
121
121
  local logout — the cookie is dropped, the browser goes to `--base-url` — and
122
122
  a warning saying the provider may log the caller back in.
123
123
 
124
+ The session itself ends here only with [`core/bcl`](back-channel-logout.md)
125
+ loaded. The cookie holds the whole session, so dropping it ends the
126
+ browser's copy: a copy kept elsewhere — a shared computer, a backup of the
127
+ browser profile, a stolen cookie — keeps working until its tokens expire.
128
+ With `core/bcl`, `/logout` records the session it ends, named by an id given
129
+ at login, and that copy is refused. Without it, the first `/logout` logs a
130
+ warning saying so.
131
+
124
132
  `GET /v1/config` (with `core/configApi`) publishes the route, so a front-end
125
133
  knows it can offer a sign-out control. The key is `openidconnect` whatever
126
134
  the instance is named:
@@ -731,8 +731,14 @@ Response shape (truncated):
731
731
 
732
732
  Notable fields:
733
733
 
734
- - `schemaUrls` — direct download URLs when `core/static` is loaded (editor UIs
735
- typically fetch them to render attribute pickers).
734
+ - `schemaUrls` — direct download URLs of the schema files SCIM reads
735
+ (`User.json` and `Group.json` under `--schemas-path`, with the
736
+ `default-mapping.json` template beside them when it is there), when a
737
+ `core/static` plugin serves them, under its own `--static-name` (editor
738
+ UIs typically fetch them to render attribute pickers). Among plugins
739
+ serving different paths, the one registered as `static` is preferred; a
740
+ path an earlier plugin already serves is left to that one. Absent when
741
+ none serves them: read it with a guard.
736
742
  - `resourceTypes[].mapping` — the live SCIM↔LDAP mapping used server-side, so
737
743
  UI builders can render filter helpers / attribute selectors without second
738
744
  round-trip.
@@ -11,20 +11,30 @@ The `static` plugin serves static files (HTML, CSS, JS, JSON, images) from a dir
11
11
  ```bash
12
12
  --plugin core/static \
13
13
  --static-path ./static \
14
- --static-prefix /static
14
+ --static-name static
15
15
  ```
16
16
 
17
17
  **Environment Variables:**
18
18
 
19
19
  ```bash
20
20
  DM_STATIC_PATH="./static"
21
- DM_STATIC_PREFIX="/static"
21
+ DM_STATIC_NAME="static"
22
22
  ```
23
23
 
24
24
  ### Parameters
25
25
 
26
26
  - `--static-path`: Directory containing static files (required)
27
- - `--static-prefix`: URL prefix for serving files (default: `/static`)
27
+ - `--static-name`: the path the files are served under, without a slash
28
+ around it (default: `static`, for `/static/…`). Slashes around it are left
29
+ out and an empty value is `static`; a segment that is empty, `.` or `..`,
30
+ or that holds what a URL encodes or Express reads as route syntax (a
31
+ space, a letter outside ASCII, `:`, `*`, `?`, `#`, `%`, brackets, quotes…), stops the server at startup
32
+ - `--static-csp` (`DM_STATIC_CSP`): the `Content-Security-Policy` of the
33
+ files it serves, default `frame-ancestors 'self'`; `off` sends none. See
34
+ [Security headers](#security-headers)
35
+ - `--static-frame-options` (`DM_STATIC_FRAME_OPTIONS`): the
36
+ `X-Frame-Options` of the files it serves, default `SAMEORIGIN`; `off`
37
+ sends none
28
38
 
29
39
  ## How It Works
30
40
 
@@ -105,7 +115,7 @@ Configuration keys are transformed:
105
115
  | `__LDAP_BASE__` | `--ldap-base` | `dc=example,dc=com` |
106
116
  | `__LDAP_TOP_ORGANIZATION__` | `--ldap-top-organization` | `ou=organization,dc=example,dc=com` |
107
117
  | `__API_PREFIX__` | `--api-prefix` | `/api` |
108
- | `__STATIC_PREFIX__` | `--static-prefix` | `/static` |
118
+ | `__STATIC_NAME__` | `--static-name` | `static` |
109
119
 
110
120
  ## Use Cases
111
121
 
@@ -137,17 +147,17 @@ Serve web-based LDAP management interface:
137
147
  ```bash
138
148
  --plugin core/static \
139
149
  --static-path ./static \
140
- --static-prefix /
150
+ --static-name app
141
151
  ```
142
152
 
143
153
  Files:
144
154
 
145
155
  ```
146
156
  ./static/
147
- index.html → http://localhost:8081/
148
- css/app.css → http://localhost:8081/css/app.css
149
- js/ldap-tree.js → http://localhost:8081/js/ldap-tree.js
150
- schemas/users.json → http://localhost:8081/schemas/users.json
157
+ index.html → http://localhost:8081/app/
158
+ css/app.css → http://localhost:8081/app/css/app.css
159
+ js/ldap-tree.js → http://localhost:8081/app/js/ldap-tree.js
160
+ schemas/users.json → http://localhost:8081/app/schemas/users.json
151
161
  ```
152
162
 
153
163
  ### 3. API Documentation
@@ -157,7 +167,7 @@ Serve OpenAPI/Swagger documentation:
157
167
  ```bash
158
168
  --plugin core/static \
159
169
  --static-path ./api-docs \
160
- --static-prefix /docs
170
+ --static-name docs
161
171
  ```
162
172
 
163
173
  ```
@@ -172,7 +182,7 @@ GET /docs/swagger-ui/ → ./api-docs/swagger-ui/index.html
172
182
  ```bash
173
183
  --plugin core/static \
174
184
  --static-path ./public \
175
- --static-prefix /static
185
+ --static-name static
176
186
  ```
177
187
 
178
188
  **Directory structure:**
@@ -198,25 +208,17 @@ http://localhost:8081/static/js/app.js
198
208
 
199
209
  ### Example 2: Root Path Serving
200
210
 
201
- ```bash
202
- --plugin core/static \
203
- --static-path ./web \
204
- --static-prefix /
205
- ```
206
-
207
- Serves files from root:
208
-
209
- ```
210
- http://localhost:8081/index.html
211
- http://localhost:8081/about.html
212
- ```
211
+ Not supported: the files are always served under `/<static-name>/`, and
212
+ the schema routes, which replace the placeholders, are built on that name.
213
+ To show an application at `/`, have the reverse proxy redirect `/` to it,
214
+ for instance to `/static/index.html`.
213
215
 
214
216
  ### Example 3: Schema Deployment
215
217
 
216
218
  ```bash
217
219
  --plugin core/static \
218
220
  --static-path ./static \
219
- --static-prefix /static \
221
+ --static-name static \
220
222
  --ldap-base "dc=example,dc=com"
221
223
  ```
222
224
 
@@ -298,6 +300,43 @@ For restricted access:
298
300
  2. Implement custom middleware
299
301
  3. Use reverse proxy with access controls
300
302
 
303
+ ### Security headers
304
+
305
+ The files served here are applications, an administration console among
306
+ them. So that no other origin frames one signed in (clickjacking), another
307
+ sub-domain or port of the same site included, the responses under the prefix
308
+ carry:
309
+
310
+ ```
311
+ Content-Security-Policy: frame-ancestors 'self'
312
+ X-Frame-Options: SAMEORIGIN
313
+ ```
314
+
315
+ `X-Frame-Options` is for browsers that ignore `frame-ancestors`. A page that
316
+ must be framed by another origin names it:
317
+
318
+ ```bash
319
+ --static-csp "frame-ancestors 'self' https://portal.example.com"
320
+ --static-frame-options off
321
+ ```
322
+
323
+ A full policy can be given the same way, for instance
324
+ `default-src 'self'; frame-ancestors 'none'`, as long as the applications
325
+ served allow it: the policy applies to every file under the prefix. One
326
+ written on several lines, as a YAML block holds it, is sent as one line; a
327
+ value a header cannot hold stops the server at startup. An empty value keeps
328
+ the default, as a compose file expands an unset `${VAR:-}` to one; `off`
329
+ sends no header (`false` in a plugin's JSON override), when the reverse
330
+ proxy sets its own. Keeping both is harmless as long as they agree.
331
+
332
+ A redirect or a 404 that Express answers itself (a directory without its
333
+ slash, a missing file) carries `Content-Security-Policy: default-src 'none'`
334
+ instead; `X-Frame-Options`, unless set to `off`, still keeps it from being
335
+ framed, and it shows nothing to click.
336
+
337
+ Every response of the server, API routes included, also carries
338
+ `X-Content-Type-Options: nosniff`, and none says `X-Powered-By: Express`.
339
+
301
340
  ## MIME Types
302
341
 
303
342
  Express automatically sets Content-Type based on file extension:
@@ -374,9 +413,9 @@ GET /static/index.html → 404
374
413
  --static-path ./static
375
414
  ```
376
415
 
377
- 3. Verify prefix matches URL:
416
+ 3. Verify that the name matches the first segment of the URL:
378
417
  ```bash
379
- --static-prefix /static
418
+ --static-name static
380
419
  ```
381
420
 
382
421
  ### Problem: Placeholders Not Replaced
@@ -4,6 +4,32 @@ What to check before deploying, newest first. Only releases that need a
4
4
  decision or a configuration change appear here; see the
5
5
  [CHANGELOG](../../CHANGELOG.md) for everything else.
6
6
 
7
+ ## From 1.0 to 1.1.0
8
+
9
+ ### The static files can no longer be framed by another origin
10
+
11
+ `core/static` sends `Content-Security-Policy: frame-ancestors 'self'` and
12
+ `X-Frame-Options: SAMEORIGIN`. A page of another origin, another sub-domain
13
+ or port of the same site included, that showed one of its applications in an
14
+ `<iframe>`, a portal for instance, now shows an empty frame. Name it:
15
+
16
+ ```bash
17
+ --static-csp "frame-ancestors 'self' https://portal.example.com"
18
+ --static-frame-options off
19
+ ```
20
+
21
+ See [security headers](plugins/utilities/static.md#security-headers).
22
+
23
+ ### `--static-name`: an empty value is `static`
24
+
25
+ With `DM_STATIC_NAME=` the files were served at the root while the
26
+ configuration API and SCIM announced `/static`; they are now served under
27
+ `/static`. Slashes around the name are left out (`/static/` is `static`). A
28
+ name Express would not match as written, with an empty, `.` or `..` segment,
29
+ or what a URL encodes or Express reads as route syntax in it (a space, a
30
+ letter outside ASCII, `:`, `*`, `?`, `#`, `%`, brackets, quotes…), stops
31
+ the server at startup.
32
+
7
33
  ## From 0.26 to 1.0.0
8
34
 
9
35
  Every breaking change since 0.26.0. Authorization fails closed in 1.0.0:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-rest",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Light directory manager",
5
5
  "keywords": [
6
6
  "Identity-Manager"