ldap-rest 0.3.8 → 0.3.10

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.
@@ -38,6 +38,11 @@ Environment variables:
38
38
  process.exit(0);
39
39
  }
40
40
 
41
+ // A base-scoped LDAP search on a non-existent entry returns noSuchObject (32)
42
+ // rather than an empty result set, so callers must treat it as "absent".
43
+ const isNoSuchObject = err =>
44
+ err?.code === 32 || /no such object|0x20/i.test(err?.message || '');
45
+
41
46
  async function syncAppAccounts() {
42
47
  const dm = new DM();
43
48
  await dm.ready;
@@ -56,6 +61,20 @@ async function syncAppAccounts() {
56
61
  process.exit(1);
57
62
  }
58
63
 
64
+ // Fail fast with a clear message if the applicative branch is missing,
65
+ // instead of letting the orphan scan abort with a raw NoSuchObjectError.
66
+ try {
67
+ await dm.ldap.search({ scope: 'base', paged: false }, applicativeBase);
68
+ } catch (err) {
69
+ if (isNoSuchObject(err)) {
70
+ dm.logger.error(
71
+ `Applicative base ${applicativeBase} does not exist. Create it first (e.g. an organizationalUnit entry) before running this sync.`
72
+ );
73
+ process.exit(1);
74
+ }
75
+ throw err;
76
+ }
77
+
59
78
  if (!quiet) {
60
79
  dm.logger.info('Starting applicative accounts synchronization...');
61
80
  dm.logger.info(`User base: ${userBase}`);
@@ -101,15 +120,26 @@ async function syncAppAccounts() {
101
120
  if (!mail) continue;
102
121
 
103
122
  try {
104
- // Check if principal account exists
123
+ // Check if principal account exists. A base search on a missing entry
124
+ // throws noSuchObject rather than returning an empty set, so we treat
125
+ // that exception as "absent" and fall through to creation.
105
126
  const principalDn = `uid=${mail},${applicativeBase}`;
106
- const principalResult = await dm.ldap.search(
107
- {
108
- scope: 'base',
109
- paged: false,
110
- },
111
- principalDn
112
- );
127
+ let principalResult;
128
+ try {
129
+ principalResult = await dm.ldap.search(
130
+ {
131
+ scope: 'base',
132
+ paged: false,
133
+ },
134
+ principalDn
135
+ );
136
+ } catch (err) {
137
+ if (isNoSuchObject(err)) {
138
+ principalResult = { searchEntries: [] };
139
+ } else {
140
+ throw err;
141
+ }
142
+ }
113
143
 
114
144
  if (
115
145
  !principalResult.searchEntries ||
@@ -136,17 +166,23 @@ async function syncAppAccounts() {
136
166
  'displayName',
137
167
  ];
138
168
  for (const attr of attrsToCopy) {
139
- if (user[attr]) {
140
- const value = user[attr];
141
- if (Array.isArray(value)) {
142
- attrs[attr] = value.map(v =>
143
- Buffer.isBuffer(v) ? v.toString() : String(v)
144
- );
145
- } else {
146
- attrs[attr] = Buffer.isBuffer(value)
147
- ? value.toString()
148
- : String(value);
149
- }
169
+ const value = user[attr];
170
+ // Skip absent values. A requested-but-missing attribute comes
171
+ // back as an empty array (truthy in JS), which would otherwise
172
+ // produce an `add` with no values → ProtocolError "no values for
173
+ // attribute type".
174
+ if (value === undefined || value === null || value === '') {
175
+ continue;
176
+ }
177
+ if (Array.isArray(value)) {
178
+ if (value.length === 0) continue;
179
+ attrs[attr] = value.map(v =>
180
+ Buffer.isBuffer(v) ? v.toString() : String(v)
181
+ );
182
+ } else {
183
+ attrs[attr] = Buffer.isBuffer(value)
184
+ ? value.toString()
185
+ : String(value);
150
186
  }
151
187
  }
152
188
 
@@ -265,12 +301,19 @@ async function syncAppAccounts() {
265
301
  dm.logger.error('Error during synchronization:', err);
266
302
  process.exit(1);
267
303
  } finally {
268
- await dm.ldap.unbind();
304
+ // ldapActions exposes no public unbind; pooled connections are released on
305
+ // process exit. Guard so this teardown never throws if the method is absent.
306
+ if (typeof dm.ldap.unbind === 'function') {
307
+ await dm.ldap.unbind();
308
+ }
269
309
  }
270
310
  }
271
311
 
272
- // Run the sync
273
- syncAppAccounts().catch(err => {
274
- console.error('Fatal error:', err);
275
- process.exit(1);
276
- });
312
+ // Run the sync. Pooled LDAP connections keep the event loop alive, so exit
313
+ // explicitly once the sync resolves instead of hanging.
314
+ syncAppAccounts()
315
+ .then(() => process.exit(0))
316
+ .catch(err => {
317
+ console.error('Fatal error:', err);
318
+ process.exit(1);
319
+ });
package/dist/bin/index.js CHANGED
@@ -1 +1 @@
1
- import{fileURLToPath as e}from"url";import{join as r,dirname as i}from"path";import s from"express";import o from"body-parser";import{parseConfig as t}from"../lib/parseConfig.js";import n from"../config/args.js";import a from"../lib/ldapActions.js";import{buildLogger as l}from"../logger/winston.js";import{setLogger as d}from"../lib/expressFormatedResponses.js";import g from"../plugins/priority.json.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,launchHooks,launchHooksChained,parseDn,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this._errorMiddlewareSetup=!1,this.config=t(n),this.app=s(),this.app.use(o.json()),this.app.use(o.urlencoded({extended:!0})),this.logger=l(this.config),this.ldap=new a(this),d(this.logger),this.operationSequence=0;const e=[];if(this.config.plugin){const r=this.config.plugin.find(e=>e.includes("configApi"));let i=this.config.plugin.filter(e=>!e.includes("configApi"));const s=(async()=>{for(const e of g)i.includes(e)&&(i=i.filter(r=>r!==e),await this.loadPlugin(e))})();e.push(s),e.push(s.then(async()=>{const e=i.map(e=>this.loadPlugin(e));await Promise.all(e)})),r&&e.push(Promise.all(e).then(()=>this.loadPlugin(r)))}this.ready=new Promise((r,i)=>{e.length>0?Promise.all(e).then(()=>{this.setupErrorMiddleware(),r()}).catch(e=>i(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),r())})}setupErrorMiddleware(){if(this._errorMiddlewareSetup&&this._errorMiddleware){const e=this.app._router?.stack;if(e){const r=e.findIndex(e=>e.handle===this._errorMiddleware);-1!==r&&e.splice(r,1)}}this._errorMiddleware=(e,r,i,s)=>{let o="statusCode"in e?e.statusCode:500,t=e.message;if(500===o&&/\[authz-forbidden\]/.test(e.message)?(o=403,t="Token does not have permission on this branch"):403===o&&/\[authz-forbidden\]/.test(e.message)&&(t="Token does not have permission on this branch"),o>=400&&o<500)return this.logger.warn(`Client error ${o} in request ${r.method} ${r.path}: ${t}`),i.headersSent?void 0:i.status(o).json({error:t});this.logger.error(`Server error ${o} in request ${r.method} ${r.path}: ${e.message}`,{stack:e.stack,url:r.url,method:r.method}),i.headersSent||i.status(o).json({error:"Internal Server Error",message:this.config.debug?e.message:"An error occurred"})},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}run(){return process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack})}),process.on("unhandledRejection",(e,r)=>{this.logger.error(`Unhandled promise rejection: ${e}`,{reason:e,promise:r})}),new Promise((e,r)=>{this.server=this.app.listen(this.config.port,i=>{i?(this.logger.error(`Error starting server: ${i}`),r(i)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}loadPlugin(s){let o,t;if(/:/.test(s)){let e=s.substring(s.indexOf(":")+1);if(s=s.substring(0,s.indexOf(":")),/:/.test(e)){o=e.substring(0,e.indexOf(":")),o||(o=void 0),e=e.substring(e.indexOf(":")+1);try{t=JSON.parse(e),"object"!=typeof t?(this.logger.error(`Overrides for plugin ${s} are not valid: ${e}`),t=void 0):this.logger.debug(`Overrides for plugin ${o||s}: ${e}`)}catch(r){this.logger.error(`Failed to parse overrides for plugin ${s}: ${r}, using ${e}`),t=void 0}}else o=e,o||(o=void 0)}else o=void 0;return this.logger.debug(`Loading plugin ${s}`),s.startsWith("core/")&&(s=s.replace("core/",r(i(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js")),new Promise((e,r)=>{import(s).then(async i=>{let n;if(i&&i.default&&(i=i.default),t){const e={...this.config,...t};n=new i({...this,config:e})}else n=new i(this);if(!n)return r(new Error(`Unable to load ${s}`));e(await this.registerPlugin(s,n,o)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(e=>r(new Error(`Failed to load plugin ${s}: ${e}`)))})}async registerPlugin(e,r,i){if(r.name||(r.name=e),i&&(r.name=i),this.loadedPlugins[r.name])return this.logger.info(`Plugin ${e} already loaded as ${r.name}`),!1;if(this.logger.debug(`Registering plugin ${e} as ${r.name}`),r.dependencies)for(const e in r.dependencies)this.loadedPlugins[e]||(this.logger.debug(`Plugin ${r.name} depends on ${e}, loading it first`),await this.loadPlugin(r.dependencies[e]));if(r.api&&(this.logger.debug(`Plugin ${r.name} has API, registering it`),await r.api(this.app),this._errorMiddlewareSetup&&this.setupErrorMiddleware()),r.hooks)for(const e in r.hooks){this.logger.debug(`Plugin ${r.name} has hook ${e}, registering it`);const i=r.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!i||"function"!=typeof i)throw new Error(`Plugin ${r.name}: hook ${e} is invalid`);this.hooks[e].push(i)}return this.loadedPlugins[r.name]=r,!0}}export{DM};
1
+ import{fileURLToPath as e}from"url";import{join as r,dirname as i}from"path";import s from"express";import o from"body-parser";import{parseConfig as t}from"../lib/parseConfig.js";import n from"../config/args.js";import a from"../lib/ldapActions.js";import{buildLogger as l}from"../logger/winston.js";import{setLogger as d}from"../lib/expressFormatedResponses.js";import g from"../plugins/priority.json.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,isDnInBranch,launchHooks,launchHooksChained,normalizeDn,parseDn,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this._errorMiddlewareSetup=!1,this.config=t(n),this.app=s(),this.app.use(o.json()),this.app.use(o.urlencoded({extended:!0})),this.logger=l(this.config),this.ldap=new a(this),d(this.logger),this.operationSequence=0;const e=[];if(this.config.plugin){const r=this.config.plugin.find(e=>e.includes("configApi"));let i=this.config.plugin.filter(e=>!e.includes("configApi"));const s=(async()=>{for(const e of g)i.includes(e)&&(i=i.filter(r=>r!==e),await this.loadPlugin(e))})();e.push(s),e.push(s.then(async()=>{const e=i.map(e=>this.loadPlugin(e));await Promise.all(e)})),r&&e.push(Promise.all(e).then(()=>this.loadPlugin(r)))}this.ready=new Promise((r,i)=>{e.length>0?Promise.all(e).then(()=>{this.setupErrorMiddleware(),r()}).catch(e=>i(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),r())})}setupErrorMiddleware(){if(this._errorMiddlewareSetup&&this._errorMiddleware){const e=this.app._router?.stack;if(e){const r=e.findIndex(e=>e.handle===this._errorMiddleware);-1!==r&&e.splice(r,1)}}this._errorMiddleware=(e,r,i,s)=>{let o="statusCode"in e?e.statusCode:500,t=e.message;if(500===o&&/\[authz-forbidden\]/.test(e.message)?(o=403,t="Token does not have permission on this branch"):403===o&&/\[authz-forbidden\]/.test(e.message)&&(t="Token does not have permission on this branch"),o>=400&&o<500)return this.logger.warn(`Client error ${o} in request ${r.method} ${r.path}: ${t}`),i.headersSent?void 0:i.status(o).json({error:t});this.logger.error(`Server error ${o} in request ${r.method} ${r.path}: ${e.message}`,{stack:e.stack,url:r.url,method:r.method}),i.headersSent||i.status(o).json({error:"Internal Server Error",message:this.config.debug?e.message:"An error occurred"})},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}run(){return process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack})}),process.on("unhandledRejection",(e,r)=>{this.logger.error(`Unhandled promise rejection: ${e}`,{reason:e,promise:r})}),new Promise((e,r)=>{this.server=this.app.listen(this.config.port,i=>{i?(this.logger.error(`Error starting server: ${i}`),r(i)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}loadPlugin(s){let o,t;if(/:/.test(s)){let e=s.substring(s.indexOf(":")+1);if(s=s.substring(0,s.indexOf(":")),/:/.test(e)){o=e.substring(0,e.indexOf(":")),o||(o=void 0),e=e.substring(e.indexOf(":")+1);try{t=JSON.parse(e),"object"!=typeof t?(this.logger.error(`Overrides for plugin ${s} are not valid: ${e}`),t=void 0):this.logger.debug(`Overrides for plugin ${o||s}: ${e}`)}catch(r){this.logger.error(`Failed to parse overrides for plugin ${s}: ${r}, using ${e}`),t=void 0}}else o=e,o||(o=void 0)}else o=void 0;return this.logger.debug(`Loading plugin ${s}`),s.startsWith("core/")&&(s=s.replace("core/",r(i(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js")),new Promise((e,r)=>{import(s).then(async i=>{let n;if(i&&i.default&&(i=i.default),t){const e={...this.config,...t};n=new i({...this,config:e})}else n=new i(this);if(!n)return r(new Error(`Unable to load ${s}`));e(await this.registerPlugin(s,n,o)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(e=>r(new Error(`Failed to load plugin ${s}: ${e}`)))})}async registerPlugin(e,r,i){if(r.name||(r.name=e),i&&(r.name=i),this.loadedPlugins[r.name])return this.logger.info(`Plugin ${e} already loaded as ${r.name}`),!1;if(this.logger.debug(`Registering plugin ${e} as ${r.name}`),r.dependencies)for(const e in r.dependencies)this.loadedPlugins[e]||(this.logger.debug(`Plugin ${r.name} depends on ${e}, loading it first`),await this.loadPlugin(r.dependencies[e]));if(r.api&&(this.logger.debug(`Plugin ${r.name} has API, registering it`),await r.api(this.app),this._errorMiddlewareSetup&&this.setupErrorMiddleware()),r.hooks)for(const e in r.hooks){this.logger.debug(`Plugin ${r.name} has hook ${e}, registering it`);const i=r.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!i||"function"!=typeof i)throw new Error(`Plugin ${r.name}: hook ${e} is invalid`);this.hooks[e].push(i)}return this.loadedPlugins[r.name]=r,!0}}export{DM};
package/dist/lib/utils.js CHANGED
@@ -1 +1 @@
1
- import{getLogger as e}from"./expressFormatedResponses.js";const t=e(),r=new Map;function getCompiledRegex(e,t){const n=t?`${e}:${t}`:e;let a=r.get(n);return a||(a=new RegExp(e,t),r.set(n,a)),a}function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const launchHooks=async(e,...r)=>{if(e)for(const n of e)if(n)try{await n(...r)}catch(e){t.error("Hook error",e)}},launchHooksChained=async(e,t)=>{if(e)for(const r of e)r&&(t=await r(t));return t},transformSchemas=(e,t)=>e.toString().replace(/__(\S+)__/g,(e,r)=>{if(!r||"string"!=typeof r)return e;const n=r.trim().toLowerCase();return t[n]?"object"!=typeof t[n]?t[n]:JSON.stringify(t[n]):e});function escapeLdapFilter(e){return e.replace(/\\/g,"\\5c").replace(/\*/g,"\\2a").replace(/\(/g,"\\28").replace(/\)/g,"\\29").replace(/\0/g,"\\00")}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(/^#/,"\\#")}function unescapeDnValue(e){let t="",r=0;for(;r<e.length;)if("\\"===e[r]&&r+1<e.length){const n=e[r+1];if(/[0-9a-fA-F]/.test(n)&&r+2<e.length){const n=e.substring(r+1,r+3);if(/^[0-9a-fA-F]{2}$/.test(n)){t+=String.fromCharCode(parseInt(n,16)),r+=3;continue}}t+=n,r+=2}else t+=e[r],r++;return t}function validateDnValue(e,t){if(null==e||"string"!=typeof e)throw new Error(`${t} must be a string`);if(0===e.trim().length)throw new Error(`${t} must be a non-empty string`);if(/[\x00-\x1F\x7F]/.test(e))throw new Error(`${t} contains invalid control characters`);if(/[\u200B-\u200F\uFEFF]/.test(e))throw new Error(`${t} contains invalid invisible characters`)}function parseDn(e){const t=[];let r="",n=!1;for(let a=0;a<e.length;a++){const o=e[a];n?(r+=o,n=!1):"\\"===o?(r+=o,n=!0):","===o?(t.push(r.trim()),r=""):r+=o}return r&&t.push(r.trim()),t}function getParentDn(e){const t=parseDn(e);return t.length<=1?e:t.slice(1).join(",")}function getRdn(e){return parseDn(e)[0]||""}function isChildOf(e,t){const r=e.toLowerCase(),n=t.toLowerCase();if(!r.endsWith(n))return!1;if(r.length===n.length)return!1;return r.substring(0,r.length-n.length).endsWith(",")}const asyncHandler=e=>(t,r,n)=>{Promise.resolve(e(t,r,n)).catch(n)};export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,launchHooks,launchHooksChained,parseDn,transformSchemas,unescapeDnValue,validateDnValue};
1
+ import{getLogger as e}from"./expressFormatedResponses.js";const n=e(),t=new Map;function getCompiledRegex(e,n){const r=n?`${e}:${n}`:e;let i=t.get(r);return i||(i=new RegExp(e,n),t.set(r,i)),i}function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const launchHooks=async(e,...t)=>{if(e)for(const r of e)if(r)try{await r(...t)}catch(e){n.error("Hook error",e)}},launchHooksChained=async(e,n)=>{if(e)for(const t of e)t&&(n=await t(n));return n},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 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(/^#/,"\\#")}function unescapeDnValue(e){let n="",t=0;for(;t<e.length;)if("\\"===e[t]&&t+1<e.length){const r=e[t+1];if(/[0-9a-fA-F]/.test(r)&&t+2<e.length){const r=e.substring(t+1,t+3);if(/^[0-9a-fA-F]{2}$/.test(r)){n+=String.fromCharCode(parseInt(r,16)),t+=3;continue}}n+=r,t+=2}else n+=e[t],t++;return n}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 parseDn(e){const n=[];let t="",r=!1;for(let i=0;i<e.length;i++){const o=e[i];r?(t+=o,r=!1):"\\"===o?(t+=o,r=!0):","===o?(n.push(t.trim()),t=""):t+=o}return t&&n.push(t.trim()),n}function getParentDn(e){const n=parseDn(e);return n.length<=1?e:n.slice(1).join(",")}function getRdn(e){return parseDn(e)[0]||""}function isChildOf(e,n){const t=e.toLowerCase(),r=n.toLowerCase();if(!t.endsWith(r))return!1;if(t.length===r.length)return!1;return t.substring(0,t.length-r.length).endsWith(",")}function normalizeDn(e){return parseDn(e).map(e=>e.split("+").map(e=>{const n=e.indexOf("=");return(-1===n?e.trim():`${e.slice(0,n).trim()}=${e.slice(n+1).trim()}`).toLowerCase()}).sort().join("+")).filter(e=>e.length>0).join(",")}function isDnInBranch(e,n){const t=normalizeDn(n).split(",").filter(Boolean);if(0===t.length)return!1;const r=normalizeDn(e).split(",").filter(Boolean);if(r.length<t.length)return!1;return r.slice(r.length-t.length).every((e,n)=>e===t[n])}const asyncHandler=e=>(n,t,r)=>{Promise.resolve(e(n,t,r)).catch(r)};export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,isDnInBranch,launchHooks,launchHooksChained,normalizeDn,parseDn,transformSchemas,unescapeDnValue,validateDnValue};
@@ -1 +1 @@
1
- import{randomInt as t}from"crypto";import r from"../../abstract/plugin.js";import{wantJson as s}from"../../lib/expressFormatedResponses.js";import{escapeDnValue as e}from"../../lib/utils.js";class AppAccountsApi extends r{constructor(t){if(super(t),this.name="appAccountsApi",this.roles=["api","configurable"],this.dependencies={authToken:"core/auth/token",appAccountsConsistency:"core/twake/appAccountsConsistency"},this.applicativeAccountBase=this.config.applicative_account_base,this.maxAppAccounts=this.config.max_app_accounts||5,this.mailAttr=this.config.mail_attribute||"mail",!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}, max_app_accounts=${this.maxAppAccounts}`)}api(t){const r=this.config.api_prefix||"/api";t.get(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.listAccounts(t,r)),t.post(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.createAccount(t,r)),t.delete(`${r}/v1/users/:user/app-accounts/:uid`,(t,r)=>this.deleteAccount(t,r)),this.logger.info(`AppAccounts API registered at ${r}/v1/users/:user/app-accounts`)}async listAccounts(t,r){if(!s(t,r))return;const e=t.params.user;try{const t=await this.server.ldap.search({scope:"sub",filter:`(uid=${e})`,paged:!1,attributes:[this.mailAttr]},this.config.ldap_base||""),s=t.searchEntries?.[0];if(!s)return void r.status(404).json({error:`User ${e} not found`});const i=s[this.mailAttr];if(!i)return void r.status(400).json({error:`User ${e} has no mail attribute`});const a=Array.isArray(i)?String(i[0]):String(i),o=await this.server.ldap.search({scope:"sub",filter:`(${this.mailAttr}=${a})`,paged:!1},this.applicativeAccountBase),n=(o.searchEntries||[]).filter(t=>{const r=t.uid,s=Array.isArray(r)?String(r[0]):String(r);return s!==a&&s.startsWith(`${e}_`)}).map(t=>{const r=t.uid,s=Array.isArray(r)?String(r[0]):String(r),e=t.description,i=e?Array.isArray(e)?String(e[0]):String(e):void 0;return{uid:s,...i?{name:i}:{}}}).sort((t,r)=>t.uid.localeCompare(r.uid));r.json(n)}catch(t){this.logger.error(`${this.name}: Failed to list accounts for ${e}:`,t),r.status(500).json({error:"Internal server error"})}}async createAccount(t,r){if(!s(t,r))return;const i=t.params.user,{name:a}=t.body||{};try{const t=await this.server.ldap.search({scope:"sub",filter:`(uid=${i})`,paged:!1},this.config.ldap_base||""),s=t.searchEntries?.[0];if(!s)return void r.status(404).json({error:`User ${i} not found`});const o=s[this.mailAttr];if(!o)return void r.status(400).json({error:`User ${i} has no mail attribute`});const n=Array.isArray(o)?String(o[0]):String(o),c=await this.server.ldap.search({scope:"sub",filter:`(${this.mailAttr}=${n})`,paged:!1},this.applicativeAccountBase),u=(c.searchEntries||[]).filter(t=>{const r=t.uid,s=Array.isArray(r)?String(r[0]):String(r);return s!==n&&s.startsWith(`${i}_`)});if(u.length>=this.maxAppAccounts)return void r.status(400).json({error:`Maximum number of accounts (${this.maxAppAccounts}) reached`});let p,l=0;do{if(p=this.generateAccountId(),l++,l>100)throw new Error("Failed to generate unique account ID after 100 attempts")}while(u.some(t=>{const r=t.uid;return(Array.isArray(r)?String(r[0]):String(r))===`${i}_${p}`}));const d=`${i}_${p}`,h=this.generatePassword(),A={objectClass:["inetOrgPerson"],uid:d,userPassword:h},g=["cn","sn","givenName",this.mailAttr,"displayName"];for(const t of g)if(s[t]){const r=s[t];Array.isArray(r)?A[t]=r.map(t=>Buffer.isBuffer(t)?t.toString():String(t)):A[t]=Buffer.isBuffer(r)?r.toString():String(r)}a&&(A.description=a);const f=`uid=${e(d)},${this.applicativeAccountBase}`;await this.server.ldap.add(f,A),this.logger.info(`${this.name}: Created applicative account ${f} for user ${i}`);const m=`uid=${e(n)},${this.applicativeAccountBase}`;try{await this.server.ldap.modify(m,{add:{userPassword:h}})}catch(t){this.logger.warn(`${this.name}: Failed to add password to principal account ${m}:`,t)}r.json({uid:d,pwd:h,mail:n})}catch(t){this.logger.error(`${this.name}: Failed to create account for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}async deleteAccount(t,r){if(!s(t,r))return;const i=t.params.user,a=t.params.uid;try{if(!a.startsWith(`${i}_`))return void r.status(403).json({error:`Account ${a} does not belong to user ${i}`});const t=await this.server.ldap.search({scope:"sub",filter:`(uid=${a})`,paged:!1},this.applicativeAccountBase),s=t.searchEntries?.[0];if(!s)return this.logger.warn(`${this.name}: Account ${a} already deleted or not found`),void r.json({uid:a});const o=s.userPassword;o||this.logger.warn(`${this.name}: Account ${a} has no userPassword attribute`);const n=s[this.mailAttr],c=n?Array.isArray(n)?String(n[0]):String(n):null;if(c&&o){const t=`uid=${e(c)},${this.applicativeAccountBase}`;try{const r=Array.isArray(o)?o[0]:o;await this.server.ldap.modify(t,{delete:{userPassword:r}})}catch(r){this.logger.warn(`${this.name}: Failed to delete password from principal account ${t}:`,r)}}const u=s.dn;await this.server.ldap.delete(u),this.logger.info(`${this.name}: Deleted applicative account ${u}`),r.json({uid:a})}catch(t){this.logger.error(`${this.name}: Failed to delete account ${a} for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}generateAccountId(){return`c${t(0,1e8).toString().padStart(8,"0")}`}generatePassword(){const r="ABCDEFGHJKMNPQRSTUVWXYZ",s="abcdefghjkmnpqrstuvwxyz",e="23456789",i="@!#$%",a=[];for(let o=0;o<6;o++){const o=t(0,23),n=t(0,23),c=t(0,8),u=t(0,5),p=[r[o],s[n],e[c],i[u]];for(let r=p.length-1;r>0;r--){const s=t(0,r+1);[p[r],p[s]]=[p[s],p[r]]}a.push(p.join(""))}return a.join("-")}getConfigApiData(){const t=this.config.api_prefix||"/api";return{enabled:!0,base:this.applicativeAccountBase,maxAccounts:this.maxAppAccounts,mailAttribute:this.mailAttr,endpoints:{list:`${t}/v1/users/:user/app-accounts`,create:`${t}/v1/users/:user/app-accounts`,delete:`${t}/v1/users/:user/app-accounts/:uid`}}}}export{AppAccountsApi as default};
1
+ import{randomInt as t}from"crypto";import r from"../../abstract/plugin.js";import{wantJson as s}from"../../lib/expressFormatedResponses.js";import{escapeDnValue as e}from"../../lib/utils.js";class AppAccountsApi extends r{constructor(t){if(super(t),this.name="appAccountsApi",this.roles=["api","configurable"],this.dependencies={appAccountsConsistency:"core/twake/appAccountsConsistency"},this.applicativeAccountBase=this.config.applicative_account_base,this.maxAppAccounts=this.config.max_app_accounts||5,this.mailAttr=this.config.mail_attribute||"mail",!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}, max_app_accounts=${this.maxAppAccounts}`)}api(t){const r=this.config.api_prefix||"/api";t.get(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.listAccounts(t,r)),t.post(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.createAccount(t,r)),t.delete(`${r}/v1/users/:user/app-accounts/:uid`,(t,r)=>this.deleteAccount(t,r)),this.logger.info(`AppAccounts API registered at ${r}/v1/users/:user/app-accounts`)}async listAccounts(t,r){if(!s(t,r))return;const e=t.params.user;try{const t=await this.server.ldap.search({scope:"sub",filter:`(uid=${e})`,paged:!1,attributes:[this.mailAttr]},this.config.ldap_base||""),s=t.searchEntries?.[0];if(!s)return void r.status(404).json({error:`User ${e} not found`});const i=s[this.mailAttr];if(!i)return void r.status(400).json({error:`User ${e} has no mail attribute`});const a=Array.isArray(i)?String(i[0]):String(i),n=await this.server.ldap.search({scope:"sub",filter:`(${this.mailAttr}=${a})`,paged:!1},this.applicativeAccountBase),o=(n.searchEntries||[]).filter(t=>{const r=t.uid,s=Array.isArray(r)?String(r[0]):String(r);return s!==a&&s.startsWith(`${e}_`)}).map(t=>{const r=t.uid,s=Array.isArray(r)?String(r[0]):String(r),e=t.description,i=e?Array.isArray(e)?String(e[0]):String(e):void 0;return{uid:s,...i?{name:i}:{}}}).sort((t,r)=>t.uid.localeCompare(r.uid));r.json(o)}catch(t){this.logger.error(`${this.name}: Failed to list accounts for ${e}:`,t),r.status(500).json({error:"Internal server error"})}}async createAccount(t,r){if(!s(t,r))return;const i=t.params.user,{name:a}=t.body||{};try{const t=await this.server.ldap.search({scope:"sub",filter:`(uid=${i})`,paged:!1},this.config.ldap_base||""),s=t.searchEntries?.[0];if(!s)return void r.status(404).json({error:`User ${i} not found`});const n=s[this.mailAttr];if(!n)return void r.status(400).json({error:`User ${i} has no mail attribute`});const o=Array.isArray(n)?String(n[0]):String(n),c=await this.server.ldap.search({scope:"sub",filter:`(${this.mailAttr}=${o})`,paged:!1},this.applicativeAccountBase),u=(c.searchEntries||[]).filter(t=>{const r=t.uid,s=Array.isArray(r)?String(r[0]):String(r);return s!==o&&s.startsWith(`${i}_`)});if(u.length>=this.maxAppAccounts)return void r.status(400).json({error:`Maximum number of accounts (${this.maxAppAccounts}) reached`});let p,l=0;do{if(p=this.generateAccountId(),l++,l>100)throw new Error("Failed to generate unique account ID after 100 attempts")}while(u.some(t=>{const r=t.uid;return(Array.isArray(r)?String(r[0]):String(r))===`${i}_${p}`}));const d=`${i}_${p}`,h=this.generatePassword(),A={objectClass:["inetOrgPerson"],uid:d,userPassword:h},g=["cn","sn","givenName",this.mailAttr,"displayName"];for(const t of g)if(s[t]){const r=s[t];Array.isArray(r)?A[t]=r.map(t=>Buffer.isBuffer(t)?t.toString():String(t)):A[t]=Buffer.isBuffer(r)?r.toString():String(r)}a&&(A.description=a);const f=`uid=${e(d)},${this.applicativeAccountBase}`;await this.server.ldap.add(f,A),this.logger.info(`${this.name}: Created applicative account ${f} for user ${i}`);const m=`uid=${e(o)},${this.applicativeAccountBase}`;try{await this.server.ldap.modify(m,{add:{userPassword:h}})}catch(t){this.logger.warn(`${this.name}: Failed to add password to principal account ${m}:`,t)}r.json({uid:d,pwd:h,mail:o})}catch(t){this.logger.error(`${this.name}: Failed to create account for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}async deleteAccount(t,r){if(!s(t,r))return;const i=t.params.user,a=t.params.uid;try{if(!a.startsWith(`${i}_`))return void r.status(403).json({error:`Account ${a} does not belong to user ${i}`});const t=await this.server.ldap.search({scope:"sub",filter:`(uid=${a})`,paged:!1},this.applicativeAccountBase),s=t.searchEntries?.[0];if(!s)return this.logger.warn(`${this.name}: Account ${a} already deleted or not found`),void r.json({uid:a});const n=s.userPassword;n||this.logger.warn(`${this.name}: Account ${a} has no userPassword attribute`);const o=s[this.mailAttr],c=o?Array.isArray(o)?String(o[0]):String(o):null;if(c&&n){const t=`uid=${e(c)},${this.applicativeAccountBase}`;try{const r=Array.isArray(n)?n[0]:n;await this.server.ldap.modify(t,{delete:{userPassword:r}})}catch(r){this.logger.warn(`${this.name}: Failed to delete password from principal account ${t}:`,r)}}const u=s.dn;await this.server.ldap.delete(u),this.logger.info(`${this.name}: Deleted applicative account ${u}`),r.json({uid:a})}catch(t){this.logger.error(`${this.name}: Failed to delete account ${a} for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}generateAccountId(){return`c${t(0,1e8).toString().padStart(8,"0")}`}generatePassword(){const r="ABCDEFGHJKMNPQRSTUVWXYZ",s="abcdefghjkmnpqrstuvwxyz",e="23456789",i="@!#$%",a=[];for(let n=0;n<6;n++){const n=t(0,23),o=t(0,23),c=t(0,8),u=t(0,5),p=[r[n],s[o],e[c],i[u]];for(let r=p.length-1;r>0;r--){const s=t(0,r+1);[p[r],p[s]]=[p[s],p[r]]}a.push(p.join(""))}return a.join("-")}getConfigApiData(){const t=this.config.api_prefix||"/api";return{enabled:!0,base:this.applicativeAccountBase,maxAccounts:this.maxAppAccounts,mailAttribute:this.mailAttr,endpoints:{list:`${t}/v1/users/:user/app-accounts`,create:`${t}/v1/users/:user/app-accounts`,delete:`${t}/v1/users/:user/app-accounts/:uid`}}}}export{AppAccountsApi as default};
@@ -1 +1 @@
1
- import t from"../../abstract/plugin.js";import{escapeDnValue as e}from"../../lib/utils.js";class AppAccountsConsistency extends t{constructor(t){if(super(t),this.name="appAccountsConsistency",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange"},this.hooks={onLdapMailChange:async(t,e,i)=>{try{const a=null!=e?Array.isArray(e)?String(e[0]):String(e):null,c=null!=i?Array.isArray(i)?String(i[0]):String(i):null;if(!a&&c)return void await this.createApplicativeAccount(t,c);if(a&&!c)return void await this.deleteApplicativeAccount(a);if(a&&c&&a!==c)return void await this.updateApplicativeAccount(t,a,c)}catch(e){this.logger.error(`${this.name}: Failed to handle mail change for ${t}:`,e)}}},this.mailAttr=this.config.mail_attribute||"mail",this.applicativeAccountBase=this.config.applicative_account_base,this.operationalAttributes=this.config.ldap_operational_attribute||[],!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}`)}shouldExcludeAttribute(t){return"dn"===t||this.operationalAttributes.includes(t)}async createApplicativeAccount(t,i){const a=`uid=${e(i)},${this.applicativeAccountBase}`;try{const e=await this.server.ldap.search({scope:"base",paged:!1},t),c=e.searchEntries?.[0];if(!c)return void this.logger.warn(`${this.name}: Could not find user ${t} to create applicative account`);const s={};for(const[t,e]of Object.entries(c))this.shouldExcludeAttribute(t)||null!=e&&(Array.isArray(e)&&0===e.length||(s[t]=e));s.uid=i,await this.server.ldap.add(a,s),this.logger.info(`${this.name}: Created applicative account ${a} for user ${t}`)}catch(e){if(68===e.code||e.message?.includes("AlreadyExists"))return void this.logger.debug(`${this.name}: Applicative account ${a} already exists`);this.logger.error(`${this.name}: Failed to create applicative account for ${t}:`,e)}}async deleteApplicativeAccount(t){try{const e=`(${this.mailAttr}=${t})`,i=(await this.server.ldap.search({filter:e,scope:"sub",paged:!1},this.applicativeAccountBase)).searchEntries||[];if(0===i.length)return void this.logger.debug(`${this.name}: No applicative accounts found for mail ${t}`);for(const t of i){const e=t.dn;try{await this.server.ldap.delete(e),this.logger.info(`${this.name}: Deleted applicative account ${e}`)}catch(t){32===t.code||t.message?.includes("NoSuchObject")?this.logger.debug(`${this.name}: Applicative account ${e} already deleted`):this.logger.error(`${this.name}: Failed to delete applicative account ${e}:`,t)}}}catch(e){if(32===e.code||e.message?.includes("NoSuchObject"))return void this.logger.debug(`${this.name}: Applicative account base does not exist or no accounts found for mail ${t}`);this.logger.error(`${this.name}: Failed to delete applicative account for mail ${t}:`,e)}}async updateApplicativeAccount(t,i,a){try{const c=`(${this.mailAttr}=${i})`,s=(await this.server.ldap.search({filter:c,scope:"sub",paged:!1},this.applicativeAccountBase)).searchEntries||[];if(0===s.length)return void this.logger.debug(`${this.name}: No applicative account found for old mail ${i}`);for(const c of s){const s=c.dn,r=Array.isArray(c.uid)?String(c.uid[0]):String(c.uid),o=r===i;let n;n=o?`uid=${e(a)},${this.applicativeAccountBase}`:`uid=${e(r)},${this.applicativeAccountBase}`;try{const e={};for(const[t,i]of Object.entries(c))this.shouldExcludeAttribute(t)||null!=i&&(Array.isArray(i)&&0===i.length||(e[t]=i));try{await this.server.ldap.delete(s),this.logger.info(`${this.name}: Deleted old applicative account ${s}`)}catch(t){if(32!==t.code&&!t.message?.includes("NoSuchObject"))throw t;this.logger.debug(`${this.name}: Applicative account ${s} already deleted`)}const l={...e},u=await this.server.ldap.search({scope:"base",paged:!1},t),d=u.searchEntries?.[0];if(!d)return void this.logger.warn(`${this.name}: Could not find user ${t} to update applicative account`);for(const[t,e]of Object.entries(d))this.shouldExcludeAttribute(t)||null!=e&&(Array.isArray(e)&&0===e.length||(l[t]=e));l.uid=o?a:r,await this.server.ldap.add(n,l),this.logger.info(`${this.name}: Created new applicative account ${n} (mail changed from ${i} to ${a})`)}catch(t){if(68===t.code||t.message?.includes("AlreadyExists"))return void this.logger.debug(`${this.name}: Applicative account ${n} already exists after mail change`);this.logger.error(`${this.name}: Failed to update applicative account for mail change:`,t)}}}catch(e){this.logger.error(`${this.name}: Failed to update applicative account for ${t}:`,e)}}}export{AppAccountsConsistency as default};
1
+ import t from"../../abstract/plugin.js";import{isDnInBranch as e,escapeDnValue as i}from"../../lib/utils.js";class AppAccountsConsistency extends t{constructor(t){if(super(t),this.name="appAccountsConsistency",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange"},this.hooks={onLdapMailChange:async(t,e,i)=>{if(this.isInApplicativeBranch(t))this.logger.debug(`${this.name}: Ignoring mail change event originating in the applicative branch`);else try{const a=null!=e?Array.isArray(e)?String(e[0]):String(e):null,c=null!=i?Array.isArray(i)?String(i[0]):String(i):null;if(!a&&c)return void await this.createApplicativeAccount(t,c);if(a&&!c)return void await this.deleteApplicativeAccount(a);if(a&&c&&a!==c)return void await this.updateApplicativeAccount(t,a,c)}catch(e){this.logger.error(`${this.name}: Failed to handle mail change for ${t}:`,e)}}},this.mailAttr=this.config.mail_attribute||"mail",this.applicativeAccountBase=this.config.applicative_account_base,this.operationalAttributes=this.config.ldap_operational_attribute||[],!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}`)}shouldExcludeAttribute(t){return"dn"===t||this.operationalAttributes.includes(t)}isInApplicativeBranch(t){return e(t,this.applicativeAccountBase)}async createApplicativeAccount(t,e){const a=`uid=${i(e)},${this.applicativeAccountBase}`;try{const i=await this.server.ldap.search({scope:"base",paged:!1},t),c=i.searchEntries?.[0];if(!c)return void this.logger.warn(`${this.name}: Could not find user ${t} to create applicative account`);const s={};for(const[t,e]of Object.entries(c))this.shouldExcludeAttribute(t)||null!=e&&(Array.isArray(e)&&0===e.length||(s[t]=e));s.uid=e,await this.server.ldap.add(a,s),this.logger.info(`${this.name}: Created applicative account ${a} for user ${t}`)}catch(e){if(68===e.code||e.message?.includes("AlreadyExists"))return void this.logger.debug(`${this.name}: Applicative account ${a} already exists`);this.logger.error(`${this.name}: Failed to create applicative account for ${t}:`,e)}}async deleteApplicativeAccount(t){try{const e=`(${this.mailAttr}=${t})`,i=(await this.server.ldap.search({filter:e,scope:"sub",paged:!1},this.applicativeAccountBase)).searchEntries||[];if(0===i.length)return void this.logger.debug(`${this.name}: No applicative accounts found for mail ${t}`);for(const t of i){const e=t.dn;try{await this.server.ldap.delete(e),this.logger.info(`${this.name}: Deleted applicative account ${e}`)}catch(t){32===t.code||t.message?.includes("NoSuchObject")?this.logger.debug(`${this.name}: Applicative account ${e} already deleted`):this.logger.error(`${this.name}: Failed to delete applicative account ${e}:`,t)}}}catch(e){if(32===e.code||e.message?.includes("NoSuchObject"))return void this.logger.debug(`${this.name}: Applicative account base does not exist or no accounts found for mail ${t}`);this.logger.error(`${this.name}: Failed to delete applicative account for mail ${t}:`,e)}}async updateApplicativeAccount(t,e,a){try{const c=`(${this.mailAttr}=${e})`,s=(await this.server.ldap.search({filter:c,scope:"sub",paged:!1},this.applicativeAccountBase)).searchEntries||[];if(0===s.length)return void this.logger.debug(`${this.name}: No applicative account found for old mail ${e}`);for(const c of s){const s=c.dn,r=Array.isArray(c.uid)?String(c.uid[0]):String(c.uid),n=r===e;let o;o=n?`uid=${i(a)},${this.applicativeAccountBase}`:`uid=${i(r)},${this.applicativeAccountBase}`;try{const i={};for(const[t,e]of Object.entries(c))this.shouldExcludeAttribute(t)||null!=e&&(Array.isArray(e)&&0===e.length||(i[t]=e));try{await this.server.ldap.delete(s),this.logger.info(`${this.name}: Deleted old applicative account ${s}`)}catch(t){if(32!==t.code&&!t.message?.includes("NoSuchObject"))throw t;this.logger.debug(`${this.name}: Applicative account ${s} already deleted`)}const l={...i},h=await this.server.ldap.search({scope:"base",paged:!1},t),u=h.searchEntries?.[0];if(!u)return void this.logger.warn(`${this.name}: Could not find user ${t} to update applicative account`);for(const[t,e]of Object.entries(u))this.shouldExcludeAttribute(t)||null!=e&&(Array.isArray(e)&&0===e.length||(l[t]=e));l.uid=n?a:r,await this.server.ldap.add(o,l),this.logger.info(`${this.name}: Created new applicative account ${o} (mail changed from ${e} to ${a})`)}catch(t){if(68===t.code||t.message?.includes("AlreadyExists"))return void this.logger.debug(`${this.name}: Applicative account ${o} already exists after mail change`);this.logger.error(`${this.name}: Failed to update applicative account for mail change:`,t)}}}catch(e){this.logger.error(`${this.name}: Failed to update applicative account for ${t}:`,e)}}}export{AppAccountsConsistency as default};
@@ -158,6 +158,51 @@ export declare function getRdn(dn: string): string;
158
158
  * ```
159
159
  */
160
160
  export declare function isChildOf(dn: string, parentDn: string): boolean;
161
+ /**
162
+ * Normalize a DN into a canonical, comparable form.
163
+ *
164
+ * Parses the DN into RDNs (honouring escaped commas via {@link parseDn}), then
165
+ * for each RDN lowercases it, collapses the optional whitespace around the `=`
166
+ * of every attribute-value assertion, and sorts the assertions of a
167
+ * multi-valued RDN so their ordering is irrelevant. The RDNs are re-joined with
168
+ * `,`. Empty components (e.g. from a trailing separator) are dropped.
169
+ *
170
+ * This makes equality / suffix comparisons robust to case, surrounding
171
+ * whitespace and multi-valued RDN ordering — unlike a raw string match.
172
+ *
173
+ * @param dn - The DN to normalize
174
+ * @returns The canonical form
175
+ *
176
+ * @example
177
+ * ```typescript
178
+ * normalizeDn('UID=x, ou=AppAccounts ,dc=Example,dc=com')
179
+ * // => 'uid=x,ou=appaccounts,dc=example,dc=com'
180
+ * ```
181
+ */
182
+ export declare function normalizeDn(dn: string): string;
183
+ /**
184
+ * Check whether a DN is a branch base itself or sits anywhere below it.
185
+ *
186
+ * Comparison is done RDN by RDN on the {@link normalizeDn} form, so it is
187
+ * robust to case, whitespace and escaped separators. A naive string suffix
188
+ * match can give a false negative (e.g. `ou=AppAccounts` vs `ou=appaccounts`,
189
+ * or `uid=x, ou=…` with stray whitespace), which is exactly what lets a
190
+ * re-entrant LDAP change event slip through a branch guard.
191
+ *
192
+ * Unlike {@link isChildOf}, this returns true when `dn` equals `base` as well.
193
+ *
194
+ * @param dn - The candidate DN
195
+ * @param base - The branch base DN
196
+ * @returns true if `dn` equals `base` or is a descendant of it
197
+ *
198
+ * @example
199
+ * ```typescript
200
+ * isDnInBranch('uid=x,ou=app,dc=e,dc=c', 'ou=app,dc=e,dc=c') // => true
201
+ * isDnInBranch('ou=app,dc=e,dc=c', 'ou=app,dc=e,dc=c') // => true
202
+ * isDnInBranch('uid=x,ou=users,dc=e,dc=c', 'ou=app,dc=e,dc=c') // => false
203
+ * ```
204
+ */
205
+ export declare function isDnInBranch(dn: string, base: string): boolean;
161
206
  /**
162
207
  * Wrapper for async Express route handlers to catch errors and pass them to error middleware
163
208
  * This ensures that errors in async routes are properly handled and don't crash the server
@@ -68,7 +68,6 @@ export default class AppAccountsApi extends DmPlugin {
68
68
  name: string;
69
69
  roles: Role[];
70
70
  dependencies: {
71
- authToken: string;
72
71
  appAccountsConsistency: string;
73
72
  };
74
73
  private applicativeAccountBase;
@@ -42,6 +42,21 @@ export default class AppAccountsConsistency extends DmPlugin {
42
42
  * @returns true if the attribute should be excluded
43
43
  */
44
44
  private shouldExcludeAttribute;
45
+ /**
46
+ * Whether a DN belongs to the applicative branch this plugin manages.
47
+ *
48
+ * Entries under `applicative_account_base` are *outputs* of this plugin
49
+ * (principal and app accounts), never source users. Since every LDAP write
50
+ * re-fires the change hooks — including our own writes — we must ignore
51
+ * events originating in this branch. Otherwise creating/renaming/deleting an
52
+ * applicative entry would re-enter `onLdapMailChange` and cause spurious
53
+ * `AlreadyExists` attempts or a re-entrant deletion cascade during a mail
54
+ * change.
55
+ *
56
+ * @param dn - The DN that changed
57
+ * @returns true if `dn` is the applicative base itself or sits below it
58
+ */
59
+ private isInApplicativeBranch;
45
60
  hooks: Hooks;
46
61
  /**
47
62
  * Create applicative account for a user
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldap-rest",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Light directory manager",
5
5
  "keywords": [
6
6
  "Identity-Manager"