ldap-rest 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/index.js CHANGED
@@ -1 +1 @@
1
- import{fileURLToPath as e}from"url";import{join as t,dirname as s}from"path";import i from"express";import r 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 d,setDebug as u,serverErrorBody as c}from"../lib/expressFormatedResponses.js";import{prefixCoversPath as g}from"../lib/auth/base.js";import{assertAuthzComposition as p,assertEnforceable as f,LDAP_JUDGING_HOOKS as m}from"../lib/authz/composition.js";import{errorBody as w,AUTHZ_REFUSED as b}from"../lib/errors.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";import{registerAttributeTypes as y,recordHookOwner as $}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,runAsHookOwner,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";import{runWithLogContext as v,runForRequest as P}from"../lib/requestContext.js";import{requestId as j}from"../lib/changeContext.js";import{mountHealth as S,HEALTH_PATHS as x}from"../lib/health.js";import{httpRequestDuration as k,routeTemplate as M,secondsSince as E,unhandledRejections as O}from"../lib/metrics.js";import{registerProcessShutdown as C,shutdownTimeout as D}from"../lib/shutdown.js";import A from"../plugins/priority.json.js";const _=m.filter(e=>"ldapsearchfilter"!==e);class DM{get server(){return this.life.server}set server(e){this.life.server=e}get composed(){return this._composed}get stopping(){return void 0!==this.life.shutdown}get unhandledRejections(){return this.life.unhandledRejections}constructor(){this.hooks={},this.loadedPlugins={},this.authenticators=[],this._authState={dispatcherMounted:!1},this._errorMiddlewareSetup=!1,this.bodyParsers=i.Router(),this._composed=!1,this.life={unhandledRejections:0},this.inFlight=new Set,this.pluginLoads=new Map,this.authzHooks=new WeakSet,this.config=o(a),this.app=i(),this.app.disable("x-powered-by"),this.app.use((e,t,s)=>{const i=process.hrtime.bigint();t.setHeader("X-Content-Type-Options","nosniff"),this.stopping&&t.setHeader("Connection","close"),this.inFlight.add(t),t.once("finish",()=>{k.observe({method:e.method,route:M(e),status:String(t.statusCode)},E(i)),this.stopping&&this.server?.closeIdleConnections()}),t.once("close",()=>this.inFlight.delete(t)),s()}),this.app.use((e,t,s)=>{const i=j(e);t.setHeader("X-Request-Id",i),v({requestId:i},s)}),this.app.use(this.bodyParsers),this.app.use(r.json({verify:n})),this.app.use(r.urlencoded({extended:!0,verify:n})),S(this.app,this),this.logger=l(this.config),this.ldap=new h(this),d(this.logger),u(this.config.debug),this.operationSequence=0;const e=[];if(this.config.plugin){const t=this.config.plugin.find(e=>e.includes("configApi"));let s=this.config.plugin.filter(e=>!e.includes("configApi"));const i=this.logger,r=(async()=>{for(const e of A){const t=this.resolveModule(DM.moduleOf(e)),r=s.filter(e=>this.resolveModule(DM.moduleOf(e))===t);if(0!==r.length){s=s.filter(e=>!r.includes(e));for(const e of r)i.debug(`Loading ${e} in the priority pass`),await this.loadPlugin(e)}}})();e.push(r),e.push(r.then(async()=>{const e=s.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,s)=>{e.length>0?Promise.all(e).then(async()=>{this.setupErrorMiddleware(),this.warnUnauthenticatedRoutes(),await p(this),f(this),await this.learnAttributeTypes(),this._composed=!0,this.afterLoad(),t()}).catch(e=>s(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{y((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,s)=>{this.dispatchAuth(e,t,s)}))}registerAuthenticator(e){this.mountAuthDispatcherNow(),this.authenticators.includes(e)||this.authenticators.push(e)}registerBodyParser(e,t){this.bodyParsers.use(e,t)}dispatchAuth(e,t,s){let i=-1,r=[];for(const t of this.authenticators)for(const s of t.pathPrefixes)g(s,e.path)&&(s.length>i?(i=s.length,r=[t]):s.length!==i||r.includes(t)||r.push(t));if(i<0&&(r=this.authenticators.filter(e=>0===e.pathPrefixes.length)),0===r.length)return s();const finish=i=>{if(i>=r.length)return P(e,s);const o=r[i];if(!o.afterChain)return finish(i+1);o.afterChain(e,t,()=>finish(i+1))},o=[];e.authenticators=o;const run=s=>{if(s>=r.length)return finish(0);const i=r[s];i.authenticate(e,t,()=>{i.vouchedFor(e)&&o.push(i.name),run(s+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 s of e){if(!s.pathPrefixes||0===s.pathPrefixes.length)return[];t.push(...s.pathPrefixes)}const s=this.routerStack(),i=new Set;for(const e of s||[]){const t=e.route?.path;"string"==typeof t?i.add(t):Array.isArray(t)&&t.forEach(e=>"string"==typeof e&&i.add(e))}const r=[...i].filter(e=>!x.includes(e)&&!t.some(t=>e===t||e.startsWith(`${t}/`))),o=Object.values(this.loadedPlugins).some(e=>e.roles?.includes("authz"));return r.length>0&&this.logger.warn(`Authentication is restricted to ${t.join(", ")}, so these routes are served without authentication`+(o?" nor authorization":"")+`: ${r.sort().join(", ")}`),r.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,s,i)=>{let r="statusCode"in e?e.statusCode:500,o=e.message;if(500===r&&/\[authz-forbidden\]/.test(e.message)?(r=403,o=b):403===r&&/\[authz-forbidden\]/.test(e.message)&&(o=b),r>=400&&r<500)return this.logger.warn(`Client error ${r} in request ${t.method} ${t.path}: ${o}`),void(s.headersSent||s.status(r).json(w(e,o)));this.logger.error(`Server error ${r} in request ${t.method} ${t.path}: ${e.message}`,{stack:e.stack,url:t.url,method:t.method}),s.headersSent||s.status(r).json(c(e,this.config.debug))},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}exitOnCrash(){process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack}),setTimeout(()=>process.exit(1),this.config.shutdown_timeout),this.shutdown().finally(()=>process.exit(1))}),process.on("unhandledRejection",e=>{this.life.unhandledRejections++,O.inc(),this.logger.error(`Unhandled promise rejection: ${e instanceof Error?e.message:String(e)}`,{stack:e instanceof Error?e.stack:void 0})})}run(){return new Promise((e,t)=>{this.server=this.app.listen(this.config.port,s=>{s?(this.logger.error(`Error starting server: ${s}`),t(s)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}shutdown(){var e;return(e=this.life).shutdown??(e.shutdown=this.letGo()),this.life.shutdown}async letGo(){const e=this.config.shutdown_timeout,t=Date.now(),s=Math.max(1e3,e-5e3);this.logger.notice(`Shutting down, ${s} ms for what is in flight`);const i=this.server;if(i){const e=new Promise(e=>i.close(()=>e()));i.closeIdleConnections();for(const e of this.inFlight)e.headersSent||e.setHeader("Connection","close");await DM.within(e,s),i.closeAllConnections()}const r=new Map;for(const[e,t]of Object.entries(this.loadedPlugins))r.set(e,Promise.resolve().then(()=>t.close?.()).catch(t=>{this.logger.warn(`${e}: close failed: ${t instanceof Error?t.message:String(t)}`)}).finally(()=>r.delete(e)));const o=Promise.all(r.values()).then(()=>this.ldap.close().catch(()=>{})),n=Math.max(1e3,e-(Date.now()-t)-1e3);await DM.within(o,n)||(this.logger.warn(`Shut down with a close still pending: ${[...r.keys()].join(", ")||"the directory connections"}`),this.ldap.close().catch(()=>{})),this.logger.notice("Shut down")}static async within(e,t){let s;const i=new Promise(e=>{s=setTimeout(()=>e(!1),t)});try{return await Promise.race([e.then(()=>!0),i])}finally{clearTimeout(s)}}shutdownOnSignals(){D.ms=this.config.shutdown_timeout,C(()=>this.shutdown())}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(s(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js"):i.startsWith(".")?e(new URL(i,import.meta.url)):i}loadPlugin(e){let t,s;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{s=JSON.parse(i),"object"!=typeof s?(this.logger.error(`Overrides for plugin ${e} are not valid: ${i}`),s=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}`),s=void 0}}else t=i,t||(t=void 0)}else t=void 0;const i=s?void 0:`${this.resolveModule(e)}\0${t??""}`,r=void 0===i?void 0:this.pluginLoads.get(i);if(r)return this.logger.debug(`Plugin ${e} already loaded, skipping`),r;const o=this.importPlugin(e,t,s);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,s){return this.logger.debug(`Loading plugin ${e}`),e.startsWith("core/")&&(e=this.resolveModule(e)),new Promise((i,r)=>{import(e).then(async o=>{let n;if(o&&o.default&&(o=o.default),s){const e={...this.config,...s};n=new o(this.withConfig(e))}else n=new o(this);if(!n)return r(new Error(`Unable to load ${e}`));i(await this.registerPlugin(e,n,t)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(t=>r(new Error(`Failed to load plugin ${e}: ${t}`)))})}async admitToComposition(e){this.loadedPlugins[e.name]=e;try{await p(this,e)}finally{delete this.loadedPlugins[e.name]}}async registerPlugin(e,t,s){if(t.name||(t.name=e),s&&(t.name=s),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 s=t.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!s||"function"!=typeof s)throw new Error(`Plugin ${t.name}: hook ${e} is invalid`);{const i=this.hooks[e];if(t.roles?.includes("authz")&&_.includes(e)){const e=i.findIndex(e=>!this.authzHooks.has(e));i.splice(-1===e?i.length:e,0,s),this.authzHooks.add(s)}else i.push(s);$(s,t.name,e)}}return this.loadedPlugins[t.name]=t,!0}}export{DM,$ as recordHookOwner,y as registerAttributeTypes};
1
+ import{fileURLToPath as e}from"url";import{join as t,dirname as s}from"path";import i from"express";import r 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 d,setDebug as u,serverErrorBody as c}from"../lib/expressFormatedResponses.js";import{prefixCoversPath as g}from"../lib/auth/base.js";import{assertAuthzComposition as p,assertEnforceable as f,LDAP_JUDGING_HOOKS as m}from"../lib/authz/composition.js";import{errorBody as w,AUTHZ_REFUSED as b}from"../lib/errors.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";import{registerAttributeTypes as y,recordHookOwner as $}from"../lib/utils.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,fillPattern,getCompiledRegex,getParentDn,getRdn,hookOwner,isAuthzRefusal,isChildOf,isDnInBranch,isDnSyntax,isDummyMemberDn,knownAttributeName,launchHooks,launchHooksChained,movedToTrash,normalizeDn,organizationLink,parseDn,rdnTypes,rdnValue,runAsHookOwner,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";import{runWithLogContext as v,runForRequest as P}from"../lib/requestContext.js";import{requestId as j}from"../lib/changeContext.js";import{mountHealth as S,HEALTH_PATHS as x}from"../lib/health.js";import{httpRequestDuration as k,routeTemplate as M,secondsSince as E,unhandledRejections as O}from"../lib/metrics.js";import{registerProcessShutdown as C,shutdownTimeout as D}from"../lib/shutdown.js";import A from"../plugins/priority.json.js";const _=m.filter(e=>"ldapsearchfilter"!==e);class DM{get server(){return this.life.server}set server(e){this.life.server=e}get composed(){return this._composed}get stopping(){return void 0!==this.life.shutdown}get unhandledRejections(){return this.life.unhandledRejections}constructor(){this.hooks={},this.loadedPlugins={},this.authenticators=[],this._authState={dispatcherMounted:!1},this._errorMiddlewareSetup=!1,this.bodyParsers=i.Router(),this._composed=!1,this.life={unhandledRejections:0},this.inFlight=new Set,this.pluginLoads=new Map,this.authzHooks=new WeakSet,this.config=o(a),this.app=i(),this.app.disable("x-powered-by"),this.app.use((e,t,s)=>{const i=process.hrtime.bigint();t.setHeader("X-Content-Type-Options","nosniff"),this.stopping&&t.setHeader("Connection","close"),this.inFlight.add(t),t.once("finish",()=>{k.observe({method:e.method,route:M(e),status:String(t.statusCode)},E(i)),this.stopping&&this.server?.closeIdleConnections()}),t.once("close",()=>this.inFlight.delete(t)),s()}),this.app.use((e,t,s)=>{const i=j(e);t.setHeader("X-Request-Id",i),v({requestId:i},s)}),this.app.use(this.bodyParsers),this.app.use(r.json({verify:n})),this.app.use(r.urlencoded({extended:!0,verify:n})),S(this.app,this),this.logger=l(this.config),this.ldap=new h(this),d(this.logger),u(this.config.debug),this.operationSequence=0;const e=[];if(this.config.plugin){const t=this.config.plugin.find(e=>e.includes("configApi"));let s=this.config.plugin.filter(e=>!e.includes("configApi"));const i=this.logger,r=(async()=>{for(const e of A){const t=this.resolveModule(DM.moduleOf(e)),r=s.filter(e=>this.resolveModule(DM.moduleOf(e))===t);if(0!==r.length){s=s.filter(e=>!r.includes(e));for(const e of r)i.debug(`Loading ${e} in the priority pass`),await this.loadPlugin(e)}}})();e.push(r),e.push(r.then(async()=>{const e=s.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,s)=>{e.length>0?Promise.all(e).then(async()=>{this.setupErrorMiddleware(),this.warnUnauthenticatedRoutes(),await p(this),f(this),await this.learnAttributeTypes(),this._composed=!0,this.afterLoad(),t()}).catch(e=>s(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{y((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,s)=>{this.dispatchAuth(e,t,s)}))}registerAuthenticator(e){this.mountAuthDispatcherNow(),this.authenticators.includes(e)||this.authenticators.push(e)}registerBodyParser(e,t){this.bodyParsers.use(e,t)}dispatchAuth(e,t,s){let i=-1,r=[];for(const t of this.authenticators)for(const s of t.pathPrefixes)g(s,e.path)&&(s.length>i?(i=s.length,r=[t]):s.length!==i||r.includes(t)||r.push(t));if(i<0&&(r=this.authenticators.filter(e=>0===e.pathPrefixes.length)),0===r.length)return s();const finish=i=>{if(i>=r.length)return P(e,s);const o=r[i];if(!o.afterChain)return finish(i+1);o.afterChain(e,t,()=>finish(i+1))},o=[];e.authenticators=o;const run=s=>{if(s>=r.length)return finish(0);const i=r[s];i.authenticate(e,t,()=>{i.vouchedFor(e)&&o.push(i.name),run(s+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 s of e){if(!s.pathPrefixes||0===s.pathPrefixes.length)return[];t.push(...s.pathPrefixes)}const s=this.routerStack(),i=new Set;for(const e of s||[]){const t=e.route?.path;"string"==typeof t?i.add(t):Array.isArray(t)&&t.forEach(e=>"string"==typeof e&&i.add(e))}const r=[...i].filter(e=>!x.includes(e)&&!t.some(t=>e===t||e.startsWith(`${t}/`))),o=Object.values(this.loadedPlugins).some(e=>e.roles?.includes("authz"));return r.length>0&&this.logger.warn(`Authentication is restricted to ${t.join(", ")}, so these routes are served without authentication`+(o?" nor authorization":"")+`: ${r.sort().join(", ")}`),r.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,s,i)=>{let r="statusCode"in e?e.statusCode:500,o=e.message;if(500===r&&/\[authz-forbidden\]/.test(e.message)?(r=403,o=b):403===r&&/\[authz-forbidden\]/.test(e.message)&&(o=b),r>=400&&r<500)return this.logger.warn(`Client error ${r} in request ${t.method} ${t.path}: ${o}`),void(s.headersSent||s.status(r).json(w(e,o)));this.logger.error(`Server error ${r} in request ${t.method} ${t.path}: ${e.message}`,{stack:e.stack,url:t.url,method:t.method}),s.headersSent||s.status(r).json(c(e,this.config.debug))},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}exitOnCrash(){process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack}),setTimeout(()=>process.exit(1),this.config.shutdown_timeout),this.shutdown().finally(()=>process.exit(1))}),process.on("unhandledRejection",e=>{this.life.unhandledRejections++,O.inc(),this.logger.error(`Unhandled promise rejection: ${e instanceof Error?e.message:String(e)}`,{stack:e instanceof Error?e.stack:void 0})})}run(){return new Promise((e,t)=>{this.server=this.app.listen(this.config.port,s=>{s?(this.logger.error(`Error starting server: ${s}`),t(s)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}shutdown(){var e;return(e=this.life).shutdown??(e.shutdown=this.letGo()),this.life.shutdown}async letGo(){const e=this.config.shutdown_timeout,t=Date.now(),s=Math.max(1e3,e-5e3);this.logger.notice(`Shutting down, ${s} ms for what is in flight`);const i=this.server;if(i){const e=new Promise(e=>i.close(()=>e()));i.closeIdleConnections();for(const e of this.inFlight)e.headersSent||e.setHeader("Connection","close");await DM.within(e,s),i.closeAllConnections()}const r=new Map;for(const[e,t]of Object.entries(this.loadedPlugins))r.set(e,Promise.resolve().then(()=>t.close?.()).catch(t=>{this.logger.warn(`${e}: close failed: ${t instanceof Error?t.message:String(t)}`)}).finally(()=>r.delete(e)));const o=Promise.all(r.values()).then(()=>this.ldap.close().catch(()=>{})),n=Math.max(1e3,e-(Date.now()-t)-1e3);await DM.within(o,n)||(this.logger.warn(`Shut down with a close still pending: ${[...r.keys()].join(", ")||"the directory connections"}`),this.ldap.close().catch(()=>{})),this.logger.notice("Shut down")}static async within(e,t){let s;const i=new Promise(e=>{s=setTimeout(()=>e(!1),t)});try{return await Promise.race([e.then(()=>!0),i])}finally{clearTimeout(s)}}shutdownOnSignals(){D.ms=this.config.shutdown_timeout,C(()=>this.shutdown())}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(s(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js"):i.startsWith(".")?e(new URL(i,import.meta.url)):i}loadPlugin(e){let t,s;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{s=JSON.parse(i),"object"!=typeof s?(this.logger.error(`Overrides for plugin ${e} are not valid: ${i}`),s=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}`),s=void 0}}else t=i,t||(t=void 0)}else t=void 0;const i=s?void 0:`${this.resolveModule(e)}\0${t??""}`,r=void 0===i?void 0:this.pluginLoads.get(i);if(r)return this.logger.debug(`Plugin ${e} already loaded, skipping`),r;const o=this.importPlugin(e,t,s);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,s){return this.logger.debug(`Loading plugin ${e}`),e.startsWith("core/")&&(e=this.resolveModule(e)),new Promise((i,r)=>{import(e).then(async o=>{let n;if(o&&o.default&&(o=o.default),s){const e={...this.config,...s};n=new o(this.withConfig(e))}else n=new o(this);if(!n)return r(new Error(`Unable to load ${e}`));i(await this.registerPlugin(e,n,t)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(t=>r(new Error(`Failed to load plugin ${e}: ${t}`)))})}async admitToComposition(e){this.loadedPlugins[e.name]=e;try{await p(this,e)}finally{delete this.loadedPlugins[e.name]}}async registerPlugin(e,t,s){if(t.name||(t.name=e),s&&(t.name=s),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 s=t.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!s||"function"!=typeof s)throw new Error(`Plugin ${t.name}: hook ${e} is invalid`);{const i=this.hooks[e];if(t.roles?.includes("authz")&&_.includes(e)){const e=i.findIndex(e=>!this.authzHooks.has(e));i.splice(-1===e?i.length:e,0,s),this.authzHooks.add(s)}else i.push(s);$(s,t.name,e)}}return this.loadedPlugins[t.name]=t,!0}}export{DM,$ as recordHookOwner,y as registerAttributeTypes};
@@ -1 +1 @@
1
- import{createHash as e}from"node:crypto";import{Client as t,Attribute as s,Change as i,Control as n}from"ldapts";import{LRUCache as a}from"lru-cache";import r from"p-limit";import{isAuthzRefusal as o,hookOwner as c,launchHooksChained as l,normalizeDn as h,isDnInBranch as d,knownAttributeName as p,launchHooks as f,rdnTypes as u,escapeDnValue as g,getParentDn as y}from"./utils.js";import{changeContext as w}from"./changeContext.js";import{runOperation as m,outsideOperation as b}from"./operation.js";import{outsideAnyRequest as C,outsideRequest as A,currentRequest as v}from"./requestContext.js";import{timedLdap as $,ldapPoolWait as D,secondsSince as L,ldapSearchCache as j,ldapResult as S,ldapOperationDuration as P}from"./metrics.js";import{BadRequestError as E,NotFoundError as x,ConflictError as O}from"./errors.js";import{extractLdapCode as R,isSchemaRefusal as _}from"./ldapCodes.js";import{SchemaIndex as k,parseSchema as U}from"./ldapSchema.js";const q={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}},T={ldapaddrequest:2,ldapmodifyrequest:3,ldaprenamerequest:2,ldapdeleterequest:1},N=new Set(["search","add","modify","rename","move","delete"]),isPlainObject=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function assertModifyRequest(e){const t=Object.keys(e).filter(e=>!["add","replace","delete"].includes(e));if(t.length>0)throw new E(`A modification carries add, replace or delete, not ${t.join(", ")}`);const{add:s,replace:i,delete:n}=e;for(const[e,t]of[["add",s],["replace",i]])if(void 0!==t&&!isPlainObject(t))throw new E(`"${e}" must map attributes to values`);if(!(void 0===n||isPlainObject(n)||Array.isArray(n)&&n.every(e=>"string"==typeof e)))throw new E('"delete" must list attribute names or map attributes to values');if(0===[s,i,n].reduce((e,t)=>e+(t?Object.keys(t).length:0),0))throw new E("A modification names at least one attribute in add, replace or delete")}function ldapError(e,t){const s=new Error(`${e}: ${String(t)}`),i=R(t);return void 0===i?s:withCode(s,i)}function withCode(e,t){return e.code=t,e}const digestKey=t=>e("sha256").update(t).digest("hex").slice(0,12),foldDn=e=>e.trim().toLowerCase().split(",").map(e=>e.trim()).join(","),B=new Set(["member","uniquemember"]),isMembershipAttribute=e=>B.has(e.toLowerCase()),membersNamed=e=>Object.entries(e??{}).flatMap(([e,t])=>isMembershipAttribute(e)?[t].flat().map(e=>Buffer.isBuffer(e)?e.toString():e).filter(e=>"string"==typeof e):[]),cloneSearchResult=e=>({searchEntries:e.searchEntries.map(e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?[...t]:t]))),searchReferences:[...e.searchReferences]}),I=["*","+","1.1"],dropSelectors=e=>{for(const t of e.searchEntries)for(const e of I){const s=t[e];Array.isArray(s)&&0===s.length&&delete t[e]}return e};function ldapRefusal(e,t,s){return withCode(new E(`${e}: ${String(s)}`),t)}function assertHasValue(e,t){if(Array.isArray(t)&&0===t.length)throw new E(`Attribute "${e}" has no value`)}function ldapWriteError(e,t,s){const i=R(s);return _(i)?ldapRefusal(`The directory refused this write to ${t}`,i,s):ldapError(e,s)}function ldapRenameError(e,t,s,i){const n=R(i);return 68===n?withCode(new O(`Entry ${s} already exists`),n):_(n)?ldapRefusal(`The directory refused to rename ${t} to ${s}`,n,i):ldapError(e,i)}class ldapActions{constructor(e){this.cacheGeneration=0,this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.declaredClasses=new Map,this.schemaUnavailable=!1,this.spellings=new Map,this.spelledTypes=new Map,this.classWarnings=new a({max:1e4}),this.closed=!1,this.parent=e,this.logger=e.logger,this.config=e.config;for(const[e,t]of Object.entries(this.config))/_attributes?$/.test(e)&&this.knowAttributeNames([t].flat().filter(e=>"string"==typeof e));this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=r(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const s="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,i=this.config.ldap_cache_ttl,n="string"==typeof i?parseInt(i,10)||0:i??0,o=1e3*Math.max(n,0);if(this.cacheEnabled=o>0,this.searchCache=new a({max:s,ttl:this.cacheEnabled?o:void 0,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(this.cacheEnabled?`LDAP search cache initialized: max=${s}, ttl=${o/1e3}s`:"LDAP search cache disabled (--ldap-cache-ttl=0)"),this.attrSignatureCache=new a({max:1e3}),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP bind DN is not defined, please set --ldap-dn (or DM_LDAP_DN)");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined, please set --ldap-pwd (or DM_LDAP_PWD)");if(!e.config.ldap_base)throw new Error("LDAP base is not defined, please set --ldap-base (or DM_LDAP_BASE)");this.base=e.config.ldap_base,this.ldapUrls=e.config.ldap_url,this.logger.info(`LDAP failover configured with ${this.ldapUrls.length} URL(s): ${this.ldapUrls.join(", ")}`),this.options={url:this.ldapUrls[0],timeout:0,connectTimeout:0,strictDN:!1},this.ldapUrls[0].startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const e=[];for(let s=0;s<this.ldapUrls.length;s++){const i=(this.currentUrlIndex+s)%this.ldapUrls.length,n=this.ldapUrls[i];try{this.logger.debug(`Attempting connection to ${n}`);const e={...this.options,url:n,tlsOptions:n.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},s=new t(e);return await $("bind",()=>s.bind(this.dn,this.pwd)),i!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${n}`),this.currentUrlIndex=i),s}catch(t){this.logger.warn(`Failed to connect to ${n}: ${String(t)}`),e.push(t)}}throw this.logger.error(`LDAP connection failed for all ${this.ldapUrls.length} URL(s)`),new Error(`LDAP connection failed for all URLs: ${e.map(e=>e.message).join(", ")}`)}cleanupExpiredConnections(){if(!this.isCleaningUp){this.isCleaningUp=!0;try{const e=Date.now(),t=[];for(let s=this.availableConnections.length-1;s>=0;s--){const i=this.availableConnections[s];if(e-i.createdAt>this.connectionTtl){t.push(i),this.availableConnections.splice(s,1);const e=this.connectionPool.indexOf(i);-1!==e&&this.connectionPool.splice(e,1)}}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}finally{this.isCleaningUp=!1}}}async ping(e){if(this.closed)throw new Error("LDAP connections closed");const s=[];for(let i=0;i<this.ldapUrls.length;i++){const n=this.ldapUrls[(this.currentUrlIndex+i)%this.ldapUrls.length],a=new t({...this.options,url:n,timeout:e,connectTimeout:e,tlsOptions:n.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0});try{return await a.bind(this.dn,this.pwd),void await a.search("",{scope:"base",attributes:["1.1"]})}catch(e){s.push(`${n}: ${String(e)}`)}finally{await a.unbind().catch(()=>{})}}throw new Error(`No directory answered: ${s.join(", ")}`)}async close(){this.closed=!0;for(const e of this.waitingResolvers.splice(0))e(void 0);const e=this.connectionPool.splice(0);this.availableConnections.length=0,await Promise.allSettled(e.map(e=>e.client.unbind()))}async acquireConnection(){const e=process.hrtime.bigint();try{return await this.takeConnection()}finally{D.observe(L(e))}}poolStats(){return{busy:this.connectionPool.length-this.availableConnections.length,idle:this.availableConnections.length,waiting:this.waitingResolvers.length,max:this.poolSize}}async takeConnection(){if(this.closed)throw new Error("LDAP connections closed");if(this.cleanupExpiredConnections(),this.availableConnections.length>0){const e=this.availableConnections.pop();return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e}if(this.connectionPool.length<this.poolSize){const e=await C(()=>this.createConnection());if(this.closed)throw await e.unbind().catch(()=>{}),new Error("LDAP connections closed");const t={client:e,createdAt:Date.now(),inUse:!0};return this.connectionPool.push(t),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),t}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((e,t)=>{const s=globalThis.setTimeout(()=>{const e=this.waitingResolvers.indexOf(resolveWrapper);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),resolveWrapper=i=>{globalThis.clearTimeout(s),i?e(i):t(new Error("LDAP connections closed"))};this.waitingResolvers.push(resolveWrapper)})}releaseConnection(e){if(Date.now()-e.createdAt>this.connectionTtl){const t=this.connectionPool.indexOf(e);return-1!==t&&this.connectionPool.splice(t,1),e.client.unbind().catch(()=>{}),void this.logger.debug("Released expired LDAP connection (discarded)")}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t(e)}e.inUse=!1,this.availableConnections.push(e),this.logger.debug("Released LDAP connection back to pool")}getAttributeSignature(e){if(!e||0===e.length)return"*";const t=e.join("|");let s=this.attrSignatureCache.get(t);return s||(s=[...e].sort().join(","),this.attrSignatureCache.set(t,s)),s}getCacheKey(e,t){const s=this.getAttributeSignature(t.attributes),i=t.explicitBufferAttributes?.length?[...t.explicitBufferAttributes].sort().join(","):"-",n="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return[e,t.scope||"sub",n,s,i,!1===t.returnAttributeValues?"novalues":"values",t.derefAliases??"-",String(t.sizeLimit??"-")].join("\0")}invalidateCache(e){if(!this.cacheEnabled)return;this.cacheGeneration++;const t=foldDn(e),s=`,${t}`,i=[];for(const e of this.searchCache.keys()){const n=foldDn(e.split("\0")[0]);(n===t||n.endsWith(s))&&i.push(e)}for(const e of i)this.searchCache.delete(e)}invalidateReferences(e){if(!this.cacheEnabled)return;this.cacheGeneration++;const t=foldDn(e),s=`,${t}`,names=e=>{if("string"!=typeof e)return!1;const i=foldDn(e);return i===t||i.endsWith(s)},i=[];for(const[e,t]of this.searchCache.entries())t.searchEntries.some(e=>Object.values(e).some(e=>Array.isArray(e)?e.some(names):names(e)))&&i.push(e);for(const e of i)this.searchCache.delete(e)}get system(){return this.systemView??(this.systemView=new Proxy(this,{get:(e,t)=>{const s=Reflect.get(e,t,e);return"function"==typeof s&&N.has(t)?(...t)=>A(()=>s.apply(e,t)):s}})),this.systemView}forRequest(e){return{search:(t,s)=>this.search(t,s??this.base,e),add:(t,s)=>this.add(t,s,e),modify:(t,s,i)=>this.modify(t,s,e,i),rename:(t,s)=>this.rename(t,s,e),delete:t=>this.delete(t,e)}}async readableBy(e,t){if(t??(t=v()),!t)return!0;try{const s=await this.search({paged:!1,scope:"base",attributes:["1.1"]},e,t);return(s.searchEntries?.length??0)>0}catch(e){if(o(e)||403===e?.statusCode||32===R(e))return!1;throw e}}async authorize(e,t){const s=[...t];"ldapaddrequest"===e&&await this.canonicalAttributes(s[1]),"ldapmodifyrequest"===e&&await this.canonicalChanges(s[1]);const i=T[e];s[i]??(s[i]=v());const[n,...a]=s,r="ldapdeleterequest"===e?[[n].flat().map(e=>this.setDn(String(e))),...a]:[this.setDn(String(n)),...a];"ldaprenamerequest"===e&&(r[1]=this.setDn(String(r[1])));const o=(this.parent.hooks[e]??[]).filter(e=>{const t=c(e);return Boolean(t&&this.parent.loadedPlugins[t.plugin]?.roles?.includes("authz"))});await l(o,r)}declareObjectClasses(e,t,s=!1){const i=(Array.isArray(t)?t:[t]).filter(e=>"string"==typeof e&&""!==e);if(!e||0===i.length)return;const n=JSON.stringify([h(e),i.map(e=>e.toLowerCase()).sort(),s]);this.declaredClasses.set(n,{base:e,classes:i,includeBase:s})}declaredFor(e){let t=0,s=[];for(const i of this.declaredClasses.values()){if(!d(e,i.base))continue;const n=h(i.base);if(!i.includeBase&&h(e)===n)continue;const a=n.split(",").length;a>t&&(t=a,s=[]),a===t&&s.push(...i.classes)}const i=new Set;return s.filter(e=>{const t=e.toLowerCase();return!i.has(t)&&(i.add(t),!0)})}warnOnce(e){this.classWarnings.has(e)||(this.classWarnings.set(e,!0),this.logger.warn(e))}async classRepair(e,t,s=[]){if(!1===this.config.ldap_flat_auto_repair)return[];const i=[...this.declaredFor(e)];for(const e of s)i.some(t=>t.toLowerCase()===e.toLowerCase())||i.push(e);if(0===i.length)return[];const n=Array.isArray(t.delete)?t.delete:Object.entries(t.delete||{}).filter(([,e])=>!e||Array.isArray(e)&&!e.length).map(([e])=>e);if([...Object.keys(t.add||{}),...Object.keys(t.replace||{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete||{})].some(e=>"objectclass"===e.toLowerCase()))return[];let a;try{a=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){return this.schemaUnavailable||this.logger.warn(`Object classes are not repaired: the directory schema could not be read (${String(e)})`),this.schemaUnavailable=!0,[]}const attributeKey=e=>a.getAttributeType(e)?.oid??e.toLowerCase(),allowedBy=e=>{const{must:t,may:s}=a.resolveAttributes(e);return new Set([...t,...s].map(attributeKey))},valued=e=>Object.entries(e||{}).filter(([,e])=>Array.isArray(e)?e.length>0:null!=e&&""!==e).map(([e])=>e),r=[...valued(t.add),...valued(t.replace)],o=i.filter(e=>{const t=a.getObjectClass(e);if("AUXILIARY"!==t?.kind)return!1;const s=allowedBy([e]);return r.some(e=>s.has(attributeKey(e)))});if(0===o.length)return[];let c;try{c=(await this.system.search({paged:!1,scope:"base",attributes:["objectClass",...a.resolveAttributes(o).must]},e)).searchEntries[0]}catch{return[]}if(!c)return[];const l=c.objectClass??c.objectclass,h=(Array.isArray(l)?l:l?[l]:[]).map(String),d=allowedBy(h);let p=r.filter(e=>a.getAttributeType(e)&&!d.has(attributeKey(e)));if(0===p.length)return"allowed";const f=new Set(n.map(attributeKey)),u=new Set([...valued(c).filter(e=>!f.has(attributeKey(e))),...r].map(attributeKey)),{auxiliary:g,structural:y}=a.missingClasses(i,h),w=[];for(const t of g){const s=t.names[0]??t.oid,i=allowedBy([s]),n=p.filter(e=>i.has(attributeKey(e)));if(0===n.length)continue;const r=a.resolveAttributes([s]).must.filter(e=>!u.has(attributeKey(e)));r.length>0?this.warnOnce(`${e} needs ${s} for ${n.join(", ")}, and cannot be given it without ${r.join(", ")}`):(w.push(s),p=p.filter(e=>!n.includes(e)))}const m=y.filter(e=>{const t=allowedBy([e]);return p.some(e=>t.has(attributeKey(e)))});return m.length>0&&this.warnOnce(`${e} cannot hold ${p.join(", ")}: its schema declares ${m.join(", ")}, a structural class an existing entry cannot be given`),w.length>0&&this.logger.info(`${e}: adding ${w.join(", ")}, which the change needs and its schema declares`),w}async subschemaDn(){try{const e=await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["subschemaSubentry"]},""),t=e.searchEntries[0]?.subschemaSubentry,s=Array.isArray(t)?t[0]:t;if(s)return String(s)}catch(e){this.logger.warn(`Unable to read subschemaSubentry from root DSE: ${String(e)}`)}return"cn=Subschema"}async schemaIndex(){const e=1e3*(this.config.ldap_raw_schema_cache_ttl??3600);if(this.schemaCache&&Date.now()-this.schemaCache.fetchedAt<e)return this.schemaCache.index;if(this.schemaLoading)return this.schemaLoading;this.schemaLoading=(async()=>{const e=await this.subschemaDn(),t=(await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["objectClasses","attributeTypes","ldapSyntaxes","matchingRules"]},e).catch(t=>{if(32===t?.code)throw new x(`Subschema entry ${e} not found`);throw t})).searchEntries[0];if(!t)throw new x(`Subschema entry ${e} not found`);const list=e=>{if(void 0===e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString("utf8"):e)},s=new k(U({objectClasses:list(t.objectClasses),attributeTypes:list(t.attributeTypes),ldapSyntaxes:list(t.ldapSyntaxes),matchingRules:list(t.matchingRules)}));return this.schemaCache={index:s,fetchedAt:Date.now()},this.logger.info(`LDAP schema loaded from ${e}: ${s.schema.objectClasses.length} object classes, ${s.schema.attributeTypes.length} attribute types`),s})();try{return await this.schemaLoading}finally{this.schemaLoading=void 0}}knowAttributeNames(e){for(const t of e){if(!t||t.includes(";"))continue;const e=t.toLowerCase();this.spellings.has(e)||this.spellings.set(e,t);const s=(p(t)??t).toLowerCase(),i=this.spelledTypes.get(s);void 0===i?this.spelledTypes.set(s,t):i!==t&&this.logger.warn(`Attribute "${t}" is also named "${i}" in the configuration or a schema: plugins comparing with one name miss the other`)}}async canonicalAttributes(e){if(!e)return;const t=Object.keys(e),s=await this.canonicalNames(t);if(t.every((e,t)=>e===s[t]))return;const i=t.map(t=>e[t]);for(const s of t)delete e[s];s.forEach((t,s)=>e[t]=i[s])}async canonicalChanges(e){e&&(await this.canonicalAttributes(e.add),await this.canonicalAttributes(e.replace),Array.isArray(e.delete)?e.delete=await this.canonicalNames(e.delete):await this.canonicalAttributes(e.delete))}async canonicalNames(e){if(0===e.length)return e;let t;try{t=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){this.schemaUnavailable||this.logger.warn(`The directory schema could not be read (${String(e)}): attribute names are resolved from the standard types only, a name given as another OID is refused, and object classes are not repaired`),this.schemaUnavailable=!0}const s=new Map;return e.map(e=>{const i=this.canonicalName(e,t),n=s.get(i.toLowerCase());if(void 0!==n)throw new E(`"${n}" and "${e}" name the same attribute`);return s.set(i.toLowerCase(),e),i})}canonicalName(e,t){const[s,...i]=e.split(";"),n=s.replace(/^oid\./i,""),a=i.find(e=>"binary"!==e.toLowerCase());if(void 0!==a)throw new E(`Attribute option "${a}" of "${e}" is not accepted`);const r=t?.getAttributeType(n),o=t?void 0:p(n);if(!t&&!o&&/^\d+(\.\d+)+$/.test(n))throw new E(`Attribute "${e}" cannot be named by its OID here`);const c=(r?[...r.names,r.oid]:[n,...o?[o]:[]]).map(e=>this.spellings.get(e.toLowerCase())).find(Boolean)??r?.names[0]??o??n;return i.length?`${c};binary`:c}filterForCaller(e,t,s){if(!this.parent.hooks.ldapsearchfilter)return e;const i=this.parent.hooks.ldapsearchfilter,one=async e=>{const[n]=await l(i,[e,t,s]);return n??e};if("function"==typeof e[Symbol.asyncIterator]){const t=e;return async function*(){for await(const e of t)yield await one(e)}()}return one(e)}async search(e,t=this.base,s){s??(s=v());let i={...q,...e};i=await l(this.parent.hooks.ldapsearchopts,i),[t,i]=await l(this.parent.hooks.ldapsearchrequest,[t,i,s]);const n=this.cacheEnabled&&!i.paged&&"base"===i.scope,a=n?this.getCacheKey(t,i):"",r=this.cacheGeneration;if(n){const e=this.searchCache.get(a);if(j.inc({result:e?"hit":"miss"}),e)return this.logger.debug(`LDAP search cache hit: ${digestKey(a)}`),this.filterForCaller(cloneSearchResult(e),s,i)}const o=await this.acquireConnection();try{let e=i.paged?async function*(e){for await(const t of e)yield dropSelectors(t)}(o.client.searchPaginated(t,i)):$("search",()=>o.client.search(t,i)).then(dropSelectors);if(e=await l(this.parent.hooks.ldapsearchresult,e),n){const t=e;return r===this.cacheGeneration?(this.searchCache.set(a,cloneSearchResult(t)),this.logger.debug(`LDAP search cached: ${digestKey(a)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${digestKey(a)}`),this.filterForCaller(t,s,i)}return i.paged?this.filterForCaller(this.wrapPaginatedSearch(e,o),s,i):this.filterForCaller(e,s,i)}finally{i.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){let s=0,i="ok";try{for(;;){const t=process.hrtime.bigint();let n;try{n=await e.next()}catch(e){throw i=S(e),e}finally{s+=L(t)}if(n.done)return;yield n.value}}finally{try{await e.return(void 0)}catch{}finally{P.observe({operation:"search",result:i},s),this.releaseConnection(t)}}}async add(e,t,i){i??(i=v()),e=this.setDn(e),await this.canonicalAttributes(t),t.objectClass&&0!==t.objectClass.length||!this.config.user_class||(t.objectClass=this.config.user_class);const n={};for(const[e,s]of Object.entries(t))Buffer.isBuffer(s)?n[e]=s.toString():Array.isArray(s)&&s.length>0&&Buffer.isBuffer(s[0])?n[e]=s.map(e=>e.toString()):n[e]=s;[e,t]=await l(this.parent.hooks.ldapaddrequest,[e,n,i]);const a=membersNamed(t),r=[];for(const[e,i]of Object.entries(t)){assertHasValue(e,i);const t=Array.isArray(i)?i:[i];r.push(new s({type:e,values:t}))}const o=await this.acquireConnection();try{this.invalidateCache(e);for(const e of a)this.invalidateCache(e);await $("add",()=>o.client.add(e,r)),this.invalidateCache(e);for(const e of a)this.invalidateCache(e);f(this.parent.hooks.ldapadddone,[e,t],w(i))}catch(t){throw function(e,t){const s=R(t);return 68===s?withCode(new O(`Entry ${e} already exists`),s):32===s?withCode(new x(`${y(e)} does not exist`),s):20===s?ldapRefusal(`The directory refused this write to ${e}`,s,t):ldapWriteError("LDAP add error",e,t)}(e,t)}finally{this.releaseConnection(o)}return await f(this.parent.hooks.ldapaddafter,[e,t],w(i)),!0}async modify(e,t,s,i={}){s??(s=v());const n=this.opNumber(),a=[];try{return await m(n,()=>this.applyModify(e,t,s,i,n,a))}finally{this.endOperation(a,this.parent.hooks.ldapmodifyend,n)}}async applyModify(e,t,a,r,o,c){e=this.setDn(e);const h=[];if(await this.canonicalChanges(t),[e,t]=await l(this.parent.hooks.ldapmodifyrequest,[e,t,o,a]),t.add)for(const[e,n]of Object.entries(t.add))assertHasValue(e,n),h.push(new i({operation:"add",modification:new s({type:e,values:Array.isArray(n)?n:[n]})}));if(t.replace)for(const[e,n]of Object.entries(t.replace))h.push(new i({operation:"replace",modification:new s({type:e,values:Array.isArray(n)?n:[n]})}));if(t.delete)if(Array.isArray(t.delete))for(const e of t.delete)e&&h.push(new i({operation:"delete",modification:new s({type:e,values:[]})}));else for(const[e,n]of Object.entries(t.delete)){const t=new i({operation:"delete",modification:new s(n?{type:e,values:Array.isArray(n)?n:[n]}:{type:e})});h.push(t)}const d=[t.add,t.replace,t.delete].flatMap(e=>Array.isArray(e)?e:Object.keys(e??{})).some(isMembershipAttribute),invalidate=()=>{if(this.invalidateCache(e),d){this.invalidateReferences(e);for(const e of[t.add,t.replace,t.delete])for(const t of membersNamed(Array.isArray(e)?{}:e))this.invalidateCache(t)}};if(0===h.length){return Object.keys(t.add||{}).length>0||Object.keys(t.replace||{}).length>0||(Array.isArray(t.delete)?t.delete.length>0:Object.keys(t.delete||{}).length>0)?this.logger.warn(`Modify on ${e} asked for changes but emitted none; nothing was written`):this.logger.debug(`Modify on ${e} had nothing to apply`),c.push(f(this.parent.hooks.ldapmodifydone,[e,{},o],r.context??w(a))),!1}{const l=r.permissive?[new n("1.2.840.113556.1.4.1413",{critical:!1})]:void 0,send=async()=>{const t=await this.acquireConnection();try{await $("modify",()=>t.client.modify(e,h,l))}finally{this.releaseConnection(t)}};try{invalidate();try{await send()}catch(n){if(65!==R(n))throw n;const a=await this.classRepair(e,t,r.declaredClasses);if("allowed"!==a&&0===a.length)throw n;const o="allowed"===a?void 0:new i({operation:"add",modification:new s({type:"objectClass",values:a})});o&&h.push(o),this.invalidateCache(e);try{await send()}catch(e){if(!o||20!==R(e))throw e;h.splice(h.indexOf(o),1),await send()}}return invalidate(),c.push(f(this.parent.hooks.ldapmodifydone,[e,t,o],r.context??w(a))),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(h)}`),function(e,t){const s=R(t);return 32===s?withCode(new x(`Entry ${e} not found`),s):16===s?withCode(new x(`Entry ${e} does not hold the value to delete`),s):65!==s&&20!==s?ldapWriteError("LDAP modify error",e,t):withCode(new O(`Entry ${e} cannot hold this change: ${String(t)}`),s)}(e,t)}}}async rename(e,t,s){s??(s=v()),e=this.setDn(e),t=this.setDn(t);const i=u(e),n=u(t);if(1!==n.length||!i.includes(n[0]))throw new E(`${t}: an entry is renamed by one value of the attribute it is named by`);const a=this.opNumber(),r=[];try{return await m(a,()=>this.applyRename(e,t,s,r))}finally{this.endOperation(r,this.parent.hooks.ldaprenameend,a)}}async applyRename(e,t,s,i){[e,t]=await l(this.parent.hooks.ldaprenamerequest,[e,t,s]);const n=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),await $("rename",()=>n.client.modifyDN(e,t)),this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),i.push(f(this.parent.hooks.ldaprenamedone,[e,t],w(s))),!0}catch(s){throw ldapRenameError("LDAP rename error",e,t,s)}finally{this.releaseConnection(n)}}async move(e,t,s){s??(s=v()),e=this.setDn(e),t=this.setDn(t);const i=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),await $("rename",()=>i.client.modifyDN(e,t)),this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),b(()=>f(this.parent.hooks.ldaprenamedone,[e,t],w(s))),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(s){throw ldapRenameError("LDAP move error",e,t,s)}finally{this.releaseConnection(i)}}async delete(e,t,s={}){t??(t=v());const i=(Array.isArray(e)?e:[e]).map(e=>this.setDn(e)),n=this.opNumber(),a=[];try{return await m(n,()=>this.applyDelete(i,t,s,a))}finally{this.endOperation(a,this.parent?.hooks.ldapdeleteend,n)}}async applyDelete(e,t,s,i){[e]=await l(this.parent?.hooks.ldapdeleterequest,[e,t]);const n=await this.acquireConnection();try{for(const a of e){try{this.invalidateCache(a),this.invalidateReferences(a),await $("delete",()=>n.client.del(a)),this.invalidateCache(a),this.invalidateReferences(a)}catch(e){const t=R(e);throw 32===t?withCode(new x(`Entry ${a} not found`),t):ldapError("LDAP delete error",e)}i.push(f(this.parent.hooks.ldapdeletedone,a,s.context??w(t)))}return!0}finally{this.releaseConnection(n)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${g(e)},${this.base}`,e}endOperation(e,t,...s){t?.length&&Promise.all(e).then(()=>f(t,...s))}opNumber(){return this.parent.operationSequence++}}export{assertModifyRequest,ldapActions as default};
1
+ import{createHash as e}from"node:crypto";import{Client as t,Attribute as s,Change as i,Control as n}from"ldapts";import{LRUCache as a}from"lru-cache";import r from"p-limit";import{isAuthzRefusal as o,hookOwner as c,launchHooksChained as l,normalizeDn as h,isDnInBranch as d,knownAttributeName as p,launchHooks as f,rdnTypes as u,escapeDnValue as g,getParentDn as y}from"./utils.js";import{changeContext as w}from"./changeContext.js";import{runOperation as m,outsideOperation as b}from"./operation.js";import{outsideAnyRequest as C,outsideRequest as A,currentRequest as v}from"./requestContext.js";import{timedLdap as $,ldapPoolWait as D,secondsSince as L,ldapSearchCache as j,ldapResult as S,ldapOperationDuration as P}from"./metrics.js";import{BadRequestError as E,NotFoundError as x,ConflictError as O}from"./errors.js";import{extractLdapCode as R,isSchemaRefusal as _}from"./ldapCodes.js";import{SchemaIndex as k,parseSchema as U}from"./ldapSchema.js";const q={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}},T={ldapaddrequest:2,ldapmodifyrequest:3,ldaprenamerequest:2,ldapdeleterequest:1},N=new Set(["search","add","modify","rename","move","delete"]),isPlainObject=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function assertModifyRequest(e){const t=Object.keys(e).filter(e=>!["add","replace","delete"].includes(e));if(t.length>0)throw new E(`A modification carries add, replace or delete, not ${t.join(", ")}`);const{add:s,replace:i,delete:n}=e;for(const[e,t]of[["add",s],["replace",i]])if(void 0!==t&&!isPlainObject(t))throw new E(`"${e}" must map attributes to values`);if(!(void 0===n||isPlainObject(n)||Array.isArray(n)&&n.every(e=>"string"==typeof e)))throw new E('"delete" must list attribute names or map attributes to values');if(0===[s,i,n].reduce((e,t)=>e+(t?Object.keys(t).length:0),0))throw new E("A modification names at least one attribute in add, replace or delete")}function ldapError(e,t){const s=new Error(`${e}: ${String(t)}`),i=R(t);return void 0===i?s:withCode(s,i)}function withCode(e,t){return e.code=t,e}const digestKey=t=>e("sha256").update(t).digest("hex").slice(0,12),foldDn=e=>e.trim().toLowerCase().split(",").map(e=>e.trim()).join(","),z=new Set(["member","uniquemember"]),isMembershipAttribute=e=>z.has(e.toLowerCase()),membersNamed=e=>Object.entries(e??{}).flatMap(([e,t])=>isMembershipAttribute(e)?[t].flat().map(e=>Buffer.isBuffer(e)?e.toString():e).filter(e=>"string"==typeof e):[]),cloneSearchResult=e=>({searchEntries:e.searchEntries.map(e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?[...t]:t]))),searchReferences:[...e.searchReferences]}),B=["*","+","1.1"],dropSelectors=e=>{for(const t of e.searchEntries)for(const e of B){const s=t[e];Array.isArray(s)&&0===s.length&&delete t[e]}return e};function ldapRefusal(e,t,s){return withCode(new E(`${e}: ${String(s)}`),t)}function assertHasValue(e,t){if(Array.isArray(t)&&0===t.length)throw new E(`Attribute "${e}" has no value`)}function ldapWriteError(e,t,s){const i=R(s);return _(i)?ldapRefusal(`The directory refused this write to ${t}`,i,s):ldapError(e,s)}function ldapRenameError(e,t,s,i){const n=R(i);return 68===n?withCode(new O(`Entry ${s} already exists`),n):_(n)?ldapRefusal(`The directory refused to rename ${t} to ${s}`,n,i):ldapError(e,i)}class ldapActions{constructor(e){this.cacheGeneration=0,this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.declaredClasses=new Map,this.schemaUnavailable=!1,this.spellings=new Map,this.spelledTypes=new Map,this.classWarnings=new a({max:1e4}),this.closed=!1,this.parent=e,this.logger=e.logger,this.config=e.config;for(const[e,t]of Object.entries(this.config))/_attributes?$/.test(e)&&this.knowAttributeNames([t].flat().filter(e=>"string"==typeof e));this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=r(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const s="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,i=this.config.ldap_cache_ttl,n="string"==typeof i?parseInt(i,10)||0:i??0,o=1e3*Math.max(n,0);if(this.cacheEnabled=o>0,this.searchCache=new a({max:s,ttl:this.cacheEnabled?o:void 0,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(this.cacheEnabled?`LDAP search cache initialized: max=${s}, ttl=${o/1e3}s`:"LDAP search cache disabled (--ldap-cache-ttl=0)"),this.attrSignatureCache=new a({max:1e3}),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP bind DN is not defined, please set --ldap-dn (or DM_LDAP_DN)");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined, please set --ldap-pwd (or DM_LDAP_PWD)");if(!e.config.ldap_base)throw new Error("LDAP base is not defined, please set --ldap-base (or DM_LDAP_BASE)");this.base=e.config.ldap_base,this.ldapUrls=e.config.ldap_url,this.logger.info(`LDAP failover configured with ${this.ldapUrls.length} URL(s): ${this.ldapUrls.join(", ")}`),this.options={url:this.ldapUrls[0],timeout:0,connectTimeout:0,strictDN:!1},this.ldapUrls[0].startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const e=[];for(let s=0;s<this.ldapUrls.length;s++){const i=(this.currentUrlIndex+s)%this.ldapUrls.length,n=this.ldapUrls[i];try{this.logger.debug(`Attempting connection to ${n}`);const e={...this.options,url:n,tlsOptions:n.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},s=new t(e);return await $("bind",()=>s.bind(this.dn,this.pwd)),i!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${n}`),this.currentUrlIndex=i),s}catch(t){this.logger.warn(`Failed to connect to ${n}: ${String(t)}`),e.push(t)}}throw this.logger.error(`LDAP connection failed for all ${this.ldapUrls.length} URL(s)`),new Error(`LDAP connection failed for all URLs: ${e.map(e=>e.message).join(", ")}`)}cleanupExpiredConnections(){if(!this.isCleaningUp){this.isCleaningUp=!0;try{const e=Date.now(),t=[];for(let s=this.availableConnections.length-1;s>=0;s--){const i=this.availableConnections[s];if(e-i.createdAt>this.connectionTtl){t.push(i),this.availableConnections.splice(s,1);const e=this.connectionPool.indexOf(i);-1!==e&&this.connectionPool.splice(e,1)}}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}finally{this.isCleaningUp=!1}}}async ping(e){if(this.closed)throw new Error("LDAP connections closed");const s=[];for(let i=0;i<this.ldapUrls.length;i++){const n=this.ldapUrls[(this.currentUrlIndex+i)%this.ldapUrls.length],a=new t({...this.options,url:n,timeout:e,connectTimeout:e,tlsOptions:n.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0});try{return await a.bind(this.dn,this.pwd),void await a.search("",{scope:"base",attributes:["1.1"]})}catch(e){s.push(`${n}: ${String(e)}`)}finally{await a.unbind().catch(()=>{})}}throw new Error(`No directory answered: ${s.join(", ")}`)}async close(){this.closed=!0;for(const e of this.waitingResolvers.splice(0))e(void 0);const e=this.connectionPool.splice(0);this.availableConnections.length=0,await Promise.allSettled(e.map(e=>e.client.unbind()))}async acquireConnection(){const e=process.hrtime.bigint();try{return await this.takeConnection()}finally{D.observe(L(e))}}poolStats(){return{busy:this.connectionPool.length-this.availableConnections.length,idle:this.availableConnections.length,waiting:this.waitingResolvers.length,max:this.poolSize}}async takeConnection(){if(this.closed)throw new Error("LDAP connections closed");if(this.cleanupExpiredConnections(),this.availableConnections.length>0){const e=this.availableConnections.pop();return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e}if(this.connectionPool.length<this.poolSize){const e=await C(()=>this.createConnection());if(this.closed)throw await e.unbind().catch(()=>{}),new Error("LDAP connections closed");const t={client:e,createdAt:Date.now(),inUse:!0};return this.connectionPool.push(t),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),t}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((e,t)=>{const s=globalThis.setTimeout(()=>{const e=this.waitingResolvers.indexOf(resolveWrapper);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),resolveWrapper=i=>{globalThis.clearTimeout(s),i?e(i):t(new Error("LDAP connections closed"))};this.waitingResolvers.push(resolveWrapper)})}releaseConnection(e){if(Date.now()-e.createdAt>this.connectionTtl){const t=this.connectionPool.indexOf(e);return-1!==t&&this.connectionPool.splice(t,1),e.client.unbind().catch(()=>{}),void this.logger.debug("Released expired LDAP connection (discarded)")}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t(e)}e.inUse=!1,this.availableConnections.push(e),this.logger.debug("Released LDAP connection back to pool")}getAttributeSignature(e){if(!e||0===e.length)return"*";const t=e.join("|");let s=this.attrSignatureCache.get(t);return s||(s=[...e].sort().join(","),this.attrSignatureCache.set(t,s)),s}getCacheKey(e,t){const s=this.getAttributeSignature(t.attributes),i=t.explicitBufferAttributes?.length?[...t.explicitBufferAttributes].sort().join(","):"-",n="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return[e,t.scope||"sub",n,s,i,!1===t.returnAttributeValues?"novalues":"values",t.derefAliases??"-",String(t.sizeLimit??"-")].join("\0")}invalidateCache(e){if(!this.cacheEnabled)return;this.cacheGeneration++;const t=foldDn(e),s=`,${t}`,i=[];for(const e of this.searchCache.keys()){const n=foldDn(e.split("\0")[0]);(n===t||n.endsWith(s))&&i.push(e)}for(const e of i)this.searchCache.delete(e)}invalidateReferences(e){if(!this.cacheEnabled)return;this.cacheGeneration++;const t=foldDn(e),s=`,${t}`,names=e=>{if("string"!=typeof e)return!1;const i=foldDn(e);return i===t||i.endsWith(s)},i=[];for(const[e,t]of this.searchCache.entries())t.searchEntries.some(e=>Object.values(e).some(e=>Array.isArray(e)?e.some(names):names(e)))&&i.push(e);for(const e of i)this.searchCache.delete(e)}get system(){return this.systemView??(this.systemView=new Proxy(this,{get:(e,t)=>{const s=Reflect.get(e,t,e);return"function"==typeof s&&N.has(t)?(...t)=>A(()=>s.apply(e,t)):s}})),this.systemView}forRequest(e){return{search:(t,s)=>this.search(t,s??this.base,e),add:(t,s)=>this.add(t,s,e),modify:(t,s,i)=>this.modify(t,s,e,i),rename:(t,s)=>this.rename(t,s,e),delete:t=>this.delete(t,e)}}async readableBy(e,t){if(t??(t=v()),!t)return!0;try{const s=await this.search({paged:!1,scope:"base",attributes:["1.1"]},e,t);return(s.searchEntries?.length??0)>0}catch(e){if(o(e)||403===e?.statusCode||32===R(e))return!1;throw e}}async authorize(e,t){const s=[...t];"ldapaddrequest"===e&&await this.canonicalAttributes(s[1]),"ldapmodifyrequest"===e&&await this.canonicalChanges(s[1]);const i=T[e];s[i]??(s[i]=v());const[n,...a]=s,r="ldapdeleterequest"===e?[[n].flat().map(e=>this.setDn(String(e))),...a]:[this.setDn(String(n)),...a];"ldaprenamerequest"===e&&(r[1]=this.setDn(String(r[1])));const o=(this.parent.hooks[e]??[]).filter(e=>{const t=c(e);return Boolean(t&&this.parent.loadedPlugins[t.plugin]?.roles?.includes("authz"))});await l(o,r)}declareObjectClasses(e,t,s=!1){const i=(Array.isArray(t)?t:[t]).filter(e=>"string"==typeof e&&""!==e);if(!e||0===i.length)return;const n=JSON.stringify([h(e),i.map(e=>e.toLowerCase()).sort(),s]);this.declaredClasses.set(n,{base:e,classes:i,includeBase:s})}declaredFor(e){let t=0,s=[];for(const i of this.declaredClasses.values()){if(!d(e,i.base))continue;const n=h(i.base);if(!i.includeBase&&h(e)===n)continue;const a=n.split(",").length;a>t&&(t=a,s=[]),a===t&&s.push(...i.classes)}const i=new Set;return s.filter(e=>{const t=e.toLowerCase();return!i.has(t)&&(i.add(t),!0)})}warnOnce(e){this.classWarnings.has(e)||(this.classWarnings.set(e,!0),this.logger.warn(e))}async classRepair(e,t,s=[]){if(!1===this.config.ldap_flat_auto_repair)return[];const i=[...this.declaredFor(e)];for(const e of s)i.some(t=>t.toLowerCase()===e.toLowerCase())||i.push(e);if(0===i.length)return[];const n=Array.isArray(t.delete)?t.delete:Object.entries(t.delete||{}).filter(([,e])=>!e||Array.isArray(e)&&!e.length).map(([e])=>e);if([...Object.keys(t.add||{}),...Object.keys(t.replace||{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete||{})].some(e=>"objectclass"===e.toLowerCase()))return[];let a;try{a=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){return this.schemaUnavailable||this.logger.warn(`Object classes are not repaired: the directory schema could not be read (${String(e)})`),this.schemaUnavailable=!0,[]}const attributeKey=e=>a.getAttributeType(e)?.oid??e.toLowerCase(),allowedBy=e=>{const{must:t,may:s}=a.resolveAttributes(e);return new Set([...t,...s].map(attributeKey))},valued=e=>Object.entries(e||{}).filter(([,e])=>Array.isArray(e)?e.length>0:null!=e&&""!==e).map(([e])=>e),r=[...valued(t.add),...valued(t.replace)],o=i.filter(e=>{const t=a.getObjectClass(e);if("AUXILIARY"!==t?.kind)return!1;const s=allowedBy([e]);return r.some(e=>s.has(attributeKey(e)))});if(0===o.length)return[];let c;try{c=(await this.system.search({paged:!1,scope:"base",attributes:["objectClass",...a.resolveAttributes(o).must]},e)).searchEntries[0]}catch{return[]}if(!c)return[];const l=c.objectClass??c.objectclass,h=(Array.isArray(l)?l:l?[l]:[]).map(String),d=allowedBy(h);let p=r.filter(e=>a.getAttributeType(e)&&!d.has(attributeKey(e)));if(0===p.length)return"allowed";const f=new Set(n.map(attributeKey)),u=new Set([...valued(c).filter(e=>!f.has(attributeKey(e))),...r].map(attributeKey)),{auxiliary:g,structural:y}=a.missingClasses(i,h),w=[];for(const t of g){const s=t.names[0]??t.oid,i=allowedBy([s]),n=p.filter(e=>i.has(attributeKey(e)));if(0===n.length)continue;const r=a.resolveAttributes([s]).must.filter(e=>!u.has(attributeKey(e)));r.length>0?this.warnOnce(`${e} needs ${s} for ${n.join(", ")}, and cannot be given it without ${r.join(", ")}`):(w.push(s),p=p.filter(e=>!n.includes(e)))}const m=y.filter(e=>{const t=allowedBy([e]);return p.some(e=>t.has(attributeKey(e)))});return m.length>0&&this.warnOnce(`${e} cannot hold ${p.join(", ")}: its schema declares ${m.join(", ")}, a structural class an existing entry cannot be given`),w.length>0&&this.logger.info(`${e}: adding ${w.join(", ")}, which the change needs and its schema declares`),w}async subschemaDn(){try{const e=await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["subschemaSubentry"]},""),t=e.searchEntries[0]?.subschemaSubentry,s=Array.isArray(t)?t[0]:t;if(s)return String(s)}catch(e){this.logger.warn(`Unable to read subschemaSubentry from root DSE: ${String(e)}`)}return"cn=Subschema"}async schemaIndex(){const e=1e3*(this.config.ldap_raw_schema_cache_ttl??3600);if(this.schemaCache&&Date.now()-this.schemaCache.fetchedAt<e)return this.schemaCache.index;if(this.schemaLoading)return this.schemaLoading;this.schemaLoading=(async()=>{const e=await this.subschemaDn(),t=(await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["objectClasses","attributeTypes","ldapSyntaxes","matchingRules"]},e).catch(t=>{if(32===t?.code)throw new x(`Subschema entry ${e} not found`);throw t})).searchEntries[0];if(!t)throw new x(`Subschema entry ${e} not found`);const list=e=>{if(void 0===e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString("utf8"):e)},s=new k(U({objectClasses:list(t.objectClasses),attributeTypes:list(t.attributeTypes),ldapSyntaxes:list(t.ldapSyntaxes),matchingRules:list(t.matchingRules)}));return this.schemaCache={index:s,fetchedAt:Date.now()},this.logger.info(`LDAP schema loaded from ${e}: ${s.schema.objectClasses.length} object classes, ${s.schema.attributeTypes.length} attribute types`),s})();try{return await this.schemaLoading}finally{this.schemaLoading=void 0}}knowAttributeNames(e){for(const t of e){if(!t||t.includes(";"))continue;const e=t.toLowerCase();this.spellings.has(e)||this.spellings.set(e,t);const s=(p(t)??t).toLowerCase(),i=this.spelledTypes.get(s);void 0===i?this.spelledTypes.set(s,t):i!==t&&this.logger.warn(`Attribute "${t}" is also named "${i}" in the configuration or a schema: plugins comparing with one name miss the other`)}}async canonicalAttributes(e){if(!e)return;const t=Object.keys(e),s=await this.canonicalNames(t);if(t.every((e,t)=>e===s[t]))return;const i=t.map(t=>e[t]);for(const s of t)delete e[s];s.forEach((t,s)=>e[t]=i[s])}async canonicalChanges(e){e&&(await this.canonicalAttributes(e.add),await this.canonicalAttributes(e.replace),Array.isArray(e.delete)?e.delete=await this.canonicalNames(e.delete):await this.canonicalAttributes(e.delete))}async canonicalNames(e){if(0===e.length)return e;let t;try{t=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){this.schemaUnavailable||this.logger.warn(`The directory schema could not be read (${String(e)}): attribute names are resolved from the standard types only, a name given as another OID is refused, and object classes are not repaired`),this.schemaUnavailable=!0}const s=new Map;return e.map(e=>{const i=this.canonicalName(e,t),n=s.get(i.toLowerCase());if(void 0!==n)throw new E(`"${n}" and "${e}" name the same attribute`);return s.set(i.toLowerCase(),e),i})}canonicalName(e,t){const[s,...i]=e.split(";"),n=s.replace(/^oid\./i,""),a=i.find(e=>"binary"!==e.toLowerCase());if(void 0!==a)throw new E(`Attribute option "${a}" of "${e}" is not accepted`);if(!/^(?:[A-Za-z][A-Za-z0-9-]*|\d+(?:\.\d+)+)$/.test(n))throw new E(`"${e}" is not an attribute description`);const r=t?.getAttributeType(n),o=t?void 0:p(n);if(!t&&!o&&/^\d+(\.\d+)+$/.test(n))throw new E(`Attribute "${e}" cannot be named by its OID here`);const c=(r?[...r.names,r.oid]:[n,...o?[o]:[]]).map(e=>this.spellings.get(e.toLowerCase())).find(Boolean)??r?.names[0]??o??n;return i.length?`${c};binary`:c}filterForCaller(e,t,s){if(!this.parent.hooks.ldapsearchfilter)return e;const i=this.parent.hooks.ldapsearchfilter,one=async e=>{const[n]=await l(i,[e,t,s]);return n??e};if("function"==typeof e[Symbol.asyncIterator]){const t=e;return async function*(){for await(const e of t)yield await one(e)}()}return one(e)}async search(e,t=this.base,s){s??(s=v());let i={...q,...e};i=await l(this.parent.hooks.ldapsearchopts,i),[t,i]=await l(this.parent.hooks.ldapsearchrequest,[t,i,s]);const n=this.cacheEnabled&&!i.paged&&"base"===i.scope,a=n?this.getCacheKey(t,i):"",r=this.cacheGeneration;if(n){const e=this.searchCache.get(a);if(j.inc({result:e?"hit":"miss"}),e)return this.logger.debug(`LDAP search cache hit: ${digestKey(a)}`),this.filterForCaller(cloneSearchResult(e),s,i)}const o=await this.acquireConnection();try{let e=i.paged?async function*(e){for await(const t of e)yield dropSelectors(t)}(o.client.searchPaginated(t,i)):$("search",()=>o.client.search(t,i)).then(dropSelectors);if(e=await l(this.parent.hooks.ldapsearchresult,e),n){const t=e;return r===this.cacheGeneration?(this.searchCache.set(a,cloneSearchResult(t)),this.logger.debug(`LDAP search cached: ${digestKey(a)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${digestKey(a)}`),this.filterForCaller(t,s,i)}return i.paged?this.filterForCaller(this.wrapPaginatedSearch(e,o),s,i):this.filterForCaller(e,s,i)}finally{i.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){let s=0,i="ok";try{for(;;){const t=process.hrtime.bigint();let n;try{n=await e.next()}catch(e){throw i=S(e),e}finally{s+=L(t)}if(n.done)return;yield n.value}}finally{try{await e.return(void 0)}catch{}finally{P.observe({operation:"search",result:i},s),this.releaseConnection(t)}}}async add(e,t,i){i??(i=v()),e=this.setDn(e),await this.canonicalAttributes(t),t.objectClass&&0!==t.objectClass.length||!this.config.user_class||(t.objectClass=this.config.user_class);const n={};for(const[e,s]of Object.entries(t))Buffer.isBuffer(s)?n[e]=s.toString():Array.isArray(s)&&s.length>0&&Buffer.isBuffer(s[0])?n[e]=s.map(e=>e.toString()):n[e]=s;[e,t]=await l(this.parent.hooks.ldapaddrequest,[e,n,i]);const a=membersNamed(t),r=[];for(const[e,i]of Object.entries(t)){assertHasValue(e,i);const t=Array.isArray(i)?i:[i];r.push(new s({type:e,values:t}))}const o=await this.acquireConnection();try{this.invalidateCache(e);for(const e of a)this.invalidateCache(e);await $("add",()=>o.client.add(e,r)),this.invalidateCache(e);for(const e of a)this.invalidateCache(e);f(this.parent.hooks.ldapadddone,[e,t],w(i))}catch(t){throw function(e,t){const s=R(t);return 68===s?withCode(new O(`Entry ${e} already exists`),s):32===s?withCode(new x(`${y(e)} does not exist`),s):20===s?ldapRefusal(`The directory refused this write to ${e}`,s,t):ldapWriteError("LDAP add error",e,t)}(e,t)}finally{this.releaseConnection(o)}return await f(this.parent.hooks.ldapaddafter,[e,t],w(i)),!0}async modify(e,t,s,i={}){s??(s=v());const n=this.opNumber(),a=[];try{return await m(n,()=>this.applyModify(e,t,s,i,n,a))}finally{this.endOperation(a,this.parent.hooks.ldapmodifyend,n)}}async applyModify(e,t,a,r,o,c){e=this.setDn(e);const h=[];if(await this.canonicalChanges(t),[e,t]=await l(this.parent.hooks.ldapmodifyrequest,[e,t,o,a]),t.add)for(const[e,n]of Object.entries(t.add))assertHasValue(e,n),h.push(new i({operation:"add",modification:new s({type:e,values:Array.isArray(n)?n:[n]})}));if(t.replace)for(const[e,n]of Object.entries(t.replace))h.push(new i({operation:"replace",modification:new s({type:e,values:Array.isArray(n)?n:[n]})}));if(t.delete)if(Array.isArray(t.delete))for(const e of t.delete)h.push(new i({operation:"delete",modification:new s({type:e,values:[]})}));else for(const[e,n]of Object.entries(t.delete)){const t=new i({operation:"delete",modification:new s(n?{type:e,values:Array.isArray(n)?n:[n]}:{type:e})});h.push(t)}const d=[t.add,t.replace,t.delete].flatMap(e=>Array.isArray(e)?e:Object.keys(e??{})).some(isMembershipAttribute),invalidate=()=>{if(this.invalidateCache(e),d){this.invalidateReferences(e);for(const e of[t.add,t.replace,t.delete])for(const t of membersNamed(Array.isArray(e)?{}:e))this.invalidateCache(t)}};if(0===h.length){return Object.keys(t.add||{}).length>0||Object.keys(t.replace||{}).length>0||(Array.isArray(t.delete)?t.delete.length>0:Object.keys(t.delete||{}).length>0)?this.logger.warn(`Modify on ${e} asked for changes but emitted none; nothing was written`):this.logger.debug(`Modify on ${e} had nothing to apply`),c.push(f(this.parent.hooks.ldapmodifydone,[e,{},o],r.context??w(a))),!1}{const l=r.permissive?[new n("1.2.840.113556.1.4.1413",{critical:!1})]:void 0,send=async()=>{const t=await this.acquireConnection();try{await $("modify",()=>t.client.modify(e,h,l))}finally{this.releaseConnection(t)}};try{invalidate();try{await send()}catch(n){if(65!==R(n))throw n;const a=await this.classRepair(e,t,r.declaredClasses);if("allowed"!==a&&0===a.length)throw n;const o="allowed"===a?void 0:new i({operation:"add",modification:new s({type:"objectClass",values:a})});o&&h.push(o),this.invalidateCache(e);try{await send()}catch(e){if(!o||20!==R(e))throw e;h.splice(h.indexOf(o),1),await send()}}return invalidate(),c.push(f(this.parent.hooks.ldapmodifydone,[e,t,o],r.context??w(a))),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(h)}`),function(e,t){const s=R(t);return 32===s?withCode(new x(`Entry ${e} not found`),s):16===s?withCode(new x(`Entry ${e} does not hold the value to delete`),s):65!==s&&20!==s?ldapWriteError("LDAP modify error",e,t):withCode(new O(`Entry ${e} cannot hold this change: ${String(t)}`),s)}(e,t)}}}async rename(e,t,s){s??(s=v()),e=this.setDn(e),t=this.setDn(t);const i=u(e),n=u(t);if(1!==n.length||!i.includes(n[0]))throw new E(`${t}: an entry is renamed by one value of the attribute it is named by`);const a=this.opNumber(),r=[];try{return await m(a,()=>this.applyRename(e,t,s,r))}finally{this.endOperation(r,this.parent.hooks.ldaprenameend,a)}}async applyRename(e,t,s,i){[e,t]=await l(this.parent.hooks.ldaprenamerequest,[e,t,s]);const n=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),await $("rename",()=>n.client.modifyDN(e,t)),this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),i.push(f(this.parent.hooks.ldaprenamedone,[e,t],w(s))),!0}catch(s){throw ldapRenameError("LDAP rename error",e,t,s)}finally{this.releaseConnection(n)}}async move(e,t,s){s??(s=v()),e=this.setDn(e),t=this.setDn(t);const i=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),await $("rename",()=>i.client.modifyDN(e,t)),this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),b(()=>f(this.parent.hooks.ldaprenamedone,[e,t],w(s))),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(s){throw ldapRenameError("LDAP move error",e,t,s)}finally{this.releaseConnection(i)}}async delete(e,t,s={}){t??(t=v());const i=(Array.isArray(e)?e:[e]).map(e=>this.setDn(e)),n=this.opNumber(),a=[];try{return await m(n,()=>this.applyDelete(i,t,s,a))}finally{this.endOperation(a,this.parent?.hooks.ldapdeleteend,n)}}async applyDelete(e,t,s,i){[e]=await l(this.parent?.hooks.ldapdeleterequest,[e,t]);const n=await this.acquireConnection();try{for(const a of e){try{this.invalidateCache(a),this.invalidateReferences(a),await $("delete",()=>n.client.del(a)),this.invalidateCache(a),this.invalidateReferences(a)}catch(e){const t=R(e);throw 32===t?withCode(new x(`Entry ${a} not found`),t):ldapError("LDAP delete error",e)}i.push(f(this.parent.hooks.ldapdeletedone,a,s.context??w(t)))}return!0}finally{this.releaseConnection(n)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${g(e)},${this.base}`,e}endOperation(e,t,...s){t?.length&&Promise.all(e).then(()=>f(t,...s))}opNumber(){return this.parent.operationSequence++}}export{assertModifyRequest,ldapActions as default};
@@ -1 +1 @@
1
- const t=new Set(["1.3.6.1.4.1.1466.115.121.1.5","1.3.6.1.4.1.1466.115.121.1.8","1.3.6.1.4.1.1466.115.121.1.9","1.3.6.1.4.1.1466.115.121.1.10","1.3.6.1.4.1.1466.115.121.1.28","1.3.6.1.4.1.1466.115.121.1.40","1.3.6.1.4.1.1466.115.121.1.49","1.3.6.1.1.16.1"]),e=new Set(["userpassword","jpegphoto","usercertificate","cacertificate","certificaterevocationlist","authoritiyrevocationlist","crosscertificatepair","photo","personalsignature","audio","thumbnailphoto","thumbnaillogo","objectguid","objectsid"]);function readList(t,e){if("("!==t[e])return{values:[t[e]],next:e+1};const s=[];let n=e+1;for(;n<t.length&&")"!==t[n];)"$"!==t[n]&&s.push(t[n]),n++;return{values:s,next:n+1}}function parseDefinition(t,e){const s=function(t){const e=[];let s=0;for(;s<t.length;){const n=t[s];if(" "===n||"\t"===n||"\n"===n||"\r"===n)s++;else if("("===n||")"===n)e.push(n),s++;else if("'"===n){let n="";for(s++;s<t.length&&"'"!==t[s];){if("\\"===t[s]&&s+2<t.length){const e=t.substring(s+1,s+3);if(/^[0-9a-fA-F]{2}$/.test(e)){n+=String.fromCharCode(parseInt(e,16)),s+=3;continue}}n+=t[s],s++}s++,e.push(n)}else{let n="";for(;s<t.length&&!/[\s()']/.test(t[s]);)n+=t[s],s++;e.push(n)}}return e}(t);if(s.length<2||"("!==s[0])return null;const n=s[1],i={};let o=2;for(;o<s.length&&")"!==s[o];){const t=s[o];if(o++,e.has(t)){const{values:e,next:n}=readList(s,o);i[t]=e,o=n}else if(t.startsWith("X-")){const{next:e}=readList(s,o);i[t]=[],o=e}else i[t]=[]}return{oid:n,fields:i}}const s=new Set(["NAME","DESC","SUP","EQUALITY","ORDERING","SUBSTR","SYNTAX","USAGE"]),n=new Set(["NAME","DESC","SUP","MUST","MAY"]),i=new Set(["DESC"]),o=new Set(["NAME","DESC","SUP","SYNTAX"]);function parseAttributeType(t){const e=parseDefinition(t,s);if(!e)return null;const{oid:n,fields:i}=e;let o,a=i.SYNTAX?.[0];if(a){const t=/^(.*?)\{(\d+)\}$/.exec(a);t&&(a=t[1],o=parseInt(t[2],10))}return{oid:n,names:i.NAME||[],desc:i.DESC?.[0],obsolete:"OBSOLETE"in i,sup:i.SUP?.[0],equality:i.EQUALITY?.[0],ordering:i.ORDERING?.[0],substr:i.SUBSTR?.[0],syntax:a,syntaxLength:o,singleValue:"SINGLE-VALUE"in i,collective:"COLLECTIVE"in i,noUserModification:"NO-USER-MODIFICATION"in i,usage:i.USAGE?.[0]||"userApplications"}}function parseObjectClass(t){const e=parseDefinition(t,n);if(!e)return null;const{oid:s,fields:i}=e,o="ABSTRACT"in i?"ABSTRACT":"AUXILIARY"in i?"AUXILIARY":"STRUCTURAL";return{oid:s,names:i.NAME||[],desc:i.DESC?.[0],obsolete:"OBSOLETE"in i,sup:i.SUP||[],kind:o,must:i.MUST||[],may:i.MAY||[]}}function parseSyntax(e){const s=parseDefinition(e,i);return s?{oid:s.oid,desc:s.fields.DESC?.[0],binary:t.has(s.oid)}:null}function parseMatchingRule(t){const e=parseDefinition(t,o);return e?{oid:e.oid,names:e.fields.NAME||[],desc:e.fields.DESC?.[0],syntax:e.fields.SYNTAX?.[0]}:null}function parseSchema(t){const parseAll=(t,e)=>(t||[]).map(e).filter(t=>null!==t);return{attributeTypes:parseAll(t.attributeTypes,parseAttributeType),objectClasses:parseAll(t.objectClasses,parseObjectClass),syntaxes:parseAll(t.ldapSyntaxes,parseSyntax),matchingRules:parseAll(t.matchingRules,parseMatchingRule)}}class SchemaIndex{constructor(t){this.objectClassesByName=new Map,this.attributeTypesByName=new Map,this.syntaxesByOid=new Map,this.schema=t;for(const e of t.objectClasses){this.objectClassesByName.set(e.oid.toLowerCase(),e);for(const t of e.names)this.objectClassesByName.set(t.toLowerCase(),e)}for(const e of t.attributeTypes){this.attributeTypesByName.set(e.oid.toLowerCase(),e);for(const t of e.names)this.attributeTypesByName.set(t.toLowerCase(),e)}for(const e of t.syntaxes)this.syntaxesByOid.set(e.oid,e)}getObjectClass(t){return this.objectClassesByName.get(t.toLowerCase())}getAttributeType(t){const e=t.split(";")[0];return this.attributeTypesByName.get(e.toLowerCase())}getAttributeSyntax(t){const e=new Set;let s=this.getAttributeType(t);for(;s&&!e.has(s.oid);){if(s.syntax)return s.syntax;e.add(s.oid),s=s.sup?this.getAttributeType(s.sup):void 0}}isBinaryAttribute(s){const n=s.split(";")[0].toLowerCase();if(s.split(";").slice(1).some(t=>"binary"===t.toLowerCase()))return!0;if(e.has(n))return!0;const i=this.getAttributeSyntax(n);return!!i&&t.has(i)}resolveAttributes(t){const e=new Set,s=new Set,n=new Set,walk=t=>{const i=t.toLowerCase();if(n.has(i))return;n.add(i);const o=this.getObjectClass(t);o&&(o.must.forEach(t=>e.add(t)),o.may.forEach(t=>s.add(t)),o.sup.forEach(walk))};t.forEach(walk);for(const t of e)for(const e of s)e.toLowerCase()===t.toLowerCase()&&s.delete(e);return{must:[...e],may:[...s]}}missingClasses(t,e){const s=new Set(e.map(t=>this.getObjectClass(t)?.oid??t.toLowerCase())),n=[],i=[];for(const e of t){const t=this.getObjectClass(e);t&&!s.has(t.oid)&&(s.add(t.oid),"AUXILIARY"===t.kind?n.push(t):"STRUCTURAL"===t.kind&&i.push(t.names[0]??e))}return{auxiliary:n,structural:i}}}export{SchemaIndex,parseAttributeType,parseMatchingRule,parseObjectClass,parseSchema,parseSyntax};
1
+ const t=new Set(["1.3.6.1.4.1.1466.115.121.1.5","1.3.6.1.4.1.1466.115.121.1.8","1.3.6.1.4.1.1466.115.121.1.9","1.3.6.1.4.1.1466.115.121.1.10","1.3.6.1.4.1.1466.115.121.1.28","1.3.6.1.4.1.1466.115.121.1.40","1.3.6.1.4.1.1466.115.121.1.49","1.3.6.1.1.16.1"]),e=new Set(["userpassword","jpegphoto","usercertificate","cacertificate","certificaterevocationlist","authoritiyrevocationlist","crosscertificatepair","photo","personalsignature","audio","thumbnailphoto","thumbnaillogo","objectguid","objectsid"]);function readList(t,e){if(e>=t.length||")"===t[e])return{values:[],next:e};if("("!==t[e])return{values:[t[e]],next:e+1};const s=[];let n=e+1;for(;n<t.length&&")"!==t[n];)"$"!==t[n]&&s.push(t[n]),n++;return{values:s,next:n+1}}function parseDefinition(t,e){const s=function(t){const e=[];let s=0;for(;s<t.length;){const n=t[s];if(/\s/.test(n))s++;else if("("===n||")"===n)e.push(n),s++;else if("'"===n){let n="";for(s++;s<t.length&&"'"!==t[s];){if("\\"===t[s]&&s+2<t.length){const e=t.substring(s+1,s+3);if(/^[0-9a-fA-F]{2}$/.test(e)){n+=String.fromCharCode(parseInt(e,16)),s+=3;continue}}n+=t[s],s++}s++,e.push(n)}else{let n="";for(;s<t.length&&!/[\s()']/.test(t[s]);)n+=t[s],s++;e.push(n)}}return e}(t);if(s.length<2||"("!==s[0])return null;const n=s[1],i={};let o=2;for(;o<s.length&&")"!==s[o];){const t=s[o];if(o++,e.has(t)){const{values:e,next:n}=readList(s,o);i[t]=e,o=n}else if(t.startsWith("X-")){const{next:e}=readList(s,o);i[t]=[],o=e}else i[t]=[]}return{oid:n,fields:i}}const s=new Set(["NAME","DESC","SUP","EQUALITY","ORDERING","SUBSTR","SYNTAX","USAGE"]),n=new Set(["NAME","DESC","SUP","MUST","MAY"]),i=new Set(["DESC"]),o=new Set(["NAME","DESC","SUP","SYNTAX"]);function parseAttributeType(t){const e=parseDefinition(t,s);if(!e)return null;const{oid:n,fields:i}=e;let o,a=i.SYNTAX?.[0];if(a){const t=/^(.*?)\{(\d+)\}$/.exec(a);t&&(a=t[1],o=parseInt(t[2],10))}return{oid:n,names:i.NAME||[],desc:i.DESC?.[0],obsolete:"OBSOLETE"in i,sup:i.SUP?.[0],equality:i.EQUALITY?.[0],ordering:i.ORDERING?.[0],substr:i.SUBSTR?.[0],syntax:a,syntaxLength:o,singleValue:"SINGLE-VALUE"in i,collective:"COLLECTIVE"in i,noUserModification:"NO-USER-MODIFICATION"in i,usage:i.USAGE?.[0]||"userApplications"}}function parseObjectClass(t){const e=parseDefinition(t,n);if(!e)return null;const{oid:s,fields:i}=e,o="ABSTRACT"in i?"ABSTRACT":"AUXILIARY"in i?"AUXILIARY":"STRUCTURAL";return{oid:s,names:i.NAME||[],desc:i.DESC?.[0],obsolete:"OBSOLETE"in i,sup:i.SUP||[],kind:o,must:i.MUST||[],may:i.MAY||[]}}function parseSyntax(e){const s=parseDefinition(e,i);return s?{oid:s.oid,desc:s.fields.DESC?.[0],binary:t.has(s.oid)}:null}function parseMatchingRule(t){const e=parseDefinition(t,o);return e?{oid:e.oid,names:e.fields.NAME||[],desc:e.fields.DESC?.[0],syntax:e.fields.SYNTAX?.[0]}:null}function parseSchema(t){const parseAll=(t,e)=>(t||[]).map(e).filter(t=>null!==t);return{attributeTypes:parseAll(t.attributeTypes,parseAttributeType),objectClasses:parseAll(t.objectClasses,parseObjectClass),syntaxes:parseAll(t.ldapSyntaxes,parseSyntax),matchingRules:parseAll(t.matchingRules,parseMatchingRule)}}class SchemaIndex{constructor(t){this.objectClassesByName=new Map,this.attributeTypesByName=new Map,this.syntaxesByOid=new Map,this.schema=t;for(const e of t.objectClasses){this.objectClassesByName.set(e.oid.toLowerCase(),e);for(const t of e.names)this.objectClassesByName.set(t.toLowerCase(),e)}for(const e of t.attributeTypes){this.attributeTypesByName.set(e.oid.toLowerCase(),e);for(const t of e.names)this.attributeTypesByName.set(t.toLowerCase(),e)}for(const e of t.syntaxes)this.syntaxesByOid.set(e.oid,e)}getObjectClass(t){return this.objectClassesByName.get(t.toLowerCase())}getAttributeType(t){const e=t.split(";")[0];return this.attributeTypesByName.get(e.toLowerCase())}getAttributeSyntax(t){const e=new Set;let s=this.getAttributeType(t);for(;s&&!e.has(s.oid);){if(s.syntax)return s.syntax;e.add(s.oid),s=s.sup?this.getAttributeType(s.sup):void 0}}isBinaryAttribute(s){const n=s.split(";")[0].toLowerCase();if(s.split(";").slice(1).some(t=>"binary"===t.toLowerCase()))return!0;if(e.has(n))return!0;const i=this.getAttributeSyntax(n);return!!i&&t.has(i)}resolveAttributes(t){const e=new Set,s=new Set,n=new Set,walk=t=>{const i=t.toLowerCase();if(n.has(i))return;n.add(i);const o=this.getObjectClass(t);o&&(o.must.forEach(t=>e.add(t)),o.may.forEach(t=>s.add(t)),o.sup.forEach(walk))};t.forEach(walk);for(const t of e)for(const e of s)e.toLowerCase()===t.toLowerCase()&&s.delete(e);return{must:[...e],may:[...s]}}missingClasses(t,e){const s=new Set(e.map(t=>this.getObjectClass(t)?.oid??t.toLowerCase())),n=[],i=[];for(const e of t){const t=this.getObjectClass(e);t&&!s.has(t.oid)&&(s.add(t.oid),"AUXILIARY"===t.kind?n.push(t):"STRUCTURAL"===t.kind&&i.push(t.names[0]??e))}return{auxiliary:n,structural:i}}}export{SchemaIndex,parseAttributeType,parseMatchingRule,parseObjectClass,parseSchema,parseSyntax};
package/dist/lib/utils.js CHANGED
@@ -1 +1 @@
1
- import{BadRequestError as e}from"./errors.js";import{getLogger as n}from"./expressFormatedResponses.js";import{hookFailures as t}from"./metrics.js";import{runAsPlugin as r,outsideRequest as o}from"./requestContext.js";const i=new Map;function getCompiledRegex(e,n){const t=n?`${e}:${n}`:e;let r=i.get(t);return r||(r=new RegExp(e,n),i.set(t,r)),r}function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const describeThrown=e=>{try{return JSON.stringify(e)??String(e)}catch{return String(e)}},hookLabel=e=>{const n=hookOwner(e);return n?` in ${n.plugin} (${n.hook})`:e.name?` in ${e.name}`:""},runAsHookOwner=(e,n)=>{const t=hookOwner(e);return r(t?.plugin,t?.hook,n)},launchHooks=async(e,...r)=>{if(e)for(const i of e)i&&await runAsHookOwner(i,async()=>{try{await o(()=>i(...r))}catch(e){const r=hookLabel(i),o=hookOwner(i);t.inc({hook:o?.hook??(i.name||"anonymous"),plugin:o?.plugin??"none"}),e instanceof Error?n()?.error(`Hook error${r}`,e):n()?.error(`Hook error${r}: ${describeThrown(e)}`)}})},s=new WeakMap,recordHookOwner=(e,n,t)=>{s.set(e,{plugin:n,hook:t})},hookOwner=e=>s.get(e),isAuthzRefusal=e=>/\[authz-forbidden\]/.test(String(e?.message??"")),isRefusal=e=>403===e?.statusCode||isAuthzRefusal(e),launchHooksChained=async(e,t)=>{if(e)for(const r of e)r&&(t=await runAsHookOwner(r,async()=>{try{return await o(()=>r(t))}catch(e){if(isRefusal(e)){const t=hookOwner(r);n()?.warn(`${t?`${t.plugin} refused ${t.hook}`:`a hook no plugin registered (${r.name||"anonymous"}) refused`}: ${e.message}`)}throw e}}));return t},transformSchemas=(e,n)=>e.toString().replace(/__(\S+)__/g,(e,t)=>{if(!t||"string"!=typeof t)return e;const r=t.trim().toLowerCase();return n[r]?"object"!=typeof n[r]?n[r]:JSON.stringify(n[r]):e});function escapeLdapFilter(e){return e.replace(/\\/g,"\\5c").replace(/\*/g,"\\2a").replace(/\(/g,"\\28").replace(/\)/g,"\\29").replace(/\0/g,"\\00")}function substringSearchFilter(n,t){const r=/^[a-zA-Z][a-zA-Z0-9-]*$/,o=t.split(",").map(e=>e.trim()).filter(Boolean);if(0===o.length||!o.every(e=>r.test(e)))throw new e("Invalid LDAP attribute name");const i=escapeLdapFilter(n),s=o.map(e=>`(${e}=*${i}*)`);return 1===s.length?s[0]:`(|${s.join("")})`}function escapeDnValue(e){return e.replace(/\\/g,"\\\\").replace(/,/g,"\\,").replace(/\+/g,"\\+").replace(/"/g,'\\"').replace(/</g,"\\<").replace(/>/g,"\\>").replace(/;/g,"\\;").replace(/=/g,"\\=").replace(/\0/g,"\\00").replace(/^\s/,"\\ ").replace(/\s$/,"\\ ").replace(/^#/,"\\#")}const a=new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0});function unescapeDnValue(n){if("string"!=typeof n)throw new e("DN value must be a string");let t="",r=0,o=[];const flushBytes=()=>{if(o.length){try{t+=a.decode(new Uint8Array(o))}catch{t+=o.map(e=>String.fromCharCode(e)).join("")}o=[]}};for(;r<n.length;)if("\\"===n[r]&&r+1<n.length){const e=n[r+1];if(/[0-9a-fA-F]/.test(e)&&r+2<n.length){const e=n.substring(r+1,r+3);if(/^[0-9a-fA-F]{2}$/.test(e)){o.push(parseInt(e,16)),r+=3;continue}}flushBytes(),t+=e,r+=2}else flushBytes(),t+=n[r],r++;return flushBytes(),t}function validateDnValue(e,n){if(null==e||"string"!=typeof e)throw new Error(`${n} must be a string`);if(0===e.trim().length)throw new Error(`${n} must be a non-empty string`);if(/[\x00-\x1F\x7F]/.test(e))throw new Error(`${n} contains invalid control characters`);if(/[\u200B-\u200F\uFEFF]/.test(e))throw new Error(`${n} contains invalid invisible characters`)}function asciiSpace(e){return" "===e||e>="\t"&&e<="\r"}function trimUnescaped(e){let n=0,t=e.length;for(;n<t&&asciiSpace(e[n]);)n++;for(;t>n&&asciiSpace(e[t-1]);)t--;if(t<e.length){let r=0;for(let o=t-1;o>=n&&"\\"===e[o];o--)r++;r%2==1&&t++}return e.slice(n,t)}function parseDn(n){if("string"!=typeof n)throw new e("DN must be a string");const t=[];let r="",o=!1,i=!1;for(let e=0;e<n.length;e++){const s=n[e];o?(r+=s,o=!1):"\\"===s?(r+=s,o=!0):'"'===s&&(i||opensValue(r))?(r+=s,i=!i):i||","!==s&&";"!==s?r+=s:(t.push(trimUnescaped(r)),r="")}if(i)throw new e("DN has an unbalanced quote");return r&&t.push(trimUnescaped(r)),t}function opensValue(e){const n=e.trimEnd();if(!n.endsWith("="))return!1;let t=0;for(let e=n.length-2;e>=0&&"\\"===n[e];e--)t++;return t%2==0}function isDnSyntax(e){let n;try{n=parseDn(e)}catch{return!1}if(0===n.length)return!1;const t=/^\s*(?:[A-Za-z][A-Za-z0-9-]*|(?:OID\.)?\d+(?:\.\d+)+)\s*=/i;return n.every(e=>{if(!e)return!1;const n=[];let r="",o=!1;for(const t of e){if(o)o=!1;else if("\\"===t)o=!0;else if("+"===t){n.push(r),r="";continue}r+=t}return n.push(r),n.every(e=>t.test(e))})}function getParentDn(e){const n=parseDn(e);return n.length<=1?e:n.slice(1).join(",")}function getRdn(e){return parseDn(e)[0]||""}function rdnValue(e){const n=/^[^=]+=((?:\\.|[^,])*)/.exec(e);return n?n[1].replace(/\\(.)/g,"$1"):""}function isChildOf(e,n){try{return isDnInBranch(e,n)&&!isDnInBranch(n,e)}catch{return!1}}const c=/((?:\\[0-9a-fA-F]{2})+)|\\([^])/g;function hexByte(e){return e.toString(16).padStart(2,"0")}const l=new Map,u=new Map;function registerAttributeTypes(e){for(const{oid:n,names:t}of e){const e=(t[0]??n).toLowerCase();for(const r of[n,...t])l.set(r.toLowerCase(),e),u.set(r.toLowerCase(),t[0]??n)}}function knownAttributeName(e){return u.get(e.toLowerCase().replace(/^oid\./,""))}function rdnTypes(e){return splitRdn(parseDn(e)[0]??"").map(e=>{const n=e.indexOf("=");return-1===n?"":canonicalType(e.slice(0,n).trim())})}function canonicalType(e){const n=e.toLowerCase().replace(/^oid\./,"");return l.get(n)??n}function splitRdn(e){const n=[];let t="",r=!1,o=!1;for(const i of e){if(r)r=!1;else if("\\"===i)r=!0;else if('"'===i&&(o||opensValue(t)))o=!o;else if("+"===i&&!o){n.push(t),t="";continue}t+=i}return n.push(t),n}function normalizeDn(e){return normalizedRdns(e).join(",")}function normalizedRdns(e){return parseDn(e).map(e=>splitRdn(e).map(e=>{const n=e.indexOf("=");return-1===n?e.trim().toLowerCase():`${canonicalType(e.slice(0,n).trim())}=${function(e){let n=0,t=e.length;for(;n<t&&asciiSpace(e[n]);)n++;for(;t>n&&asciiSpace(e[t-1]);)t--;const r=e.slice(n,t);if(r.startsWith("#"))return r;let o=0;for(let e=r.length-2;e>0&&"\\"===r[e];e--)o++;r.length>=2&&r.startsWith('"')&&r.endsWith('"')&&o%2==0&&(e=r.slice(1,-1));const i=[];let s=0;const plain=e=>{for(const n of e)i.push(n)};for(const n of e.matchAll(c)){plain(e.slice(s,n.index)),s=n.index+n[0].length;const[,t,r]=n;if(void 0!==r){i.push(r);continue}const o=t.split("\\").filter(Boolean).map(e=>parseInt(e,16));try{plain(new TextDecoder("utf-8",{fatal:!0}).decode(Uint8Array.from(o)))}catch{for(const e of o)i.push(e<128?String.fromCharCode(e):`\\${hexByte(e)}`)}}s<e.length&&plain(e.slice(s));const kept=e=>3===e.length&&"\\"===e[0],a=[];let l="";for(const e of i)kept(e)?(a.push(...l.normalize("NFKC"),e),l=""):l+=e;a.push(...l.normalize("NFKC"));const u=a.filter((e,n)=>" "!==e||n>0&&" "!==a[n-1]);let f=0,p=u.length;for(;f<p&&" "===u[f];)f++;for(;p>f&&" "===u[p-1];)p--;let m="";for(let e=f;e<p;e++){const n=u[e];kept(n)?m+=n:n.charCodeAt(0)<32||127===n.charCodeAt(0)?m+=`\\${hexByte(n.charCodeAt(0))}`:',+"\\<>;='.includes(n)||e===f&&"#"===n?m+=`\\${n}`:m+=n}return m}(e.slice(n+1)).toLowerCase()}`}).sort().join("+")).filter(e=>e.length>0)}function isDummyMemberDn(e,n){if(!n||"string"!=typeof e)return!1;if(e===n)return!0;try{return normalizeDn(e)===normalizeDn(n)}catch{return!1}}function organizationLink(e){for(const n of Array.isArray(e)?e:[e]){if(null==n)continue;const e=String(n).trim();if(e.length>0)return e}}function isDnInBranch(e,n){const t=normalizedRdns(n);if(0===t.length)return!1;const r=normalizedRdns(e);if(r.length<t.length)return!1;return r.slice(r.length-t.length).every((e,n)=>e===t[n])}function movedToTrash(e,n,t){return!!e&&isDnInBranch(t,e)&&!isDnInBranch(n,e)}registerAttributeTypes([{oid:"2.5.4.0",names:["objectClass"]},{oid:"2.5.4.3",names:["cn","commonName"]},{oid:"2.5.4.4",names:["sn","surname"]},{oid:"2.5.4.5",names:["serialNumber"]},{oid:"2.5.4.6",names:["c","countryName"]},{oid:"2.5.4.7",names:["l","localityName"]},{oid:"2.5.4.8",names:["st","stateOrProvinceName"]},{oid:"2.5.4.9",names:["street","streetAddress"]},{oid:"2.5.4.10",names:["o","organizationName"]},{oid:"2.5.4.11",names:["ou","organizationalUnitName"]},{oid:"2.5.4.12",names:["title"]},{oid:"2.5.4.42",names:["givenName","gn"]},{oid:"2.5.4.43",names:["initials"]},{oid:"2.5.4.46",names:["dnQualifier"]},{oid:"2.5.4.65",names:["pseudonym"]},{oid:"0.9.2342.19200300.100.1.1",names:["uid","userid"]},{oid:"0.9.2342.19200300.100.1.3",names:["mail","rfc822Mailbox"]},{oid:"0.9.2342.19200300.100.1.25",names:["dc","domainComponent"]},{oid:"2.16.840.1.113730.3.1.241",names:["displayName"]},{oid:"2.16.840.1.113730.3.1.3",names:["employeeNumber"]},{oid:"1.3.6.1.1.1.1.0",names:["uidNumber"]},{oid:"1.3.6.1.1.1.1.1",names:["gidNumber"]}]);const asyncHandler=e=>(n,t,r)=>{Promise.resolve(e(n,t,r)).catch(r)};export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,hookOwner,isAuthzRefusal,isChildOf,isDnInBranch,isDnSyntax,isDummyMemberDn,knownAttributeName,launchHooks,launchHooksChained,movedToTrash,normalizeDn,organizationLink,parseDn,rdnTypes,rdnValue,recordHookOwner,registerAttributeTypes,runAsHookOwner,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue};
1
+ import{BadRequestError as e}from"./errors.js";import{getLogger as n}from"./expressFormatedResponses.js";import{hookFailures as t}from"./metrics.js";import{runAsPlugin as r,outsideRequest as o}from"./requestContext.js";const i=new Map;function getCompiledRegex(e,n){const t=n?`${e}:${n}`:e;let r=i.get(t);return r||(r=new RegExp(e,n),i.set(t,r)),r}function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const describeThrown=e=>{try{return JSON.stringify(e)??String(e)}catch{return String(e)}},hookLabel=e=>{const n=hookOwner(e);return n?` in ${n.plugin} (${n.hook})`:e.name?` in ${e.name}`:""},runAsHookOwner=(e,n)=>{const t=hookOwner(e);return r(t?.plugin,t?.hook,n)},launchHooks=async(e,...r)=>{if(e)for(const i of e)i&&await runAsHookOwner(i,async()=>{try{await o(()=>i(...r))}catch(e){const r=hookLabel(i),o=hookOwner(i);t.inc({hook:o?.hook??(i.name||"anonymous"),plugin:o?.plugin??"none"}),e instanceof Error?n()?.error(`Hook error${r}`,e):n()?.error(`Hook error${r}: ${describeThrown(e)}`)}})},s=new WeakMap,recordHookOwner=(e,n,t)=>{s.set(e,{plugin:n,hook:t})},hookOwner=e=>s.get(e),isAuthzRefusal=e=>/\[authz-forbidden\]/.test(String(e?.message??"")),isRefusal=e=>403===e?.statusCode||isAuthzRefusal(e),launchHooksChained=async(e,t)=>{if(e)for(const r of e)r&&(t=await runAsHookOwner(r,async()=>{try{return await o(()=>r(t))}catch(e){if(isRefusal(e)){const t=hookOwner(r);n()?.warn(`${t?`${t.plugin} refused ${t.hook}`:`a hook no plugin registered (${r.name||"anonymous"}) refused`}: ${e.message}`)}throw e}}));return t},transformSchemas=(e,n)=>e.toString().replace(/__(\S+)__/g,(e,t)=>{if(!t||"string"!=typeof t)return e;const r=t.trim().toLowerCase();return n[r]?"object"!=typeof n[r]?n[r]:JSON.stringify(n[r]):e});function escapeLdapFilter(e){return e.replace(/\\/g,"\\5c").replace(/\*/g,"\\2a").replace(/\(/g,"\\28").replace(/\)/g,"\\29").replace(/\0/g,"\\00")}function substringSearchFilter(n,t){const r=/^[a-zA-Z][a-zA-Z0-9-]*$/,o=t.split(",").map(e=>e.trim()).filter(Boolean);if(0===o.length||!o.every(e=>r.test(e)))throw new e("Invalid LDAP attribute name");const i=escapeLdapFilter(n),s=o.map(e=>`(${e}=*${i}*)`);return 1===s.length?s[0]:`(|${s.join("")})`}function fillPattern(e,n,t){return e.split(n).join(t)}function escapeDnValue(e){let n=e.replace(/[\\,+"<>;=]/g,e=>`\\${e}`).replace(/[\0-\x1f\x7f]/g,e=>`\\${hexByte(e.charCodeAt(0))}`);return e.startsWith(" ")&&(n=`\\${n}`),e.length>1&&e.endsWith(" ")&&(n=`${n.slice(0,-1)}\\ `),e.startsWith("#")&&(n=`\\${n}`),n}const a=new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0});function unescapeDnValue(n){if("string"!=typeof n)throw new e("DN value must be a string");let t="",r=0,o=[];const flushBytes=()=>{if(o.length){for(const e of utf8Chars(o))t+="number"==typeof e?String.fromCharCode(e):e;o=[]}};for(;r<n.length;)if("\\"===n[r]&&r+1<n.length){const e=n[r+1];if(/[0-9a-fA-F]/.test(e)&&r+2<n.length){const e=n.substring(r+1,r+3);if(/^[0-9a-fA-F]{2}$/.test(e)){o.push(parseInt(e,16)),r+=3;continue}}flushBytes(),t+=e,r+=2}else flushBytes(),t+=n[r],r++;return flushBytes(),t}function validateDnValue(e,n){if(null==e||"string"!=typeof e)throw new Error(`${n} must be a string`);if(0===e.trim().length)throw new Error(`${n} must be a non-empty string`);if(/[\x00-\x1F\x7F]/.test(e))throw new Error(`${n} contains invalid control characters`);if(/[\u200B-\u200F\uFEFF]/.test(e))throw new Error(`${n} contains invalid invisible characters`)}function asciiSpace(e){return" "===e||e>="\t"&&e<="\r"}function trimUnescaped(e){let n=0,t=e.length;for(;n<t&&asciiSpace(e[n]);)n++;for(;t>n&&asciiSpace(e[t-1]);)t--;if(t<e.length){let r=0;for(let o=t-1;o>=n&&"\\"===e[o];o--)r++;r%2==1&&t++}return e.slice(n,t)}function parseDn(n){if("string"!=typeof n)throw new e("DN must be a string");const t=[];let r="",o=!1,i=!1;for(let e=0;e<n.length;e++){const s=n[e];o?(r+=s,o=!1):"\\"===s?(r+=s,o=!0):'"'===s&&(i||opensValue(r))?(r+=s,i=!i):i||","!==s&&";"!==s?r+=s:(t.push(trimUnescaped(r)),r="")}if(i)throw new e("DN has an unbalanced quote");return r&&t.push(trimUnescaped(r)),t}function opensValue(e){const n=e.trimEnd();if(!n.endsWith("="))return!1;let t=0;for(let e=n.length-2;e>=0&&"\\"===n[e];e--)t++;return t%2==0}function isDnSyntax(e){let n;try{n=parseDn(e)}catch{return!1}if(0===n.length)return!1;const t=/^\s*(?:[A-Za-z][A-Za-z0-9-]*|(?:OID\.)?\d+(?:\.\d+)+)\s*=/i;return n.every(e=>{if(!e)return!1;const n=[];let r="",o=!1;for(const t of e){if(o)o=!1;else if("\\"===t)o=!0;else if("+"===t){n.push(r),r="";continue}r+=t}return n.push(r),n.every(e=>t.test(e))})}function getParentDn(e){const n=parseDn(e);return n.length<=1?e:n.slice(1).join(",")}function getRdn(e){return parseDn(e)[0]||""}function rdnValue(e){const[n]=parseDn(e),t=n?n.indexOf("="):-1;if(t<=0)return"";let r=t+1;for(;r<n.length&&asciiSpace(n[r]);)r++;return unescapeDnValue(n.slice(r))}function isChildOf(e,n){try{return isDnInBranch(e,n)&&!isDnInBranch(n,e)}catch{return!1}}const c=/((?:\\[0-9a-fA-F]{2})+)|\\([^])/g;function hexByte(e){return e.toString(16).padStart(2,"0")}function utf8Chars(e){const n=[],t=Uint8Array.from(e);for(let r=0;r<e.length;){const o=utf8Length(e[r]);let i;if(o&&r+o<=e.length)try{i=a.decode(t.subarray(r,r+o))}catch{}void 0===i?n.push(e[r++]):(n.push(...i),r+=o)}return n}function utf8Length(e){return e<128?1:e>>5==6?2:e>>4==14?3:e>>3==30?4:0}const u=new Map,l=new Map;function registerAttributeTypes(e){for(const{oid:n,names:t}of e){const e=(t[0]??n).toLowerCase();for(const r of[n,...t])u.set(r.toLowerCase(),e),l.set(r.toLowerCase(),t[0]??n)}}function knownAttributeName(e){return l.get(e.toLowerCase().replace(/^oid\./,""))}function rdnTypes(e){return splitRdn(parseDn(e)[0]??"").map(e=>{const n=e.indexOf("=");return-1===n?"":canonicalType(e.slice(0,n).trim())})}function canonicalType(e){const n=e.toLowerCase().replace(/^oid\./,"");return u.get(n)??n}function splitRdn(e){const n=[];let t="",r=!1,o=!1;for(const i of e){if(r)r=!1;else if("\\"===i)r=!0;else if('"'===i&&(o||opensValue(t)))o=!o;else if("+"===i&&!o){n.push(t),t="";continue}t+=i}return n.push(t),n}function normalizeDn(e){return normalizedRdns(e).join(",")}function normalizedRdns(e){return parseDn(e).map(e=>splitRdn(e).map(e=>{const n=e.indexOf("=");return-1===n?e.trim().toLowerCase():`${canonicalType(e.slice(0,n).trim())}=${function(e){const n=trimUnescaped(e);if(/^#(?:[0-9a-fA-F]{2})+$/.test(n))return n;let t=0;for(let e=n.length-2;e>0&&"\\"===n[e];e--)t++;e=n.length>=2&&n.startsWith('"')&&n.endsWith('"')&&t%2==0?n.slice(1,-1):n;const r=[];let o=0;const plain=e=>{for(const n of e)r.push(n)};for(const n of e.matchAll(c)){plain(e.slice(o,n.index)),o=n.index+n[0].length;const[,t,i]=n;if(void 0!==i){r.push(i);continue}const s=t.split("\\").filter(Boolean).map(e=>parseInt(e,16));for(const e of utf8Chars(s))"number"==typeof e?r.push(`\\${hexByte(e)}`):plain(e)}o<e.length&&plain(e.slice(o));const kept=e=>3===e.length&&"\\"===e[0],i=[];let s="";for(const e of r)kept(e)?(i.push(...s.toLowerCase().normalize("NFKC"),e),s=""):s+=e;i.push(...s.toLowerCase().normalize("NFKC"));const a=i.filter((e,n)=>" "!==e||n>0&&" "!==i[n-1]);let u=0,l=a.length;for(;u<l&&" "===a[u];)u++;for(;l>u&&" "===a[l-1];)l--;let f="";for(let e=u;e<l;e++){const n=a[e];kept(n)?f+=n:n.charCodeAt(0)<32||127===n.charCodeAt(0)?f+=`\\${hexByte(n.charCodeAt(0))}`:',+"\\<>;='.includes(n)||e===u&&"#"===n?f+=`\\${n}`:f+=n}return f}(e.slice(n+1)).toLowerCase()}`}).sort().join("+")).filter(e=>e.length>0)}function isDummyMemberDn(e,n){if(!n||"string"!=typeof e)return!1;if(e===n)return!0;try{return normalizeDn(e)===normalizeDn(n)}catch{return!1}}function organizationLink(e){for(const n of Array.isArray(e)?e:[e]){if(null==n)continue;const e=String(n).trim();if(e.length>0)return e}}function isDnInBranch(e,n){const t=normalizedRdns(n);if(0===t.length)return!1;const r=normalizedRdns(e);if(r.length<t.length)return!1;return r.slice(r.length-t.length).every((e,n)=>e===t[n])}function movedToTrash(e,n,t){return!!e&&isDnInBranch(t,e)&&!isDnInBranch(n,e)}registerAttributeTypes([{oid:"2.5.4.0",names:["objectClass"]},{oid:"2.5.4.3",names:["cn","commonName"]},{oid:"2.5.4.4",names:["sn","surname"]},{oid:"2.5.4.5",names:["serialNumber"]},{oid:"2.5.4.6",names:["c","countryName"]},{oid:"2.5.4.7",names:["l","localityName"]},{oid:"2.5.4.8",names:["st","stateOrProvinceName"]},{oid:"2.5.4.9",names:["street","streetAddress"]},{oid:"2.5.4.10",names:["o","organizationName"]},{oid:"2.5.4.11",names:["ou","organizationalUnitName"]},{oid:"2.5.4.12",names:["title"]},{oid:"2.5.4.42",names:["givenName","gn"]},{oid:"2.5.4.43",names:["initials"]},{oid:"2.5.4.46",names:["dnQualifier"]},{oid:"2.5.4.65",names:["pseudonym"]},{oid:"0.9.2342.19200300.100.1.1",names:["uid","userid"]},{oid:"0.9.2342.19200300.100.1.3",names:["mail","rfc822Mailbox"]},{oid:"0.9.2342.19200300.100.1.25",names:["dc","domainComponent"]},{oid:"2.16.840.1.113730.3.1.241",names:["displayName"]},{oid:"2.16.840.1.113730.3.1.3",names:["employeeNumber"]},{oid:"1.3.6.1.1.1.1.0",names:["uidNumber"]},{oid:"1.3.6.1.1.1.1.1",names:["gidNumber"]}]);const asyncHandler=e=>(n,t,r)=>{Promise.resolve(e(n,t,r)).catch(r)};export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,fillPattern,getCompiledRegex,getParentDn,getRdn,hookOwner,isAuthzRefusal,isChildOf,isDnInBranch,isDnSyntax,isDummyMemberDn,knownAttributeName,launchHooks,launchHooksChained,movedToTrash,normalizeDn,organizationLink,parseDn,rdnTypes,rdnValue,recordHookOwner,registerAttributeTypes,runAsHookOwner,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue};
@@ -1 +1 @@
1
- import t from"../../abstract/plugin.js";import{roleAttribute as e}from"../../config/schema.js";import{isDnInBranch as a,parseDn as r,normalizeDn as n,getParentDn as i,rdnValue as s,escapeLdapFilter as o}from"../../lib/utils.js";class LdapDepartmentSync extends t{constructor(t){super(t),this.name="ldapDepartmentSync",this.roles=["consistency"],this.hooks={ldaprenamedone:async([t,e])=>{if(/^ou=/.test(t)){this.logger.info(`Organization renamed from ${t} to ${e}, synchronizing linked resources...`);try{const a=this.config.ldap_top_organization.replace(/^ou=[^,]+,/,"");await this.updateOrganizationPaths(e);for(const r of this.linkedAttributes())await this.updateLinkedResources(t,e,a,r),await this.updateDescendantReferences(t,e,a,r)}catch(t){this.logger.error(`Error synchronizing department links after rename: ${t}`)}}}},this.linkAttr=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",this.pathAttr=this.config.ldap_organization_path_attribute||"twakeDepartmentPath"}organizationPathAttribute(){const t=this.server.loadedPlugins.ldapOrganizations;return e(t?.schema,"organizationPath")||this.pathAttr}linkedAttributes(){const t=[],a=this.server.loadedPlugins.ldapFlatGeneric;for(const e of a?.instances||[])t.push(e.schema);const r=this.server.loadedPlugins.ldapGroups;t.push(r?.schema);const n=[];for(const a of t){const t=e(a,"organizationLink");t&&n.push({link:t,path:e(a,"organizationPath")||this.pathAttr})}n.push({link:this.linkAttr,path:this.pathAttr});const i=new Set;return n.filter(({link:t,path:e})=>{const a=`${t.toLowerCase()} ${e.toLowerCase()}`;return!i.has(a)&&(i.add(a),!0)})}async updateOrganizationPaths(t){const e=this.organizationPathAttribute(),o=this.config.ldap_top_organization;if(!o||!a(t,o))return;const h=this.config.ldap_organization_path_separator||" / ",c=await this.server.ldap.system.search({paged:!0,scope:"sub",filter:`(${e}=*)`,attributes:[e]},t),d=[];for await(const t of c)for(const a of t.searchEntries){const t=String(a.dn);if(!/^ou=/i.test(t))continue;const r=a[e];d.push({dn:t,path:String(Array.isArray(r)?r[0]:r)})}d.sort((t,e)=>r(t.dn).length-r(e.dn).length);const l=n(o),g=new Map;let p=0;for(const{dn:t,path:a}of d){const r=i(t),o=n(r);let c;if(o!==l&&(c=g.get(o)??await this.readPath(r)??void 0,void 0===c)){this.logger.warn(`Organization ${r} has no ${e}: the path of ${t} is left as it is`);continue}const d=s(t),u=c?`${c}${h}${d}`:d;if(g.set(n(t),u),u!==a)try{await this.server.ldap.system.modify(t,{replace:{[e]:u}}),p++}catch(e){this.logger.error(`Failed to update the path of ${t} after organization rename: ${e}`)}}this.logger.info(`Updated the path of ${p} organizations below ${t}`)}async readPath(t){const e=this.organizationPathAttribute();try{const a=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[e]},t),r=a.searchEntries[0]?.[e];return void 0===r?null:String(Array.isArray(r)?r[0]:r)}catch{return null}}async updateLinkedResources(t,e,a,r){const n=`(${r.link}=${o(t)})`;this.logger.debug(`Searching for resources directly linked to ${t}: ${n}`);const i=await this.server.ldap.system.search({paged:!0,filter:n,attributes:[r.link,r.path]},a);let s=0;for await(const t of i)for(const a of t.searchEntries){const t=String(a.dn);try{const a=await this.getDepartmentPath(e);await this.server.ldap.system.modify(t,{replace:{[r.link]:e,[r.path]:a}}),s++,this.logger.debug(`Updated ${t}: ${r.link}=${e}, ${r.path}=${a}`)}catch(e){this.logger.error(`Failed to update ${t} after organization rename: ${e}`)}}this.logger.info(`Updated ${s} resources directly linked to the renamed organization`)}async updateDescendantReferences(t,e,a,r){const n=`(${r.link}=*)`;this.logger.debug(`Searching for resources linked to descendants of ${t}: ${n}`);const i=await this.server.ldap.system.search({paged:!0,filter:n,attributes:[r.link,r.path]},a);let s=0;for await(const a of i)for(const n of a.searchEntries){const a=String(n.dn),i=n[r.link],o=Array.isArray(i)?String(i[0]):String(i);if(o!==t&&o.endsWith(`,${t}`))try{const n=o.replace(`,${t}`,`,${e}`),i=await this.getDepartmentPath(n);await this.server.ldap.system.modify(a,{replace:{[r.link]:n,[r.path]:i}}),s++,this.logger.debug(`Updated descendant link ${a}: ${r.link}=${n}, ${r.path}=${i}`)}catch(t){this.logger.error(`Failed to update descendant reference ${a}: ${t}`)}}this.logger.info(`Updated ${s} resources linked to descendants of the renamed organization`)}async getDepartmentPath(t){const e=this.organizationPathAttribute();try{const a=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[e,"ou","o"]},t);if(!a.searchEntries||0===a.searchEntries.length)throw new Error(`Organization ${t} not found`);const r=a.searchEntries[0];if(r[e]){const t=r[e];return Array.isArray(t)?String(t[0]):String(t)}const n=r.ou||r.o;if(n){return`/${Array.isArray(n)?String(n[0]):String(n)}`}return this.logger.warn(`Organization ${t} has no ${e} attribute, using DN`),t}catch(e){throw new Error(`Failed to fetch organization ${t}: ${e}`)}}}export{LdapDepartmentSync as default};
1
+ import t from"../../abstract/plugin.js";import{roleAttribute as e}from"../../config/schema.js";import{isDnInBranch as a,parseDn as r,normalizeDn as n,getParentDn as i,rdnValue as s,escapeLdapFilter as o}from"../../lib/utils.js";class LdapDepartmentSync extends t{constructor(t){super(t),this.name="ldapDepartmentSync",this.roles=["consistency"],this.hooks={ldaprenamedone:async([t,e])=>{if(/^ou=/.test(t)){this.logger.info(`Organization renamed from ${t} to ${e}, synchronizing linked resources...`);try{const a=this.config.ldap_top_organization.replace(/^ou=[^,]+,/,"");await this.updateOrganizationPaths(e);for(const r of this.linkedAttributes())await this.updateLinkedResources(t,e,a,r),await this.updateDescendantReferences(t,e,a,r)}catch(t){this.logger.error(`Error synchronizing department links after rename: ${t}`)}}}},this.linkAttr=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",this.pathAttr=this.config.ldap_organization_path_attribute||"twakeDepartmentPath"}organizationPathAttribute(){const t=this.server.loadedPlugins.ldapOrganizations;return e(t?.schema,"organizationPath")||this.pathAttr}linkedAttributes(){const t=[],a=this.server.loadedPlugins.ldapFlatGeneric;for(const e of a?.instances||[])t.push(e.schema);const r=this.server.loadedPlugins.ldapGroups;t.push(r?.schema);const n=[];for(const a of t){const t=e(a,"organizationLink");t&&n.push({link:t,path:e(a,"organizationPath")||this.pathAttr})}n.push({link:this.linkAttr,path:this.pathAttr});const i=new Set;return n.filter(({link:t,path:e})=>{const a=`${t.toLowerCase()} ${e.toLowerCase()}`;return!i.has(a)&&(i.add(a),!0)})}async updateOrganizationPaths(t){const e=this.organizationPathAttribute(),o=this.config.ldap_top_organization;if(!o||!a(t,o))return;const h=this.config.ldap_organization_path_separator||" / ",c=await this.server.ldap.system.search({paged:!0,scope:"sub",filter:`(${e}=*)`,attributes:[e]},t),d=[];for await(const t of c)for(const a of t.searchEntries){const t=String(a.dn);if(!/^ou=/i.test(t))continue;const r=a[e];d.push({dn:t,path:String(Array.isArray(r)?r[0]:r)})}d.sort((t,e)=>r(t.dn).length-r(e.dn).length);const l=n(o),g=new Map;let p=0;for(const{dn:t,path:a}of d){const r=i(t),o=n(r);let c;if(o!==l&&(c=g.get(o)??await this.readPath(r)??void 0,void 0===c)){this.logger.warn(`Organization ${r} has no ${e}: the path of ${t} is left as it is`);continue}const d=s(t),u=c?`${c}${h}${d}`:d;if(g.set(n(t),u),u!==a)try{await this.server.ldap.system.modify(t,{replace:{[e]:u}}),p++}catch(e){this.logger.error(`Failed to update the path of ${t} after organization rename: ${e}`)}}this.logger.info(`Updated the path of ${p} organizations below ${t}`)}async readPath(t){const e=this.organizationPathAttribute();try{const a=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[e]},t),r=a.searchEntries[0]?.[e];return void 0===r?null:String(Array.isArray(r)?r[0]:r)}catch{return null}}async updateLinkedResources(t,e,a,r){const n=`(${r.link}=${o(t)})`;this.logger.debug(`Searching for resources directly linked to ${t}: ${n}`);const i=await this.server.ldap.system.search({paged:!0,filter:n,attributes:[r.link,r.path]},a);let s=0;for await(const t of i)for(const a of t.searchEntries){const t=String(a.dn);try{const a=await this.getDepartmentPath(e);await this.server.ldap.system.modify(t,{replace:{[r.link]:e,[r.path]:a}}),s++,this.logger.debug(`Updated ${t}: ${r.link}=${e}, ${r.path}=${a}`)}catch(e){this.logger.error(`Failed to update ${t} after organization rename: ${e}`)}}this.logger.info(`Updated ${s} resources directly linked to the renamed organization`)}async updateDescendantReferences(t,e,a,r){const n=`(${r.link}=*)`;this.logger.debug(`Searching for resources linked to descendants of ${t}: ${n}`);const i=await this.server.ldap.system.search({paged:!0,filter:n,attributes:[r.link,r.path]},a);let s=0;for await(const a of i)for(const n of a.searchEntries){const a=String(n.dn),i=n[r.link],o=Array.isArray(i)?String(i[0]):String(i);if(o!==t&&o.endsWith(`,${t}`))try{const n=o.replace(`,${t}`,()=>`,${e}`),i=await this.getDepartmentPath(n);await this.server.ldap.system.modify(a,{replace:{[r.link]:n,[r.path]:i}}),s++,this.logger.debug(`Updated descendant link ${a}: ${r.link}=${n}, ${r.path}=${i}`)}catch(t){this.logger.error(`Failed to update descendant reference ${a}: ${t}`)}}this.logger.info(`Updated ${s} resources linked to descendants of the renamed organization`)}async getDepartmentPath(t){const e=this.organizationPathAttribute();try{const a=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[e,"ou","o"]},t);if(!a.searchEntries||0===a.searchEntries.length)throw new Error(`Organization ${t} not found`);const r=a.searchEntries[0];if(r[e]){const t=r[e];return Array.isArray(t)?String(t[0]):String(t)}const n=r.ou||r.o;if(n){return`/${Array.isArray(n)?String(n[0]):String(n)}`}return this.logger.warn(`Organization ${t} has no ${e} attribute, using DN`),t}catch(e){throw new Error(`Failed to fetch organization ${t}: ${e}`)}}}export{LdapDepartmentSync as default};
@@ -1 +1 @@
1
- import e from"fs";import{assertIdentityMode as s,identityFor as r}from"../../lib/auth/base.js";import{escapeDnValue as t,isChildOf as a}from"../../lib/utils.js";class BaseResolver{constructor(r){s(r,"scim base resolver"),this.config=r;const t=r.ldap_base||"";this.defaultUserBase=r.scim_user_base||t,this.defaultGroupBase=r.scim_group_base||t,this.userTemplate=r.scim_user_base_template||"",this.groupTemplate=r.scim_group_base_template||"",this.userBaseHeader=(r.scim_user_base_header||"").toLowerCase(),this.groupBaseHeader=(r.scim_group_base_header||"").toLowerCase(),this.headerRoot=r.scim_base_header_root||"";const a=r.scim_base_map||"";if(a)try{const s=e.readFileSync(a,"utf8");this.map=JSON.parse(s)}catch(e){throw new Error(`Failed to load SCIM base map from ${a}: ${e instanceof Error?e.message:String(e)}`)}}applyTemplate(e,s){const r=s?t(s):"";return e.replace(/\{user\}/g,r)}readHeader(e,s){if(!e||"object"!=typeof e)return;const r=e;if("function"==typeof r.get){const e=r.get(s);if("string"==typeof e&&e.length>0)return e}const t=r.headers?.[s];return"string"==typeof t&&t.length>0?t:Array.isArray(t)&&"string"==typeof t[0]&&t[0].length>0?t[0]:void 0}resolve(e,s){const t=s&&"object"==typeof s?r(s,this.config).value:void 0;if(this.map&&t&&this.map[t]){const s=this.map[t];if("user"===e&&s.userBase)return s.userBase;if("group"===e&&s.groupBase)return s.groupBase}const o="user"===e?this.userBaseHeader:this.groupBaseHeader;if(o&&this.headerRoot){const e=this.readHeader(s,o)?.trim();if(e&&!/[\u0000-\u001f\u007f]/.test(e)&&(e.toLowerCase()===this.headerRoot.toLowerCase()||a(e,this.headerRoot)))return e}if(this.map&&this.map["*"]){const s=this.map["*"];if("user"===e&&s.userBase)return this.applyTemplate(s.userBase,t);if("group"===e&&s.groupBase)return this.applyTemplate(s.groupBase,t)}const i="user"===e?this.userTemplate:this.groupTemplate;return i?this.applyTemplate(i,t):"user"===e?this.defaultUserBase:this.defaultGroupBase}userBase(e){return this.resolve("user",e)}groupBase(e){return this.resolve("group",e)}}export{BaseResolver};
1
+ import e from"fs";import{assertIdentityMode as s,identityFor as r}from"../../lib/auth/base.js";import{fillPattern as t,escapeDnValue as a,isChildOf as o}from"../../lib/utils.js";class BaseResolver{constructor(r){s(r,"scim base resolver"),this.config=r;const t=r.ldap_base||"";this.defaultUserBase=r.scim_user_base||t,this.defaultGroupBase=r.scim_group_base||t,this.userTemplate=r.scim_user_base_template||"",this.groupTemplate=r.scim_group_base_template||"",this.userBaseHeader=(r.scim_user_base_header||"").toLowerCase(),this.groupBaseHeader=(r.scim_group_base_header||"").toLowerCase(),this.headerRoot=r.scim_base_header_root||"";const a=r.scim_base_map||"";if(a)try{const s=e.readFileSync(a,"utf8");this.map=JSON.parse(s)}catch(e){throw new Error(`Failed to load SCIM base map from ${a}: ${e instanceof Error?e.message:String(e)}`)}}applyTemplate(e,s){return t(e,"{user}",s?a(s):"")}readHeader(e,s){if(!e||"object"!=typeof e)return;const r=e;if("function"==typeof r.get){const e=r.get(s);if("string"==typeof e&&e.length>0)return e}const t=r.headers?.[s];return"string"==typeof t&&t.length>0?t:Array.isArray(t)&&"string"==typeof t[0]&&t[0].length>0?t[0]:void 0}resolve(e,s){const t=s&&"object"==typeof s?r(s,this.config).value:void 0;if(this.map&&t&&this.map[t]){const s=this.map[t];if("user"===e&&s.userBase)return s.userBase;if("group"===e&&s.groupBase)return s.groupBase}const a="user"===e?this.userBaseHeader:this.groupBaseHeader;if(a&&this.headerRoot){const e=this.readHeader(s,a)?.trim();if(e&&!/[\u0000-\u001f\u007f]/.test(e)&&(e.toLowerCase()===this.headerRoot.toLowerCase()||o(e,this.headerRoot)))return e}if(this.map&&this.map["*"]){const s=this.map["*"];if("user"===e&&s.userBase)return this.applyTemplate(s.userBase,t);if("group"===e&&s.groupBase)return this.applyTemplate(s.groupBase,t)}const i="user"===e?this.userTemplate:this.groupTemplate;return i?this.applyTemplate(i,t):"user"===e?this.defaultUserBase:this.defaultGroupBase}userBase(e){return this.resolve("user",e)}groupBase(e){return this.resolve("group",e)}}export{BaseResolver};
@@ -1 +1 @@
1
- import{launchHooks as t}from"../../lib/utils.js";import{SCHEMA_BULK_RESPONSE as s}from"./types.js";import{scimInvalidValue as e,scimErrorFromException as o}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";const r=/^bulkId:([A-Za-z0-9_-]+)$/;class ScimBulk{constructor(t){this.config=t.config,this.logger=t.logger,this.users=t.users,this.groups=t.groups,this.scimPrefix=t.scimPrefix,this.hooks=t.hooks}async execute(o,r){const i=this.config.scim_bulk_max_operations||100;if(!r.Operations||!Array.isArray(r.Operations))throw e("Missing Operations array");if(r.Operations.length>i)throw e(`Too many operations: ${r.Operations.length} (max ${i})`);const a=new Map,n=[];let c=0;const u=r.failOnErrors??0;for(const t of r.Operations){if(u>0&&c>=u)break;const s=await this.executeOne(o,t,a);n.push(s),parseInt(s.status,10)>=400&&c++}const h={schemas:[s],Operations:n};return t(this.hooks.scimbulkdone,h),h}async executeOne(t,s,i){const a={method:s.method,bulkId:s.bulkId,status:"200"};try{if(!s.method||!s.path)throw e("method and path are required");s.data&&"object"==typeof s.data&&this.resolveRefs(s.data,i);const o=s.path.replace(/^\//,"").split("/"),n=o[0],c=o[1];if("Users"!==n&&"Groups"!==n)throw e(`Unsupported bulk path '${s.path}'`);const u="Users"===n;let h=c;if(h&&r.test(h)){const t=r.exec(h),s=i.get(t[1]);if(!s)throw e(`Unknown bulkId reference '${t[1]}'`);h=s.id}switch(s.method){case"POST":{if(h)throw e("POST must not include an id");const o=u?await this.users.create(t,s.data):await this.groups.create(t,s.data);return a.status="201",a.location=this.buildLocation(n,o.id,t),a.version=o.meta?.version||void 0,s.bulkId&&i.set(s.bulkId,{id:o.id,type:u?"User":"Group"}),a}case"PUT":{if(!h)throw e("PUT requires an id");const o=u?await this.users.replace(t,h,s.data):await this.groups.replace(t,h,s.data);return a.status="200",a.location=this.buildLocation(n,o.id,t),a}case"PATCH":{if(!h)throw e("PATCH requires an id");const o=u?await this.users.patch(t,h,s.data):await this.groups.patch(t,h,s.data);return a.status="200",a.location=this.buildLocation(n,o.id,t),a}case"DELETE":if(!h)throw e("DELETE requires an id");return u?await this.users.delete(t,h):await this.groups.delete(t,h),a.status="204",a;default:throw e(`Unsupported method '${String(s.method)}'`)}}catch(t){const s=o(t);return a.status=s.status,a.response=s,a}}resolveRefs(t,s){for(const[e,o]of Object.entries(t)){if("string"==typeof o){const i=r.exec(o);if(i){const o=s.get(i[1]);o&&(t[e]=o.id)}continue}if(Array.isArray(o)){const t=o;for(let e=0;e<t.length;e++){const o=t[e];if("string"==typeof o){const i=r.exec(o);if(i){const o=s.get(i[1]);o&&(t[e]=o.id)}}else o&&"object"==typeof o&&this.resolveRefs(o,s)}continue}o&&"object"==typeof o&&this.resolveRefs(o,s)}}buildLocation(t,s,e){const o=this.config.scim_base_url||"";return`${o?o.replace(/\/$/,""):e?.protocol&&e.get?`${e.protocol}://${String(e.get("host")||"")}`:""}${this.scimPrefix}/${t}/${encodeURIComponent(s)}`}}export{ScimBulk};
1
+ import{launchHooks as t}from"../../lib/utils.js";import{SCHEMA_BULK_RESPONSE as e}from"./types.js";import{scimInvalidValue as s,kindOf as o,scimErrorFromException as r}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";const i=/^bulkId:([A-Za-z0-9_-]+)$/;class ScimBulk{constructor(t){this.config=t.config,this.logger=t.logger,this.users=t.users,this.groups=t.groups,this.scimPrefix=t.scimPrefix,this.hooks=t.hooks}async execute(o,r){const i=this.config.scim_bulk_max_operations||100;if(!r.Operations||!Array.isArray(r.Operations))throw s("Missing Operations array");if(r.Operations.length>i)throw s(`Too many operations: ${r.Operations.length} (max ${i})`);const a=new Map,n=[];let c=0;const h=r.failOnErrors??0;for(const t of r.Operations){if(h>0&&c>=h)break;const e=await this.executeOne(o,t,a);n.push(e),parseInt(e.status,10)>=400&&c++}const u={schemas:[e],Operations:n};return t(this.hooks.scimbulkdone,u),u}async executeOne(t,e,a){const n=!!e&&"object"==typeof e&&!Array.isArray(e),c={method:n?e.method:void 0,bulkId:n?e.bulkId:void 0,status:"200"};try{if(!n)throw s(`Each operation must be an object, got ${o(e)}`);if("string"!=typeof e.method||"string"!=typeof e.path)throw s("method and path are required, as strings");if("DELETE"!==e.method&&(!e.data||"object"!=typeof e.data||Array.isArray(e.data)))throw s(`data must be an object, got ${o(e.data)}`);e.data&&"object"==typeof e.data&&this.resolveRefs(e.data,a);const r=e.path.replace(/^\//,"").split("/"),h=r[0],u=r[1];if("Users"!==h&&"Groups"!==h)throw s(`Unsupported bulk path '${e.path}'`);const d="Users"===h;let p=u;if(p&&i.test(p)){const t=i.exec(p),e=a.get(t[1]);if(!e)throw s(`Unknown bulkId reference '${t[1]}'`);p=e.id}switch(e.method){case"POST":{if(p)throw s("POST must not include an id");const o=d?await this.users.create(t,e.data):await this.groups.create(t,e.data);return c.status="201",c.location=this.buildLocation(h,o.id,t),c.version=o.meta?.version||void 0,e.bulkId&&a.set(e.bulkId,{id:o.id,type:d?"User":"Group"}),c}case"PUT":{if(!p)throw s("PUT requires an id");const o=d?await this.users.replace(t,p,e.data):await this.groups.replace(t,p,e.data);return c.status="200",c.location=this.buildLocation(h,o.id,t),c}case"PATCH":{if(!p)throw s("PATCH requires an id");const o=d?await this.users.patch(t,p,e.data):await this.groups.patch(t,p,e.data);return c.status="200",c.location=this.buildLocation(h,o.id,t),c}case"DELETE":if(!p)throw s("DELETE requires an id");return d?await this.users.delete(t,p):await this.groups.delete(t,p),c.status="204",c;default:throw s(`Unsupported method '${String(e.method)}'`)}}catch(t){const e=r(t);return c.status=e.status,c.response=e,c}}resolveRefs(t,e,o=0){if(o>64)throw s("Operation data nested too deeply");for(const[s,r]of Object.entries(t)){if("string"==typeof r){const o=i.exec(r);if(o){const r=e.get(o[1]);r&&(t[s]=r.id)}continue}if(Array.isArray(r)){const t=r;for(let s=0;s<t.length;s++){const r=t[s];if("string"==typeof r){const o=i.exec(r);if(o){const r=e.get(o[1]);r&&(t[s]=r.id)}}else r&&"object"==typeof r&&this.resolveRefs(r,e,o+1)}continue}r&&"object"==typeof r&&this.resolveRefs(r,e,o+1)}}buildLocation(t,e,s){const o=this.config.scim_base_url||"";return`${o?o.replace(/\/$/,""):s?.protocol&&s.get?`${s.protocol}://${String(s.get("host")||"")}`:""}${this.scimPrefix}/${t}/${encodeURIComponent(e)}`}}export{ScimBulk};
@@ -1 +1 @@
1
- import{HttpError as r,AUTHZ_REFUSED as t}from"../../lib/errors.js";import{isSchemaRefusal as e,extractLdapCode as i}from"../../lib/ldapCodes.js";import{SCHEMA_ERROR as o}from"./types.js";const n="application/scim+json";class ScimError extends r{constructor(r,t,e){super(t,r),this.name="ScimError",this.scimType=e}}const scimInvalidFilter=(r="Invalid filter")=>new ScimError(400,r,"invalidFilter"),scimInvalidPath=(r="Invalid path")=>new ScimError(400,r,"invalidPath"),scimInvalidValue=(r="Invalid value")=>new ScimError(400,r,"invalidValue"),scimInvalidSyntax=(r="Invalid syntax")=>new ScimError(400,r,"invalidSyntax"),scimNoTarget=(r="No target")=>new ScimError(400,r,"noTarget"),scimMutability=(r="Immutable attribute")=>new ScimError(400,r,"mutability"),scimUniqueness=(r="Uniqueness violation")=>new ScimError(409,r,"uniqueness"),scimTooMany=(r="Too many results")=>new ScimError(400,r,"tooMany"),scimNotFound=(r="Resource not found")=>new ScimError(404,r);function writeScimError(r,t,e,i){const s={schemas:[o],status:String(t),...i?{scimType:i}:{},detail:e};r.status(t).type(n).json(s)}function scimErrorFromException(n,s=500){const sanitize=r=>/\[authz-forbidden\]/.test(r)?t:r,body=(r,t,e)=>({schemas:[o],status:String(r),...e?{scimType:e}:{},detail:t});if(n instanceof ScimError)return body(n.statusCode,sanitize(n.message),n.scimType);if(n instanceof r){const r=400===n.statusCode&&e(i(n))?"invalidValue":void 0;return body(n.statusCode,sanitize(n.message),r)}const c=n instanceof Error?n.message:String(n);if(/\[authz-forbidden\]/.test(c))return body(403,t);const a=i(n);return 32===a?body(404,"Resource not found"):68===a?body(409,sanitize(c),"uniqueness"):e(a)?body(400,`The directory rejected this write against its schema: ${sanitize(c)}`,"invalidValue"):body(s,sanitize(c)||"Internal error")}function writeScimErrorFromException(r,t,e=500){const i=scimErrorFromException(t,e);r.status(Number(i.status)).type(n).json(i)}const scimAsyncHandler=r=>(t,e,i)=>{Promise.resolve().then(()=>r(t,e,i)).catch(r=>{e.headersSent?i(r):writeScimErrorFromException(e,r)})};export{n as SCIM_CONTENT_TYPE,ScimError,i as extractLdapCode,scimAsyncHandler,scimErrorFromException,scimInvalidFilter,scimInvalidPath,scimInvalidSyntax,scimInvalidValue,scimMutability,scimNoTarget,scimNotFound,scimTooMany,scimUniqueness,writeScimError,writeScimErrorFromException};
1
+ import{HttpError as r,AUTHZ_REFUSED as t}from"../../lib/errors.js";import{isSchemaRefusal as e,extractLdapCode as i}from"../../lib/ldapCodes.js";import{SCHEMA_ERROR as n}from"./types.js";const o="application/scim+json";class ScimError extends r{constructor(r,t,e){super(t,r),this.name="ScimError",this.scimType=e}}const kindOf=r=>{if(null===r)return"null";if(Array.isArray(r))return"an array";const t=typeof r;return`${/^[aeiou]/.test(t)?"an":"a"} ${t}`},scimInvalidFilter=(r="Invalid filter")=>new ScimError(400,r,"invalidFilter"),scimInvalidPath=(r="Invalid path")=>new ScimError(400,r,"invalidPath"),scimInvalidValue=(r="Invalid value")=>new ScimError(400,r,"invalidValue"),scimInvalidSyntax=(r="Invalid syntax")=>new ScimError(400,r,"invalidSyntax"),scimNoTarget=(r="No target")=>new ScimError(400,r,"noTarget"),scimMutability=(r="Immutable attribute")=>new ScimError(400,r,"mutability"),scimUniqueness=(r="Uniqueness violation")=>new ScimError(409,r,"uniqueness"),scimTooMany=(r="Too many results")=>new ScimError(400,r,"tooMany"),scimNotFound=(r="Resource not found")=>new ScimError(404,r);function writeScimError(r,t,e,i){const s={schemas:[n],status:String(t),...i?{scimType:i}:{},detail:e};r.status(t).type(o).json(s)}function scimErrorFromException(o,s=500){const sanitize=r=>/\[authz-forbidden\]/.test(r)?t:r,body=(r,t,e)=>({schemas:[n],status:String(r),...e?{scimType:e}:{},detail:t});if(o instanceof ScimError)return body(o.statusCode,sanitize(o.message),o.scimType);if(o instanceof r){const r=400===o.statusCode&&e(i(o))?"invalidValue":void 0;return body(o.statusCode,sanitize(o.message),r)}const a=o instanceof Error?o.message:String(o);if(/\[authz-forbidden\]/.test(a))return body(403,t);const c=i(o);return 32===c?body(404,"Resource not found"):68===c?body(409,sanitize(a),"uniqueness"):e(c)?body(400,`The directory rejected this write against its schema: ${sanitize(a)}`,"invalidValue"):body(s,sanitize(a)||"Internal error")}function writeScimErrorFromException(r,t,e=500){const i=scimErrorFromException(t,e);r.status(Number(i.status)).type(o).json(i)}const scimAsyncHandler=r=>(t,e,i)=>{Promise.resolve().then(()=>r(t,e,i)).catch(r=>{e.headersSent?i(r):writeScimErrorFromException(e,r)})};export{o as SCIM_CONTENT_TYPE,ScimError,i as extractLdapCode,kindOf,scimAsyncHandler,scimErrorFromException,scimInvalidFilter,scimInvalidPath,scimInvalidSyntax,scimInvalidValue,scimMutability,scimNoTarget,scimNotFound,scimTooMany,scimUniqueness,writeScimError,writeScimErrorFromException};
@@ -1 +1 @@
1
- import{escapeLdapFilter as t}from"../../lib/utils.js";import{scimPathToLdapAttribute as e,DEFAULT_LOCK_ATTRIBUTE as s,DEFAULT_LOCK_VALUE as o}from"./mapping.js";import{scimInvalidFilter as n}from"./errors.js";const r=new Set(["and","or","not","pr","eq","ne","co","sw","ew","gt","ge","lt","le","true","false","null"]);const i="\0active";class Parser{constructor(t,e,s={}){this.tokens=t,this.mapping=e,this.opts=s,this.pos=0}peek(t=0){return this.tokens[this.pos+t]}consume(){return this.tokens[this.pos++]}expect(t,e){const s=this.tokens[this.pos];if(s.kind!==t||e&&s.value.toLowerCase()!==e)throw n(`Expected ${e||t} at position ${s.pos}, got '${s.value}'`);return this.pos++,s}parse(){const t=this.parseOr();if("EOF"!==this.peek().kind)throw n(`Unexpected token '${this.peek().value}' at position ${this.peek().pos}`);return t}parseOr(){const t=[this.parseAnd()];for(;"WORD"===this.peek().kind&&"or"===this.peek().value.toLowerCase();)this.consume(),t.push(this.parseAnd());return 1===t.length?t[0]:`(|${t.join("")})`}parseAnd(){const t=[this.parseNot()];for(;"WORD"===this.peek().kind&&"and"===this.peek().value.toLowerCase();)this.consume(),t.push(this.parseNot());return 1===t.length?t[0]:`(&${t.join("")})`}parseNot(){if("WORD"===this.peek().kind&&"not"===this.peek().value.toLowerCase()){this.consume();return`(!${this.parsePrimary()})`}return this.parsePrimary()}parsePrimary(){const t=this.peek();if("LPAREN"===t.kind){this.consume();const t=this.parseOr();return this.expect("RPAREN"),t}if("WORD"===t.kind){const t=this.consume().value;if("LBRACK"===this.peek().kind)throw n(`Complex multi-valued filters ('${t}[...]') are not supported; use 'and' at top level instead`);const e=this.consume();if("WORD"!==e.kind)throw n(`Expected comparison operator at position ${e.pos}`);const s=e.value.toLowerCase();if("pr"===s){const e=this.resolvePath(t);return e===i?"(objectClass=*)":`(${e}=*)`}if(!r.has(s)||["and","or","not"].includes(s))throw n(`Unknown operator '${e.value}' at position ${e.pos}`);const o=this.consume(),a=this.tokenToValue(o);return this.emitComparison(t,s,a)}throw n(`Unexpected token '${t.value}' at position ${t.pos}`)}tokenToValue(t){if("STRING"===t.kind)return t.value;if("NUMBER"===t.kind){const e=Number(t.value);if(Number.isNaN(e))throw n(`Invalid number '${t.value}' at position ${t.pos}`);return e}if("WORD"===t.kind){const e=t.value.toLowerCase();return"true"===e||"false"!==e&&("null"===e?null:t.value)}throw n(`Unexpected value token at position ${t.pos}`)}resolvePath(t){if("id"===t)throw n("filter on 'id' only supports 'eq' with a string value");if("active"===t&&this.opts.supportsActive)return i;const s=e(t,this.mapping);if(!s)throw n(`Unknown attribute path '${t}'`);return s}emitComparison(e,r,a){const u=this.resolvePath(e);if(u===i){const e=!0===a||"string"==typeof a&&"true"===a.toLowerCase(),i=this.opts.lockAttribute||s;if(!this.opts.lockValue&&i.toLowerCase()!==s.toLowerCase())throw new Error(`No lock value given for '${i}'`);const u=`(${i}=${t(this.opts.lockValue||o)})`,p=`(!${u})`;if("eq"===r)return e?p:u;if("ne"===r)return e?u:p;throw n(`Operator '${r}' not supported for 'active'`)}if(null===a){if("eq"===r)return`(!(${u}=*))`;if("ne"===r)return`(${u}=*)`;throw n(`Operator '${r}' not supported with null`)}const p=String(a),l=t(p);switch(r){case"eq":return`(${u}=${l})`;case"ne":return`(!(${u}=${l}))`;case"co":return`(${u}=*${l}*)`;case"sw":return`(${u}=${l}*)`;case"ew":return`(${u}=*${l})`;case"gt":return`(&(${u}>=${l})(!(${u}=${l})))`;case"ge":return`(${u}>=${l})`;case"lt":return`(&(${u}<=${l})(!(${u}=${l})))`;case"le":return`(${u}<=${l})`;default:throw n(`Unknown operator '${r}'`)}}}function scimFilterToLdap(t,e,s={}){const o=t.trim();if(!o)return{ldapFilter:"(objectClass=*)",touchesId:!1};const r=/^id\s+eq\s+"([^"\\]*(?:\\.[^"\\]*)*)"\s*$/i.exec(o);if(r)return{ldapFilter:"(objectClass=*)",touchesId:!0,idEquals:r[1].replace(/\\(.)/g,"$1")};const i=function(t){const e=[];let s=0;for(;s<t.length;){const o=t[s];if(" "!==o&&"\t"!==o&&"\n"!==o&&"\r"!==o)if("("!==o)if(")"!==o)if("["!==o){if("]"!==o){if('"'===o){const o=s;s++;let r="";for(;s<t.length&&'"'!==t[s];){if("\\"===t[s]&&s+1<t.length){const e=t[s+1];if('"'===e){r+='"',s+=2;continue}if("\\"===e){r+="\\",s+=2;continue}if("n"===e){r+="\n",s+=2;continue}if("t"===e){r+="\t",s+=2;continue}}r+=t[s],s++}if(s>=t.length)throw n(`Unterminated string literal at position ${o}`);s++,e.push({kind:"STRING",value:r,pos:o});continue}if(o>="0"&&o<="9"){const o=s;for(;s<t.length&&/[0-9.+\-eE]/.test(t[s])&&!/[\s()[\]]/.test(t[s]);)s++;e.push({kind:"NUMBER",value:t.slice(o,s),pos:o});continue}if(/[A-Za-z_$:]/.test(o)){const o=s;for(;s<t.length&&/[A-Za-z0-9_.$:]/.test(t[s]);)s++;e.push({kind:"WORD",value:t.slice(o,s),pos:o});continue}throw n(`Unexpected character '${o}' at position ${s}`)}e.push({kind:"RBRACK",value:"]",pos:s}),s++}else e.push({kind:"LBRACK",value:"[",pos:s}),s++;else e.push({kind:"RPAREN",value:")",pos:s}),s++;else e.push({kind:"LPAREN",value:"(",pos:s}),s++;else s++}return e.push({kind:"EOF",value:"",pos:t.length}),e}(o);return{ldapFilter:new Parser(i,e,s).parse(),touchesId:/\bid\b/.test(o)}}export{scimFilterToLdap};
1
+ import{escapeLdapFilter as t}from"../../lib/utils.js";import{scimPathToLdapAttribute as e,DEFAULT_LOCK_ATTRIBUTE as s,DEFAULT_LOCK_VALUE as o}from"./mapping.js";import{scimInvalidFilter as n}from"./errors.js";const r=new Set(["and","or","not","pr","eq","ne","co","sw","ew","gt","ge","lt","le","true","false","null"]);const i="\0active";class Parser{constructor(t,e,s={}){this.tokens=t,this.mapping=e,this.opts=s,this.pos=0,this.depth=0}peek(t=0){return this.tokens[this.pos+t]}consume(){return this.tokens[this.pos++]}expect(t,e){const s=this.tokens[this.pos];if(s.kind!==t||e&&s.value.toLowerCase()!==e)throw n(`Expected ${e||t} at position ${s.pos}, got '${s.value}'`);return this.pos++,s}parse(){const t=this.parseOr();if("EOF"!==this.peek().kind)throw n(`Unexpected token '${this.peek().value}' at position ${this.peek().pos}`);return t}parseOr(){const t=[this.parseAnd()];for(;"WORD"===this.peek().kind&&"or"===this.peek().value.toLowerCase();)this.consume(),t.push(this.parseAnd());return 1===t.length?t[0]:`(|${t.join("")})`}parseAnd(){const t=[this.parseNot()];for(;"WORD"===this.peek().kind&&"and"===this.peek().value.toLowerCase();)this.consume(),t.push(this.parseNot());return 1===t.length?t[0]:`(&${t.join("")})`}parseNot(){if("WORD"===this.peek().kind&&"not"===this.peek().value.toLowerCase()){this.consume();return`(!${this.nested(()=>this.parsePrimary())})`}return this.parsePrimary()}nested(t){if(++this.depth>32)throw n("Filter nested too deeply");try{return t()}finally{this.depth--}}parsePrimary(){const t=this.peek();if("LPAREN"===t.kind){this.consume();const t=this.nested(()=>this.parseOr());return this.expect("RPAREN"),t}if("WORD"===t.kind){const t=this.consume().value;if("LBRACK"===this.peek().kind)throw n(`Complex multi-valued filters ('${t}[...]') are not supported; use 'and' at top level instead`);const e=this.consume();if("WORD"!==e.kind)throw n(`Expected comparison operator at position ${e.pos}`);const s=e.value.toLowerCase();if("pr"===s){const e=this.resolvePath(t);return e===i?"(objectClass=*)":`(${e}=*)`}if(!r.has(s)||["and","or","not"].includes(s))throw n(`Unknown operator '${e.value}' at position ${e.pos}`);const o=this.consume(),a=this.tokenToValue(o);return this.emitComparison(t,s,a)}throw n(`Unexpected token '${t.value}' at position ${t.pos}`)}tokenToValue(t){if("STRING"===t.kind)return t.value;if("NUMBER"===t.kind){const e=Number(t.value);if(Number.isNaN(e))throw n(`Invalid number '${t.value}' at position ${t.pos}`);return e}if("WORD"===t.kind){const e=t.value.toLowerCase();return"true"===e||"false"!==e&&("null"===e?null:t.value)}throw n(`Unexpected value token at position ${t.pos}`)}resolvePath(t){if("id"===t)throw n("filter on 'id' only supports 'eq' with a string value");if("active"===t&&this.opts.supportsActive)return i;const s=e(t,this.mapping);if(!s)throw n(`Unknown attribute path '${t}'`);return s}emitComparison(e,r,a){const p=this.resolvePath(e);if(p===i){const e=!0===a||"string"==typeof a&&"true"===a.toLowerCase(),i=this.opts.lockAttribute||s;if(!this.opts.lockValue&&i.toLowerCase()!==s.toLowerCase())throw new Error(`No lock value given for '${i}'`);const p=`(${i}=${t(this.opts.lockValue||o)})`,u=`(!${p})`;if("eq"===r)return e?u:p;if("ne"===r)return e?p:u;throw n(`Operator '${r}' not supported for 'active'`)}if(null===a){if("eq"===r)return`(!(${p}=*))`;if("ne"===r)return`(${p}=*)`;throw n(`Operator '${r}' not supported with null`)}const u=String(a),h=t(u);switch(r){case"eq":return`(${p}=${h})`;case"ne":return`(!(${p}=${h}))`;case"co":return`(${p}=*${h}*)`;case"sw":return`(${p}=${h}*)`;case"ew":return`(${p}=*${h})`;case"gt":return`(&(${p}>=${h})(!(${p}=${h})))`;case"ge":return`(${p}>=${h})`;case"lt":return`(&(${p}<=${h})(!(${p}=${h})))`;case"le":return`(${p}<=${h})`;default:throw n(`Unknown operator '${r}'`)}}}function scimFilterToLdap(t,e,s={}){const o=t.trim();if(!o)return{ldapFilter:"(objectClass=*)",touchesId:!1};const r=/^id\s+eq\s+"([^"\\]*(?:\\.[^"\\]*)*)"\s*$/i.exec(o);if(r)return{ldapFilter:"(objectClass=*)",touchesId:!0,idEquals:r[1].replace(/\\(.)/g,"$1")};const i=function(t){const e=[];let s=0;for(;s<t.length;){const o=t[s];if(" "!==o&&"\t"!==o&&"\n"!==o&&"\r"!==o)if("("!==o)if(")"!==o)if("["!==o){if("]"!==o){if('"'===o){const o=s;s++;let r="";for(;s<t.length&&'"'!==t[s];){if("\\"===t[s]&&s+1<t.length){const e=t[s+1];if('"'===e){r+='"',s+=2;continue}if("\\"===e){r+="\\",s+=2;continue}if("n"===e){r+="\n",s+=2;continue}if("t"===e){r+="\t",s+=2;continue}}r+=t[s],s++}if(s>=t.length)throw n(`Unterminated string literal at position ${o}`);s++,e.push({kind:"STRING",value:r,pos:o});continue}if(o>="0"&&o<="9"){const o=s;for(;s<t.length&&/[0-9.+\-eE]/.test(t[s])&&!/[\s()[\]]/.test(t[s]);)s++;e.push({kind:"NUMBER",value:t.slice(o,s),pos:o});continue}if(/[A-Za-z_$:]/.test(o)){const o=s;for(;s<t.length&&/[A-Za-z0-9_.$:]/.test(t[s]);)s++;e.push({kind:"WORD",value:t.slice(o,s),pos:o});continue}throw n(`Unexpected character '${o}' at position ${s}`)}e.push({kind:"RBRACK",value:"]",pos:s}),s++}else e.push({kind:"LBRACK",value:"[",pos:s}),s++;else e.push({kind:"RPAREN",value:")",pos:s}),s++;else e.push({kind:"LPAREN",value:"(",pos:s}),s++;else s++}return e.push({kind:"EOF",value:"",pos:t.length}),e}(o);return{ldapFilter:new Parser(i,e,s).parse(),touchesId:/\bid\b/.test(o)}}export{scimFilterToLdap};
@@ -1 +1 @@
1
- import{escapeDnValue as t,isDummyMemberDn as e,launchHooksChained as s,validateDnValue as r,launchHooks as i,isChildOf as o,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as n}from"./types.js";import{withExternalId as c,mergeMapping as h,loadMappingFile as u,DEFAULT_GROUP_MAPPING as d,requiredLdapAttributes as l,ldapToScimGroup as m,scimGroupToLdap as p}from"./mapping.js";import{scimFilterToLdap as f}from"./filter.js";import{pagedSearch as g}from"./list.js";import{writePatch as b,patchToModifyRequest as w}from"./patch.js";import{scimNotFound as y,ScimError as R,scimInvalidValue as _,scimUniqueness as $}from"./errors.js";import{extractLdapCode as v}from"../../lib/ldapCodes.js";class ScimGroups{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_group_rdn_attribute||"cn",this.objectClass=this.config.scim_group_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=this.config.scim_group_mapping||"";this.mapping=c(h(d,e?u(e):void 0),this.config.scim_group_external_id_attribute||""),this.attributes=l(this.mapping,[this.rdnAttribute,"member"])}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"Group",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:"",lockValue:""}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const s=this.baseResolver.userBase(t),r=this.baseResolver.groupBase(t),i=this.scimPrefix,o=this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),a=this.config.scim_user_rdn_attribute||"uid",n=this.rdnAttribute,c=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(e(t,c))return;const h=/^([^=]+)=((?:\\.|[^,])+)/.exec(t);if(!h)return;const u=h[2].replace(/\\(.)/g,"$1"),d=t.toLowerCase();let l;s&&d.endsWith(s.toLowerCase())?l="User":r&&d.endsWith(r.toLowerCase())?l="Group":h[1]===a?l="User":h[1]===n&&(l="Group");const m={value:u};if(l&&(m.type=l,o)){const t="User"===l?"Users":"Groups";m.$ref=`${o.replace(/\/$/,"")}${i}/${t}/${encodeURIComponent(u)}`}return m}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.attributes},s)}catch(t){if(32===v(t))throw y(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw y(`Group ${e} not found`);return m(r.searchEntries[0],this.mapping,this.ctx(t),this.buildMemberResolver(t))}async list(t,e){const s=this.baseResolver.groupBase(t),r=Math.max(1,e.startIndex||1),i=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let o,a=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=f(e.filter,this.mapping);t.idEquals?o=t.idEquals:a=`(&${a}${t.ldapFilter})`}if(o)try{const e=await this.get(t,o);return{schemas:[n],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof R&&404===t.statusCode)return{schemas:[n],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:c,totalResults:h}=await g({directory:this.ldap.forRequest(t),base:s,filter:a,attributes:this.attributes,startIndex:r,count:i,maxScanned:this.maxScanned}),u=this.buildMemberResolver(t),d=c.map(e=>m(e,this.mapping,this.ctx(t),u));return{schemas:[n],totalResults:h,startIndex:r,itemsPerPage:d.length,Resources:d}}async create(e,o){if(!o.displayName)throw _("displayName is required");const a=this.baseResolver.groupBase(e),n=(await s(this.hooks.scimgroupcreate,[{...o,id:void 0},e,a]))[0],{rdn:c,attributes:h}=p(n,this.mapping,this.objectClass),u=!h[this.rdnAttribute]&&n.id||c;if(!u)throw _("displayName is required");r(u,this.rdnAttribute),h[this.rdnAttribute]=u;const d=[];if(n.members&&Array.isArray(n.members))for(const t of n.members){if(!t||!t.value)continue;const s=await this.users.resolveRef(e,t.value);s&&d.push(s)}if(0===d.length){const t=this.config.group_dummy_user||"cn=fakeuser";d.push(t)}h.member=d;const l=`${this.rdnAttribute}=${t(u)},${a}`;try{await this.ldap.forRequest(e).add(l,h)}catch(t){if(68===v(t))throw $(`Group ${u} already exists`);throw t}const m=await this.get(e,u);return i(this.hooks.scimgroupcreatedone,m),m}async replace(t,e,r){await this.get(t,e);const o=(await s(this.hooks.scimgroupupdate,[e,r,t]))[1],{attributes:a}=p(o,this.mapping,this.objectClass),n=[];if(o.members&&Array.isArray(o.members))for(const e of o.members){if(!e||!e.value)continue;const s=await this.users.resolveRef(t,e.value);s&&n.push(s)}if(0===n.length){const t=this.config.group_dummy_user||"cn=fakeuser";n.push(t)}const c=this.dnForId(e,t),h={replace:{}};for(const[t,e]of Object.entries(a))"objectClass"!==t&&t!==this.rdnAttribute&&(h.replace[t]=e);h.replace.member=n,await this.ldap.forRequest(t).modify(c,h);const u=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,u),u}async patch(t,e,s){const r=this.dnForId(e,t);await b(r,async()=>{const i=await this.currentEntry(t,e),o=await w(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e),current:i}),a=o.delete&&!Array.isArray(o.delete)?o.delete.member:void 0;if(void 0!==a){const t=this.config.group_dummy_user||"cn=fakeuser";if(""===a)delete o.delete.member,0===Object.keys(o.delete).length&&delete o.delete,o.replace||(o.replace={}),o.replace.member=[t];else{const e=[].concat(a);0===[].concat(i.member??[],o.add?.member??[]).map(String).filter(t=>!e.includes(t)).length&&(o.add||(o.add={}),o.add.member=t)}}await this.ldap.forRequest(t).modify(r,o,{permissive:!0})});const o=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,o),o}async currentEntry(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.attributes},s)}catch(t){if(32===v(t))throw y(`Group ${e} not found`);throw t}if(!r.searchEntries?.length)throw y(`Group ${e} not found`);return r.searchEntries[0]}async delete(t,e){await this.get(t,e);const r=(await s(this.hooks.scimgroupdelete,[e,t]))[0],o=this.dnForId(r,t);await this.ldap.forRequest(t).delete(o),i(this.hooks.scimgroupdeletedone,r)}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e),i=s.includes("=")&&s.includes(",");if(i&&!o(s,r))return;const n=i?s:`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},n);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==v(t))throw t}if(!i)try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},r);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==v(t))throw t}}get groupMapping(){return this.mapping}}export{ScimGroups};
1
+ import{escapeDnValue as t,isDummyMemberDn as e,parseDn as s,rdnValue as r,launchHooksChained as i,validateDnValue as o,launchHooks as a,isChildOf as n,escapeLdapFilter as c}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as u}from"./types.js";import{withExternalId as h,mergeMapping as l,loadMappingFile as d,DEFAULT_GROUP_MAPPING as m,requiredLdapAttributes as p,ldapToScimGroup as f,scimGroupToLdap as g}from"./mapping.js";import{scimFilterToLdap as b}from"./filter.js";import{pagedSearch as w}from"./list.js";import{writePatch as y,patchToModifyRequest as R}from"./patch.js";import{scimNotFound as v,ScimError as _,scimInvalidValue as $,kindOf as x,scimUniqueness as A}from"./errors.js";import{extractLdapCode as j}from"../../lib/ldapCodes.js";class ScimGroups{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_group_rdn_attribute||"cn",this.objectClass=this.config.scim_group_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=this.config.scim_group_mapping||"";this.mapping=h(l(m,e?d(e):void 0),this.config.scim_group_external_id_attribute||""),this.attributes=p(this.mapping,[this.rdnAttribute,"member"])}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"Group",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:"",lockValue:""}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const i=this.baseResolver.userBase(t),o=this.baseResolver.groupBase(t),a=this.scimPrefix,n=this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),c=this.config.scim_user_rdn_attribute||"uid",u=this.rdnAttribute,h=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(e(t,h))return;const[l]=s(t),d=l?l.indexOf("="):-1;if(d<=0)return;const m=l.slice(0,d).trim(),p=r(t),f=t.toLowerCase();let g;i&&f.endsWith(i.toLowerCase())?g="User":o&&f.endsWith(o.toLowerCase())?g="Group":m===c?g="User":m===u&&(g="Group");const b={value:p};if(g&&(b.type=g,n)){const t="User"===g?"Users":"Groups";b.$ref=`${n.replace(/\/$/,"")}${a}/${t}/${encodeURIComponent(p)}`}return b}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.attributes},s)}catch(t){if(32===j(t))throw v(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw v(`Group ${e} not found`);return f(r.searchEntries[0],this.mapping,this.ctx(t),this.buildMemberResolver(t))}async list(t,e){const s=this.baseResolver.groupBase(t),r=Math.max(1,e.startIndex||1),i=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let o,a=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=b(e.filter,this.mapping);t.idEquals?o=t.idEquals:a=`(&${a}${t.ldapFilter})`}if(o)try{const e=await this.get(t,o);return{schemas:[u],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof _&&404===t.statusCode)return{schemas:[u],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:n,totalResults:c}=await w({directory:this.ldap.forRequest(t),base:s,filter:a,attributes:this.attributes,startIndex:r,count:i,maxScanned:this.maxScanned}),h=this.buildMemberResolver(t),l=n.map(e=>f(e,this.mapping,this.ctx(t),h));return{schemas:[u],totalResults:c,startIndex:r,itemsPerPage:l.length,Resources:l}}async create(e,s){if(!s.displayName)throw $("displayName is required");const r=this.baseResolver.groupBase(e);g({...s,id:void 0},this.mapping,this.objectClass);const n=(await i(this.hooks.scimgroupcreate,[{...s,id:void 0},e,r]))[0],{rdn:c,attributes:u}=g(n,this.mapping,this.objectClass),h=!u[this.rdnAttribute]&&n.id||c;if(!h)throw $("displayName is required");o(h,this.rdnAttribute),u[this.rdnAttribute]=h;const l=[];if(n.members&&Array.isArray(n.members))for(const t of n.members){if(!t||null==t.value)continue;if("string"!=typeof t.value)throw $(`Each member value must be a string, got ${x(t.value)}`);const s=await this.users.resolveRef(e,t.value);s&&l.push(s)}if(0===l.length){const t=this.config.group_dummy_user||"cn=fakeuser";l.push(t)}u.member=l;const d=`${this.rdnAttribute}=${t(h)},${r}`;try{await this.ldap.forRequest(e).add(d,u)}catch(t){if(68===j(t))throw A(`Group ${h} already exists`);throw t}const m=await this.get(e,h);return a(this.hooks.scimgroupcreatedone,m),m}async replace(t,e,s){await this.get(t,e);const r=(await i(this.hooks.scimgroupupdate,[e,s,t]))[1],{attributes:o}=g(r,this.mapping,this.objectClass),n=[];if(r.members&&Array.isArray(r.members))for(const e of r.members){if(!e||null==e.value)continue;if("string"!=typeof e.value)throw $(`Each member value must be a string, got ${x(e.value)}`);const s=await this.users.resolveRef(t,e.value);s&&n.push(s)}if(0===n.length){const t=this.config.group_dummy_user||"cn=fakeuser";n.push(t)}const c=this.dnForId(e,t),u={replace:{}};for(const[t,e]of Object.entries(o))"objectClass"!==t&&t!==this.rdnAttribute&&(u.replace[t]=e);u.replace.member=n,await this.ldap.forRequest(t).modify(c,u);const h=await this.get(t,e);return a(this.hooks.scimgroupupdatedone,e,h),h}async patch(t,e,s){const r=this.dnForId(e,t);await y(r,async()=>{const i=await this.currentEntry(t,e),o=await R(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e),current:i}),a=o.delete&&!Array.isArray(o.delete)?o.delete.member:void 0;if(void 0!==a){const t=this.config.group_dummy_user||"cn=fakeuser";if(""===a)delete o.delete.member,0===Object.keys(o.delete).length&&delete o.delete,o.replace||(o.replace={}),o.replace.member=[t];else{const e=[].concat(a);0===[].concat(i.member??[],o.add?.member??[]).map(String).filter(t=>!e.includes(t)).length&&(o.add||(o.add={}),o.add.member=t)}}await this.ldap.forRequest(t).modify(r,o,{permissive:!0})});const i=await this.get(t,e);return a(this.hooks.scimgroupupdatedone,e,i),i}async currentEntry(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.attributes},s)}catch(t){if(32===j(t))throw v(`Group ${e} not found`);throw t}if(!r.searchEntries?.length)throw v(`Group ${e} not found`);return r.searchEntries[0]}async delete(t,e){await this.get(t,e);const s=(await i(this.hooks.scimgroupdelete,[e,t]))[0],r=this.dnForId(s,t);await this.ldap.forRequest(t).delete(r),a(this.hooks.scimgroupdeletedone,s)}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e),i=s.includes("=")&&s.includes(",");if(i&&!n(s,r))return;const o=i?s:`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},o);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==j(t))throw t}if(!i)try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${c(s)})`,scope:"sub",paged:!1,attributes:["dn"]},r);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==j(t))throw t}}get groupMapping(){return this.mapping}}export{ScimGroups};
@@ -1 +1 @@
1
- import e from"fs";import{scimInvalidValue as r}from"./errors.js";import{SCHEMA_GROUP as t,SCHEMA_USER as i}from"./types.js";const a=["createTimestamp","modifyTimestamp","entryUUID"],n={resourceType:"User",schemas:[i],entries:[{scim:"userName",ldap:"uid"},{scim:"externalId",ldap:"employeeNumber"},{scim:"name",sub:{familyName:"sn",givenName:"givenName",formatted:"cn",middleName:"initials"}},{scim:"displayName",ldap:"displayName"},{scim:"nickName",ldap:"displayName"},{scim:"title",ldap:"title"},{scim:"preferredLanguage",ldap:"preferredLanguage"},{scim:"emails",ldapPrimary:"mail",ldapSecondary:"mailAlternateAddress",multi:"array"},{scim:"phoneNumbers",ldapPrimary:"telephoneNumber",ldapSecondary:"mobile",multi:"array"}]},o={resourceType:"Group",schemas:[t],entries:[{scim:"displayName",ldap:"cn"}]};function withExternalId(e,r){const t=(r||"").trim();return t?e.entries.some(e=>"externalId"===e.scim)?e:{...e,entries:[...e.entries,{scim:"externalId",ldap:t}]}:e}function asString(e){if(null!=e){if(Array.isArray(e)){const r=e[0];return null==r?void 0:String(r)}return Buffer.isBuffer(e)?e.toString():String(e)}}function ldapTimeToIso(e){if(!e)return;const r=/^(\d{4})(\d{2})(\d{2})(\d{2})(?:(\d{2})(?:(\d{2}))?)?(?:[.,](\d+))?(Z|[+-]\d{2}(?:\d{2})?)?$/.exec(e);if(!r)return e;const[,t,i,a,n,o,s,c,l]=r;let d=0;if(c){const e=s?1e3:o?6e4:36e5;d=Math.round(Number(`0.${c}`)*e)}let u=1e3*(60*(60*Number(n)+Number(o||0))+Number(s||0))+d;const m=Math.floor(u/36e5);u-=36e5*m;const f=Math.floor(u/6e4);u-=6e4*f;const p=Math.floor(u/1e3),y=u-1e3*p;let b="Z";l&&"Z"!==l&&(b=`${l[0]}${l.slice(1,3)}:${l.slice(3,5)||"00"}`);const pad=e=>String(e).padStart(2,"0"),g=`${t}-${i}-${a}T${pad(m)}:${pad(f)}:${pad(p)}${y?`.${String(y).padStart(3,"0")}`:""}${b}`;return Number.isNaN(Date.parse(g))?e:g}function asArray(e){return null==e?[]:Array.isArray(e)?e.map(e=>Buffer.isBuffer(e)?e.toString():String(e)):Buffer.isBuffer(e)?[e.toString()]:[String(e)]}function loadMappingFile(r){if(!r)return;const t=e.readFileSync(r,"utf8");return JSON.parse(t)}function mergeMapping(e,r){if(!r)return e;const t=new Map;for(const r of e.entries)t.set(r.scim,r);for(const e of r.entries)t.set(e.scim,e);return{...e,entries:Array.from(t.values())}}const s="pwdAccountLockedTime",c="000001010000Z",l=/^\d{10,14}(\.\d+)?(Z|[+-]\d{2,4})$/;let warn=e=>{console.warn(`WARNING: ${e}`)};function setLockConfigWarn(e){const r=warn;return warn=e,r}const d=/^([A-Za-z][A-Za-z0-9-]*|\d+(\.\d+)+)$/;function resolveLockConfig(e,r,t={attribute:"--scim-user-lock-attribute",value:"--scim-user-lock-value"}){const i=(e||"").trim()||s;if(!d.test(i))throw new Error(`${t.attribute} must be an LDAP attribute name, got '${i}'`);const a=i.toLowerCase()===s.toLowerCase(),n=(r||"").trim();if(!n){if(a)return{attribute:i,value:c};throw new Error(`${t.attribute} is '${i}', so ${t.value} must say what marks an account locked (for nsAccountLock on 389-ds, 'TRUE'). The default '${c}' only means anything to ${s}.`)}return a||n!==c||warn(`${t.value} is the ppolicy convention '${c}' but ${t.attribute} is '${i}', not ${s}. Unless '${i}' is a GeneralizedTime your directory reads as a lock, every deactivation will answer 200 and read back as active:false while the account keeps binding. Verify a deactivation actually prevents a bind.`),a&&!l.test(n)&&warn(`${t.value} '${n}' is not a GeneralizedTime, and ${s} holds one. The directory will refuse the write, or — where the attribute was redefined locally — store a value it does not read as a lock. The ppolicy convention is '${c}'.`),{attribute:i,value:n}}function readActive(e){if(void 0!==e){if("boolean"==typeof e)return e;if("string"==typeof e){const r=e.trim().toLowerCase();if("true"===r)return!0;if("false"===r)return!1}throw r(`Cannot read ${JSON.stringify(e)} as active`)}}function isLocked(e,r){return holdsValue(e,r.lockAttribute,r.lockValue)}function holdsValue(e,r,t){if(!r)return!1;const i=r.toLowerCase(),a=Object.keys(e).find(e=>e.toLowerCase()===i),n=void 0===a?void 0:e[a];if(null==n)return!1;const o=t.toLowerCase();return(Array.isArray(n)?n:[n]).some(e=>null!=e&&e.toString().toLowerCase()===o)}function resolveScimId(e,r){if("rdn"===r.idAttribute)return asString(e[r.rdnAttribute]);return asString(e[r.idAttribute])??asString(e[r.rdnAttribute])}function buildLocation(e,r){if(!r.baseUrl)return;const t="User"===r.resourceType?"Users":"Groups";return`${r.baseUrl.replace(/\/$/,"")}${r.scimPrefix}/${t}/${encodeURIComponent(e)}`}function ldapToScimUser(e,r,t){const a={schemas:[i]},n=resolveScimId(e,t);n&&(a.id=n);for(const t of r.entries){if(t.sub){const r={};for(const[i,a]of Object.entries(t.sub)){const t=asString(e[a]);null!=t&&(r[i]=t)}Object.keys(r).length>0&&(a[t.scim]=r);continue}if(t.ldapPrimary||t.ldapSecondary){const r=[];if(t.ldapPrimary){const i=asString(e[t.ldapPrimary]);null!=i&&r.push({value:i,primary:!0})}if(t.ldapSecondary)for(const i of asArray(e[t.ldapSecondary]))r.push({value:i});r.length>0&&(a[t.scim]=r);continue}if(t.ldap)if("array"===t.multi){const r=asArray(e[t.ldap]);r.length>0&&(a[t.scim]=r)}else{const r=asString(e[t.ldap]);null!=r&&(a[t.scim]=r)}}return a.active=!isLocked(e,t),n&&(a.meta={resourceType:"User",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(n,t)}),a}function scimUserToLdap(e,r,t,i){const a={objectClass:i},n=e.userName||e.id||"";for(const t of r.entries){if(t.readOnly||t.operational)continue;const r=e[t.scim];if(null!=r)if(t.sub&&"object"==typeof r)for(const[e,i]of Object.entries(t.sub)){const t=r[e];"string"==typeof t&&t.length>0&&(a[i]=t)}else{if(t.ldapPrimary||t.ldapSecondary){if(!Array.isArray(r))continue;const e=r,i=e.find(e=>!0===e.primary)||e[0],n=e.filter(e=>e!==i);t.ldapPrimary&&i&&i.value&&(a[t.ldapPrimary]=i.value),t.ldapSecondary&&n.length>0&&(a[t.ldapSecondary]=n.map(e=>e.value).filter(Boolean));continue}t.ldap&&(Array.isArray(r)?a[t.ldap]=r.map(e=>String(e)):"string"!=typeof r&&"number"!=typeof r||(a[t.ldap]=String(r)))}}const o=readActive(e.active);if(!1===o&&(a[t.lockAttribute]=t.lockValue||c),!a.cn&&n&&(a.cn=n),!a.sn){const r=e.name?.familyName||e.displayName||n;r&&(a.sn=r)}return{rdn:n,attributes:a,active:o}}function ldapToScimGroup(e,r,i,a){const n={schemas:[t]},o=resolveScimId(e,i);o&&(n.id=o);for(const t of r.entries){if(!t.ldap)continue;const r=e[t.ldap];if(null!=r)if("array"===t.multi){const e=asArray(r);e.length>0&&(n[t.scim]=e)}else{const e=asString(r);null!=e&&(n[t.scim]=e)}}const s=asArray(e.member),c=[];for(const e of s)if(e)if(a){const r=a(e);r&&c.push(r)}else c.push({value:e,type:"User"});return c.length>0&&(n.members=c),o&&(n.meta={resourceType:"Group",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(o,i)}),n}function scimGroupToLdap(e,r,t){const i={objectClass:t},a=e.displayName||e.id||"";for(const t of r.entries){if(t.readOnly||t.operational)continue;if(!t.ldap)continue;const r=e[t.scim];null!=r&&(Array.isArray(r)?i[t.ldap]=r.map(e=>"string"==typeof e?e:JSON.stringify(e)):"string"!=typeof r&&"number"!=typeof r||(i[t.ldap]=String(r)))}return{rdn:a,attributes:i}}function requiredLdapAttributes(e,r=[]){const t=new Set(["objectClass",...a,...r]);for(const r of e.entries)if(r.ldap&&t.add(r.ldap),r.ldapPrimary&&t.add(r.ldapPrimary),r.ldapSecondary&&t.add(r.ldapSecondary),r.sub)for(const e of Object.values(r.sub))t.add(e);return Array.from(t)}function scimPathToLdapAttribute(e,r){const t=e.split(".")[0],i=e.includes(".")?e.split(".").slice(1).join("."):"";for(const e of r.entries)if(e.scim===t){if(e.sub&&i)return e.sub[i];if((e.ldapPrimary||e.ldapSecondary)&&("value"===i||!i))return e.ldapPrimary||e.ldapSecondary;if(e.ldap)return e.ldap}}export{o as DEFAULT_GROUP_MAPPING,s as DEFAULT_LOCK_ATTRIBUTE,c as DEFAULT_LOCK_VALUE,n as DEFAULT_USER_MAPPING,buildLocation,holdsValue,isLocked,ldapTimeToIso,ldapToScimGroup,ldapToScimUser,loadMappingFile,mergeMapping,readActive,requiredLdapAttributes,resolveLockConfig,resolveScimId,scimGroupToLdap,scimPathToLdapAttribute,scimUserToLdap,setLockConfigWarn,warn,withExternalId};
1
+ import e from"fs";import{scimInvalidValue as r,kindOf as t}from"./errors.js";import{SCHEMA_GROUP as a,SCHEMA_USER as i}from"./types.js";const n=["createTimestamp","modifyTimestamp","entryUUID"],o={resourceType:"User",schemas:[i],entries:[{scim:"userName",ldap:"uid"},{scim:"externalId",ldap:"employeeNumber"},{scim:"name",sub:{familyName:"sn",givenName:"givenName",formatted:"cn",middleName:"initials"}},{scim:"displayName",ldap:"displayName"},{scim:"nickName",ldap:"displayName"},{scim:"title",ldap:"title"},{scim:"preferredLanguage",ldap:"preferredLanguage"},{scim:"emails",ldapPrimary:"mail",ldapSecondary:"mailAlternateAddress",multi:"array"},{scim:"phoneNumbers",ldapPrimary:"telephoneNumber",ldapSecondary:"mobile",multi:"array"}]},s={resourceType:"Group",schemas:[a],entries:[{scim:"displayName",ldap:"cn"}]};function withExternalId(e,r){const t=(r||"").trim();return t?e.entries.some(e=>"externalId"===e.scim)?e:{...e,entries:[...e.entries,{scim:"externalId",ldap:t}]}:e}function asString(e){if(null!=e){if(Array.isArray(e)){const r=e[0];return null==r?void 0:String(r)}return Buffer.isBuffer(e)?e.toString():String(e)}}function ldapTimeToIso(e){if(!e)return;const r=/^(\d{4})(\d{2})(\d{2})(\d{2})(?:(\d{2})(?:(\d{2}))?)?(?:[.,](\d+))?(Z|[+-]\d{2}(?:\d{2})?)?$/.exec(e);if(!r)return e;const[,t,a,i,n,o,s,l,c]=r;let u=0;if(l){const e=s?1e3:o?6e4:36e5;u=Math.round(Number(`0.${l}`)*e)}let d=1e3*(60*(60*Number(n)+Number(o||0))+Number(s||0))+u;const m=Math.floor(d/36e5);d-=36e5*m;const f=Math.floor(d/6e4);d-=6e4*f;const p=Math.floor(d/1e3),y=d-1e3*p;let b="Z";c&&"Z"!==c&&(b=`${c[0]}${c.slice(1,3)}:${c.slice(3,5)||"00"}`);const pad=e=>String(e).padStart(2,"0"),g=`${t}-${a}-${i}T${pad(m)}:${pad(f)}:${pad(p)}${y?`.${String(y).padStart(3,"0")}`:""}${b}`;return Number.isNaN(Date.parse(g))?e:g}function asArray(e){return null==e?[]:Array.isArray(e)?e.map(e=>Buffer.isBuffer(e)?e.toString():String(e)):Buffer.isBuffer(e)?[e.toString()]:[String(e)]}function loadMappingFile(r){if(!r)return;const t=e.readFileSync(r,"utf8");return JSON.parse(t)}function mergeMapping(e,r){if(!r)return e;const t=new Map;for(const r of e.entries)t.set(r.scim,r);for(const e of r.entries)t.set(e.scim,e);return{...e,entries:Array.from(t.values())}}const l="pwdAccountLockedTime",c="000001010000Z",u=/^\d{10,14}(\.\d+)?(Z|[+-]\d{2,4})$/;let warn=e=>{console.warn(`WARNING: ${e}`)};function setLockConfigWarn(e){const r=warn;return warn=e,r}const d=/^([A-Za-z][A-Za-z0-9-]*|\d+(\.\d+)+)$/;function resolveLockConfig(e,r,t={attribute:"--scim-user-lock-attribute",value:"--scim-user-lock-value"}){const a=(e||"").trim()||l;if(!d.test(a))throw new Error(`${t.attribute} must be an LDAP attribute name, got '${a}'`);const i=a.toLowerCase()===l.toLowerCase(),n=(r||"").trim();if(!n){if(i)return{attribute:a,value:c};throw new Error(`${t.attribute} is '${a}', so ${t.value} must say what marks an account locked (for nsAccountLock on 389-ds, 'TRUE'). The default '${c}' only means anything to ${l}.`)}return i||n!==c||warn(`${t.value} is the ppolicy convention '${c}' but ${t.attribute} is '${a}', not ${l}. Unless '${a}' is a GeneralizedTime your directory reads as a lock, every deactivation will answer 200 and read back as active:false while the account keeps binding. Verify a deactivation actually prevents a bind.`),i&&!u.test(n)&&warn(`${t.value} '${n}' is not a GeneralizedTime, and ${l} holds one. The directory will refuse the write, or — where the attribute was redefined locally — store a value it does not read as a lock. The ppolicy convention is '${c}'.`),{attribute:a,value:n}}function readActive(e){if(void 0!==e){if("boolean"==typeof e)return e;if("string"==typeof e){const r=e.trim().toLowerCase();if("true"===r)return!0;if("false"===r)return!1}throw r(`Cannot read ${t(e)} as active`)}}function isLocked(e,r){return holdsValue(e,r.lockAttribute,r.lockValue)}function holdsValue(e,r,t){if(!r)return!1;const a=r.toLowerCase(),i=Object.keys(e).find(e=>e.toLowerCase()===a),n=void 0===i?void 0:e[i];if(null==n)return!1;const o=t.toLowerCase();return(Array.isArray(n)?n:[n]).some(e=>null!=e&&e.toString().toLowerCase()===o)}function resolveScimId(e,r){if("rdn"===r.idAttribute)return asString(e[r.rdnAttribute]);return asString(e[r.idAttribute])??asString(e[r.rdnAttribute])}function buildLocation(e,r){if(!r.baseUrl)return;const t="User"===r.resourceType?"Users":"Groups";return`${r.baseUrl.replace(/\/$/,"")}${r.scimPrefix}/${t}/${encodeURIComponent(e)}`}function ldapToScimUser(e,r,t){const a={schemas:[i]},n=resolveScimId(e,t);n&&(a.id=n);for(const t of r.entries){if(t.sub){const r={};for(const[a,i]of Object.entries(t.sub)){const t=asString(e[i]);null!=t&&(r[a]=t)}Object.keys(r).length>0&&(a[t.scim]=r);continue}if(t.ldapPrimary||t.ldapSecondary){const r=[];if(t.ldapPrimary){const a=asString(e[t.ldapPrimary]);null!=a&&r.push({value:a,primary:!0})}if(t.ldapSecondary)for(const a of asArray(e[t.ldapSecondary]))r.push({value:a});r.length>0&&(a[t.scim]=r);continue}if(t.ldap)if("array"===t.multi){const r=asArray(e[t.ldap]);r.length>0&&(a[t.scim]=r)}else{const r=asString(e[t.ldap]);null!=r&&(a[t.scim]=r)}}return a.active=!isLocked(e,t),n&&(a.meta={resourceType:"User",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(n,t)}),a}function scimUserToLdap(e,a,i,n){const o={objectClass:n},s=namingValue(e,"userName")||namingValue(e,"id");for(const i of a.entries){if(i.readOnly||i.operational)continue;const a=e[i.scim];if(null!=a)if(i.sub){if(!a||"object"!=typeof a||Array.isArray(a))throw r(`${i.scim} must be an object, got ${t(a)}`);for(const[e,r]of Object.entries(i.sub)){const t=scalarOrValue(a[e],`${i.scim}.${e}`);void 0!==t&&""!==t&&(o[r]=t)}}else{if(i.ldapPrimary||i.ldapSecondary){if(!Array.isArray(a))throw r(`${i.scim} must be a list, got ${t(a)}`);const e=a.map(e=>multiValued(e,i.scim)).filter(e=>null!=e.value),n=e.find(e=>!0===e.primary)||e[0],s=e.filter(e=>e!==n);i.ldapPrimary&&n&&n.value&&(o[i.ldapPrimary]=n.value),i.ldapSecondary&&s.length>0&&(o[i.ldapSecondary]=s.map(e=>e.value).filter(Boolean));continue}i.ldap&&(o[i.ldap]=scalarValues(a,i.scim))}}const l=readActive(e.active);if(!1===l&&(o[i.lockAttribute]=i.lockValue||c),!o.cn&&s&&(o.cn=s),!o.sn){const r=[e.name?.familyName,e.displayName,s].find(e=>"string"==typeof e&&e.length>0);r&&(o.sn=r)}return{rdn:s,attributes:o,active:l}}function ldapToScimGroup(e,r,t,i){const n={schemas:[a]},o=resolveScimId(e,t);o&&(n.id=o);for(const t of r.entries){if(!t.ldap)continue;const r=e[t.ldap];if(null!=r)if("array"===t.multi){const e=asArray(r);e.length>0&&(n[t.scim]=e)}else{const e=asString(r);null!=e&&(n[t.scim]=e)}}const s=asArray(e.member),l=[];for(const e of s)if(e)if(i){const r=i(e);r&&l.push(r)}else l.push({value:e,type:"User"});return l.length>0&&(n.members=l),o&&(n.meta={resourceType:"Group",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(o,t)}),n}function scimGroupToLdap(e,r,t){const a={objectClass:t},i=namingValue(e,"displayName")||namingValue(e,"id");for(const t of r.entries){if(t.readOnly||t.operational)continue;if(!t.ldap)continue;const r=e[t.scim];null!=r&&(a[t.ldap]=scalarValues(r,t.scim))}return{rdn:i,attributes:a}}function namingValue(e,a){const i=e[a];if(null==i)return"";if("string"!=typeof i)throw r(`${a} must be a string, got ${t(i)}`);return i}function scalarOrValue(e,a){const i=e&&"object"==typeof e&&!Array.isArray(e)&&"value"in e?e.value:e;if(null!=i){if(isScalar(i))return String(i);throw r(`${a} must be a string, got ${t(i)}`)}}const isScalar=e=>"string"==typeof e||"number"==typeof e||"boolean"==typeof e;function scalarValues(e,a){if(isScalar(e))return String(e);if(Array.isArray(e)&&e.every(isScalar))return e.map(String);throw r(`${a} must be a string, a number, a boolean or a list of them, got ${t(e)}`)}function multiValued(e,a){if(e&&"object"==typeof e&&!Array.isArray(e)){const{value:r}=e;if(null==r)return e;if(isScalar(r))return{...e,value:String(r)}}throw r(`Each element of ${a} must be an object whose value is a scalar, got ${t(e)}`)}function requiredLdapAttributes(e,r=[]){const t=new Set(["objectClass",...n,...r]);for(const r of e.entries)if(r.ldap&&t.add(r.ldap),r.ldapPrimary&&t.add(r.ldapPrimary),r.ldapSecondary&&t.add(r.ldapSecondary),r.sub)for(const e of Object.values(r.sub))t.add(e);return Array.from(t)}function scimPathToLdapAttribute(e,r){if("string"!=typeof e)return;const t=e.split(".")[0],a=e.includes(".")?e.split(".").slice(1).join("."):"";for(const e of r.entries)if(e.scim===t){if(e.sub&&a)return Object.prototype.hasOwnProperty.call(e.sub,a)?e.sub[a]:void 0;if((e.ldapPrimary||e.ldapSecondary)&&("value"===a||!a))return e.ldapPrimary||e.ldapSecondary;if(e.ldap)return e.ldap}}export{s as DEFAULT_GROUP_MAPPING,l as DEFAULT_LOCK_ATTRIBUTE,c as DEFAULT_LOCK_VALUE,o as DEFAULT_USER_MAPPING,buildLocation,holdsValue,isLocked,ldapTimeToIso,ldapToScimGroup,ldapToScimUser,loadMappingFile,mergeMapping,readActive,requiredLdapAttributes,resolveLockConfig,resolveScimId,scimGroupToLdap,scimPathToLdapAttribute,scimUserToLdap,setLockConfigWarn,warn,withExternalId};
@@ -1 +1 @@
1
- import{extractLdapCode as e}from"../../lib/ldapCodes.js";import{scimInvalidValue as t,ScimError as r,scimInvalidPath as o,scimNoTarget as n}from"./errors.js";import{DEFAULT_LOCK_ATTRIBUTE as i,DEFAULT_LOCK_VALUE as a,holdsValue as s,scimPathToLdapAttribute as l,readActive as u}from"./mapping.js";function asValues(e){if(null==e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString():String(e)).filter(e=>e.length>0)}class WorkingEntry{constructor(e){this.current=e,this.values=new Map,this.touched=new Set,this.whole=new Set}get(e){return this.values.has(e)||this.values.set(e,asValues(this.current[e])),this.values.get(e)}set(e,t){this.whole.add(e),this.write(e,t)}append(e,t){const r=this.get(e);this.write(e,[...r,...t.filter(e=>!r.includes(e))])}drop(e,t){if(!t)return this.set(e,[]);this.write(e,this.get(e).filter(e=>!t.includes(e)))}write(e,t){this.touched.add(e),this.values.set(e,t)}diff(){const e={};for(const t of this.touched){const r=asValues(this.current[t]),o=this.get(t),n=o.filter(e=>!r.includes(e)),i=r.filter(e=>!o.includes(e));if(0!==n.length||0!==i.length)if(this.whole.has(t))if(0!==o.length)0===i.length&&r.length>0?(e.add||(e.add={}),e.add[t]=1===n.length?n[0]:n):(e.replace||(e.replace={}),e.replace[t]=1===o.length?o[0]:o);else{if(0===r.length)continue;e.delete||(e.delete={}),e.delete[t]=""}else n.length>0&&(e.add||(e.add={}),e.add[t]=1===n.length?n[0]:n),i.length>0&&(e.delete||(e.delete={}),e.delete[t]=1===i.length?i[0]:i)}return e}}function normalizeOp(e){const r=e.toLowerCase();if("add"===r||"remove"===r||"replace"===r)return r;throw t(`Unknown patch op '${e}'`)}const f=new Set(["__proto__","constructor","prototype"]),c=/^[A-Za-z_$:][\w$:]*$/;function assertSafeKey(e){if(f.has(e))throw o(`Forbidden attribute name '${e}'`)}function parsePath(e){if("string"!=typeof e||0===e.length||e.length>512)throw o(`Malformed path '${String(e)}'`);let t,r,n;const i=e.indexOf("[");if(i>=0){const a=e.lastIndexOf("]");if(a<=i)throw o(`Malformed path '${e}'`);t=e.slice(0,i),r=e.slice(i+1,a);const s=e.slice(a+1);if(s.length>0){if("."!==s[0])throw o(`Malformed path '${e}'`);n=s.slice(1)}}else{const r=e.indexOf(".");r>=0?(t=e.slice(0,r),n=e.slice(r+1)):t=e}if(!c.test(t))throw o(`Malformed path '${e}'`);if(assertSafeKey(t),null!=n){if(!c.test(n))throw o(`Malformed sub-attribute in '${e}'`);assertSafeKey(n)}return{top:t,filter:r,sub:n}}async function applyOperation(e,r,f){const c=normalizeOp(e.op),p=f.memberAttribute||"member";if(!e.path){if("remove"===c)throw n("PATCH 'remove' requires a path");if(null==e.value||"object"!=typeof e.value||Array.isArray(e.value))throw n("PATCH without path requires an object value");const t=e.value;for(const[o,n]of Object.entries(t))await applyOperation({op:e.op,path:o,value:n},r,f);return}const{top:h,sub:d,filter:y}=parsePath(e.path);if("members"===h){const t=f.resolveMemberRef;if(!t)throw n("Member operations require a member resolver");const asMemberValues=async e=>{const r=Array.isArray(e)?e:[e],o=[];for(const e of r){const r="string"==typeof e?e:"object"==typeof e&&null!=e&&"value"in e?String(e.value):"";if(!r)continue;const n=await t(r);n&&o.push(n)}return o};if("add"===c)return void r.append(p,await asMemberValues(e.value));if("replace"===c){const t=await asMemberValues(e.value);if(0===t.length)return;return void r.set(p,t)}if(!y&&null==e.value)return void r.drop(p);const o=y?/value\s+eq\s+"([^"]+)"/i.exec(y)?.[1]??"":"",i=[...o?await asMemberValues(o):[],...null!=e.value?await asMemberValues(e.value):[]];return void r.drop(p,i)}if("active"===h&&f.supportsActive){if(d||y)throw o(`'active' has no sub-attribute (got '${e.path}')`);const n=f.lockAttribute||i,l=Object.keys(f.current).find(e=>e.toLowerCase()===n.toLowerCase())??n,p="remove"===c||function(e){const r=u(e);if(void 0===r)throw t("PATCH on 'active' requires a value");return r}(e.value),h=f.lockValue||a;return void(p?s({[l]:r.get(l)},l,h)&&r.drop(l):r.set(l,[h]))}if(y)throw o(`Complex multi-valued filters are only supported on 'members' (got '${e.path}')`);const v=l(d?`${h}.${d}`:h,f.mapping);if(!v)throw o(`Unknown SCIM attribute path '${e.path}'`);if("remove"===c)return void r.drop(v);const w=function(e){if(null!=e)return"string"==typeof e?e:"number"==typeof e||"boolean"==typeof e?String(e):Array.isArray(e)?e.map(e=>{if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);if(e&&"object"==typeof e&&!Array.isArray(e)&&"value"in e&&("string"==typeof e.value||"number"==typeof e.value||"boolean"==typeof e.value))return String(e.value);throw t(`Unsupported array element in PATCH value: ${JSON.stringify(e)}`)}):void 0}(e.value);if(null==w)throw t(`PATCH ${e.op} ${e.path} missing value`);const g=Array.isArray(w)?w:[w];"add"===c?r.append(v,g):r.set(v,g)}async function patchToModifyRequest(e,r){if(!e.Operations||!Array.isArray(e.Operations))throw t("Missing Operations array");const o=new WorkingEntry(r.current);for(const t of e.Operations)await applyOperation(t,o,r);return o.diff()}async function writePatch(t,o){for(let n=1;;n++)try{return await o()}catch(o){if(16!==e(o))throw o;if(2===n)throw new r(409,`${t} changed while this PATCH was being applied; nothing was written. Retry the request.`)}}function applyPatchToResource(e,t){const r={...e};for(const e of t.Operations){const t=normalizeOp(e.op);if(!e.path){if(e.value&&"object"==typeof e.value&&!Array.isArray(e.value))for(const[t,o]of Object.entries(e.value))assertSafeKey(t),r[t]=o;continue}const{top:o,sub:n}=parsePath(e.path);if("remove"!==t)if(n){const t=r[o],i=t&&"object"==typeof t&&!Array.isArray(t)?{...t}:Object.create(null);i[n]=e.value,r[o]=i}else r[o]=e.value;else if(n){const e=r[o];e&&"object"==typeof e&&delete e[n]}else delete r[o]}return r}export{applyPatchToResource,patchToModifyRequest,writePatch};
1
+ import{extractLdapCode as e}from"../../lib/ldapCodes.js";import{scimInvalidValue as t,kindOf as r,ScimError as o,scimInvalidPath as a,scimNoTarget as n}from"./errors.js";import{DEFAULT_LOCK_ATTRIBUTE as i,DEFAULT_LOCK_VALUE as s,holdsValue as l,scimPathToLdapAttribute as u,readActive as f}from"./mapping.js";function asValues(e){if(null==e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString():String(e)).filter(e=>e.length>0)}class WorkingEntry{constructor(e){this.current=e,this.values=new Map,this.touched=new Set,this.whole=new Set}get(e){return this.values.has(e)||this.values.set(e,asValues(this.current[e])),this.values.get(e)}set(e,t){this.whole.add(e),this.write(e,t)}append(e,t){const r=this.get(e);this.write(e,[...r,...t.filter(e=>!r.includes(e))])}drop(e,t){if(!t)return this.set(e,[]);this.write(e,this.get(e).filter(e=>!t.includes(e)))}write(e,t){this.touched.add(e),this.values.set(e,t)}diff(){const e={};for(const t of this.touched){const r=asValues(this.current[t]),o=this.get(t),a=o.filter(e=>!r.includes(e)),n=r.filter(e=>!o.includes(e));if(0!==a.length||0!==n.length)if(this.whole.has(t))if(0!==o.length)0===n.length&&r.length>0?(e.add||(e.add={}),e.add[t]=1===a.length?a[0]:a):(e.replace||(e.replace={}),e.replace[t]=1===o.length?o[0]:o);else{if(0===r.length)continue;e.delete||(e.delete={}),e.delete[t]=""}else a.length>0&&(e.add||(e.add={}),e.add[t]=1===a.length?a[0]:a),n.length>0&&(e.delete||(e.delete={}),e.delete[t]=1===n.length?n[0]:n)}return e}}function normalizeOp(e){if("string"!=typeof e)throw t(`Patch op must be a string, got ${r(e)}`);const o=e.toLowerCase();if("add"===o||"remove"===o||"replace"===o)return o;throw t(`Unknown patch op '${e}'`)}const c=new Set(["__proto__","constructor","prototype"]),p=/^[A-Za-z_$:][\w$:]*$/;function assertSafeKey(e){if(c.has(e))throw a(`Forbidden attribute name '${e}'`)}function parsePath(e){if("string"!=typeof e||0===e.length||e.length>512)throw a(`Malformed path '${String(e)}'`);let t,r,o;const n=e.indexOf("[");if(n>=0){const i=e.lastIndexOf("]");if(i<=n)throw a(`Malformed path '${e}'`);t=e.slice(0,n),r=e.slice(n+1,i);const s=e.slice(i+1);if(s.length>0){if("."!==s[0])throw a(`Malformed path '${e}'`);o=s.slice(1)}}else{const r=e.indexOf(".");r>=0?(t=e.slice(0,r),o=e.slice(r+1)):t=e}if(!p.test(t))throw a(`Malformed path '${e}'`);if(assertSafeKey(t),null!=o){if(!p.test(o))throw a(`Malformed sub-attribute in '${e}'`);assertSafeKey(o)}return{top:t,filter:r,sub:o}}function memberRef(e){const o=e&&"object"==typeof e&&!Array.isArray(e)?e.value:e;if(null==o)return"";if("string"==typeof o||"number"==typeof o)return String(o);throw t(`Each member must be a string or an object whose value is one, got ${r(o)}`)}async function applyOperation(e,o,c){const p=normalizeOp(e.op),h=c.memberAttribute||"member";if(!e.path){if("remove"===p)throw n("PATCH 'remove' requires a path");if(null==e.value||"object"!=typeof e.value||Array.isArray(e.value))throw n("PATCH without path requires an object value");const t=e.value;for(const[r,n]of Object.entries(t)){if(!r)throw a("Empty attribute name");await applyOperation({op:e.op,path:r,value:n},o,c)}return}if("string"!=typeof e.path)throw a(`Path must be a string, got ${r(e.path)}`);const{top:d,sub:y,filter:w}=parsePath(e.path);if("members"===d){const t=c.resolveMemberRef;if(!t)throw n("Member operations require a member resolver");const asMemberValues=async e=>{const r=Array.isArray(e)?e:[e],o=[];for(const e of r){const r=memberRef(e);if(!r)continue;const a=await t(r);a&&o.push(a)}return o};if("add"===p)return void o.append(h,await asMemberValues(e.value));if("replace"===p){const t=await asMemberValues(e.value);if(0===t.length)return;return void o.set(h,t)}if(!w&&null==e.value)return void o.drop(h);const r=w?/value\s+eq\s+"([^"]+)"/i.exec(w)?.[1]??"":"",a=[...r?await asMemberValues(r):[],...null!=e.value?await asMemberValues(e.value):[]];return void o.drop(h,a)}if("active"===d&&c.supportsActive){if(y||w)throw a(`'active' has no sub-attribute (got '${e.path}')`);const r=c.lockAttribute||i,n=Object.keys(c.current).find(e=>e.toLowerCase()===r.toLowerCase())??r,u="remove"===p||function(e){const r=f(e);if(void 0===r)throw t("PATCH on 'active' requires a value");return r}(e.value),h=c.lockValue||s;return void(u?l({[n]:o.get(n)},n,h)&&o.drop(n):o.set(n,[h]))}if(w)throw a(`Complex multi-valued filters are only supported on 'members' (got '${e.path}')`);const m=u(y?`${d}.${y}`:d,c.mapping);if(!m)throw a(`Unknown SCIM attribute path '${e.path}'`);if("remove"===p)return void o.drop(m);const v=function(e){if(null!=e)return"string"==typeof e?e:"number"==typeof e||"boolean"==typeof e?String(e):Array.isArray(e)?e.map(e=>{if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);if(e&&"object"==typeof e&&!Array.isArray(e)&&"value"in e&&("string"==typeof e.value||"number"==typeof e.value||"boolean"==typeof e.value))return String(e.value);throw t(`Unsupported array element in PATCH value: ${r(e)}`)}):void 0}(e.value);if(null==v)throw t(`PATCH ${e.op} ${e.path} missing value`);const g=Array.isArray(v)?v:[v];"add"===p?o.append(m,g):o.set(m,g)}async function patchToModifyRequest(e,o){if(!e.Operations||!Array.isArray(e.Operations))throw t("Missing Operations array");const a=new WorkingEntry(o.current);for(const n of e.Operations){if(!n||"object"!=typeof n||Array.isArray(n))throw t(`Each operation must be an object, got ${r(n)}`);await applyOperation(n,a,o)}return a.diff()}async function writePatch(t,r){for(let a=1;;a++)try{return await r()}catch(r){if(16!==e(r))throw r;if(2===a)throw new o(409,`${t} changed while this PATCH was being applied; nothing was written. Retry the request.`)}}function applyPatchToResource(e,t){const r={...e};for(const e of t.Operations){const t=normalizeOp(e.op);if(!e.path){if(e.value&&"object"==typeof e.value&&!Array.isArray(e.value))for(const[t,o]of Object.entries(e.value))assertSafeKey(t),r[t]=o;continue}const{top:o,sub:a}=parsePath(e.path);if("remove"!==t)if(a){const t=r[o],n=t&&"object"==typeof t&&!Array.isArray(t)?{...t}:Object.create(null);n[a]=e.value,r[o]=n}else r[o]=e.value;else if(a){const e=r[o];e&&"object"==typeof e&&delete e[a]}else delete r[o]}return r}export{applyPatchToResource,patchToModifyRequest,writePatch};