ldap-rest 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/bin/index.mjs +8 -7
  2. package/dist/abstract/ldapFlat.js +1 -1
  3. package/dist/abstract/plugin.js +1 -1
  4. package/dist/abstract/twakePlugin.js +1 -1
  5. package/dist/bin/index.js +1 -1
  6. package/dist/config/args.js +1 -1
  7. package/dist/config/schema.js +1 -1
  8. package/dist/lib/changeContext.js +1 -1
  9. package/dist/lib/entitySchema/validator.js +1 -0
  10. package/dist/lib/expressFormatedResponses.js +1 -1
  11. package/dist/lib/health.js +1 -0
  12. package/dist/lib/instanceProviders.js +1 -1
  13. package/dist/lib/ldapActions.js +1 -1
  14. package/dist/lib/metrics.js +1 -0
  15. package/dist/lib/requestContext.js +1 -1
  16. package/dist/lib/schemaFile.js +1 -1
  17. package/dist/lib/shutdown.js +1 -0
  18. package/dist/lib/staticFiles.js +1 -0
  19. package/dist/lib/storage/store.js +1 -1
  20. package/dist/lib/utils.js +1 -1
  21. package/dist/logger/winston.js +1 -1
  22. package/dist/plugins/auth/crowdsec.js +1 -1
  23. package/dist/plugins/auth/openidconnect.js +1 -1
  24. package/dist/plugins/demo/helloworld.js +1 -1
  25. package/dist/plugins/ldap/bulkImport.js +1 -1
  26. package/dist/plugins/ldap/flatGeneric.js +1 -1
  27. package/dist/plugins/ldap/groups.js +1 -1
  28. package/dist/plugins/ldap/organizations.js +1 -1
  29. package/dist/plugins/metrics.js +1 -0
  30. package/dist/plugins/rabbitmq.js +1 -1
  31. package/dist/plugins/scim/scim.js +1 -1
  32. package/dist/plugins/static.js +1 -1
  33. package/dist/plugins/storage/file.js +1 -1
  34. package/dist/plugins/storage/postgres.js +1 -1
  35. package/dist/plugins/storage/valkey.js +1 -1
  36. package/dist/plugins/storage.js +1 -1
  37. package/dist/plugins/twake/calendar.js +1 -1
  38. package/dist/plugins/twake/clouderyProvision.js +1 -1
  39. package/dist/plugins/twake/cozyProvision.js +1 -1
  40. package/dist/plugins/twake/drive.js +1 -1
  41. package/dist/plugins/twake/james.js +1 -1
  42. package/dist/plugins/twake/migrationProxy.js +1 -0
  43. package/dist/plugins/weblogs.js +1 -1
  44. package/dist/src/abstract/ldapFlat.d.ts +3 -13
  45. package/dist/src/abstract/plugin.d.ts +20 -0
  46. package/dist/src/abstract/twakePlugin.d.ts +23 -1
  47. package/dist/src/bin/index.d.ts +46 -1
  48. package/dist/src/config/args.d.ts +17 -1
  49. package/dist/src/lib/changeContext.d.ts +11 -0
  50. package/dist/src/lib/entitySchema/validator.d.ts +64 -0
  51. package/dist/src/lib/expressFormatedResponses.d.ts +19 -0
  52. package/dist/src/lib/health.d.ts +63 -0
  53. package/dist/src/lib/instanceProviders.d.ts +4 -0
  54. package/dist/src/lib/ldapActions.d.ts +28 -0
  55. package/dist/src/lib/metrics.d.ts +44 -0
  56. package/dist/src/lib/requestContext.d.ts +40 -0
  57. package/dist/src/lib/schemaFile.d.ts +10 -0
  58. package/dist/src/lib/shutdown.d.ts +20 -0
  59. package/dist/src/lib/staticFiles.d.ts +33 -0
  60. package/dist/src/lib/storage/store.d.ts +8 -0
  61. package/dist/src/lib/utils.d.ts +9 -0
  62. package/dist/src/logger/winston.d.ts +4 -0
  63. package/dist/src/plugins/ldap/groups.d.ts +18 -3
  64. package/dist/src/plugins/ldap/organizations.d.ts +3 -5
  65. package/dist/src/plugins/metrics.d.ts +17 -0
  66. package/dist/src/plugins/rabbitmq.d.ts +37 -1
  67. package/dist/src/plugins/scim/scim.d.ts +2 -0
  68. package/dist/src/plugins/static.d.ts +28 -0
  69. package/dist/src/plugins/storage/file.d.ts +4 -0
  70. package/dist/src/plugins/storage/postgres.d.ts +7 -14
  71. package/dist/src/plugins/storage/valkey.d.ts +2 -15
  72. package/dist/src/plugins/storage.d.ts +8 -0
  73. package/dist/src/plugins/twake/drive.d.ts +8 -0
  74. package/dist/src/plugins/twake/james.d.ts +2 -0
  75. package/dist/src/plugins/twake/migrationProxy.d.ts +71 -0
  76. package/docs/client-development/api/reference.md +5 -9
  77. package/docs/client-development/api/rest-api.md +4 -10
  78. package/docs/client-development/schemas/README.md +60 -21
  79. package/docs/plugin-development/README.md +15 -13
  80. package/docs/plugin-development/dependencies.md +8 -0
  81. package/docs/plugin-development/hooks.md +1 -1
  82. package/docs/usage/README.md +1 -0
  83. package/docs/usage/configuration.md +57 -15
  84. package/docs/usage/operations.md +193 -0
  85. package/docs/usage/plugins/README.md +2 -0
  86. package/docs/usage/plugins/auth/authz-per-branch.md +2 -1
  87. package/docs/usage/plugins/integrations/README.md +8 -4
  88. package/docs/usage/plugins/integrations/mail-migration.md +102 -0
  89. package/docs/usage/plugins/integrations/scim.md +10 -2
  90. package/docs/usage/plugins/ldap/bulk-import.md +3 -1
  91. package/docs/usage/plugins/ldap/flat-generic.md +5 -2
  92. package/docs/usage/plugins/ldap/on-change.md +2 -1
  93. package/docs/usage/plugins/ldap/organizations.md +7 -7
  94. package/docs/usage/plugins/utilities/metrics.md +21 -0
  95. package/docs/usage/plugins/utilities/static.md +81 -34
  96. package/docs/usage/plugins/utilities/storage.md +7 -0
  97. package/docs/usage/plugins/utilities/weblogs.md +51 -30
  98. package/docs/usage/upgrading.md +97 -0
  99. package/package.json +11 -2
  100. package/static/schemas/example/README.md +1 -0
@@ -1 +1 @@
1
- import e from"fs";import{join as t,dirname as r,resolve as s,relative as a,isAbsolute as o}from"path";import{fileURLToPath as i}from"url";import n from"../config/args.js";import{transformSchemas as c}from"./utils.js";const h=t(r(i(import.meta.url)),"..","..","static","schemas"),l="ldap-rest:",isPlainObject=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function mergeSchema(e,t){if(!isPlainObject(t))return t;const r=isPlainObject(e)?{...e}:{};for(const[e,s]of Object.entries(t))null!==s?Object.defineProperty(r,e,{value:mergeSchema(Object.prototype.hasOwnProperty.call(r,e)?r[e]:void 0,s),enumerable:!0,writable:!0,configurable:!0}):delete r[e];return r}function resolveSchemaBase(e,t){if(!t.startsWith(l))return s(r(e),t);const i=s(h,t.slice(10)),n=a(h,i);if(!n||n.startsWith("..")||o(n))throw new Error(`${e}: "${t}" is not a shipped schema`);return i}class SchemaChain{constructor(e){this.options=e,this.files=[],this.realPaths=[],this.layers=[]}readError(e,t){return this.files.length?new Error(`${this.files[this.files.length-1]} extends ${e}, which cannot be read: ${t.message}`):t}add(e,t,r){if(this.realPaths.includes(t))throw new Error(`"extends" loop: ${[...this.files,e].join(" -> ")}`);this.files.push(e),this.realPaths.push(t);const s=this.options.config?c(r,this.options.config):r;let a;try{a=JSON.parse(s)}catch(t){throw new Error(`${e}: ${t.message}`)}if(!isPlainObject(a)||!("extends"in a))return void this.layers.push(a);const{extends:o,...i}=a;if("string"!=typeof o||!o)throw new Error(`${e}: "extends" must be the path of a schema`);return this.layers.push(i),resolveSchemaBase(e,o)}result(){return this.layers.reduceRight((e,t)=>mergeSchema(e,t))}}function loadSchemaFile(t,r={}){const s=new SchemaChain(r);for(let r=t;r;){let t;try{t=e.readFileSync(r,"utf8")}catch(e){throw s.readError(r,e)}r=s.add(r,e.realpathSync(r),t)}return s.result()}async function loadSchemaFileAsync(t,r={}){const s=new SchemaChain(r);for(let r=t;r;){let t;try{t=await e.promises.readFile(r,"utf8")}catch(e){throw s.readError(r,e)}r=s.add(r,await e.promises.realpath(r),t)}return s.result()}function schemaUrl(e,t){const r=t.indexOf("/schemas/");if(-1!==r)return`/${e.static_name||"static"}${t.substring(r)}`}const f=[["group_schema","--group-schema"],["organization_schema","--organization-schema"],["ldap_flat_schema","--ldap-flat-schema"]];function configuredSchemaFiles(e){const t=[];for(const[r,a]of f){const o=[n.find(e=>e[0]===a)?.[2]].flat().filter(e=>"string"==typeof e&&!!e).map(e=>s(e));for(const a of[e[r]].flat())"string"==typeof a&&a&&!o.includes(s(a))&&t.push(a)}return t}export{l as SHIPPED_SCHEMA_PREFIX,configuredSchemaFiles,loadSchemaFile,loadSchemaFileAsync,mergeSchema,resolveSchemaBase,schemaUrl,h as shippedSchemasPath};
1
+ import e from"fs";import{join as t,dirname as r,resolve as s,relative as a,isAbsolute as o}from"path";import{fileURLToPath as i}from"url";import n from"../config/args.js";import{transformSchemas as c}from"./utils.js";const h=t(r(i(import.meta.url)),"..","..","static","schemas"),l="ldap-rest:",isPlainObject=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function mergeSchema(e,t){if(!isPlainObject(t))return t;const r=isPlainObject(e)?{...e}:{};for(const[e,s]of Object.entries(t))null!==s?Object.defineProperty(r,e,{value:mergeSchema(Object.prototype.hasOwnProperty.call(r,e)?r[e]:void 0,s),enumerable:!0,writable:!0,configurable:!0}):delete r[e];return r}function resolveSchemaBase(e,t){if(!t.startsWith(l))return s(r(e),t);const i=s(h,t.slice(10)),n=a(h,i);if(!n||n.startsWith("..")||o(n))throw new Error(`${e}: "${t}" is not a shipped schema`);return i}class SchemaChain{constructor(e){this.options=e,this.files=[],this.realPaths=[],this.layers=[]}readError(e,t){return this.files.length?new Error(`${this.files[this.files.length-1]} extends ${e}, which cannot be read: ${t.message}`):t}add(e,t,r){if(this.realPaths.includes(t))throw new Error(`"extends" loop: ${[...this.files,e].join(" -> ")}`);this.files.push(e),this.realPaths.push(t);const s=this.options.config?c(r,this.options.config):r;let a;try{a=JSON.parse(s)}catch(t){throw new Error(`${e}: ${t.message}`)}if(!isPlainObject(a)||!("extends"in a))return void this.layers.push(a);const{extends:o,...i}=a;if("string"!=typeof o||!o)throw new Error(`${e}: "extends" must be the path of a schema`);return this.layers.push(i),resolveSchemaBase(e,o)}result(){return this.layers.reduceRight((e,t)=>mergeSchema(e,t))}}function loadSchemaFile(t,r={}){const s=new SchemaChain(r);for(let r=t;r;){let t;try{t=e.readFileSync(r,"utf8")}catch(e){throw s.readError(r,e)}r=s.add(r,e.realpathSync(r),t)}return s.result()}async function loadSchemaFileAsync(t,r={}){const s=new SchemaChain(r);for(let r=t;r;){let t;try{t=await e.promises.readFile(r,"utf8")}catch(e){throw s.readError(r,e)}r=s.add(r,await e.promises.realpath(r),t)}return s.result()}function staticName(e){const t=e.static_name,r=("string"==typeof t?t:"").trim();let s=0,a=r.length;for(;s<a&&"/"===r[s];)s++;for(;a>s&&"/"===r[a-1];)a--;return r.slice(s,a)||"static"}function schemaUrl(e,t){const r=t.indexOf("/schemas/");if(-1!==r)return`/${staticName(e)}${t.substring(r)}`}const f=[["group_schema","--group-schema"],["organization_schema","--organization-schema"],["ldap_flat_schema","--ldap-flat-schema"]];function configuredSchemaFiles(e){const t=[];for(const[r,a]of f){const o=[n.find(e=>e[0]===a)?.[2]].flat().filter(e=>"string"==typeof e&&!!e).map(e=>s(e));for(const a of[e[r]].flat())"string"==typeof a&&a&&!o.includes(s(a))&&t.push(a)}return t}export{l as SHIPPED_SCHEMA_PREFIX,configuredSchemaFiles,loadSchemaFile,loadSchemaFileAsync,mergeSchema,resolveSchemaBase,schemaUrl,h as shippedSchemasPath,staticName};
@@ -0,0 +1 @@
1
+ import{constants as e}from"os";const s=["SIGTERM","SIGINT"],o={ms:2e4},t=[],r=new Map;let c=[],n=!1;const settle=e=>Promise.resolve().then(e).catch(()=>{});function registerProcessShutdown(e){if(n)c.push(settle(e));else if(t.push(e),!(r.size>0))for(const e of s){const listener=()=>fanOut(e);r.set(e,listener),process.prependListener(e,listener)}}function fanOut(s){const i=process.listenerCount(s)-1;n=!0;for(const[e,s]of r)process.removeListener(e,s);r.clear(),c=t.splice(0).map(settle);const p=(async()=>{for(;c.length>0;)await Promise.allSettled(c.splice(0))})();let l;const a=new Promise(e=>{l=setTimeout(e,o.ms)}),f=128+e.signals[s];Promise.race([p,a]).then(()=>{clearTimeout(l),i>0||(process.kill(process.pid,s),process.exit(f))}).catch(()=>process.exit(f))}export{registerProcessShutdown,o as shutdownTimeout};
@@ -0,0 +1 @@
1
+ const isStaticFiles=t=>"function"==typeof t.schemaUrlOf&&"function"==typeof t.servedPath;function staticPlugins(t){const s=[],e=[];for(const n of Object.values(t.loadedPlugins)){if(!isStaticFiles(n))continue;const t=n.servedPath().toLowerCase();s.some(s=>(t+"/").startsWith(s+"/")||(s+"/").startsWith(t+"/"))||(s.push(t),e.push(n))}return e.sort((t,s)=>Number("static"===s.name)-Number("static"===t.name))}function staticSchemaUrl(t,s){for(const e of staticPlugins(t)){const t=e.schemaUrlOf(s);if(t)return t}}export{isStaticFiles,staticPlugins,staticSchemaUrl};
@@ -1 +1 @@
1
- class Store{constructor(e){this.logger=e}static scoped(e,t){return`${e}\0${t}`}static readable(e){return e.replace("\0",":")}static encodeRecord(e,t){return`${t.deadline} ${Store.readable(e)}\n${t.value}`}static decodeRecord(e){const t=String(e),s=t.indexOf("\n"),r=s<0?t:t.slice(0,s),a=parseInt(r.split(" ")[0],10);return Number.isFinite(a)?{deadline:a,value:s<0?"":t.slice(s+1)}:null}async open(){}close(){return this.stopSweeping(),Promise.resolve()}async get(e,t){const s=Store.scoped(e,t),r=await this.load(s);return null===r?null:r.deadline<=Date.now()?(this.drop(s).catch(()=>{}),null):r.value}async set(e,t,s,r){await this.save(Store.scoped(e,t),{value:s,deadline:r})}async delete(e,t){await this.drop(Store.scoped(e,t))}startSweeping(e){if(this.sweeper||e<=0)return;const run=()=>{this.sweep().then(e=>{e>0&&this.logger.debug(`${this.name}: swept ${e} record(s)`)}).catch(e=>{this.logger.warn(`${this.name}: sweep failed: ${e instanceof Error?e.message:JSON.stringify(e)}`)})};this.sweeper=setInterval(run,1e3*e),this.sweeper.unref?.(),run()}stopSweeping(){this.sweeper&&clearInterval(this.sweeper),this.sweeper=void 0}}export{Store as default};
1
+ class Store{constructor(e){this.closed=!1,this.logger=e}health(){return Promise.resolve("ok")}static scoped(e,t){return`${e}\0${t}`}static readable(e){return e.replace("\0",":")}static encodeRecord(e,t){return`${t.deadline} ${Store.readable(e)}\n${t.value}`}static decodeRecord(e){const t=String(e),s=t.indexOf("\n"),r=s<0?t:t.slice(0,s),i=parseInt(r.split(" ")[0],10);return Number.isFinite(i)?{deadline:i,value:s<0?"":t.slice(s+1)}:null}async open(){}close(){return this.closed=!0,this.stopSweeping(),Promise.resolve()}async get(e,t){const s=Store.scoped(e,t),r=await this.load(s);return null===r?null:r.deadline<=Date.now()?(this.drop(s).catch(()=>{}),null):r.value}async set(e,t,s,r){await this.save(Store.scoped(e,t),{value:s,deadline:r})}async delete(e,t){await this.drop(Store.scoped(e,t))}startSweeping(e){if(this.sweeper||e<=0)return;const run=()=>{this.sweep().then(e=>{e>0&&this.logger.debug(`${this.name}: swept ${e} record(s)`)}).catch(e=>{this.logger.warn(`${this.name}: sweep failed: ${e instanceof Error?e.message:JSON.stringify(e)}`)})};this.sweeper=setInterval(run,1e3*e),this.sweeper.unref?.(),run()}stopSweeping(){this.sweeper&&clearInterval(this.sweeper),this.sweeper=void 0}}export{Store as default};
package/dist/lib/utils.js CHANGED
@@ -1 +1 @@
1
- import{BadRequestError as e}from"./errors.js";import{getLogger as n}from"./expressFormatedResponses.js";import{outsideRequest as t}from"./requestContext.js";const r=new Map;function getCompiledRegex(e,n){const t=n?`${e}:${n}`:e;let o=r.get(t);return o||(o=new RegExp(e,n),r.set(t,o)),o}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}`:""},launchHooks=async(e,...r)=>{if(e)for(const o of e)if(o)try{await t(()=>o(...r))}catch(e){const t=hookLabel(o);e instanceof Error?n()?.error(`Hook error${t}`,e):n()?.error(`Hook error${t}: ${describeThrown(e)}`)}},o=new WeakMap,recordHookOwner=(e,n,t)=>{o.set(e,{plugin:n,hook:t})},hookOwner=e=>o.get(e),isAuthzRefusal=e=>/\[authz-forbidden\]/.test(String(e?.message??"")),isRefusal=e=>403===e?.statusCode||isAuthzRefusal(e),launchHooksChained=async(e,r)=>{if(e)for(const o of e)if(o)try{r=await t(()=>o(r))}catch(e){if(isRefusal(e)){const t=hookOwner(o);n()?.warn(`${t?`${t.plugin} refused ${t.hook}`:`a hook no plugin registered (${o.name||"anonymous"}) refused`}: ${e.message}`)}throw e}return r},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 i=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+=i.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 s=/((?:\\[0-9a-fA-F]{2})+)|\\([^])/g;function hexByte(e){return e.toString(16).padStart(2,"0")}const a=new Map,c=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])a.set(r.toLowerCase(),e),c.set(r.toLowerCase(),t[0]??n)}}function knownAttributeName(e){return c.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 a.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 a=0;const plain=e=>{for(const n of e)i.push(n)};for(const n of e.matchAll(s)){plain(e.slice(a,n.index)),a=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)}`)}}a<e.length&&plain(e.slice(a));const kept=e=>3===e.length&&"\\"===e[0],c=[];let l="";for(const e of i)kept(e)?(c.push(...l.normalize("NFKC"),e),l=""):l+=e;c.push(...l.normalize("NFKC"));const u=c.filter((e,n)=>" "!==e||n>0&&" "!==c[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,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 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 +1 @@
1
- import e from"winston";const r={levels:{error:0,warn:1,notice:2,info:3,debug:4}},buildLogger=o=>e.createLogger({levels:r.levels,level:o.log_level,format:e.format.combine(e.format.timestamp(),e.format.printf(({timestamp:e,level:r,message:o})=>`${e} [${r}]: ${o}`)),transports:["console"===o.logger?new e.transports.Console({stderrLevels:["error","warn"],format:e.format.json()}):new e.transports.File({filename:"error.log"})]});export{buildLogger};
1
+ import{inspect as e}from"util";import r from"winston";import{currentLogContext as o}from"../lib/requestContext.js";const t={levels:{error:0,warn:1,notice:2,info:3,debug:4}},n=["console","file"],l=r.format(e=>{const r=o();return r?(r.requestId&&(e.requestId=r.requestId),!r.plugin||void 0!==e.plugin&&e.plugin!==r.plugin||(e.plugin=r.plugin,e.op??(e.op=r.op)),e):e}),buildLogger=o=>{const i=o.logger||"console";if(!n.includes(i))throw new Error(`Unknown logger "${String(i)}": --logger takes ${n.join(" or ")}`);if("file"===i&&!o.log_file)throw new Error("--logger file needs a file: set --log-file (or DM_LOG_FILE)");return r.createLogger({levels:t.levels,level:o.log_level,format:r.format.combine(r.format.timestamp(),l(),r.format.printf(({timestamp:r,level:o,message:t,requestId:n,plugin:l,op:i,error:s})=>`${r} [${o}]${"string"==typeof n?` [${n}]`:""}${"string"==typeof l?` [${l}${"string"==typeof i?` ${i}`:""}]`:""}: ${t}${void 0===s?"":` (${"string"==typeof s?s:s instanceof Error?String(s):e(s,{breakLength:1/0})})`}`)),transports:["console"===i?new r.transports.Console({stderrLevels:["error","warn"],format:r.format.json()}):new r.transports.File({filename:o.log_file})]})};export{buildLogger};
@@ -1 +1 @@
1
- import e from"node-fetch";import r from"../../abstract/plugin.js";class CrowdSec extends r{constructor(...e){if(super(...e),this.name="crowdsec",this.roles=["protect"],this.cache=new Map,this.apiUrl=this.config.crowdsec_url||"http://localhost:8080/v1/decisions",this.apiKey=this.config.crowdsec_api_key,this.cacheTtl=this.config.crowdsec_cache_ttl||60,!this.apiKey)throw new Error("CrowdSec plugin requires crowdsec_api_key configuration");this.logger.info(`CrowdSec integration enabled with API at ${this.apiUrl} (cache TTL: ${this.cacheTtl}s)`)}api(e){e.use(async(e,r,t)=>{const s=this.getClientIp(e);try{if(await this.checkIpBanned(s))return this.logger.warn(`Blocked request from CrowdSec-banned IP: ${s}`),r.status(403).json({error:"Access denied",reason:"IP address is banned by security policies"});t()}catch(e){this.logger.error(`CrowdSec API error for IP ${s}: ${e instanceof Error?e.message:String(e)}`),t()}})}async checkIpBanned(r){const t=this.cache.get(r),s=Date.now();if(t&&s<t.expiresAt)return this.logger.debug(`CrowdSec cache hit for IP ${r}: ${t.isBanned}`),t.isBanned;const o=`${this.apiUrl}?ip=${encodeURIComponent(r)}`;this.logger.debug(`Querying CrowdSec API: ${o}`);const i=await e(o,{headers:{"X-Api-Key":this.apiKey}});if(!i.ok)throw new Error(`CrowdSec API returned status ${i.status}: ${i.statusText}`);let c;try{const e=await i.text();c="null"===e||""===e?null:JSON.parse(e)}catch(e){throw this.logger.error(`Failed to parse CrowdSec API response: ${e instanceof Error?e.message:String(e)}`),new Error("Invalid JSON response from CrowdSec API")}let n=!1;return null!==c&&Array.isArray(c)&&(n=c.some(e=>"ban"===e.type)),this.cache.set(r,{isBanned:n,expiresAt:s+1e3*this.cacheTtl}),this.logger.debug(`CrowdSec decision for IP ${r}: ${n?"BANNED":"ALLOWED"}`),n}getClientIp(e){const r=e.headers["x-forwarded-for"];if(r){return(Array.isArray(r)?r[0]:r).split(",")[0].trim()}return e.socket.remoteAddress||"unknown"}}export{CrowdSec as default};
1
+ import e from"../../abstract/plugin.js";class CrowdSec extends e{constructor(...e){if(super(...e),this.name="crowdsec",this.roles=["protect"],this.cache=new Map,this.apiUrl=this.config.crowdsec_url||"http://localhost:8080/v1/decisions",this.apiKey=this.config.crowdsec_api_key,this.cacheTtl=this.config.crowdsec_cache_ttl||60,!this.apiKey)throw new Error("CrowdSec plugin requires crowdsec_api_key configuration");this.logger.info(`CrowdSec integration enabled with API at ${this.apiUrl} (cache TTL: ${this.cacheTtl}s)`)}api(e){e.use(async(e,r,t)=>{const s=this.getClientIp(e);try{if(await this.checkIpBanned(s))return this.logger.warn(`Blocked request from CrowdSec-banned IP: ${s}`),r.status(403).json({error:"Access denied",reason:"IP address is banned by security policies"});t()}catch(e){this.logger.error(`CrowdSec API error for IP ${s}: ${e instanceof Error?e.message:String(e)}`),t()}})}async checkIpBanned(e){const r=this.cache.get(e),t=Date.now();if(r&&t<r.expiresAt)return this.logger.debug(`CrowdSec cache hit for IP ${e}: ${r.isBanned}`),r.isBanned;const s=`${this.apiUrl}?ip=${encodeURIComponent(e)}`;this.logger.debug(`Querying CrowdSec API: ${s}`);const i=await this.fetch(s,{headers:{"X-Api-Key":this.apiKey}});if(!i.ok)throw new Error(`CrowdSec API returned status ${i.status}: ${i.statusText}`);let o;try{const e=await i.text();o="null"===e||""===e?null:JSON.parse(e)}catch(e){throw this.logger.error(`Failed to parse CrowdSec API response: ${e instanceof Error?e.message:String(e)}`),new Error("Invalid JSON response from CrowdSec API")}let c=!1;return null!==o&&Array.isArray(o)&&(c=o.some(e=>"ban"===e.type)),this.cache.set(e,{isBanned:c,expiresAt:t+1e3*this.cacheTtl}),this.logger.debug(`CrowdSec decision for IP ${e}: ${c?"BANNED":"ALLOWED"}`),c}getClientIp(e){const r=e.headers["x-forwarded-for"];if(r){return(Array.isArray(r)?r[0]:r).split(",")[0].trim()}return e.socket.remoteAddress||"unknown"}}export{CrowdSec as default};
@@ -1 +1 @@
1
- import{randomUUID as e}from"crypto";import o from"express-openid-connect";import s from"../../lib/auth/base.js";import{launchHooksChained as t}from"../../lib/utils.js";import{serverError as i,unauthorized as n}from"../../lib/expressFormatedResponses.js";const{auth:r,requiresAuth:a}=o,c=["/login","/logout","/callback","/backchannel-logout"],d=/^end_session_endpoint must be configured/,h=/^\/logout\/?$/i,u="dmSession",sessionClaims=e=>{const{oidc:o,appSession:s}=e,t=o?.idTokenClaims;if(!t)return;const i=s?.[u];return"string"==typeof i?{...t,session:i}:t},subjectOf=e=>{try{const o=JSON.parse(Buffer.from(String(e).split(".")[1],"base64url").toString());return"string"==typeof o.sub?o.sub:void 0}catch{return}},walkSubscribers=async(e,o)=>{let s;for(const t of e??[])if(t)try{await t(o)}catch(e){s??(s=e instanceof Error?e:new Error(String(e)))}if(s)throw s};class OpenIDConnect extends s{constructor(e){super(e),this.name="openidconnect",this.configApiKey="openidconnect",this.roles=["auth","configurable"],this.warnedNoEndSession=!1,this.warnedNoRecord=!1,this.warnedUnnamed=!1;for(const e of["oidc_server","oidc_client_id","oidc_client_secret","base_url"])if(!this.config[e])throw new Error(`Missing config parameter ${e}`)}buildConfig(){return{authRequired:!1,issuerBaseURL:this.config.oidc_server,clientID:this.config.oidc_client_id,secret:this.config.oidc_client_secret,baseURL:this.config.base_url,clientSecret:this.config.oidc_client_secret,authorizationParams:{response_type:"code",scope:"openid profile email"},idpLogout:!0,routes:{logout:!1},afterCallback:async(e,o,s)=>({...s,[u]:await this.sessionId(e,subjectOf(s.id_token))}),backchannelLogout:{onLogoutToken:async e=>{this.server.hooks.oidclogouttoken?await walkSubscribers(this.server.hooks.oidclogouttoken,e):this.logger.warn("openidconnect: a logout token arrived but no Back-Channel Logout plugin is loaded, so nothing records it")},onLogin:async e=>{if(!this.server.hooks.oidclogin)return;const o=e.oidc?.idTokenClaims;if(o)try{await walkSubscribers(this.server.hooks.oidclogin,o)}catch(o){const s=e.appSession;if(s)for(const e of Object.keys(s))delete s[e];throw o}},isLoggedOut:e=>this.loggedOut(e)}}}identitySource(){const e=this.config.oidc_username_claim||"sub";return`req.user: the OIDC sub, an opaque provider identifier; req.userName: the "${e}" claim`+("sub"===e?" — the same value, until --oidc-username-claim names another":"")}getConfigApiData(){return{enabled:!0,endpoints:{logout:"/logout"}}}get pathPrefixes(){const e=super.pathPrefixes;return 0===e.length?e:[...e,...c]}api(e){this.router=r(this.buildConfig()),super.api(e)}authMethod(e,o,s){if(!this.router)return i(o,new Error(`${this.name}: api() has not built the router`));this.router(e,o,t=>{if(t&&h.test(e.path)&&this.noEndSession(t))return o.redirect(this.config.base_url);if(t)return i(o,t);if(("GET"===e.method||"HEAD"===e.method)&&h.test(e.path))return void this.endHere(e).then(()=>o.oidc.logout({logoutParams:{post_logout_redirect_uri:void 0}}));const guard=()=>{a()(e,o,t=>{if(t)return n(o);const i=e.oidc.user,r=this.config.oidc_username_claim||"sub",a=i[r];"sub"!==r&&"string"!=typeof a&&this.logger.warn(`${this.name}: no "${r}" claim on this session, so req.userName falls back to the sub. Check --oidc-username-claim against the scopes the provider is asked for`),this.publishIdentity(e,String(i.sub),"string"==typeof a?a:String(i.sub)),s()})},r=this.renewExpired(e);r?r.then(guard):guard()})}renewExpired(e){const o=e.oidc,s=o.accessToken;if(!s?.isExpired())return;const end=()=>{e.appSession=void 0};if(o.refreshToken)return s.refresh().then(()=>{},e=>{this.logger.info(`${this.name}: the provider refused to renew an access token, so the session ends: ${String(e)}`),end()});end()}async loggedOut(e){if(!this.server.hooks.oidcsessionvalid)return!1;const o=sessionClaims(e),[,s]=await t(this.server.hooks.oidcsessionvalid,[o,!0]);return!s}async sessionId(o,s){const{oidc:t,appSession:i}=o,n=i?.[u];return"string"==typeof n&&void 0!==s&&t?.isAuthenticated()&&t.idTokenClaims?.sub===s&&!await this.loggedOut(o).catch(()=>!0)?n:e()}async endHere(e){if(!e.oidc?.isAuthenticated())return;const o=sessionClaims(e);if(!o)return;const{iss:s,sid:t,session:i}=o,n=i?{iss:s,session:i}:t?{iss:s,sid:t}:void 0;if(n)if(this.server.hooks.oidclogouttoken)try{await walkSubscribers(this.server.hooks.oidclogouttoken,n)}catch(e){this.logger.error(`${this.name}: /logout could not record the session it ends, so a copy of its cookie stays valid until its tokens expire: ${String(e)}`)}else this.warnedNoRecord||(this.warnedNoRecord=!0,this.logger.warn(`${this.name}: no Back-Channel Logout plugin is loaded, so /logout drops the browser's cookie and nothing else: a copy of it stays valid until its tokens expire. Load core/bcl to end the session here as well`));else this.warnedUnnamed||(this.warnedUnnamed=!0,this.logger.warn(`${this.name}: /logout ended a session nothing names — one established before this version, from a provider that sends no sid — so a copy of its cookie stays valid until its tokens expire`))}noEndSession(e){return!!(e instanceof Error&&d.test(e.message))&&(this.warnedNoEndSession||(this.warnedNoEndSession=!0,this.logger.warn(`${this.name}: the provider publishes no end_session_endpoint, so /logout ends the session here only, and the provider may log the caller back in`)),!0)}}export{OpenIDConnect as default};
1
+ import{randomUUID as e}from"crypto";import o from"express-openid-connect";import s from"../../lib/auth/base.js";import{launchHooksChained as t,runAsHookOwner as i}from"../../lib/utils.js";import{serverError as n,unauthorized as r}from"../../lib/expressFormatedResponses.js";const{auth:a,requiresAuth:c}=o,d=["/login","/logout","/callback","/backchannel-logout"],h=/^end_session_endpoint must be configured/,u=/^\/logout\/?$/i,l="dmSession",sessionClaims=e=>{const{oidc:o,appSession:s}=e,t=o?.idTokenClaims;if(!t)return;const i=s?.[l];return"string"==typeof i?{...t,session:i}:t},subjectOf=e=>{try{const o=JSON.parse(Buffer.from(String(e).split(".")[1],"base64url").toString());return"string"==typeof o.sub?o.sub:void 0}catch{return}},walkSubscribers=async(e,o)=>{let s;for(const t of e??[])if(t)try{await i(t,()=>t(o))}catch(e){s??(s=e instanceof Error?e:new Error(String(e)))}if(s)throw s};class OpenIDConnect extends s{constructor(e){super(e),this.name="openidconnect",this.configApiKey="openidconnect",this.roles=["auth","configurable"],this.warnedNoEndSession=!1,this.warnedNoRecord=!1,this.warnedUnnamed=!1;for(const e of["oidc_server","oidc_client_id","oidc_client_secret","base_url"])if(!this.config[e])throw new Error(`Missing config parameter ${e}`)}buildConfig(){return{authRequired:!1,issuerBaseURL:this.config.oidc_server,clientID:this.config.oidc_client_id,secret:this.config.oidc_client_secret,baseURL:this.config.base_url,clientSecret:this.config.oidc_client_secret,authorizationParams:{response_type:"code",scope:"openid profile email"},idpLogout:!0,routes:{logout:!1},afterCallback:async(e,o,s)=>({...s,[l]:await this.sessionId(e,subjectOf(s.id_token))}),backchannelLogout:{onLogoutToken:async e=>{this.server.hooks.oidclogouttoken?await walkSubscribers(this.server.hooks.oidclogouttoken,e):this.logger.warn("openidconnect: a logout token arrived but no Back-Channel Logout plugin is loaded, so nothing records it")},onLogin:async e=>{if(!this.server.hooks.oidclogin)return;const o=e.oidc?.idTokenClaims;if(o)try{await walkSubscribers(this.server.hooks.oidclogin,o)}catch(o){const s=e.appSession;if(s)for(const e of Object.keys(s))delete s[e];throw o}},isLoggedOut:e=>this.loggedOut(e)}}}identitySource(){const e=this.config.oidc_username_claim||"sub";return`req.user: the OIDC sub, an opaque provider identifier; req.userName: the "${e}" claim`+("sub"===e?" — the same value, until --oidc-username-claim names another":"")}getConfigApiData(){return{enabled:!0,endpoints:{logout:"/logout"}}}get pathPrefixes(){const e=super.pathPrefixes;return 0===e.length?e:[...e,...d]}api(e){this.router=a(this.buildConfig()),super.api(e)}authMethod(e,o,s){if(!this.router)return n(o,new Error(`${this.name}: api() has not built the router`));this.router(e,o,t=>{if(t&&u.test(e.path)&&this.noEndSession(t))return o.redirect(this.config.base_url);if(t)return n(o,t);if(("GET"===e.method||"HEAD"===e.method)&&u.test(e.path))return void this.endHere(e).then(()=>o.oidc.logout({logoutParams:{post_logout_redirect_uri:void 0}}));const guard=()=>{c()(e,o,t=>{if(t)return r(o);const i=e.oidc.user,n=this.config.oidc_username_claim||"sub",a=i[n];"sub"!==n&&"string"!=typeof a&&this.logger.warn(`${this.name}: no "${n}" claim on this session, so req.userName falls back to the sub. Check --oidc-username-claim against the scopes the provider is asked for`),this.publishIdentity(e,String(i.sub),"string"==typeof a?a:String(i.sub)),s()})},i=this.renewExpired(e);i?i.then(guard):guard()})}renewExpired(e){const o=e.oidc,s=o.accessToken;if(!s?.isExpired())return;const end=()=>{e.appSession=void 0};if(o.refreshToken)return s.refresh().then(()=>{},e=>{this.logger.info(`${this.name}: the provider refused to renew an access token, so the session ends: ${String(e)}`),end()});end()}async loggedOut(e){if(!this.server.hooks.oidcsessionvalid)return!1;const o=sessionClaims(e),[,s]=await t(this.server.hooks.oidcsessionvalid,[o,!0]);return!s}async sessionId(o,s){const{oidc:t,appSession:i}=o,n=i?.[l];return"string"==typeof n&&void 0!==s&&t?.isAuthenticated()&&t.idTokenClaims?.sub===s&&!await this.loggedOut(o).catch(()=>!0)?n:e()}async endHere(e){if(!e.oidc?.isAuthenticated())return;const o=sessionClaims(e);if(!o)return;const{iss:s,sid:t,session:i}=o,n=i?{iss:s,session:i}:t?{iss:s,sid:t}:void 0;if(n)if(this.server.hooks.oidclogouttoken)try{await walkSubscribers(this.server.hooks.oidclogouttoken,n)}catch(e){this.logger.error(`${this.name}: /logout could not record the session it ends, so a copy of its cookie stays valid until its tokens expire: ${String(e)}`)}else this.warnedNoRecord||(this.warnedNoRecord=!0,this.logger.warn(`${this.name}: no Back-Channel Logout plugin is loaded, so /logout drops the browser's cookie and nothing else: a copy of it stays valid until its tokens expire. Load core/bcl to end the session here as well`));else this.warnedUnnamed||(this.warnedUnnamed=!0,this.logger.warn(`${this.name}: /logout ended a session nothing names — one established before this version, from a provider that sends no sid — so a copy of its cookie stays valid until its tokens expire`))}noEndSession(e){return!!(e instanceof Error&&h.test(e.message))&&(this.warnedNoEndSession||(this.warnedNoEndSession=!0,this.logger.warn(`${this.name}: the provider publishes no end_session_endpoint, so /logout ends the session here only, and the provider may log the caller back in`)),!0)}}export{OpenIDConnect as default};
@@ -1 +1 @@
1
- import o from"../../abstract/plugin.js";import{asyncHandler as s}from"../../lib/utils.js";class HelloWorld extends o{constructor(){super(...arguments),this.name="hello",this.roles=["demo"]}api(o){o.get(`${this.config.api_prefix}/hello`,s(async(o,s)=>{const e={message:"Hello",hookResults:[]};if(this.server.hooks&&this.server.hooks.hello)for(const o of this.server.hooks.hello)e.hookResults.push(await o());s.json(e)}))}}export{HelloWorld as default};
1
+ import o from"../../abstract/plugin.js";import{asyncHandler as s,runAsHookOwner as e}from"../../lib/utils.js";class HelloWorld extends o{constructor(){super(...arguments),this.name="hello",this.roles=["demo"]}api(o){o.get(`${this.config.api_prefix}/hello`,s(async(o,s)=>{const l={message:"Hello",hookResults:[]};if(this.server.hooks&&this.server.hooks.hello)for(const o of this.server.hooks.hello){const s=o;l.hookResults.push(await e(o,()=>s()))}s.json(l)}))}}export{HelloWorld as default};
@@ -1 +1 @@
1
- import t from"multer";import{parse as e}from"csv-parse/sync";import r from"../../abstract/plugin.js";import{badRequest as i,serverError as s}from"../../lib/expressFormatedResponses.js";import{errorBody as o,AUTHZ_REFUSED as a,HttpError as n}from"../../lib/errors.js";import{loadSchemaFile as c}from"../../lib/schemaFile.js";import{isAuthzRefusal as l,validateDnValue as p,escapeDnValue as d}from"../../lib/utils.js";const u=/^([A-Za-z][A-Za-z0-9-]*|\d+(\.\d+)+)(;[A-Za-z0-9-]+)*$/;class LdapBulkImport extends r{constructor(e){super(e),this.name="ldapBulkImport",this.roles=["configurable"],this.resources=new Map;const r=parseInt(this.config.bulk_import_max_file_size,10)||10485760;this.upload=t({storage:t.memoryStorage(),limits:{fileSize:r},fileFilter:(t,e,r)=>{"text/csv"===e.mimetype||e.originalname.endsWith(".csv")?r(null,!0):r(new Error("Only CSV files are allowed"))}});const i=this.config.bulk_import_schemas;if(!i)return void this.logger.warn("No bulk import schemas configured");const s=i.split(",").map(t=>t.trim());for(const t of s){const[e,r]=t.split(":").map(t=>t.trim());if(e&&r)try{const t=c(r,{config:this.config});this.resources.set(e,{name:e,schema:t,base:t.base||this.config.ldap_base,mainAttribute:t.mainAttribute||"cn"}),this.logger.info(`Loaded bulk import resource: ${e} from ${r}`)}catch(t){this.logger.error(`Failed to load schema for ${e}: ${t.message}`)}else this.logger.warn(`Invalid schema entry: ${t}`)}}api(t){for(const[e,r]of this.resources)t.get(`${this.config.api_prefix}/v1/ldap/bulk-import/${e}/template.csv`,(t,e)=>this.getTemplate(t,e,r)),t.post(`${this.config.api_prefix}/v1/ldap/bulk-import/${e}`,this.upload.single("file"),async(t,e)=>this.bulkImport(t,e,r))}getTemplate(t,e,r){const i=this.getEditableAttributes(r.schema);i.push("organizationDn"),e.setHeader("Content-Type","text/csv; charset=utf-8"),e.setHeader("Content-Disposition",`attachment; filename="${r.name}-template.csv"`),e.send(i.join(",")+"\n")}async bulkImport(t,r,c){if(!t.file)return i(r,"No file uploaded");const p="true"===t.body.dryRun||!0===t.body.dryRun,d="true"===t.body.updateExisting||!0===t.body.updateExisting,u="false"!==t.body.continueOnError&&!1!==t.body.continueOnError,h=Date.now(),m={success:!0,total:0,created:0,updated:0,skipped:0,failed:0,errors:[],details:{duration:"",linesProcessed:0}};try{const i=t.file.buffer.toString("utf-8"),s=e(i,{columns:!0,skip_empty_lines:!0,trim:!0});m.total=s.length;for(let e=0;e<s.length;e++){const r=e+2,i=s[e];try{const{dn:e,entry:r,orgDn:s}=this.buildRecord(i,c);await this.authorizeRow(e,r,t),await this.completeRecord(r,s,c);const o=await this.entryExists(e);if(o&&!d){m.skipped++;continue}if(p){o||await this.server.ldap.authorize("ldapaddrequest",[e,r,t]),o&&await this.server.ldap.authorize("ldapmodifyrequest",[e,{replace:r},this.opNumber(),t]),o?m.updated++:m.created++;continue}o&&d?(await this.server.ldap.modify(e,{replace:r},t),m.updated++):(await this.server.ldap.add(e,r,t),m.created++)}catch(t){if(m.failed++,m.errors.push({line:r,identifier:i[c.mainAttribute],...l(t)?o(t,a):{error:t.message,...t instanceof n?t.refusal:void 0}}),!u)throw t}}m.details.linesProcessed=s.length,m.details.duration=`${((Date.now()-h)/1e3).toFixed(1)}s`,r.json(m)}catch(t){return s(r,t)}}async authorizeRow(t,e,r){try{await this.server.ldap.authorize("ldapaddrequest",[t,e,r])}catch(i){if(!l(i))throw i;try{await this.server.ldap.authorize("ldapmodifyrequest",[t,{replace:e},this.opNumber(),r])}catch{throw i}}}buildRecord(t,e){const r={},i=e.schema,s=t.organizationDn;delete t.organizationDn;for(const[t,e]of Object.entries(i.properties||{}))if(!0===e.fixed){const i=e.default||e.const;void 0!==i&&(r[t]=i)}for(const[e,i]of Object.entries(t))if(i&&""!==i.trim()){if(!u.test(e))throw new Error(`Invalid column name: ${e}`);r[e]=i.includes(";")?i.split(";").map(t=>t.trim()):i}if(s){const t=this.findAttributeByRole(i,"organizationLink");t&&(r[t]=s)}const o=e.mainAttribute,a=r[o];if(!a)throw new Error(`Missing main attribute: ${o}`);const n=Array.isArray(a)?String(a[0]):String(a);p(n,o);return{dn:`${o}=${d(n)},${e.base}`,entry:r,orgDn:s}}async completeRecord(t,e,r){const i=r.schema;if(e){const r=this.findAttributeByRole(i,"organizationPath");if(r){const i=(await this.fetchOrganization(e))[r];t[r]=Array.isArray(i)?i[0]:i}}for(const[e,r]of Object.entries(i.properties||{}))if(!0===r.required&&!t[e])throw new Error(`Missing required attribute: ${e}`)}getEditableAttributes(t){const e=[];for(const[r,i]of Object.entries(t.properties||{}))!0!==i.fixed&&(i.role?.includes("organizationLink")||i.role?.includes("organizationPath")||e.push(r));return e}findAttributeByRole(t,e){for(const[r,i]of Object.entries(t.properties||{}))if(i.role?.includes(e))return r;return null}async fetchOrganization(t){const e=await this.server.ldap.system.search({paged:!1,scope:"base"},t);if(0===e.searchEntries.length)throw new Error(`Organization not found: ${t}`);return e.searchEntries[0]}async entryExists(t){try{return(await this.server.ldap.system.search({paged:!1,scope:"base"},t)).searchEntries.length>0}catch(t){return!1}}getConfigApiData(){const t=this.config.api_prefix||"/api",e=[];return this.resources.forEach((r,i)=>{e.push({name:i,mainAttribute:r.mainAttribute,base:r.base,maxFileSize:parseInt(this.config.bulk_import_max_file_size,10)||10485760,batchSize:parseInt(this.config.bulk_import_batch_size,10)||100,endpoints:{template:`${t}/v1/ldap/bulk-import/${i}/template.csv`,import:`${t}/v1/ldap/bulk-import/${i}`}})}),{enabled:!0,resources:e}}}export{LdapBulkImport as default};
1
+ import t from"multer";import{parse as e}from"csv-parse/sync";import r from"../../abstract/plugin.js";import{badRequest as i,serverError as s}from"../../lib/expressFormatedResponses.js";import{errorBody as o,AUTHZ_REFUSED as a,HttpError as n}from"../../lib/errors.js";import{loadSchemaFile as c}from"../../lib/schemaFile.js";import{isAuthzRefusal as l,validateDnValue as d,escapeDnValue as p}from"../../lib/utils.js";const u=/^([A-Za-z][A-Za-z0-9-]*|\d+(\.\d+)+)(;[A-Za-z0-9-]+)*$/;class LdapBulkImport extends r{constructor(e){super(e),this.name="ldapBulkImport",this.roles=["configurable"],this.resources=new Map;const r=parseInt(this.config.bulk_import_max_file_size,10)||10485760;this.upload=t({storage:t.memoryStorage(),limits:{fileSize:r},fileFilter:(t,e,r)=>{"text/csv"===e.mimetype||e.originalname.endsWith(".csv")?r(null,!0):r(new Error("Only CSV files are allowed"))}});const i=this.config.bulk_import_schemas;if(!i)return void this.logger.warn("No bulk import schemas configured");const s=i.split(",").map(t=>t.trim());for(const t of s){const[e,r]=t.split(":").map(t=>t.trim());if(e&&r)try{const t=c(r,{config:this.config});this.resources.set(e,{name:e,schema:t,base:t.base||this.config.ldap_base,mainAttribute:t.mainAttribute||"cn"}),this.logger.info(`Loaded bulk import resource: ${e} from ${r}`)}catch(t){this.logger.error(`Failed to load schema for ${e}: ${t.message}`)}else this.logger.warn(`Invalid schema entry: ${t}`)}}api(t){for(const[e,r]of this.resources)t.get(`${this.config.api_prefix}/v1/ldap/bulk-import/${e}/template.csv`,(t,e)=>this.getTemplate(t,e,r)),t.post(`${this.config.api_prefix}/v1/ldap/bulk-import/${e}`,this.upload.single("file"),async(t,e)=>this.bulkImport(t,e,r))}getTemplate(t,e,r){const i=this.getEditableAttributes(r.schema);i.push("organizationDn"),e.setHeader("Content-Type","text/csv; charset=utf-8"),e.setHeader("Content-Disposition",`attachment; filename="${r.name}-template.csv"`),e.send(i.join(",")+"\n")}async bulkImport(t,r,c){if(!t.file)return i(r,"No file uploaded");const d="true"===t.body.dryRun||!0===t.body.dryRun,p="true"===t.body.updateExisting||!0===t.body.updateExisting,u="false"!==t.body.continueOnError&&!1!==t.body.continueOnError,h=Date.now(),f={success:!0,total:0,created:0,updated:0,skipped:0,failed:0,errors:[],details:{duration:"",linesProcessed:0}};try{const i=t.file.buffer.toString("utf-8"),s=e(i,{columns:!0,skip_empty_lines:!0,trim:!0});f.total=s.length;for(let e=0;e<s.length;e++){const r=e+2,i=s[e];try{const{dn:e,entry:r,orgDn:s}=this.buildRecord(i,c);await this.authorizeRow(e,r,t),await this.completeRecord(r,s,c);const o=await this.entryExists(e);if(o&&!p){f.skipped++;continue}if(d){o||await this.server.ldap.authorize("ldapaddrequest",[e,r,t]),o&&await this.server.ldap.authorize("ldapmodifyrequest",[e,{replace:r},this.opNumber(),t]),o?f.updated++:f.created++;continue}o&&p?(await this.server.ldap.modify(e,{replace:r},t),f.updated++):(await this.server.ldap.add(e,r,t),f.created++)}catch(t){if(f.failed++,f.errors.push({line:r,identifier:i[c.mainAttribute],...l(t)?o(t,a):{error:t.message,...t instanceof n?t.refusal:void 0}}),!u)throw t}}f.details.linesProcessed=s.length,f.details.duration=`${((Date.now()-h)/1e3).toFixed(1)}s`,r.json(f)}catch(t){return s(r,t)}}async authorizeRow(t,e,r){try{await this.server.ldap.authorize("ldapaddrequest",[t,e,r])}catch(i){if(!l(i))throw i;try{await this.server.ldap.authorize("ldapmodifyrequest",[t,{replace:e},this.opNumber(),r])}catch{throw i}}}buildRecord(t,e){const r={},i=e.schema,s=t.organizationDn;delete t.organizationDn;const o=new Set;for(const[t,e]of Object.entries(i.properties||{}))if(!0===e.fixed){const i=e.default||e.const;void 0!==i&&(r[t]=i,o.add(t.toLowerCase()))}for(const[e,i]of Object.entries(t))if(i&&""!==i.trim()){if(!u.test(e))throw new Error(`Invalid column name: ${e}`);if(o.has(e.split(";")[0].toLowerCase()))throw new Error(`Attribute "${e}" is fixed and cannot be imported`);r[e]=i.includes(";")?i.split(";").map(t=>t.trim()):i}if(s){const t=this.findAttributeByRole(i,"organizationLink");t&&(r[t]=s)}const a=e.mainAttribute,n=r[a];if(!n)throw new Error(`Missing main attribute: ${a}`);const c=Array.isArray(n)?String(n[0]):String(n);d(c,a);return{dn:`${a}=${p(c)},${e.base}`,entry:r,orgDn:s}}async completeRecord(t,e,r){const i=r.schema;if(e){const r=this.findAttributeByRole(i,"organizationPath");if(r){const i=(await this.fetchOrganization(e))[r];t[r]=Array.isArray(i)?i[0]:i}}for(const[e,r]of Object.entries(i.properties||{}))if(!0===r.required&&!t[e])throw new Error(`Missing required attribute: ${e}`)}getEditableAttributes(t){const e=[];for(const[r,i]of Object.entries(t.properties||{}))!0!==i.fixed&&(i.role?.includes("organizationLink")||i.role?.includes("organizationPath")||e.push(r));return e}findAttributeByRole(t,e){for(const[r,i]of Object.entries(t.properties||{}))if(i.role?.includes(e))return r;return null}async fetchOrganization(t){const e=await this.server.ldap.system.search({paged:!1,scope:"base"},t);if(0===e.searchEntries.length)throw new Error(`Organization not found: ${t}`);return e.searchEntries[0]}async entryExists(t){try{return(await this.server.ldap.system.search({paged:!1,scope:"base"},t)).searchEntries.length>0}catch(t){return!1}}getConfigApiData(){const t=this.config.api_prefix||"/api",e=[];return this.resources.forEach((r,i)=>{e.push({name:i,mainAttribute:r.mainAttribute,base:r.base,maxFileSize:parseInt(this.config.bulk_import_max_file_size,10)||10485760,batchSize:parseInt(this.config.bulk_import_batch_size,10)||100,endpoints:{template:`${t}/v1/ldap/bulk-import/${i}/template.csv`,import:`${t}/v1/ldap/bulk-import/${i}`}})}),{enabled:!0,resources:e}}}export{LdapBulkImport as default};
@@ -1 +1 @@
1
- import e from"../../abstract/plugin.js";import a from"../../abstract/ldapFlat.js";import{loadSchemaFile as t,schemaUrl as l}from"../../lib/schemaFile.js";const i=[{plugin:"ldapGroups",modules:["ldap/groups"],collection:"groups"},{plugin:"ldapOrganizations",modules:["ldap/organizations","ldap/organization"],collection:"organizations"},{plugin:"ldapRaw",modules:["ldap/raw"],collection:"raw"},{plugin:"ldapBulkImport",modules:["ldap/bulkImport"],collection:"bulk-import"}];function pluginModule(e){return e.split(":")[0].replace(/\.[cm]?js$/,"").split("/").slice(-2).join("/")}class LdapFlatInstance extends a{constructor(e,a){super(e,a),this.name="ldapFlatInstance",this.roles=["api"],this.dependencies={departmentSync:"core/ldap/departmentSync"}}}class LdapFlatGeneric extends e{constructor(e){super(e),this.name="ldapFlatGeneric",this.roles=["configurable"],this.instances=[],this.schemaPaths=[];const a=this.config.ldap_flat_schema||[],l=new Map,n=new Map;if(0===a.length)return void this.logger.warn("No schemas provided for ldapFlatGeneric plugin");const s=(this.config.plugin||[]).map(pluginModule),r=new Map;for(const{plugin:e,modules:a,collection:t}of i)(this.server.loadedPlugins[e]||a.some(e=>s.includes(e)))&&r.set(t,`plugin ${e}`);a.forEach(a=>{try{const i=t(a,{config:this.config});if(!i.entity)throw new Error(`Schema ${a} is missing "entity" metadata section`);const s=["name","mainAttribute","objectClass","singularName","pluralName","base"];for(const e of s)if(!i.entity[e])throw new Error(`Schema ${a} is missing entity.${e}`);const o=l.get(i.entity.name);if(o)throw new Error(`entity "${i.entity.name}" is already declared by ${o}`);const p=n.get(i.entity.pluralName)||r.get(i.entity.pluralName);if(p)throw new Error(`entity "${i.entity.name}" wants the URL of "${i.entity.pluralName}", already served by ${p}`);let c=i.entity.base;c=c.replace(/\{([^}]+)\}/g,(e,a)=>{const t=a,l=this.config[t];return"string"==typeof l?l:e});const u=new LdapFlatInstance(e,{base:c,mainAttribute:i.entity.mainAttribute,objectClass:i.entity.objectClass,defaultAttributes:i.entity.defaultAttributes||{},schemaPath:a,singularName:i.entity.singularName,pluralName:i.entity.pluralName,hookPrefix:`ldap${i.entity.name}`});u.label=i.entity.label,u.singularLabel=i.entity.singularLabel,u.valueLabels=i.entity.valueLabels,u.name=`ldapFlat:${i.entity.name}`,this.instances.push(u),this.schemaPaths.push(a),l.set(i.entity.name,a),n.set(i.entity.pluralName,a),this.logger.info(`Created ldapFlat instance for "${i.entity.name}" (${i.entity.pluralName})`)}catch(e){this.logger.error(`Failed to load schema ${a}: ${e}`)}})}api(e){this.instances.forEach(a=>{a.api(e)})}getConfigApiData(){const e=this.config.api_prefix||"/api";return{flatResources:this.instances.map((a,t)=>{const i=this.server.loadedPlugins.static&&this.schemaPaths[t]?l(this.config,this.schemaPaths[t]):void 0;return{name:a.name.replace("ldapFlat:",""),singularName:a.singularName,pluralName:a.pluralName,label:a.label,singularLabel:a.singularLabel,valueLabels:a.valueLabels,mainAttribute:a.mainAttribute,objectClass:a.objectClass,base:a.base,schema:a.schema||{strict:!1,attributes:{}},schemaUrl:i,endpoints:{list:`${e}/v1/ldap/${a.pluralName}`,get:`${e}/v1/ldap/${a.pluralName}/:id`,create:`${e}/v1/ldap/${a.pluralName}`,update:`${e}/v1/ldap/${a.pluralName}/:id`,delete:`${e}/v1/ldap/${a.pluralName}/:id`,rename:`${e}/v1/ldap/${a.pluralName}/:id/rename`}}})}}}export{LdapFlatGeneric as default,i as ldapPluginCollections};
1
+ import e from"../../abstract/plugin.js";import t from"../../abstract/ldapFlat.js";import{loadSchemaFile as a}from"../../lib/schemaFile.js";import{staticSchemaUrl as l}from"../../lib/staticFiles.js";const i=[{plugin:"ldapGroups",modules:["ldap/groups"],collection:"groups"},{plugin:"ldapOrganizations",modules:["ldap/organizations","ldap/organization"],collection:"organizations"},{plugin:"ldapRaw",modules:["ldap/raw"],collection:"raw"},{plugin:"ldapBulkImport",modules:["ldap/bulkImport"],collection:"bulk-import"}];function pluginModule(e){return e.split(":")[0].replace(/\.[cm]?js$/,"").split("/").slice(-2).join("/")}class LdapFlatInstance extends t{constructor(e,t){super(e,t),this.name="ldapFlatInstance",this.roles=["api"],this.dependencies={departmentSync:"core/ldap/departmentSync"}}}class LdapFlatGeneric extends e{constructor(e){super(e),this.name="ldapFlatGeneric",this.roles=["configurable"],this.instances=[],this.schemaPaths=[];const t=this.config.ldap_flat_schema||[],l=new Map,n=new Map;if(0===t.length)return void this.logger.warn("No schemas provided for ldapFlatGeneric plugin");const s=(this.config.plugin||[]).map(pluginModule),r=new Map;for(const{plugin:e,modules:t,collection:a}of i)(this.server.loadedPlugins[e]||t.some(e=>s.includes(e)))&&r.set(a,`plugin ${e}`);t.forEach(t=>{try{const i=a(t,{config:this.config});if(!i.entity)throw new Error(`Schema ${t} is missing "entity" metadata section`);const s=["name","mainAttribute","objectClass","singularName","pluralName","base"];for(const e of s)if(!i.entity[e])throw new Error(`Schema ${t} is missing entity.${e}`);const o=l.get(i.entity.name);if(o)throw new Error(`entity "${i.entity.name}" is already declared by ${o}`);const p=n.get(i.entity.pluralName)||r.get(i.entity.pluralName);if(p)throw new Error(`entity "${i.entity.name}" wants the URL of "${i.entity.pluralName}", already served by ${p}`);let c=i.entity.base;c=c.replace(/\{([^}]+)\}/g,(e,t)=>{const a=t,l=this.config[a];return"string"==typeof l?l:e});const u=new LdapFlatInstance(e,{base:c,mainAttribute:i.entity.mainAttribute,objectClass:i.entity.objectClass,defaultAttributes:i.entity.defaultAttributes||{},schemaPath:t,singularName:i.entity.singularName,pluralName:i.entity.pluralName,hookPrefix:`ldap${i.entity.name}`});u.label=i.entity.label,u.singularLabel=i.entity.singularLabel,u.valueLabels=i.entity.valueLabels,u.name=`ldapFlat:${i.entity.name}`,this.instances.push(u),this.schemaPaths.push(t),l.set(i.entity.name,t),n.set(i.entity.pluralName,t),this.logger.info(`Created ldapFlat instance for "${i.entity.name}" (${i.entity.pluralName})`)}catch(e){this.logger.error(`Failed to load schema ${t}: ${e}`)}})}api(e){this.instances.forEach(t=>{t.api(e)})}getConfigApiData(){const e=this.config.api_prefix||"/api";return{flatResources:this.instances.map((t,a)=>{const i=this.schemaPaths[a]?l(this.server,this.schemaPaths[a]):void 0;return{name:t.name.replace("ldapFlat:",""),singularName:t.singularName,pluralName:t.pluralName,label:t.label,singularLabel:t.singularLabel,valueLabels:t.valueLabels,mainAttribute:t.mainAttribute,objectClass:t.objectClass,base:t.base,schema:t.schema||{strict:!1,attributes:{}},schemaUrl:i,endpoints:{list:`${e}/v1/ldap/${t.pluralName}`,get:`${e}/v1/ldap/${t.pluralName}/:id`,create:`${e}/v1/ldap/${t.pluralName}`,update:`${e}/v1/ldap/${t.pluralName}/:id`,delete:`${e}/v1/ldap/${t.pluralName}/:id`,rename:`${e}/v1/ldap/${t.pluralName}/:id/rename`}}})}}}export{LdapFlatGeneric as default,i as ldapPluginCollections};
@@ -1 +1 @@
1
- import e from"../../abstract/plugin.js";import{assertModifyRequest as t}from"../../lib/ldapActions.js";import{wantJson as r,jsonBody as i,tryMethod as s,ok as a}from"../../lib/expressFormatedResponses.js";import{extractLdapCode as o}from"../../lib/ldapCodes.js";import{loadSchemaFileAsync as n,schemaUrl as h}from"../../lib/schemaFile.js";import{movedToTrash as d,asyncHandler as c,isDummyMemberDn as p,validateDnValue as u,escapeDnValue as m,launchHooksChained as l,launchHooks as f,escapeLdapFilter as g,organizationLink as w,isDnSyntax as b,normalizeDn as y,isDnInBranch as $,getRdn as v,isAuthzRefusal as G,substringSearchFilter as _}from"../../lib/utils.js";import{NotFoundError as A,BadRequestError as D,HttpError as q,ConflictError as C}from"../../lib/errors.js";import{assertClientMaySet as j,schemaAttribute as k,modifiedAttributeNames as E,toDirectoryEntry as M,invalidValueMessage as O,missingRequiredAttribute as x,toDirectoryChanges as N,checkDnValues as z,matchesPattern as F}from"../../config/schema.js";class LdapGroups extends e{constructor(e){if(super(e),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeletedone:async e=>{const t=Array.isArray(e)?e:[e];this.logger.debug(`User deletion detected, removing from groups: ${t.join(", ")}`),await Promise.all(t.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)})},ldaprenamedone:async([e,t])=>{d(this.config.trash_base,e,t)&&await this.deleteMemberFromAll(e).catch(e=>{this.logger.error("Failed to process a move to the trash in groups:",e)})}},this.ldap=e.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");if(this.config.ldap_group_base&&e.ldap.declareObjectClasses(this.base,this.config.group_class||[]),this.config.group_schema){const e=this.config.group_schema;n(e,{config:this.config}).then(e=>{this.schema=this.adaptSchema(e),this.server.ldap.knowAttributeNames(Object.keys(this.schema.attributes)),this.logger.debug("Group schema loaded")}).catch(t=>this.logger.error(`Failed to load group schema from ${e}: ${t}`))}}adaptSchema(e){return e}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,c(async(e,t)=>{if(!r(e,t))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new D("Invalid match query");if("string"==typeof e.query.attribute&&e.query.attribute)i.filter=_(e.query.match,e.query.attribute);else if(/=/.test(e.query.match)){if(!/^[\w*=()&|, .@'-]+$/.test(e.query.match))throw new D("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=g(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const s=await this.listGroups({...i,req:e});return a(t,this.hideNeverReturn(s))})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,c(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,c(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,c(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,c(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,c(async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)throw new D("cn is required");const a=this.routeGroupDn(r),o=i(e,t,"member");if(!o)return;const n=Array.isArray(o.member)?o.member:[o.member];if(n.some(e=>"string"!=typeof e))throw new D("member must be a string or string array");await this.judgedGroup(a,()=>this.ldap.authorize("ldapmodifyrequest",[a,{add:{member:n}},this.opNumber(),e])),await s(t,this.addMember.bind(this),r,o.member,e)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,c(async(e,t)=>{if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!i||!a)throw new D("cn and member are required");const o=this.routeGroupDn(i);await this.judgedGroup(o,()=>this.ldap.authorize("ldapmodifyrequest",[o,{delete:{member:[a]}},this.opNumber(),e])),await s(t,this.deleteMember.bind(this),i,a,e)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,c(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new D("cn is required");const o=this.routeGroupDn(a),n=i(e,t,"targetOrgDn");if(!n)return;if("string"!=typeof n.targetOrgDn)throw new D("targetOrgDn must be a string");const h=this.config.ldap_organization_link_attribute;await this.judgedGroup(o,()=>this.ldap.authorize("ldapmodifyrequest",[o,{replace:{[h]:n.targetOrgDn}},this.opNumber(),e])),await s(t,this.moveGroup.bind(this),a,n.targetOrgDn,e)})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,c(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new D("cn is required");const o=this.routeGroupDn(a),n=i(e,t,"newCn");if(!n)return;if("string"!=typeof n.newCn)throw new D("newCn must be a string");const h=/,/.test(n.newCn)?this.routeGroupDn(n.newCn):this.toGroupDn(n.newCn);await this.judgedGroup(o,()=>this.ldap.authorize("ldaprenamerequest",[o,h,e])),await s(t,this.renameGroup.bind(this),a,n.newCn,e)}))}isDummyMember(e){return p(e,this.config.group_dummy_user)}withoutDummyMember(e){const t=e.member;if(void 0===t)return e;const r=Array.isArray(t)?t:[t];return{...e,member:r.filter(e=>!this.isDummyMember(e))}}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),s=this.routeGroupDn(i);try{const r=await this.ldap.search({paged:!1,scope:"base",filter:this.groupFilter()},s,e);if(0===r.searchEntries.length)throw new A("Group not found");t.json(this.hideNeverReturn(this.withoutDummyMember(r.searchEntries[0])))}catch(e){if(e.code&&32===e.code)throw new A("Group not found");if(34===o(e))throw new D(`${i} is not a valid DN`);throw e}}async apiAdd(e,t,...r){const a=i(e,t,...r);if(!a)return;await this.server.ldap.canonicalAttributes(a),j(this.schema,Object.keys(a),this.cn);const o=a[this.cn];"string"==typeof o&&(/,/.test(o)||new RegExp(`^${this.cn}=`,"i").test(o))&&this.routeGroupDn(/,/.test(o)?o:`${o},${this.base}`);const n=a.member?a.member:[],h=Object.fromEntries(Object.entries(a).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===k(this.schema,e)?.[1]?.fixed)));await s(t,this.addGroup.bind(this),o,n,h,e)}async apiDelete(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);if(!i)throw new D("cn is required");const a=this.routeGroupDn(i);await this.judgedGroup(a,()=>this.ldap.authorize("ldapdeleterequest",[a,e])),await s(t,this.deleteGroup.bind(this),i,e)}async apiModify(e,r){const a=i(e,r);if(!a)return;t(a);const o=decodeURIComponent(e.params.cn);if(!o)throw new D("cn is required");const n=this.routeGroupDn(o);await this.server.ldap.canonicalChanges(a),j(this.schema,E(a));const fixed=e=>!0===k(this.schema,e)?.[1]?.fixed,keep=e=>e&&Object.fromEntries(Object.entries(e).filter(([e])=>!fixed(e))),h={...a.add?{add:keep(a.add)}:{},...a.replace?{replace:keep(a.replace)}:{},...a.delete?{delete:Array.isArray(a.delete)?a.delete.filter(e=>!fixed(e)):keep(a.delete)}:{}};await this.judgedGroup(n,()=>this.ldap.authorize("ldapmodifyrequest",[n,h,this.opNumber(),e])),await s(r,this.modifyGroup.bind(this),n,h,e)}async addGroup(e,t=[],r={},i){let s;await this.server.ldap.canonicalAttributes(r),new RegExp(`^${this.cn}=`,"i").test(e)?(s=/,/.test(e)?e:`${e},${this.base}`,e=e.replace(new RegExp(`^${this.cn}=((?:\\\\.|[^,])+)(?:,.*)?$`,"i"),"$1")):(u(e,this.cn),s=`${this.cn}=${m(e)},${this.base}`);let a,o=this.candidateGroup(e,t,r);await this.ldap.authorize("ldapaddrequest",[s,{...o},i]),await this.validateMembers(s,t,i),await this.validateNewGroup(s,{objectClass:this.config.group_class,cn:e,member:t,...r}),M(this.schema,o),[s,o]=await l(this.registeredHooks.ldapgroupadd,[s,o]);try{a=await this.ldap.add(s,o,i)}catch(e){if(e instanceof q)throw e;throw new Error(`Failed to add group ${s}: ${e}`)}return f(this.registeredHooks.ldapgroupadddone,[s,o]),a}candidateGroup(e,t,r){const i=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||i.includes("twakeGroup")||i.includes("twakeStaticGroup")||i.push("twakeGroup");const s={objectClass:i,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};return t.length&&s.member.push(...t),s}async modifyGroup(e,t,r){await this.server.ldap.canonicalChanges(t);let i=this.toGroupDn(e);const s=this.opNumber();if([i,t]=await l(this.registeredHooks.ldapgroupmodify,[i,t,s]),t.add){if(t.add.member)throw new D("Use dedicated API to add members");if(t.add[this.cn])throw new D(`${this.cn} attribute is unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new D("Use dedicated API to delete members");if(t.delete[this.cn])throw new D(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new D("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new D(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new D("Use dedicated API to replace members");if(t.replace[this.cn])throw new D(`Use dedicated API to change ${this.cn} attribute`)}await this.ldap.authorize("ldapmodifyrequest",[i,t,s,r]),await this.validateChanges(i,t);const a=await this.ldap.modify(i,t,r);return f(this.registeredHooks.ldapgroupmodifydone,[i,t,s]),a}async renameGroup(e,t,r){/,/.test(e)||u(e,this.cn),/,/.test(t)||u(t,this.cn);let i=this.toGroupDn(e),s=this.toGroupDn(t);[i,s]=await l(this.registeredHooks.ldapgrouprename,[i,s]);const a=await this.ldap.rename(i,s,r);return f(this.registeredHooks.ldapgrouprenamedone,[i,s]),a}async deleteGroup(e,t){let r=this.toGroupDn(e);r=await l(this.registeredHooks.ldapgroupdelete,r);const i=await this.ldap.delete(r,t);return f(this.registeredHooks.ldapgroupdeletedone,r),i}async addMember(e,t,r){const i=this.toGroupDn(e);return Array.isArray(t)||(t=[t]),[e,t]=await l(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.ldap.authorize("ldapmodifyrequest",[i,{add:{member:t}},this.opNumber(),r]),await this.validateMembers(i,t,r),await this.ldap.modify(i,{add:{member:t}},r).catch(e=>{if(e instanceof q)throw e;throw new Error(`Failed to add member(s) to ${i}: ${e}`)})}async deleteMember(e,t,r){const i=this.toGroupDn(e);if([e,t]=await l(this.registeredHooks.ldapgroupdeletemember,[e,t]),this.isDummyMember(t))throw new D("Cannot delete dummy member from group");return await this.removeMember(i,t,r).catch(e=>{if(16===o(e))throw new A(`${t} is not a member of ${i}`);if(e instanceof q)throw e;throw new Error(`Failed to delete member ${t} from ${i}: ${e}`)})}async removeMember(e,t,r){return await this.ldap.modify(e,{delete:{member:t}},r).catch(i=>{if(65!==o(i))throw i;const s=this.config.group_dummy_user;if(!s)throw new C(`Cannot remove ${t}, the last member of ${e}: a group needs at least one member`);return this.ldap.modify(e,{add:{member:s},delete:{member:t}},r)})}async deleteMemberFromAll(e){const t=await this.ldap.system.search({filter:`member=${g(e)}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.removeMember(t.dn,e).catch(r=>{const i=o(r);16!==i&&32!==i&&this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r)})))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=this.toGroupDn(e);let n;await this.ldap.authorize("ldapmodifyrequest",[a,{replace:{[i]:t}},this.opNumber(),r]);try{n=await this.ldap.system.search({paged:!1,scope:"base"},a)}catch(e){if(32===o(e))throw new A(`Group ${a} not found`);throw e}if(0===n.searchEntries.length)throw new A(`Group ${a} not found`);const h=n.searchEntries[0],d=w(h[i]);if(!d)throw new D(`Group ${a} does not have ${i} attribute and cannot be moved`);if(d===t)throw new D("Group is already in the target organization");let c;try{c=await this.ldap.system.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){const r=o(e);if(32===r)throw new A(`Target organization ${t} not found`);if(34===r)throw new D(`${t} is not a valid DN`);throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===c.searchEntries.length)throw new A(`Target organization ${t} not found`);const p=c.searchEntries[0][s];if(!p)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:p}},r),this.logger.info(`Group ${a} moved from ${d} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t,req:r}={}){const i=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base,r).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let s={};for await(const e of i)for(const t of e.searchEntries){const e=t[this.cn];if(Array.isArray(e)&&0===e.length)continue;e&&(s[e]=this.withoutDummyMember(t))}return s=await l(this.registeredHooks._ldapgrouplist,s),s}async searchGroupsByName(e,t=!1,r=[this.cn,"member"],i){const s=g(e),a=t?`(${this.cn}=*${s}*)`:`(${this.cn}=${s})`;return await this.listGroups({filter:a,attributes:r,req:i})}toGroupDn(e){if(!/,/.test(e))return`${this.cn}=${m(e)},${this.base}`;if(!b(e))throw new D(`${e} is not a valid DN`);return e}routeGroupDn(e){const t=this.toGroupDn(e),r=this.base,i=y(`${this.cn}=x`).split("=")[0];if(!$(t,r)||y(t)===y(r)||!y(v(t)).split(/(?<!\\)\+/).some(e=>e.startsWith(`${i}=`)))throw new A("Group not found");return t}groupFilter(){return`(|${[...new Set([...this.config.group_class??[],"groupOfNames","groupOfUniqueNames","groupOfURLs","posixGroup"].filter(e=>e&&"top"!==e.toLowerCase()))].map(e=>`(objectClass=${g(e)})`).join("")})`}async judgedGroup(e,t){let r;await t();try{r=await this.ldap.system.search({paged:!1,scope:"base",filter:this.groupFilter(),attributes:["1.1"]},e)}catch(t){const r=o(t);if(32===r)throw new A("Group not found");if(34===r)throw new D(`${e} is not a valid DN`);throw t}if(!r.searchEntries?.length)throw new A("Group not found")}async validateMembers(e,t,r){[e,t]=await l(this.server.hooks.ldapgroupvalidatemembers,[e,t]);const i=Boolean(this.config.groups_allow_unexistent_members);if((!i||r)&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{let t;try{t=await this.ldap.search({paged:!1,scope:"base"},e,r)}catch(t){if(G(t))throw t;if(i)return;const r=o(t);if(32===r)throw new D(`Member ${e} not found`);if(34===r)throw new D(`Member ${e} is not a valid DN`);throw new Error(`Member ${e} not found: ${t}`)}if(r&&!t.searchEntries?.length)throw new D(`Member ${e} not found`)})))}catch(e){if(e instanceof q||G(e))throw e;throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return M(void 0,t),!0;[e,t]=await l(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new D(O(e,k(this.schema,e)?.[1]));const r=x(this.schema,t,this.server.loadedPlugins);if(r)throw new D(`Attribute "${r}" is required`);return M(this.schema,t),!0}async validateChanges(e,t){if(!this.schema)return N(void 0,t),!0;if([e,t]=await l(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))if(!await this._validateOneChange(e,r))throw new D(O(e,k(this.schema,e)?.[1]));if(t.replace)for(const[e,r]of Object.entries(t.replace))if(!await this._validateOneChange(e,r))throw new D(O(e,k(this.schema,e)?.[1]));if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return N(this.schema,t),!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=k(this.schema,e)?.[1];if(!r){if(this.schema.strict)throw new D(`Attribute "${e}" is not allowed`);return!0}if(null==t||Array.isArray(t)&&0===t.length){if(r.required)throw new D(`Attribute "${e}" is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new D(`Attribute "${e}" must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test&&"pointer"!==r.items.type)for(const i of t)if(typeof i!==r.items.type)throw new D(`Attribute "${e}" must be of type ${r.items.type}`)}else if("pointer"!==r.type&&typeof t!==r.type)return!1;if(await z(e,r,t,async e=>(await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},e)).searchEntries.length>0),!F(r,t))throw new D(O(e,r));return!0}getConfigApiData(){const e=this.config.api_prefix||"/api",t=this.server.loadedPlugins.static&&this.config.group_schema?h(this.config,this.config.group_schema):void 0;return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};
1
+ import e from"../../abstract/plugin.js";import{assertModifyRequest as t}from"../../lib/ldapActions.js";import{wantJson as r,jsonBody as i,tryMethod as s,ok as a}from"../../lib/expressFormatedResponses.js";import{extractLdapCode as o}from"../../lib/ldapCodes.js";import{loadSchemaFileAsync as n}from"../../lib/schemaFile.js";import{staticSchemaUrl as h}from"../../lib/staticFiles.js";import{movedToTrash as d,asyncHandler as p,isDummyMemberDn as c,validateDnValue as u,escapeDnValue as m,launchHooksChained as l,launchHooks as g,escapeLdapFilter as f,organizationLink as w,isDnSyntax as b,normalizeDn as y,isDnInBranch as $,getRdn as v,isAuthzRefusal as G,substringSearchFilter as _}from"../../lib/utils.js";import{NotFoundError as D,BadRequestError as q,HttpError as C,ConflictError as j}from"../../lib/errors.js";import{assertClientMaySet as A,modifiedAttributeNames as k,toDirectoryEntry as M}from"../../config/schema.js";import{applyFixedDefaults as E,EntityValidator as N}from"../../lib/entitySchema/validator.js";class LdapGroups extends e{constructor(e){if(super(e),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeletedone:async e=>{const t=Array.isArray(e)?e:[e];this.logger.debug(`User deletion detected, removing from groups: ${t.join(", ")}`),await Promise.all(t.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)})},ldaprenamedone:async([e,t])=>{d(this.config.trash_base,e,t)&&await this.deleteMemberFromAll(e).catch(e=>{this.logger.error("Failed to process a move to the trash in groups:",e)})}},this.ldap=e.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");if(this.config.ldap_group_base&&e.ldap.declareObjectClasses(this.base,this.config.group_class||[]),this.config.group_schema){const e=this.config.group_schema;n(e,{config:this.config}).then(e=>{this.schema=this.adaptSchema(e),this.server.ldap.knowAttributeNames(Object.keys(this.schema.attributes)),this.logger.debug("Group schema loaded")}).catch(t=>this.logger.error(`Failed to load group schema from ${e}: ${t}`))}}adaptSchema(e){return e}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,p(async(e,t)=>{if(!r(e,t))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new q("Invalid match query");if("string"==typeof e.query.attribute&&e.query.attribute)i.filter=_(e.query.match,e.query.attribute);else if(/=/.test(e.query.match)){if(!/^[\w*=()&|, .@'-]+$/.test(e.query.match))throw new q("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=f(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const s=await this.listGroups({...i,req:e});return a(t,this.hideNeverReturn(s))})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,p(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,p(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,p(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,p(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,p(async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)throw new q("cn is required");const a=this.routeGroupDn(r),o=i(e,t,"member");if(!o)return;const n=Array.isArray(o.member)?o.member:[o.member];if(n.some(e=>"string"!=typeof e))throw new q("member must be a string or string array");await this.judgedGroup(a,()=>this.ldap.authorize("ldapmodifyrequest",[a,{add:{member:n}},this.opNumber(),e])),await s(t,this.addMember.bind(this),r,o.member,e)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,p(async(e,t)=>{if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!i||!a)throw new q("cn and member are required");const o=this.routeGroupDn(i);await this.judgedGroup(o,()=>this.ldap.authorize("ldapmodifyrequest",[o,{delete:{member:[a]}},this.opNumber(),e])),await s(t,this.deleteMember.bind(this),i,a,e)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,p(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new q("cn is required");const o=this.routeGroupDn(a),n=i(e,t,"targetOrgDn");if(!n)return;if("string"!=typeof n.targetOrgDn)throw new q("targetOrgDn must be a string");const h=this.config.ldap_organization_link_attribute;await this.judgedGroup(o,()=>this.ldap.authorize("ldapmodifyrequest",[o,{replace:{[h]:n.targetOrgDn}},this.opNumber(),e])),await s(t,this.moveGroup.bind(this),a,n.targetOrgDn,e)})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,p(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new q("cn is required");const o=this.routeGroupDn(a),n=i(e,t,"newCn");if(!n)return;if("string"!=typeof n.newCn)throw new q("newCn must be a string");const h=/,/.test(n.newCn)?this.routeGroupDn(n.newCn):this.toGroupDn(n.newCn);await this.judgedGroup(o,()=>this.ldap.authorize("ldaprenamerequest",[o,h,e])),await s(t,this.renameGroup.bind(this),a,n.newCn,e)}))}isDummyMember(e){return c(e,this.config.group_dummy_user)}withoutDummyMember(e){const t=e.member;if(void 0===t)return e;const r=Array.isArray(t)?t:[t];return{...e,member:r.filter(e=>!this.isDummyMember(e))}}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),s=this.routeGroupDn(i);try{const r=await this.ldap.search({paged:!1,scope:"base",filter:this.groupFilter()},s,e);if(0===r.searchEntries.length)throw new D("Group not found");t.json(this.hideNeverReturn(this.withoutDummyMember(r.searchEntries[0])))}catch(e){if(e.code&&32===e.code)throw new D("Group not found");if(34===o(e))throw new q(`${i} is not a valid DN`);throw e}}async apiAdd(e,t,...r){const a=i(e,t,...r);if(!a)return;await this.server.ldap.canonicalAttributes(a),A(this.schema,Object.keys(a),this.cn);const o=a[this.cn];"string"==typeof o&&(/,/.test(o)||new RegExp(`^${this.cn}=`,"i").test(o))&&this.routeGroupDn(/,/.test(o)?o:`${o},${this.base}`);const n=a.member?Array.isArray(a.member)?a.member:[a.member]:[],h=Object.fromEntries(Object.entries(a).filter(([e,t])=>e!==this.cn&&"member"!==e&&"objectClass"!==e&&void 0!==t));await s(t,this.addGroup.bind(this),o,n,h,e)}async apiDelete(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);if(!i)throw new q("cn is required");const a=this.routeGroupDn(i);await this.judgedGroup(a,()=>this.ldap.authorize("ldapdeleterequest",[a,e])),await s(t,this.deleteGroup.bind(this),i,e)}async apiModify(e,r){const a=i(e,r);if(!a)return;t(a);const o=decodeURIComponent(e.params.cn);if(!o)throw new q("cn is required");const n=this.routeGroupDn(o);await this.server.ldap.canonicalChanges(a),A(this.schema,k(a)),await this.judgedGroup(n,()=>this.ldap.authorize("ldapmodifyrequest",[n,a,this.opNumber(),e])),await s(r,this.modifyGroup.bind(this),n,a,e)}async addGroup(e,t=[],r={},i){let s;await this.server.ldap.canonicalAttributes(r),new RegExp(`^${this.cn}=`,"i").test(e)?(s=/,/.test(e)?e:`${e},${this.base}`,e=e.replace(new RegExp(`^${this.cn}=((?:\\\\.|[^,])+)(?:,.*)?$`,"i"),"$1")):(u(e,this.cn),s=`${this.cn}=${m(e)},${this.base}`);let a=this.candidateGroup(e,t,r);await this.ldap.authorize("ldapaddrequest",[s,{...a},i]),await this.validateMembers(s,t,i);const o=await this.validateNewGroup(s,{objectClass:a.objectClass,[this.cn]:e,member:t,...r});let n;delete o.member,Object.assign(a,o),M(this.schema,a),[s,a]=await l(this.registeredHooks.ldapgroupadd,[s,a]);try{n=await this.ldap.add(s,a,i)}catch(e){if(e instanceof C)throw e;throw new Error(`Failed to add group ${s}: ${e}`)}return g(this.registeredHooks.ldapgroupadddone,[s,a]),n}candidateGroup(e,t,r){const i=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||i.includes("twakeGroup")||i.includes("twakeStaticGroup")||i.push("twakeGroup");const s={objectClass:i,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};return t.length&&s.member.push(...t),E(this.schema,s),s}async modifyGroup(e,t,r){await this.server.ldap.canonicalChanges(t);let i=this.toGroupDn(e);const s=this.opNumber();if([i,t]=await l(this.registeredHooks.ldapgroupmodify,[i,t,s]),t.add){if(t.add.member)throw new q("Use dedicated API to add members");if(t.add[this.cn])throw new q(`${this.cn} attribute is unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new q("Use dedicated API to delete members");if(t.delete[this.cn])throw new q(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new q("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new q(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new q("Use dedicated API to replace members");if(t.replace[this.cn])throw new q(`Use dedicated API to change ${this.cn} attribute`)}await this.ldap.authorize("ldapmodifyrequest",[i,t,s,r]),t=await this.validateChanges(i,t);const a=await this.ldap.modify(i,t,r);return g(this.registeredHooks.ldapgroupmodifydone,[i,t,s]),a}async renameGroup(e,t,r){/,/.test(e)||u(e,this.cn),/,/.test(t)||u(t,this.cn);let i=this.toGroupDn(e),s=this.toGroupDn(t);[i,s]=await l(this.registeredHooks.ldapgrouprename,[i,s]);const a=await this.ldap.rename(i,s,r);return g(this.registeredHooks.ldapgrouprenamedone,[i,s]),a}async deleteGroup(e,t){let r=this.toGroupDn(e);r=await l(this.registeredHooks.ldapgroupdelete,r);const i=await this.ldap.delete(r,t);return g(this.registeredHooks.ldapgroupdeletedone,r),i}async addMember(e,t,r){const i=this.toGroupDn(e);return Array.isArray(t)||(t=[t]),[e,t]=await l(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.ldap.authorize("ldapmodifyrequest",[i,{add:{member:t}},this.opNumber(),r]),await this.validateMembers(i,t,r),await this.ldap.modify(i,{add:{member:t}},r).catch(e=>{if(e instanceof C)throw e;throw new Error(`Failed to add member(s) to ${i}: ${e}`)})}async deleteMember(e,t,r){const i=this.toGroupDn(e);if([e,t]=await l(this.registeredHooks.ldapgroupdeletemember,[e,t]),this.isDummyMember(t))throw new q("Cannot delete dummy member from group");return await this.removeMember(i,t,r).catch(e=>{if(16===o(e))throw new D(`${t} is not a member of ${i}`);if(e instanceof C)throw e;throw new Error(`Failed to delete member ${t} from ${i}: ${e}`)})}async removeMember(e,t,r){return await this.ldap.modify(e,{delete:{member:t}},r).catch(i=>{if(65!==o(i))throw i;const s=this.config.group_dummy_user;if(!s)throw new j(`Cannot remove ${t}, the last member of ${e}: a group needs at least one member`);return this.ldap.modify(e,{add:{member:s},delete:{member:t}},r)})}async deleteMemberFromAll(e){const t=await this.ldap.system.search({filter:`member=${f(e)}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.removeMember(t.dn,e).catch(r=>{const i=o(r);16!==i&&32!==i&&this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r)})))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=this.toGroupDn(e);let n;await this.ldap.authorize("ldapmodifyrequest",[a,{replace:{[i]:t}},this.opNumber(),r]);try{n=await this.ldap.system.search({paged:!1,scope:"base"},a)}catch(e){if(32===o(e))throw new D(`Group ${a} not found`);throw e}if(0===n.searchEntries.length)throw new D(`Group ${a} not found`);const h=n.searchEntries[0],d=w(h[i]);if(!d)throw new q(`Group ${a} does not have ${i} attribute and cannot be moved`);if(d===t)throw new q("Group is already in the target organization");let p;try{p=await this.ldap.system.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){const r=o(e);if(32===r)throw new D(`Target organization ${t} not found`);if(34===r)throw new q(`${t} is not a valid DN`);throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===p.searchEntries.length)throw new D(`Target organization ${t} not found`);const c=p.searchEntries[0][s];if(!c)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:c}},r),this.logger.info(`Group ${a} moved from ${d} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t,req:r}={}){const i=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base,r).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let s={};for await(const e of i)for(const t of e.searchEntries){const e=t[this.cn];if(Array.isArray(e)&&0===e.length)continue;e&&(s[e]=this.withoutDummyMember(t))}return s=await l(this.registeredHooks._ldapgrouplist,s),s}async searchGroupsByName(e,t=!1,r=[this.cn,"member"],i){const s=f(e),a=t?`(${this.cn}=*${s}*)`:`(${this.cn}=${s})`;return await this.listGroups({filter:a,attributes:r,req:i})}toGroupDn(e){if(!/,/.test(e))return`${this.cn}=${m(e)},${this.base}`;if(!b(e))throw new q(`${e} is not a valid DN`);return e}routeGroupDn(e){const t=this.toGroupDn(e),r=this.base,i=y(`${this.cn}=x`).split("=")[0];if(!$(t,r)||y(t)===y(r)||!y(v(t)).split(/(?<!\\)\+/).some(e=>e.startsWith(`${i}=`)))throw new D("Group not found");return t}groupFilter(){return`(|${[...new Set([...this.config.group_class??[],"groupOfNames","groupOfUniqueNames","groupOfURLs","posixGroup"].filter(e=>e&&"top"!==e.toLowerCase()))].map(e=>`(objectClass=${f(e)})`).join("")})`}async judgedGroup(e,t){let r;await t();try{r=await this.ldap.system.search({paged:!1,scope:"base",filter:this.groupFilter(),attributes:["1.1"]},e)}catch(t){const r=o(t);if(32===r)throw new D("Group not found");if(34===r)throw new q(`${e} is not a valid DN`);throw t}if(!r.searchEntries?.length)throw new D("Group not found")}async validateMembers(e,t,r){[e,t]=await l(this.server.hooks.ldapgroupvalidatemembers,[e,t]);const i=Boolean(this.config.groups_allow_unexistent_members);if((!i||r)&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{let t;try{t=await this.ldap.search({paged:!1,scope:"base"},e,r)}catch(t){if(G(t))throw t;if(i)return;const r=o(t);if(32===r)throw new q(`Member ${e} not found`);if(34===r)throw new q(`Member ${e} is not a valid DN`);throw new Error(`Member ${e} not found: ${t}`)}if(r&&!t.searchEntries?.length)throw new q(`Member ${e} not found`)})))}catch(e){if(e instanceof C||G(e))throw e;throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){return[e,t]=await l(this.server.hooks.ldapgroupvalidatenew,[e,t]),await this.validator().validateNew(t),t}async validateChanges(e,t){return[e,t]=await l(this.server.hooks.ldapgroupvalidatechanges,[e,t]),await this.validator().validateChanges(t),t}validator(){return new N(this.schema,{entryExists:async e=>{try{return(await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},e)).searchEntries.length>0}catch{return!1}},loadedPlugins:this.server.loadedPlugins,singularName:"group"})}getConfigApiData(){const e=this.config.api_prefix||"/api",t=this.config.group_schema?h(this.server,this.config.group_schema):void 0;return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};
@@ -1 +1 @@
1
- import t from"../../abstract/plugin.js";import{assertModifyRequest as i}from"../../lib/ldapActions.js";import{tryMethodData as a,jsonBody as e,tryMethod as n,wantJson as s}from"../../lib/expressFormatedResponses.js";import{asyncHandler as r,validateDnValue as o,escapeDnValue as h,getRdn as c,isDnInBranch as d,rdnValue as l,normalizeDn as g,getParentDn as p,escapeLdapFilter as u,launchHooksChained as f,isAuthzRefusal as w,isChildOf as m,substringSearchFilter as z}from"../../lib/utils.js";import{BadRequestError as y,NotFoundError as b,ConflictError as $,HttpError as v}from"../../lib/errors.js";import{extractLdapCode as _}from"../../lib/ldapCodes.js";import{loadSchemaFileAsync as O,schemaUrl as A}from"../../lib/schemaFile.js";import{assertClientMaySet as C,modifiedAttributeNames as E,toDirectoryEntry as L,missingRequiredAttribute as k,invalidValueMessage as j,schemaAttribute as S,toDirectoryChanges as q,checkDnValues as x,matchesPattern as D}from"../../config/schema.js";class LdapOrganizations extends t{constructor(t){if(super(t),this.name="ldapOrganizations",this.roles=["api","consistency","configurable"],this.hooks={ldapaddrequest:async([t,i,a])=>(this.isOu(i)||await this.checkDeptLink(i),this.isOu(i)&&await this.checkDeptPath(i,t),void 0!==a?[t,i,a]:[t,i]),ldapmodifyrequest:async([t,i,a,e])=>{let n,s={},r={};const checkIsOu=async()=>{if(void 0!==n)return n;if(i.replace?.objectClass)n=this.isOu({objectClass:i.replace.objectClass});else if(i.add?.objectClass)n=this.isOu({objectClass:i.add.objectClass});else{const i=await this.server.ldap.system.search({paged:!1,scope:"base"},t);n=i.searchEntries.length>0&&this.isOu(i.searchEntries[0])}return n};if(i.delete){const t=(Array.isArray(i.delete)?i.delete.map(String):Object.keys(i.delete)).map(t=>t.split(";")[0].toLowerCase()),a=t.includes(this.linkAttr.toLowerCase()),e=t.includes(this.pathAttr.toLowerCase());if(a||e){if(!await checkIsOu()&&a)throw new y("An organization link cannot be deleted");if(e)throw new y("An organization path cannot be deleted")}}if(i.replace&&(i.replace[this.linkAttr]&&(s={...i.replace}),i.replace[this.pathAttr]&&(r={...i.replace})),i.add&&(i.add[this.linkAttr]&&(s={...s,...i.add}),i.add[this.pathAttr]&&(r={...r,...i.add})),Object.keys(s).length>0){await checkIsOu()||await this.checkDeptLink(s)}if(Object.keys(r).length>0){await checkIsOu()&&await this.checkDeptPath(r,t)}return[t,i,a,e]},ldapdeleterequest:async([t,i])=>{const a=Array.isArray(t)?t:[t];for(const t of a)/^\s*ou\s*=/i.test(t)&&await this.isEmptyOrganization(t);return[t,i]},ldaprenamerequest:([t,i,a])=>[t,i,a]},this.sizeLimitWarned=new Set,!this.config.ldap_top_organization)throw new Error("Missing --ldap-top-organization");if(this.pathAttr=this.config.ldap_organization_path_attribute,this.linkAttr=this.config.ldap_organization_link_attribute,t.ldap.declareObjectClasses(this.config.ldap_top_organization,this.config.ldap_organization_class||[],!0),this.config.organization_schema){const t=this.config.organization_schema;O(t,{config:this.config}).then(t=>{this.schema=t,this.server.ldap.knowAttributeNames(Object.keys(t.attributes)),this.logger.debug("Organization schema loaded")}).catch(i=>this.logger.error(`Failed to load organization schema from ${t}: ${i}`))}}api(t){t.get(`${this.config.api_prefix}/v1/ldap/organizations/top`,async(t,i)=>{await a(i,async()=>this.hideNeverReturn(await this.getOrganisationTop(t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,async()=>this.hideNeverReturn(await this.getOrganisationByDn(e,t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,async()=>this.hideNeverReturn(await this.getOrganisationSubnodes(e,t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes/search`,r(async(t,i)=>{const e=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)throw new y('query parameter "q" is required');await a(i,async()=>this.hideNeverReturn(await this.searchOrganisationSubnodes(e,n,t)))})),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/search`,r(async(t,i)=>{const e=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)throw new y('query parameter "q" is required');await a(i,async()=>this.hideNeverReturn(await this.searchOrganisationTree(e,n,t)))})),t.post(`${this.config.api_prefix}/v1/ldap/organizations`,r(async(t,i)=>this.apiAdd(t,i))),t.put(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,r(async(t,i)=>this.apiModify(t,i))),t.delete(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,r(async(t,i)=>this.apiDelete(t,i))),t.post(`${this.config.api_prefix}/v1/ldap/organizations/:dn/move`,r(async(t,i)=>this.apiMove(t,i)))}async apiAdd(t,i){const a=e(t,i,"ou");if(!a)return;C(this.schema,Object.keys(a),"ou"),o(a.ou,"ou");const s=a.parentDn||this.config.ldap_top_organization,r=`ou=${h(a.ou)},${s}`,c={objectClass:this.config.ldap_organization_class,ou:a.ou,...Object.fromEntries(Object.entries(a).filter(([t])=>"ou"!==t&&"parentDn"!==t))};await n(i,this.addOrganization.bind(this),r,c,t)}async apiModify(t,a){const s=e(t,a);if(!s)return;i(s);const r=decodeURIComponent(t.params.dn);if(!r)throw new y("dn is required");C(this.schema,E(s)),await n(a,this.modifyOrganization.bind(this),r,s,t)}async apiDelete(t,i){if(!s(t,i))return;const a=decodeURIComponent(t.params.dn);if(!a)throw new y("dn is required");await n(i,this.deleteOrganization.bind(this),a,t)}async apiMove(t,i){if(!s(t,i))return;const n=e(t,i,"targetOrgDn");if(!n)return;const r=decodeURIComponent(t.params.dn);if(!r)throw new y("dn is required");const{targetOrgDn:o}=n;if(!o||"string"!=typeof o)throw new y("Missing or invalid targetOrgDn in request body");await a(i,this.moveOrganization.bind(this),r,o,t)}async checkDeptLink(t){if(t[this.linkAttr]){const i=t[this.linkAttr],a=Array.isArray(i)?i[0]:i,e=await this.server.ldap.system.search({paged:!1,scope:"base"},a);if(0===e.searchEntries.length)throw new b(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new y(`Entry ${a} isn't in top organization branch`)}}async checkDeptPath(t,i){if(t[this.pathAttr]){const a=t[this.pathAttr],e=Array.isArray(a)?a[0]:a,n=this.config.ldap_organization_path_separator||" / ",s=this.config.ldap_top_organization||"",r=i&&s&&/^ou=/i.test(c(i))&&d(i,s)?l(i):void 0,o=t.ou,h=(Array.isArray(o)?o[0]:o)||r;let f=e;if((this.isOu(t)||void 0!==r)&&h){const t=s?l(s):"";if(t&&e.startsWith(h+n)&&e.endsWith(n+t))return;if(e===h){const t=s?g(s):"";if(!i||!t||!d(i,s)||g(i)===t||g(p(i))===t)return;throw new y(`Organization path "${e}" names no parent, but ${i} is not directly under ${s}`)}if(!e.endsWith(n+h))throw new y(`Organization path must end with its own name, preceded by separator "${n}"`);f=e.slice(0,e.length-n.length-h.length)}const w=await this.server.ldap.system.search({paged:!1,scope:"sub",filter:`(${this.pathAttr}=${u(f)})`,attributes:["dn"]},this.config.ldap_top_organization);if(!w.searchEntries||0===w.searchEntries.length)throw new y(`Invalid organization path ${e}: no organization holds "${f}"`)}}async isEmptyOrganization(t){if((await this.server.ldap.system.search({paged:!1,filter:`(${this.config.ldap_organization_link_attribute}=${u(t)})`})).searchEntries.length>0)throw new $(`Organization ${t} is not empty`)}isOu(t){if(!t.objectClass)return!1;const i=[].concat(t.objectClass).map(t=>String(t).toLowerCase());return this.config.ldap_organization_class.filter(t=>"top"!==t.toLowerCase()).some(t=>i.includes(t.toLowerCase()))}async getOrganisationTop(t){if(!this.config.ldap_top_organization)throw new y("No top organization configured");const i=await this.server.ldap.search({paged:!1,scope:"base"},this.config.ldap_top_organization,t);if(1!==i.searchEntries.length)throw new b("Top organization not found");const[,a]=await f(this.registeredHooks.getOrganisationTop,[t,i.searchEntries[0]]);return a}async getOrganisationByDn(t,i){let a;try{a=await this.server.ldap.search({paged:!1,scope:"base",filter:"(objectClass=organizationalUnit)"},t,i)}catch(i){const a=_(i);if(32===a)throw new b(`Organization ${t} not found`);if(34===a)throw new y(`${t} is not a valid DN`);throw i}if(1!==a.searchEntries.length)throw new b(`Organization ${t} not found`);return a.searchEntries[0]}async getOrganisationSubnodes(t,i){const a=[],e=this.config.ldap_organization_max_subnodes||50,n=i?.query?.objectClass;n&&"organizationalUnit"!==n||a.push(...await this.childOrganizations(t,"(objectClass=organizationalUnit)",e,i));const s=this.config.ldap_top_organization,r=p(s);let o=`(${this.config.ldap_organization_link_attribute}=${u(t)})`;return n&&(o=`(&${o}(objectClass=${u(n)}))`),this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${r}`),a.push(...await this.linkedEntities(r,o,e,t,i)),a}warnSizeLimit(t,i){this.sizeLimitWarned.has(t)?this.server.logger.debug(i):(this.sizeLimitWarned.size<LdapOrganizations.SIZE_LIMIT_WARNED_MAX&&this.sizeLimitWarned.add(t),this.server.logger.warn(i))}static moreIndicator(t,i,a,e){return{dn:t,cn:[i],objectClass:["moreIndicator"],_isMoreIndicator:"true",...void 0===e?{}:{_totalCount:e.toString()},_displayedCount:a.toString()}}async childOrganizations(t,i,a,e){try{const a=await this.server.ldap.search({paged:!0,scope:"one",filter:i},t,e),n=[];for await(const t of a)n.push(...t.searchEntries);return this.server.logger.debug(`Found ${n.length} sub-OUs for ${t}`),n}catch(i){const a=_(i);if(32===a)return this.server.logger.debug(`No sub-OUs for ${t}: the node holds none`),[];if(34===a)throw new y(`${t} is not a valid DN`);if(4!==a)throw i}const n=(await this.server.ldap.search({paged:!1,scope:"one",filter:i,sizeLimit:a+1},t,e)).searchEntries.slice(0,a);return this.warnSizeLimit(`children:${t}`,`Organization ${t} holds more child organizations than the directory will list in one answer, so ${n.length} are returned and the rest are hidden. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to list them all.`),[...n,LdapOrganizations.moreIndicator(`more-organizations-${t}`,"... more organizations than the directory will list",n.length)]}async linkedEntities(t,i,a,e,n){const s=[];let r=0;try{const e=await this.server.ldap.search({paged:!0,filter:i},t,n);for await(const t of e){r+=t.searchEntries.length;const i=a-s.length;i>0&&s.push(...t.searchEntries.slice(0,i))}}catch(s){if(t!==e&&(w(s)||403===s?.statusCode))return this.linkedEntities(e,i,a,e,n);if(4!==_(s))throw s;const r=await this.server.ldap.search({paged:!1,filter:i,sizeLimit:a+1},t,n);this.warnSizeLimit(`linked:${e}`,`More entries are attached to ${e} than the directory will list in one answer. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to count them all.`);const o=r.searchEntries.slice(0,a);return[...o,LdapOrganizations.moreIndicator(`more-${e}`,"... more elements than the directory will list",o.length)]}return this.server.logger.debug(`Found ${r} linked entities for ${e}, returning ${s.length}`),r<=a?s:[...s,LdapOrganizations.moreIndicator(`more-${e}`,`... ${r-a} more ${r-a===1?"element":"elements"}`,a,r)]}async addOrganization(t,i,a){return await this.server.ldap.authorize("ldapaddrequest",[t,i,a]),await this.validateNewOrganization(t,i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i,a){return await this.server.ldap.authorize("ldapmodifyrequest",[t,i,this.opNumber(),a]),await this.validateChanges(t,i),await this.server.ldap.modify(t,i,a)}async validateNewOrganization(t,i){if(!this.schema)return L(void 0,i),!0;for(const[t,a]of Object.entries(i))if(!await this._validateOneChange(t,a))throw new y(this.invalidValueMessage(t));const a=k(this.schema,i,this.server.loadedPlugins);if(a)throw new y(`Attribute "${a}" is required`);return L(this.schema,i),!0}invalidValueMessage(t){return j(t,S(this.schema,t)?.[1])}async validateChanges(t,i){if(!this.schema)return q(void 0,i),!0;if(i.add)for(const[t,a]of Object.entries(i.add))if(!await this._validateOneChange(t,a))throw new y(this.invalidValueMessage(t));if(i.replace)for(const[t,a]of Object.entries(i.replace))if(!await this._validateOneChange(t,a))throw new y(this.invalidValueMessage(t));return q(this.schema,i),!0}async _validateOneChange(t,i){if(!this.schema)return!0;const a=S(this.schema,t)?.[1];if(!a){if(this.schema.strict)throw new y(`Attribute "${t}" is not allowed`);return!0}if(null==i||Array.isArray(i)&&0===i.length){if(a.required)throw new y(`Attribute "${t}" is required`);return!0}if("string"===a.type&&Array.isArray(i))throw new y(`Attribute "${t}" must be a single value`);return await x(t,a,i,t=>this.entryExists(t)),D(a,i)}async entryExists(t){try{return(await this.server.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}}async moveOrganization(t,i,a){const e=`${c(t)},${i}`;let n,s;await this.server.ldap.authorize("ldaprenamerequest",[t,e,a]);try{n=await this.server.ldap.system.search({paged:!1,scope:"base"},i)}catch(t){throw new b(`Target organization ${i} not found: ${t instanceof Error?t.message:String(t)}`)}if(!n.searchEntries||0===n.searchEntries.length)throw new b(`Target organization ${i} not found`);if(!this.isOu(n.searchEntries[0]))throw new y(`Target ${i} is not an organizational unit`);try{s=await this.server.ldap.system.search({paged:!1,scope:"base"},t)}catch(t){throw new b(`Source organization not found: ${t}`)}if(!s.searchEntries||0===s.searchEntries.length)throw new b(`Source organization ${t} not found`);if(e===t)throw new y("Cannot move organization to its current location");if(i.toLowerCase()===t.toLowerCase()||m(i,t))throw new y("Cannot move organization into itself or its descendant");try{await this.server.ldap.rename(t,e,a),this.logger.info(`Moved organization from ${t} to ${e}`)}catch(t){if(t instanceof v)throw t;throw new Error(`Failed to move organization: ${t}`)}return{newDn:e}}async deleteOrganization(t,i){try{return await this.server.ldap.delete(t,i)}catch(i){if(32===_(i))throw new b(`Organization ${t} not found`);throw i}}async searchOrganisationSubnodes(t,i,a){const e=[],n=this.config.ldap_organization_max_subnodes||50;e.push(...await this.childOrganizations(t,`(&(objectClass=organizationalUnit)${z(i,"ou,description")})`,n,a));const s=this.config.ldap_top_organization,r=p(s),o=u(t),h=this.config.display_name_attribute||"displayName",c=`(&(${this.config.ldap_organization_link_attribute}=${o})${z(i,`uid,cn,mail,sn,givenName,${h}`)})`;return this.server.logger.debug(`Searching for linked entities with filter: ${c} in ${r}`),e.push(...await this.linkedEntities(r,c,n,t,a)),e}async searchOrganisationTree(t,i,a){const e=this.config.ldap_organization_max_subnodes||50,n=this.pathAttr?`ou,description,${this.pathAttr}`:"ou,description",s=`(&(objectClass=organizationalUnit)${z(i,n)})`,r=[];let o=0;try{const i=await this.server.ldap.search({paged:!0,scope:"sub",filter:s},t,a);for await(const t of i){o+=t.searchEntries.length;const i=e-r.length;i>0&&r.push(...t.searchEntries.slice(0,i))}}catch(i){const n=_(i);if(32===n)return this.server.logger.debug(`No organizations under ${t}: no such node`),[];if(34===n)throw new y(`${t} is not a valid DN`);if(4!==n)throw i;const r=(await this.server.ldap.search({paged:!1,scope:"sub",filter:s,sizeLimit:e+1},t,a)).searchEntries.slice(0,e);return this.warnSizeLimit(`search:${t}`,`A search under ${t} matched more organizations than the directory will list in one answer, so ${r.length} are returned and the rest are hidden. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to search them all.`),[...r,LdapOrganizations.moreIndicator(`more-organizations-${t}`,"... more organizations than the directory will list",r.length)]}if(o<=e)return r;const h=o-e;return[...r,LdapOrganizations.moreIndicator(`more-organizations-${t}`,`... ${h} more ${1===h?"organization":"organizations"}, narrow the search`,e,o)]}getConfigApiData(){const t=this.config.api_prefix||"/api",i=this.server.loadedPlugins.static&&this.config.organization_schema?A(this.config,this.config.organization_schema):void 0;return{enabled:!0,topOrganization:this.config.ldap_top_organization||"",organizationClass:this.config.ldap_organization_class||["top","organizationalUnit"],linkAttribute:this.linkAttr||"",pathAttribute:this.pathAttr||"",pathSeparator:this.config.ldap_organization_path_separator||" / ",maxSubnodes:this.config.ldap_organization_max_subnodes||50,schema:this.schema,schemaUrl:i,endpoints:{getTop:`${t}/v1/ldap/organizations/top`,get:`${t}/v1/ldap/organizations/:dn`,getSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes`,searchSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes/search`,search:`${t}/v1/ldap/organizations/:dn/search`,create:`${t}/v1/ldap/organizations`,update:`${t}/v1/ldap/organizations/:dn`,move:`${t}/v1/ldap/organizations/:dn/move`,delete:`${t}/v1/ldap/organizations/:dn`}}}}LdapOrganizations.SIZE_LIMIT_WARNED_MAX=500;export{LdapOrganizations as default};
1
+ import t from"../../abstract/plugin.js";import{assertModifyRequest as i}from"../../lib/ldapActions.js";import{tryMethodData as a,jsonBody as e,tryMethod as n,wantJson as s}from"../../lib/expressFormatedResponses.js";import{asyncHandler as r,validateDnValue as o,escapeDnValue as h,getRdn as c,isDnInBranch as d,rdnValue as l,normalizeDn as g,getParentDn as p,escapeLdapFilter as u,launchHooksChained as f,isAuthzRefusal as w,isChildOf as m,substringSearchFilter as z}from"../../lib/utils.js";import{BadRequestError as y,NotFoundError as b,ConflictError as $,HttpError as _}from"../../lib/errors.js";import{extractLdapCode as v}from"../../lib/ldapCodes.js";import{loadSchemaFileAsync as O}from"../../lib/schemaFile.js";import{staticSchemaUrl as E}from"../../lib/staticFiles.js";import{assertClientMaySet as A,modifiedAttributeNames as C}from"../../config/schema.js";import{applyFixedDefaults as L,EntityValidator as k}from"../../lib/entitySchema/validator.js";class LdapOrganizations extends t{constructor(t){if(super(t),this.name="ldapOrganizations",this.roles=["api","consistency","configurable"],this.hooks={ldapaddrequest:async([t,i,a])=>(this.isOu(i)||await this.checkDeptLink(i),this.isOu(i)&&await this.checkDeptPath(i,t),void 0!==a?[t,i,a]:[t,i]),ldapmodifyrequest:async([t,i,a,e])=>{let n,s={},r={};const checkIsOu=async()=>{if(void 0!==n)return n;if(i.replace?.objectClass)n=this.isOu({objectClass:i.replace.objectClass});else if(i.add?.objectClass)n=this.isOu({objectClass:i.add.objectClass});else{const i=await this.server.ldap.system.search({paged:!1,scope:"base"},t);n=i.searchEntries.length>0&&this.isOu(i.searchEntries[0])}return n};if(i.delete){const t=(Array.isArray(i.delete)?i.delete.map(String):Object.keys(i.delete)).map(t=>t.split(";")[0].toLowerCase()),a=t.includes(this.linkAttr.toLowerCase()),e=t.includes(this.pathAttr.toLowerCase());if(a||e){if(!await checkIsOu()&&a)throw new y("An organization link cannot be deleted");if(e)throw new y("An organization path cannot be deleted")}}if(i.replace&&(i.replace[this.linkAttr]&&(s={...i.replace}),i.replace[this.pathAttr]&&(r={...i.replace})),i.add&&(i.add[this.linkAttr]&&(s={...s,...i.add}),i.add[this.pathAttr]&&(r={...r,...i.add})),Object.keys(s).length>0){await checkIsOu()||await this.checkDeptLink(s)}if(Object.keys(r).length>0){await checkIsOu()&&await this.checkDeptPath(r,t)}return[t,i,a,e]},ldapdeleterequest:async([t,i])=>{const a=Array.isArray(t)?t:[t];for(const t of a)/^\s*ou\s*=/i.test(t)&&await this.isEmptyOrganization(t);return[t,i]},ldaprenamerequest:([t,i,a])=>[t,i,a]},this.sizeLimitWarned=new Set,!this.config.ldap_top_organization)throw new Error("Missing --ldap-top-organization");if(this.pathAttr=this.config.ldap_organization_path_attribute,this.linkAttr=this.config.ldap_organization_link_attribute,t.ldap.declareObjectClasses(this.config.ldap_top_organization,this.config.ldap_organization_class||[],!0),this.config.organization_schema){const t=this.config.organization_schema;O(t,{config:this.config}).then(t=>{this.schema=t,this.server.ldap.knowAttributeNames(Object.keys(t.attributes)),this.logger.debug("Organization schema loaded")}).catch(i=>this.logger.error(`Failed to load organization schema from ${t}: ${i}`))}}api(t){t.get(`${this.config.api_prefix}/v1/ldap/organizations/top`,async(t,i)=>{await a(i,async()=>this.hideNeverReturn(await this.getOrganisationTop(t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,async()=>this.hideNeverReturn(await this.getOrganisationByDn(e,t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,async()=>this.hideNeverReturn(await this.getOrganisationSubnodes(e,t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes/search`,r(async(t,i)=>{const e=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)throw new y('query parameter "q" is required');await a(i,async()=>this.hideNeverReturn(await this.searchOrganisationSubnodes(e,n,t)))})),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/search`,r(async(t,i)=>{const e=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)throw new y('query parameter "q" is required');await a(i,async()=>this.hideNeverReturn(await this.searchOrganisationTree(e,n,t)))})),t.post(`${this.config.api_prefix}/v1/ldap/organizations`,r(async(t,i)=>this.apiAdd(t,i))),t.put(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,r(async(t,i)=>this.apiModify(t,i))),t.delete(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,r(async(t,i)=>this.apiDelete(t,i))),t.post(`${this.config.api_prefix}/v1/ldap/organizations/:dn/move`,r(async(t,i)=>this.apiMove(t,i)))}async apiAdd(t,i){const a=e(t,i,"ou");if(!a)return;A(this.schema,Object.keys(a),"ou"),o(a.ou,"ou");const s=a.parentDn||this.config.ldap_top_organization,r=`ou=${h(a.ou)},${s}`,c={ou:a.ou,...Object.fromEntries(Object.entries(a).filter(([t])=>"ou"!==t&&"parentDn"!==t)),objectClass:this.config.ldap_organization_class};await n(i,this.addOrganization.bind(this),r,c,t)}async apiModify(t,a){const s=e(t,a);if(!s)return;i(s);const r=decodeURIComponent(t.params.dn);if(!r)throw new y("dn is required");A(this.schema,C(s)),await n(a,this.modifyOrganization.bind(this),r,s,t)}async apiDelete(t,i){if(!s(t,i))return;const a=decodeURIComponent(t.params.dn);if(!a)throw new y("dn is required");await n(i,this.deleteOrganization.bind(this),a,t)}async apiMove(t,i){if(!s(t,i))return;const n=e(t,i,"targetOrgDn");if(!n)return;const r=decodeURIComponent(t.params.dn);if(!r)throw new y("dn is required");const{targetOrgDn:o}=n;if(!o||"string"!=typeof o)throw new y("Missing or invalid targetOrgDn in request body");await a(i,this.moveOrganization.bind(this),r,o,t)}async checkDeptLink(t){if(t[this.linkAttr]){const i=t[this.linkAttr],a=Array.isArray(i)?i[0]:i,e=await this.server.ldap.system.search({paged:!1,scope:"base"},a);if(0===e.searchEntries.length)throw new b(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new y(`Entry ${a} isn't in top organization branch`)}}async checkDeptPath(t,i){if(t[this.pathAttr]){const a=t[this.pathAttr],e=Array.isArray(a)?a[0]:a,n=this.config.ldap_organization_path_separator||" / ",s=this.config.ldap_top_organization||"",r=i&&s&&/^ou=/i.test(c(i))&&d(i,s)?l(i):void 0,o=t.ou,h=(Array.isArray(o)?o[0]:o)||r;let f=e;if((this.isOu(t)||void 0!==r)&&h){const t=s?l(s):"";if(t&&e.startsWith(h+n)&&e.endsWith(n+t))return;if(e===h){const t=s?g(s):"";if(!i||!t||!d(i,s)||g(i)===t||g(p(i))===t)return;throw new y(`Organization path "${e}" names no parent, but ${i} is not directly under ${s}`)}if(!e.endsWith(n+h))throw new y(`Organization path must end with its own name, preceded by separator "${n}"`);f=e.slice(0,e.length-n.length-h.length)}const w=await this.server.ldap.system.search({paged:!1,scope:"sub",filter:`(${this.pathAttr}=${u(f)})`,attributes:["dn"]},this.config.ldap_top_organization);if(!w.searchEntries||0===w.searchEntries.length)throw new y(`Invalid organization path ${e}: no organization holds "${f}"`)}}async isEmptyOrganization(t){if((await this.server.ldap.system.search({paged:!1,filter:`(${this.config.ldap_organization_link_attribute}=${u(t)})`})).searchEntries.length>0)throw new $(`Organization ${t} is not empty`)}isOu(t){if(!t.objectClass)return!1;const i=[].concat(t.objectClass).map(t=>String(t).toLowerCase());return this.config.ldap_organization_class.filter(t=>"top"!==t.toLowerCase()).some(t=>i.includes(t.toLowerCase()))}async getOrganisationTop(t){if(!this.config.ldap_top_organization)throw new y("No top organization configured");const i=await this.server.ldap.search({paged:!1,scope:"base"},this.config.ldap_top_organization,t);if(1!==i.searchEntries.length)throw new b("Top organization not found");const[,a]=await f(this.registeredHooks.getOrganisationTop,[t,i.searchEntries[0]]);return a}async getOrganisationByDn(t,i){let a;try{a=await this.server.ldap.search({paged:!1,scope:"base",filter:"(objectClass=organizationalUnit)"},t,i)}catch(i){const a=v(i);if(32===a)throw new b(`Organization ${t} not found`);if(34===a)throw new y(`${t} is not a valid DN`);throw i}if(1!==a.searchEntries.length)throw new b(`Organization ${t} not found`);return a.searchEntries[0]}async getOrganisationSubnodes(t,i){const a=[],e=this.config.ldap_organization_max_subnodes||50,n=i?.query?.objectClass;n&&"organizationalUnit"!==n||a.push(...await this.childOrganizations(t,"(objectClass=organizationalUnit)",e,i));const s=this.config.ldap_top_organization,r=p(s);let o=`(${this.config.ldap_organization_link_attribute}=${u(t)})`;return n&&(o=`(&${o}(objectClass=${u(n)}))`),this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${r}`),a.push(...await this.linkedEntities(r,o,e,t,i)),a}warnSizeLimit(t,i){this.sizeLimitWarned.has(t)?this.server.logger.debug(i):(this.sizeLimitWarned.size<LdapOrganizations.SIZE_LIMIT_WARNED_MAX&&this.sizeLimitWarned.add(t),this.server.logger.warn(i))}static moreIndicator(t,i,a,e){return{dn:t,cn:[i],objectClass:["moreIndicator"],_isMoreIndicator:"true",...void 0===e?{}:{_totalCount:e.toString()},_displayedCount:a.toString()}}async childOrganizations(t,i,a,e){try{const a=await this.server.ldap.search({paged:!0,scope:"one",filter:i},t,e),n=[];for await(const t of a)n.push(...t.searchEntries);return this.server.logger.debug(`Found ${n.length} sub-OUs for ${t}`),n}catch(i){const a=v(i);if(32===a)return this.server.logger.debug(`No sub-OUs for ${t}: the node holds none`),[];if(34===a)throw new y(`${t} is not a valid DN`);if(4!==a)throw i}const n=(await this.server.ldap.search({paged:!1,scope:"one",filter:i,sizeLimit:a+1},t,e)).searchEntries.slice(0,a);return this.warnSizeLimit(`children:${t}`,`Organization ${t} holds more child organizations than the directory will list in one answer, so ${n.length} are returned and the rest are hidden. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to list them all.`),[...n,LdapOrganizations.moreIndicator(`more-organizations-${t}`,"... more organizations than the directory will list",n.length)]}async linkedEntities(t,i,a,e,n){const s=[];let r=0;try{const e=await this.server.ldap.search({paged:!0,filter:i},t,n);for await(const t of e){r+=t.searchEntries.length;const i=a-s.length;i>0&&s.push(...t.searchEntries.slice(0,i))}}catch(s){if(t!==e&&(w(s)||403===s?.statusCode))return this.linkedEntities(e,i,a,e,n);if(4!==v(s))throw s;const r=await this.server.ldap.search({paged:!1,filter:i,sizeLimit:a+1},t,n);this.warnSizeLimit(`linked:${e}`,`More entries are attached to ${e} than the directory will list in one answer. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to count them all.`);const o=r.searchEntries.slice(0,a);return[...o,LdapOrganizations.moreIndicator(`more-${e}`,"... more elements than the directory will list",o.length)]}return this.server.logger.debug(`Found ${r} linked entities for ${e}, returning ${s.length}`),r<=a?s:[...s,LdapOrganizations.moreIndicator(`more-${e}`,`... ${r-a} more ${r-a===1?"element":"elements"}`,a,r)]}async addOrganization(t,i,a){return L(this.schema,i),await this.server.ldap.authorize("ldapaddrequest",[t,i,a]),await this.validator().validateNew(i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i,a){return await this.server.ldap.authorize("ldapmodifyrequest",[t,i,this.opNumber(),a]),await this.validator().validateChanges(i),await this.server.ldap.modify(t,i,a)}validator(){return new k(this.schema,{entryExists:t=>this.entryExists(t),loadedPlugins:this.server.loadedPlugins,singularName:"organization"})}async entryExists(t){try{return(await this.server.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}}async moveOrganization(t,i,a){const e=`${c(t)},${i}`;let n,s;await this.server.ldap.authorize("ldaprenamerequest",[t,e,a]);try{n=await this.server.ldap.system.search({paged:!1,scope:"base"},i)}catch(t){throw new b(`Target organization ${i} not found: ${t instanceof Error?t.message:String(t)}`)}if(!n.searchEntries||0===n.searchEntries.length)throw new b(`Target organization ${i} not found`);if(!this.isOu(n.searchEntries[0]))throw new y(`Target ${i} is not an organizational unit`);try{s=await this.server.ldap.system.search({paged:!1,scope:"base"},t)}catch(t){throw new b(`Source organization not found: ${t}`)}if(!s.searchEntries||0===s.searchEntries.length)throw new b(`Source organization ${t} not found`);if(e===t)throw new y("Cannot move organization to its current location");if(i.toLowerCase()===t.toLowerCase()||m(i,t))throw new y("Cannot move organization into itself or its descendant");try{await this.server.ldap.rename(t,e,a),this.logger.info(`Moved organization from ${t} to ${e}`)}catch(t){if(t instanceof _)throw t;throw new Error(`Failed to move organization: ${t}`)}return{newDn:e}}async deleteOrganization(t,i){try{return await this.server.ldap.delete(t,i)}catch(i){if(32===v(i))throw new b(`Organization ${t} not found`);throw i}}async searchOrganisationSubnodes(t,i,a){const e=[],n=this.config.ldap_organization_max_subnodes||50;e.push(...await this.childOrganizations(t,`(&(objectClass=organizationalUnit)${z(i,"ou,description")})`,n,a));const s=this.config.ldap_top_organization,r=p(s),o=u(t),h=this.config.display_name_attribute||"displayName",c=`(&(${this.config.ldap_organization_link_attribute}=${o})${z(i,`uid,cn,mail,sn,givenName,${h}`)})`;return this.server.logger.debug(`Searching for linked entities with filter: ${c} in ${r}`),e.push(...await this.linkedEntities(r,c,n,t,a)),e}async searchOrganisationTree(t,i,a){const e=this.config.ldap_organization_max_subnodes||50,n=this.pathAttr?`ou,description,${this.pathAttr}`:"ou,description",s=`(&(objectClass=organizationalUnit)${z(i,n)})`,r=[];let o=0;try{const i=await this.server.ldap.search({paged:!0,scope:"sub",filter:s},t,a);for await(const t of i){o+=t.searchEntries.length;const i=e-r.length;i>0&&r.push(...t.searchEntries.slice(0,i))}}catch(i){const n=v(i);if(32===n)return this.server.logger.debug(`No organizations under ${t}: no such node`),[];if(34===n)throw new y(`${t} is not a valid DN`);if(4!==n)throw i;const r=(await this.server.ldap.search({paged:!1,scope:"sub",filter:s,sizeLimit:e+1},t,a)).searchEntries.slice(0,e);return this.warnSizeLimit(`search:${t}`,`A search under ${t} matched more organizations than the directory will list in one answer, so ${r.length} are returned and the rest are hidden. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to search them all.`),[...r,LdapOrganizations.moreIndicator(`more-organizations-${t}`,"... more organizations than the directory will list",r.length)]}if(o<=e)return r;const h=o-e;return[...r,LdapOrganizations.moreIndicator(`more-organizations-${t}`,`... ${h} more ${1===h?"organization":"organizations"}, narrow the search`,e,o)]}getConfigApiData(){const t=this.config.api_prefix||"/api",i=this.config.organization_schema?E(this.server,this.config.organization_schema):void 0;return{enabled:!0,topOrganization:this.config.ldap_top_organization||"",organizationClass:this.config.ldap_organization_class||["top","organizationalUnit"],linkAttribute:this.linkAttr||"",pathAttribute:this.pathAttr||"",pathSeparator:this.config.ldap_organization_path_separator||" / ",maxSubnodes:this.config.ldap_organization_max_subnodes||50,schema:this.schema,schemaUrl:i,endpoints:{getTop:`${t}/v1/ldap/organizations/top`,get:`${t}/v1/ldap/organizations/:dn`,getSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes`,searchSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes/search`,search:`${t}/v1/ldap/organizations/:dn/search`,create:`${t}/v1/ldap/organizations`,update:`${t}/v1/ldap/organizations/:dn`,move:`${t}/v1/ldap/organizations/:dn/move`,delete:`${t}/v1/ldap/organizations/:dn`}}}}LdapOrganizations.SIZE_LIMIT_WARNED_MAX=500;export{LdapOrganizations as default};
@@ -0,0 +1 @@
1
+ import{createServer as e}from"node:http";import{Registry as t,collectDefaultMetrics as s,Gauge as o}from"prom-client";import r from"../abstract/plugin.js";import{registry as i}from"../lib/metrics.js";class Metrics extends r{constructor(e){super(e),this.name="metrics",this.own=new t,s({register:this.own});const r=new o({name:"ldap_rest_ldap_pool_connections",help:"Pooled directory connections, by state",labelNames:["state"],registers:[this.own],collect:()=>{const e=this.server.ldap.poolStats();r.set({state:"busy"},e.busy),r.set({state:"idle"},e.idle)}}),n=new o({name:"ldap_rest_ldap_pool_waiting",help:"Operations waiting for a pooled directory connection",registers:[this.own],collect:()=>{n.set(this.server.ldap.poolStats().waiting)}}),a=new o({name:"ldap_rest_ldap_pool_max",help:"Pooled directory connections at most, --ldap-pool-size",registers:[this.own],collect:()=>{a.set(this.server.ldap.poolStats().max)}});this.exposed=t.merge([i,this.own])}async api(t){const s=e((e,t)=>{this.answer(e,t)});await new Promise((e,t)=>{s.once("error",t),s.listen(this.config.metrics_port,()=>{s.off("error",t),e()})}),this.listener=s,this.logger.notice(`Metrics served on port ${this.port()}`)}port(){const e=this.listener?.address();return e&&"object"==typeof e?e.port:void 0}async answer(e,t){if("GET"!==e.method||"/metrics"!==e.url?.split("?")[0])return t.statusCode=404,void t.end();try{const e=await this.exposed.metrics();t.setHeader("Content-Type",this.exposed.contentType),t.end(e)}catch(e){this.logger.warn(`Metrics not collected: ${e instanceof Error?e.message:String(e)}`),t.statusCode=500,t.end()}}async close(){const e=this.listener;e&&(this.listener=void 0,e.closeAllConnections(),await new Promise(t=>e.close(()=>t())))}}export{Metrics as default};
@@ -1 +1 @@
1
- import t from"../abstract/plugin.js";import{outsideRequest as i}from"../lib/requestContext.js";class RabbitMq extends t{constructor(t){super(t),this.name="rabbitmq",this.client=null,this.clientInit=null,this.rabbitmqUrl=this.config.rabbitmq_url||"",this.rabbitmqUrl||this.logger.warn(`${this.name}: rabbitmq_url is empty — AMQP publishes/subscribes will be skipped`),this.registerShutdown()}isAvailable(){return this.rabbitmqUrl.length>0}async publish(t,i,n,e){const s=await this.getClient();s&&await s.publish(t,i,n,e)}async subscribe(t,i,n,e,s){const r=await this.getClient();r&&await r.subscribe(t,i,n,e,s)}async unsubscribe(t){const i=await this.getClient();i&&await i.unsubscribe(t)}async getRawClient(){return this.getClient()}async getClient(){return this.client?this.client:this.rabbitmqUrl?(this.clientInit||(this.clientInit=(async()=>{try{const t=await i(()=>this.connect());return this.client=t,this.logger.info(`${this.name}: connected to RabbitMQ at ${function(t){try{const i=new URL(t);return i.username="",i.password="",i.toString()}catch{return"amqp://[broker]"}}(this.rabbitmqUrl)}`),t}catch(t){return this.logger.warn({plugin:this.name,event:"rabbitmq_init",message:"@linagora/rabbitmq-client unavailable or broker unreachable — AMQP disabled",error:`${t}`}),this.clientInit=null,null}})()),this.clientInit):null}async connect(){const t=new((await import("@linagora/rabbitmq-client")).RabbitMQClient)({url:this.rabbitmqUrl});return await t.init(),t}registerShutdown(){!function(t){if(n.push(t),e)return;e=!0;const fanOut=()=>{for(const t of n.splice(0))try{t()}catch{}};process.once("SIGTERM",fanOut),process.once("SIGINT",fanOut)}(()=>{const t=this.client;this.client=null,this.clientInit=null,t&&t.close().catch(t=>{this.logger.warn({plugin:this.name,event:"shutdown",error:`${t}`})})})}}const n=[];let e=!1;export{RabbitMq as default};
1
+ import t from"../abstract/plugin.js";import{outsideAnyRequest as i}from"../lib/requestContext.js";class RabbitMq extends t{constructor(t){super(t),this.name="rabbitmq",this.client=null,this.clientInit=null,this.closed=!1,this.rabbitmqUrl=this.config.rabbitmq_url||"",this.rabbitmqUrl||this.logger.warn(`${this.name}: rabbitmq_url is empty — AMQP publishes/subscribes will be skipped`)}isAvailable(){return this.rabbitmqUrl.length>0}health(){return this.rabbitmqUrl?this.client?.isConnected()?"ok":"degraded":"ok"}async publish(t,i,e,r){const n=await this.getClient();n&&await n.publish(t,i,e,r)}async subscribe(t,i,e,r,n){const s=await this.getClient();s&&await s.subscribe(t,i,e,r,n)}async unsubscribe(t){const i=await this.getClient();i&&await i.unsubscribe(t)}async getRawClient(){return this.getClient()}async getClient(){return this.client?this.client:!this.rabbitmqUrl||this.closed?null:(this.clientInit||(this.clientInit=(async()=>{try{const t=await i(()=>this.connect());return this.closed?(await t.close().catch(()=>{}),null):(this.client=t,this.logger.info(`${this.name}: connected to RabbitMQ at ${function(t){try{const i=new URL(t);return i.username="",i.password="",i.toString()}catch{return"amqp://[broker]"}}(this.rabbitmqUrl)}`),t)}catch(t){return this.logger.warn({plugin:this.name,event:"rabbitmq_init",message:"@linagora/rabbitmq-client unavailable or broker unreachable — AMQP disabled",error:`${t}`}),this.clientInit=null,null}})()),this.clientInit)}async connect(){const{RabbitMQClient:t}=await import("@linagora/rabbitmq-client");return this.open(t)}async open(t){const i=new t({url:this.rabbitmqUrl,logger:this.clientLogger()});return await i.init(),i}clientLogger(){const write=t=>(i,e)=>{const{error:r,...n}="object"==typeof e&&null!==e?e:{};this.logger.log(t,i,{plugin:this.name,...n,...void 0!==r&&{error:describeError(r)}})};return{debug:write("debug"),info:write("info"),warn:write("warn"),error:write("error")}}async close(){this.closed=!0;const t=this.client;this.client=null,this.clientInit=null,t&&await t.close().catch(t=>{this.logger.warn({plugin:this.name,event:"shutdown",error:`${t}`})})}}function describeError(t){if(!(t instanceof Error))return t;const{errors:i,code:e}=t;return Array.isArray(i)&&i.length?i.map(describeError).join("; "):t.message||"string"!=typeof e?String(t):`${t.name}: ${e}`}export{RabbitMq as default};
@@ -1 +1 @@
1
- import{fileURLToPath as s}from"url";import{join as e,dirname as i}from"path";import t from"../../abstract/plugin.js";import{setChangeSource as o}from"../../lib/changeContext.js";import{jsonBodyParser as r}from"../../lib/rawBody.js";import{BaseResolver as a}from"./baseResolver.js";import{ScimUsers as c}from"./users.js";import{ScimGroups as n}from"./groups.js";import{ScimBulk as p}from"./bulk.js";import{ScimDiscovery as m}from"./discovery.js";import{SCIM_CONTENT_TYPE as u,scimAsyncHandler as d,writeScimError as h}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";import{projectResource as l,assertProjection as g,projectList as f}from"./projection.js";function scimBodyParser(s){return r({type:["application/json","application/scim+json"],limit:`${s.scim_bulk_max_payload_size||1048576}b`})}class Scim extends t{constructor(t){super(t),this.name="scim",this.roles=["api","configurable"],this.ldap=t.ldap,this.scimPrefix=this.config.scim_prefix||"/scim/v2",this.baseResolver=new a(this.config),this.users=new c({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,hooks:this.registeredHooks}),this.groups=new n({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,users:this.users,hooks:this.registeredHooks}),this.bulk=new p({config:this.config,logger:this.logger,users:this.users,groups:this.groups,scimPrefix:this.scimPrefix,hooks:this.registeredHooks});const o=this.config.schemas_path||e(i(s(import.meta.url)),"..","..","..","static","schemas");this.discovery=new m({config:this.config,schemaDir:e(o,"scim"),scimPrefix:this.scimPrefix,loadedPlugins:t.loadedPlugins})}scimJson(s,e,i){s.status(e).type(u).json(i)}scimResource(s,e,i,t,o={}){const r=i.id?`${this.scimPrefix}/${t}/${encodeURIComponent(i.id)}`:void 0,a=(this.config.scim_base_url||"")&&i.meta?.location?i.meta.location:r;a&&s.set("Location",a),this.scimJson(s,e,l(i,o))}parseProjection(s){const toCsv=s=>{if("string"!=typeof s)return;const e=s.split(",").map(s=>s.trim()).filter(Boolean);return e.length>0?e:void 0},e={attributes:toCsv(s.query.attributes),excludedAttributes:toCsv(s.query.excludedAttributes)};return g(e),e}readScimBody(s){const e=String(s.headers["content-type"]||"");if(e.startsWith("application/scim+json")||e.startsWith("application/json"))return s.body}api(s){const e=this.scimPrefix;this.server.registerBodyParser(e,scimBodyParser(this.config)),s.use(e,(s,e,i)=>{o(s,"scim"),i()}),s.use(e,d((s,e,i)=>{this.parseProjection(s),i()})),s.get(`${e}/Users`,d(async(s,e)=>{const i=this.parseListQuery(s);g(i);const t=await this.users.list(s,i);this.scimJson(e,200,f(t,i))})),s.get(`${e}/Users/:id`,d(async(s,e)=>{const i=this.parseProjection(s),t=await this.users.get(s,decodeURIComponent(s.params.id));this.scimJson(e,200,l(t,i))})),s.post(`${e}/Users`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.users.create(s,i);this.scimResource(e,201,t,"Users",this.parseProjection(s))})),s.put(`${e}/Users/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.users.replace(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Users",this.parseProjection(s))})),s.patch(`${e}/Users/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.users.patch(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Users",this.parseProjection(s))})),s.delete(`${e}/Users/:id`,d(async(s,e)=>{await this.users.delete(s,decodeURIComponent(s.params.id)),e.status(204).end()})),s.get(`${e}/Groups`,d(async(s,e)=>{const i=this.parseListQuery(s);g(i);const t=await this.groups.list(s,i);this.scimJson(e,200,f(t,i))})),s.get(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.parseProjection(s),t=await this.groups.get(s,decodeURIComponent(s.params.id));this.scimJson(e,200,l(t,i))})),s.post(`${e}/Groups`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.groups.create(s,i);this.scimResource(e,201,t,"Groups",this.parseProjection(s))})),s.put(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.groups.replace(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Groups",this.parseProjection(s))})),s.patch(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.groups.patch(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Groups",this.parseProjection(s))})),s.delete(`${e}/Groups/:id`,d(async(s,e)=>{await this.groups.delete(s,decodeURIComponent(s.params.id)),e.status(204).end()})),s.post(`${e}/Bulk`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=this.config.scim_bulk_max_payload_size||1048576,o=JSON.stringify(i).length;if(o>t)return h(e,413,`Bulk payload too large: ${o} > ${t}`,"invalidValue");const r=await this.bulk.execute(s,i);this.scimJson(e,200,r)})),s.get(`${e}/ServiceProviderConfig`,d((s,e)=>{this.scimJson(e,200,this.discovery.serviceProviderConfig(s))})),s.get(`${e}/ResourceTypes`,d((s,e)=>{const i=this.discovery.resourceTypes(s);this.scimJson(e,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:i.length,Resources:i,startIndex:1,itemsPerPage:i.length})})),s.get(`${e}/ResourceTypes/:name`,d((s,e)=>{const i=this.discovery.resourceType(s.params.name,s);if(!i)return h(e,404,"ResourceType not found");this.scimJson(e,200,i)})),s.get(`${e}/Schemas`,d((s,e)=>{const i=this.discovery.schemas(s);this.scimJson(e,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:i.length,Resources:i,startIndex:1,itemsPerPage:i.length})})),s.get(`${e}/Schemas/:id`,d((s,e)=>{const i=this.discovery.schema(decodeURIComponent(s.params.id),s);if(!i)return h(e,404,"Schema not found");this.scimJson(e,200,i)}))}parseListQuery(s){const e=s.query,toInt=s=>{if("string"!=typeof s)return;const e=parseInt(s,10);return Number.isFinite(e)?e:void 0},toCsv=s=>{if("string"==typeof s)return s.split(",").map(s=>s.trim()).filter(Boolean)},i="string"==typeof e.sortOrder?e.sortOrder:void 0;return{filter:"string"==typeof e.filter?e.filter:void 0,startIndex:toInt(e.startIndex),count:toInt(e.count),attributes:toCsv(e.attributes),excludedAttributes:toCsv(e.excludedAttributes),sortBy:"string"==typeof e.sortBy?e.sortBy:void 0,sortOrder:"ascending"===i||"descending"===i?i:void 0}}getConfigApiData(){const s=this.scimPrefix;let e;if(this.server.loadedPlugins.static){const s=this.config.static_name||"static";e={user:`/${s}/schemas/scim/User.json`,group:`/${s}/schemas/scim/Group.json`,defaultMapping:`/${s}/schemas/scim/default-mapping.json`}}const i=this.users.userMapping,t=this.groups.groupMapping,o=i.entries.map(s=>s.scim).concat(["id","active"]),r=t.entries.map(s=>s.scim).concat(["id"]);return{enabled:!0,version:"2.0",prefix:s,endpoints:{users:`${s}/Users`,user:`${s}/Users/:id`,groups:`${s}/Groups`,group:`${s}/Groups/:id`,bulk:`${s}/Bulk`,serviceProviderConfig:`${s}/ServiceProviderConfig`,resourceTypes:`${s}/ResourceTypes`,schemas:`${s}/Schemas`},schemaUrls:e,capabilities:{patch:!0,bulk:!0,filter:!0,sort:!0,etag:Boolean(this.config.scim_etag),changePassword:!1,maxResults:this.config.scim_max_results||200,maxScanned:this.config.scim_max_scanned||1e4,bulkMaxOperations:this.config.scim_bulk_max_operations||100,bulkMaxPayloadSize:this.config.scim_bulk_max_payload_size||1048576},resourceTypes:[{id:"User",endpoint:`${s}/Users`,schema:"urn:ietf:params:scim:schemas:core:2.0:User",rdnAttribute:this.config.scim_user_rdn_attribute||"uid",objectClass:this.config.scim_user_object_class,filterableAttributes:o,mapping:i.entries},{id:"Group",endpoint:`${s}/Groups`,schema:"urn:ietf:params:scim:schemas:core:2.0:Group",rdnAttribute:this.config.scim_group_rdn_attribute||"cn",objectClass:this.config.scim_group_object_class,filterableAttributes:r,mapping:t.entries}],idStrategy:this.config.scim_id_attribute||"rdn",baseResolution:{userBaseTemplate:this.config.scim_user_base_template||void 0,groupBaseTemplate:this.config.scim_group_base_template||void 0,hasBaseMap:Boolean(this.config.scim_base_map)}}}}export{Scim as default,scimBodyParser};
1
+ import{fileURLToPath as s}from"url";import{join as e,dirname as i}from"path";import t from"../../abstract/plugin.js";import{setChangeSource as o}from"../../lib/changeContext.js";import{jsonBodyParser as r}from"../../lib/rawBody.js";import{staticPlugins as a}from"../../lib/staticFiles.js";import{BaseResolver as c}from"./baseResolver.js";import{ScimUsers as n}from"./users.js";import{ScimGroups as p}from"./groups.js";import{ScimBulk as m}from"./bulk.js";import{ScimDiscovery as u}from"./discovery.js";import{SCIM_CONTENT_TYPE as d,scimAsyncHandler as h,writeScimError as l}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";import{projectResource as g,assertProjection as f,projectList as y}from"./projection.js";function scimBodyParser(s){return r({type:["application/json","application/scim+json"],limit:`${s.scim_bulk_max_payload_size||1048576}b`})}class Scim extends t{constructor(t){super(t),this.name="scim",this.roles=["api","configurable"],this.ldap=t.ldap,this.scimPrefix=this.config.scim_prefix||"/scim/v2",this.baseResolver=new c(this.config),this.users=new n({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,hooks:this.registeredHooks}),this.groups=new p({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,users:this.users,hooks:this.registeredHooks}),this.bulk=new m({config:this.config,logger:this.logger,users:this.users,groups:this.groups,scimPrefix:this.scimPrefix,hooks:this.registeredHooks});const o=this.config.schemas_path||e(i(s(import.meta.url)),"..","..","..","static","schemas");this.scimSchemaDir=e(o,"scim"),this.discovery=new u({config:this.config,schemaDir:this.scimSchemaDir,scimPrefix:this.scimPrefix,loadedPlugins:t.loadedPlugins})}scimJson(s,e,i){s.status(e).type(d).json(i)}scimResource(s,e,i,t,o={}){const r=i.id?`${this.scimPrefix}/${t}/${encodeURIComponent(i.id)}`:void 0,a=(this.config.scim_base_url||"")&&i.meta?.location?i.meta.location:r;a&&s.set("Location",a),this.scimJson(s,e,g(i,o))}parseProjection(s){const toCsv=s=>{if("string"!=typeof s)return;const e=s.split(",").map(s=>s.trim()).filter(Boolean);return e.length>0?e:void 0},e={attributes:toCsv(s.query.attributes),excludedAttributes:toCsv(s.query.excludedAttributes)};return f(e),e}readScimBody(s){const e=String(s.headers["content-type"]||"");if(e.startsWith("application/scim+json")||e.startsWith("application/json"))return s.body}api(s){const e=this.scimPrefix;this.server.registerBodyParser(e,scimBodyParser(this.config)),s.use(e,(s,e,i)=>{o(s,"scim"),i()}),s.use(e,h((s,e,i)=>{this.parseProjection(s),i()})),s.get(`${e}/Users`,h(async(s,e)=>{const i=this.parseListQuery(s);f(i);const t=await this.users.list(s,i);this.scimJson(e,200,y(t,i))})),s.get(`${e}/Users/:id`,h(async(s,e)=>{const i=this.parseProjection(s),t=await this.users.get(s,decodeURIComponent(s.params.id));this.scimJson(e,200,g(t,i))})),s.post(`${e}/Users`,h(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return l(e,400,"Missing body","invalidSyntax");const t=await this.users.create(s,i);this.scimResource(e,201,t,"Users",this.parseProjection(s))})),s.put(`${e}/Users/:id`,h(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return l(e,400,"Missing body","invalidSyntax");const t=await this.users.replace(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Users",this.parseProjection(s))})),s.patch(`${e}/Users/:id`,h(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return l(e,400,"Missing body","invalidSyntax");const t=await this.users.patch(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Users",this.parseProjection(s))})),s.delete(`${e}/Users/:id`,h(async(s,e)=>{await this.users.delete(s,decodeURIComponent(s.params.id)),e.status(204).end()})),s.get(`${e}/Groups`,h(async(s,e)=>{const i=this.parseListQuery(s);f(i);const t=await this.groups.list(s,i);this.scimJson(e,200,y(t,i))})),s.get(`${e}/Groups/:id`,h(async(s,e)=>{const i=this.parseProjection(s),t=await this.groups.get(s,decodeURIComponent(s.params.id));this.scimJson(e,200,g(t,i))})),s.post(`${e}/Groups`,h(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return l(e,400,"Missing body","invalidSyntax");const t=await this.groups.create(s,i);this.scimResource(e,201,t,"Groups",this.parseProjection(s))})),s.put(`${e}/Groups/:id`,h(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return l(e,400,"Missing body","invalidSyntax");const t=await this.groups.replace(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Groups",this.parseProjection(s))})),s.patch(`${e}/Groups/:id`,h(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return l(e,400,"Missing body","invalidSyntax");const t=await this.groups.patch(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Groups",this.parseProjection(s))})),s.delete(`${e}/Groups/:id`,h(async(s,e)=>{await this.groups.delete(s,decodeURIComponent(s.params.id)),e.status(204).end()})),s.post(`${e}/Bulk`,h(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return l(e,400,"Missing body","invalidSyntax");const t=this.config.scim_bulk_max_payload_size||1048576,o=JSON.stringify(i).length;if(o>t)return l(e,413,`Bulk payload too large: ${o} > ${t}`,"invalidValue");const r=await this.bulk.execute(s,i);this.scimJson(e,200,r)})),s.get(`${e}/ServiceProviderConfig`,h((s,e)=>{this.scimJson(e,200,this.discovery.serviceProviderConfig(s))})),s.get(`${e}/ResourceTypes`,h((s,e)=>{const i=this.discovery.resourceTypes(s);this.scimJson(e,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:i.length,Resources:i,startIndex:1,itemsPerPage:i.length})})),s.get(`${e}/ResourceTypes/:name`,h((s,e)=>{const i=this.discovery.resourceType(s.params.name,s);if(!i)return l(e,404,"ResourceType not found");this.scimJson(e,200,i)})),s.get(`${e}/Schemas`,h((s,e)=>{const i=this.discovery.schemas(s);this.scimJson(e,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:i.length,Resources:i,startIndex:1,itemsPerPage:i.length})})),s.get(`${e}/Schemas/:id`,h((s,e)=>{const i=this.discovery.schema(decodeURIComponent(s.params.id),s);if(!i)return l(e,404,"Schema not found");this.scimJson(e,200,i)}))}parseListQuery(s){const e=s.query,toInt=s=>{if("string"!=typeof s)return;const e=parseInt(s,10);return Number.isFinite(e)?e:void 0},toCsv=s=>{if("string"==typeof s)return s.split(",").map(s=>s.trim()).filter(Boolean)},i="string"==typeof e.sortOrder?e.sortOrder:void 0;return{filter:"string"==typeof e.filter?e.filter:void 0,startIndex:toInt(e.startIndex),count:toInt(e.count),attributes:toCsv(e.attributes),excludedAttributes:toCsv(e.excludedAttributes),sortBy:"string"==typeof e.sortBy?e.sortBy:void 0,sortOrder:"ascending"===i||"descending"===i?i:void 0}}getConfigApiData(){const s=this.scimPrefix,i=["User.json","Group.json","default-mapping.json"].map(s=>e(this.scimSchemaDir,s));let t;for(const s of a(this.server)){const[e,o,r]=i.map(e=>s.schemaUrlOf(e));if(e&&o){t={user:e,group:o,...r?{defaultMapping:r}:{}};break}}const o=this.users.userMapping,r=this.groups.groupMapping,c=o.entries.map(s=>s.scim).concat(["id","active"]),n=r.entries.map(s=>s.scim).concat(["id"]);return{enabled:!0,version:"2.0",prefix:s,endpoints:{users:`${s}/Users`,user:`${s}/Users/:id`,groups:`${s}/Groups`,group:`${s}/Groups/:id`,bulk:`${s}/Bulk`,serviceProviderConfig:`${s}/ServiceProviderConfig`,resourceTypes:`${s}/ResourceTypes`,schemas:`${s}/Schemas`},schemaUrls:t,capabilities:{patch:!0,bulk:!0,filter:!0,sort:!0,etag:Boolean(this.config.scim_etag),changePassword:!1,maxResults:this.config.scim_max_results||200,maxScanned:this.config.scim_max_scanned||1e4,bulkMaxOperations:this.config.scim_bulk_max_operations||100,bulkMaxPayloadSize:this.config.scim_bulk_max_payload_size||1048576},resourceTypes:[{id:"User",endpoint:`${s}/Users`,schema:"urn:ietf:params:scim:schemas:core:2.0:User",rdnAttribute:this.config.scim_user_rdn_attribute||"uid",objectClass:this.config.scim_user_object_class,filterableAttributes:c,mapping:o.entries},{id:"Group",endpoint:`${s}/Groups`,schema:"urn:ietf:params:scim:schemas:core:2.0:Group",rdnAttribute:this.config.scim_group_rdn_attribute||"cn",objectClass:this.config.scim_group_object_class,filterableAttributes:n,mapping:r.entries}],idStrategy:this.config.scim_id_attribute||"rdn",baseResolution:{userBaseTemplate:this.config.scim_user_base_template||void 0,groupBaseTemplate:this.config.scim_group_base_template||void 0,hasBaseMap:Boolean(this.config.scim_base_map)}}}}export{Scim as default,scimBodyParser};
@@ -1 +1 @@
1
- import t from"fs";import{resolve as s,join as e}from"path";import n from"express";import i from"../abstract/plugin.js";import{notFound as a,serverError as r}from"../lib/expressFormatedResponses.js";import{configuredSchemaFiles as c,schemaUrl as o,loadSchemaFileAsync as h}from"../lib/schemaFile.js";class Static extends i{constructor(){super(...arguments),this.name="static",this.roles=["api","configurable"]}api(s){const e=this.config.static_path;if(!e)throw new Error("--static-path is not defined");try{if(!t.statSync(e).isDirectory())throw new Error(`${e} isn't a directory`);t.accessSync(e,t.constants.R_OK)}catch(t){throw new Error(`Bad directory ${e}: ${t}`)}const i=this.schemaServer(e);s.get(`/${this.config.static_name}/schemas/:name`,(t,s,e)=>i([t.params.name],t,s,e)),s.get(`/${this.config.static_name}/schemas/:dir/:name`,(t,s,e)=>i([t.params.dir,t.params.name],t,s,e)),s.get(`/${this.config.static_name}/schemas/*path`,(t,s,e)=>i(t.params.path,t,s,e)),s.use(`/${this.config.static_name}`,n.static(e))}schemaServer(t){const n=this.configuredSchemas(t),i=s(e(t,"schemas"));return(t,a,r,c)=>{const o=n.get((t=>{try{return decodeURIComponent(t)}catch{return t}})(a.path));if(o)return this.sendSchema(o,r);if(!t[t.length-1].endsWith(".json"))return c();if(!t.every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t)))return void r.status(400).send("Invalid schema name");const h=s(e(i,...t));h.startsWith(i+"/")?this.sendSchema(h,r):r.status(403).send("Access denied")}}configuredSchemas(t){const n=new Map;for(const i of c(this.config)){const a=o(this.config,i);if(!a)continue;const r=e(t,a.substring(a.indexOf("/schemas/")));if(s(r)===s(i))continue;const c=n.get(a);c&&s(c)!==s(i)?this.logger.warn(`${i} is not served: ${c} already has its URL ${a}`):n.set(a,i)}return n}sendSchema(t,s){h(t,{config:this.config}).then(t=>s.type("json").send(JSON.stringify(t)),t=>{if(t.code)return a(s,"Schema not found");r(s,t)})}getConfigApiData(){const t=`/${this.config.static_name||"static"}`;return{enabled:!0,staticPath:t,endpoints:{schema:`${t}/schemas/:name`,schemaInSubdir:`${t}/schemas/:dir/:name`,files:`${t}/*`}}}}export{Static as default};
1
+ import t from"fs";import{resolve as s,join as e,relative as n,sep as r,isAbsolute as i}from"path";import{validateHeaderValue as o}from"http";import c from"express";import a from"../abstract/plugin.js";import{STATIC_CSP as h,STATIC_FRAME_OPTIONS as f}from"../config/args.js";import{notFound as m,serverError as u}from"../lib/expressFormatedResponses.js";import{staticName as p,configuredSchemaFiles as d,schemaUrl as l,loadSchemaFileAsync as g}from"../lib/schemaFile.js";const headerValue=(t,s,e,n)=>{if(!1===e)return;if(null!=e&&"string"!=typeof e)throw new Error(`${t}: a text is expected, or false for none`);const r=("string"==typeof e?e:"").replace(/\s+/g," ").trim();if(!r)return n;if("off"!==r.toLowerCase()){try{o(s,r)}catch{throw new Error(`${t}: "${r}" cannot be sent as ${s}`)}return r}};class Static extends a{constructor(){super(...arguments),this.name="static",this.roles=["api","configurable"],this.configured=new Map}api(s){const e=this.config.static_path;if(!e)throw new Error("--static-path is not defined");try{if(!t.statSync(e).isDirectory())throw new Error(`${e} isn't a directory`);t.accessSync(e,t.constants.R_OK)}catch(t){throw new Error(`Bad directory ${e}: ${t}`)}const n=this.config.static_name;if(null!=n&&"string"!=typeof n)throw new Error("--static-name: a text is expected, such as static");const r=p(this.config);if(r.split("/").some(t=>!t||/^\.+$/.test(t)||/[^\x21-\x7e]|[:*?+!()[\]{}\\#%"<>|^`]/.test(t)))throw new Error(`--static-name: "${r}" is not a path to serve under, such as static`);const i=`/${r}`,o=this.schemaServer(e),a=headerValue("--static-csp","Content-Security-Policy",this.config.static_csp,h),m=headerValue("--static-frame-options","X-Frame-Options",this.config.static_frame_options,f);s.use(i,(t,s,e)=>{a&&s.setHeader("Content-Security-Policy",a),m&&s.setHeader("X-Frame-Options",m),e()}),s.get(`${i}/schemas/:name`,(t,s,e)=>o([t.params.name],t,s,e)),s.get(`${i}/schemas/:dir/:name`,(t,s,e)=>o([t.params.dir,t.params.name],t,s,e)),s.get(`${i}/schemas/*path`,(t,s,e)=>o(t.params.path,t,s,e)),s.use(i,c.static(e))}schemaServer(t){const n=this.configured=this.configuredSchemas(t),r=s(e(t,"schemas"));return(t,i,o,c)=>{const a=n.get((t=>{try{return decodeURIComponent(t)}catch{return t}})(i.path));if(a)return this.sendSchema(a,o);if(!t[t.length-1].endsWith(".json"))return c();if(!t.every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t)))return void o.status(400).send("Invalid schema name");const h=s(e(r,...t));h.startsWith(r+"/")?this.sendSchema(h,o):o.status(403).send("Access denied")}}configuredSchemas(t){const n=new Map;for(const r of d(this.config)){const i=l(this.config,r);if(!i)continue;const o=e(t,i.substring(i.indexOf("/schemas/")));if(s(o)===s(r))continue;const c=n.get(i);c&&s(c)!==s(r)?this.logger.warn(`${r} is not served: ${c} already has its URL ${i}`):n.set(i,r)}return n}sendSchema(t,s){g(t,{config:this.config}).then(t=>s.type("json").send(JSON.stringify(t)),t=>{if(t.code)return m(s,"Schema not found");u(s,t)})}urlOf(e){const o=s(e);let c;try{if(!t.statSync(o).isFile())return;c=t.realpathSync(o)}catch{return}const same=e=>{if(s(e)===o)return!0;try{return t.realpathSync(e)===c}catch{return!1}},a=this.servedPath(),url=t=>`${a}/${t.map(encodeURIComponent).join("/")}`;for(const[t,s]of this.configured)if(same(s)){const s=t.slice(a.length+1).split("/");if(s.some(t=>["",".",".."].includes(t)))continue;return url(s)}const h=this.config.static_path;if(!h)return;let f;try{f=t.realpathSync(h)}catch{return}const m=[n(s(h),o),n(f,c)].find(t=>!!t&&".."!==t&&!t.startsWith(`..${r}`)&&!i(t));if(!m)return;const u=m.split(r);return u.some(t=>t.startsWith("."))||"schemas"===u[0].toLowerCase()&&"schemas"!==u[0]&&m.endsWith(".json")||"schemas"===u[0]&&m.endsWith(".json")&&!u.slice(1).every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t))||this.configured.has(`${a}/${u.join("/")}`)?void 0:url(u)}schemaUrlOf(o){const realOf=s=>{try{return t.statSync(s).isFile()?t.realpathSync(s):void 0}catch{return}},c=realOf(o);if(!c)return;const a=this.servedPath(),url=t=>`${a}/${t.map(encodeURIComponent).join("/")}`;for(const[t,s]of this.configured)if(realOf(s)===c){const s=t.slice(a.length+1).split("/");if(s.some(t=>["",".",".."].includes(t)))continue;return url(s)}const h=this.config.static_path;if(!h)return;const f=e(h,"schemas"),m=[];for(const t of[s(o),c]){const s=t.split(r);s.forEach((t,e)=>{"schemas"===t&&m.push(s.slice(e+1))})}try{const s=n(t.realpathSync(f),c);!s||".."===s||s.startsWith(`..${r}`)||i(s)||m.push(s.split(r))}catch{}for(const t of m){if(!t.length||!t[t.length-1].endsWith(".json")||!t.every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t)))continue;const s=["schemas",...t];if(!this.configured.has(`${a}/${s.join("/")}`)&&realOf(e(f,...t))===c)return url(s)}}servedPath(){return`/${p(this.config)}`}getConfigApiData(){const t=this.servedPath();return{enabled:!0,staticPath:t,endpoints:{schema:`${t}/schemas/:name`,schemaInSubdir:`${t}/schemas/:dir/:name`,files:`${t}/*`}}}}export{Static as default};
@@ -1 +1 @@
1
- import{createHash as t}from"node:crypto";import{promises as e}from"node:fs";import{join as a}from"node:path";import r from"../../lib/storage/store.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t);class FileStore extends r{constructor(t,e,a){super(t),this.name="storage/file",this.dir=e,this.abandonedAfter=1e3*Math.max(a,60)}async ready(){await e.mkdir(this.dir,{recursive:!0,mode:448})}path(e){return a(this.dir,t("sha256").update(e).digest("hex"))}static encode(t,e){return`${e.deadline} ${FileStore.readable(t)}\n${e.value}\n`}static decode(t){const e=t.indexOf("\n");if(e<0)return null;const a=parseInt(t.slice(0,e).split(" ")[0],10);return Number.isFinite(a)?{deadline:a,value:t.slice(e+1).replace(/\n$/,"")}:null}async load(t){try{return FileStore.decode(await e.readFile(this.path(t),"utf8"))}catch(t){return"ENOENT"!==t?.code&&this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,a){await this.ready();const r=this.path(t),i=`${r}.${process.pid}.tmp`;try{await e.writeFile(i,FileStore.encode(t,a),{mode:384}),await e.rename(i,r)}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),await e.unlink(i).catch(()=>{}),t}}async drop(t){await e.unlink(this.path(t)).catch(t=>{"ENOENT"!==t?.code&&this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)})}async sweep(){const t=Date.now();let r,i=0;try{r=await e.readdir(this.dir)}catch{return 0}for(const n of r){const r=a(this.dir,n);if(n.endsWith(".tmp")){await e.stat(r).then(e=>t-e.mtimeMs).catch(()=>0)>this.abandonedAfter&&(await e.unlink(r).catch(()=>{}),i++);continue}let s=null;try{s=FileStore.decode(await e.readFile(r,"utf8"))}catch{continue}null!==s&&s.deadline>t||(await e.unlink(r).catch(()=>{}),i++)}return i}}export{FileStore as default};
1
+ import{createHash as t}from"node:crypto";import{promises as e,constants as a}from"node:fs";import{join as r}from"node:path";import i from"../../lib/storage/store.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t);class FileStore extends i{constructor(t,e,a){super(t),this.name="storage/file",this.dir=e,this.abandonedAfter=1e3*Math.max(a,60)}async ready(){await e.mkdir(this.dir,{recursive:!0,mode:448})}async open(){await this.ready()}async health(){if(!(await e.stat(this.dir)).isDirectory())throw new Error(`${this.dir} is not a directory`);return await e.access(this.dir,a.W_OK|a.X_OK),"ok"}path(e){return r(this.dir,t("sha256").update(e).digest("hex"))}static encode(t,e){return`${e.deadline} ${FileStore.readable(t)}\n${e.value}\n`}static decode(t){const e=t.indexOf("\n");if(e<0)return null;const a=parseInt(t.slice(0,e).split(" ")[0],10);return Number.isFinite(a)?{deadline:a,value:t.slice(e+1).replace(/\n$/,"")}:null}async load(t){try{return FileStore.decode(await e.readFile(this.path(t),"utf8"))}catch(t){return"ENOENT"!==t?.code&&this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,a){await this.ready();const r=this.path(t),i=`${r}.${process.pid}.tmp`;try{await e.writeFile(i,FileStore.encode(t,a),{mode:384}),await e.rename(i,r)}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),await e.unlink(i).catch(()=>{}),t}}async drop(t){await e.unlink(this.path(t)).catch(t=>{"ENOENT"!==t?.code&&this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)})}async sweep(){const t=Date.now();let a,i=0;try{a=await e.readdir(this.dir)}catch{return 0}for(const n of a){const a=r(this.dir,n);if(n.endsWith(".tmp")){await e.stat(a).then(e=>t-e.mtimeMs).catch(()=>0)>this.abandonedAfter&&(await e.unlink(a).catch(()=>{}),i++);continue}let s=null;try{s=FileStore.decode(await e.readFile(a,"utf8"))}catch{continue}null!==s&&s.deadline>t||(await e.unlink(a).catch(()=>{}),i++)}return i}}export{FileStore as default};
@@ -1 +1 @@
1
- import t from"../../lib/storage/store.js";import{outsideRequest as e}from"../../lib/requestContext.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t),quote=t=>`"${t.replace(/"/g,'""')}"`;class PostgresStore extends t{constructor(t,e,a){super(t),this.name="storage/postgres",this.url=e,this.table=a,this.quoted=a.split(".").map(quote).join(".")}async driver(){return import("pg")}async open(){await this.pool()}async close(){await super.close();const t=this.connection;this.connection=void 0,t&&await((await t.catch(()=>{}))?.end())}pool(){return this.connection??(this.connection=e(()=>this.connect()).catch(t=>{throw this.connection=void 0,t})),this.connection}async connect(){let t;try{t=await this.driver()}catch(t){throw new Error(`${this.name}: requires the optional dependency "pg", which is not installed. Install it to keep the records in PostgreSQL, or choose another --storage-backend. (${errorText(t)})`)}const e=new t.Pool({connectionString:this.url,connectionTimeoutMillis:1e4,allowExitOnIdle:!0});e.on("error",t=>this.logger.warn(`${this.name}: connection lost: ${errorText(t)}`));try{await this.ensureTable(e)}catch(t){throw await e.end().catch(()=>{}),new Error(`${this.name}: cannot use table ${this.table}: ${errorText(t)}`)}return e}async ensureTable(t){const e=await t.query("SELECT to_regclass($1) AS t",[this.quoted]);if(e.rows[0]?.t)return;const a=this.quoted,n=quote(`${this.table.split(".").pop()}_expires_at`);await t.query(`CREATE TABLE IF NOT EXISTS ${a} (\n namespace text NOT NULL,\n key text NOT NULL,\n value text NOT NULL,\n expires_at timestamptz NOT NULL,\n PRIMARY KEY (namespace, key)\n )`),await t.query(`CREATE INDEX IF NOT EXISTS ${n} ON ${a} (expires_at)`),this.logger.info(`${this.name}: created table ${this.table}`)}static parts(t){const e=t.indexOf("\0"),a=t.slice(e+1);return a.includes("\0")?null:[t.slice(0,e),a]}async load(t){const e=PostgresStore.parts(t);if(!e)return null;try{const t=await this.pool(),a=(await t.query(`SELECT value, expires_at FROM ${this.quoted}\n WHERE namespace = $1 AND key = $2`,e)).rows[0];return a?{value:a.value,deadline:a.expires_at.getTime()}:null}catch(t){return this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,e){const a=PostgresStore.parts(t);if(!a)throw new Error(`${this.name}: cannot keep a key holding a NUL`);try{const t=await this.pool();await t.query(`INSERT INTO ${this.quoted}\n (namespace, key, value, expires_at) VALUES ($1, $2, $3, $4)\n ON CONFLICT (namespace, key) DO UPDATE\n SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,[...a,e.value,new Date(e.deadline)])}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),t}}async drop(t){const e=PostgresStore.parts(t);if(e)try{const t=await this.pool();await t.query(`DELETE FROM ${this.quoted}\n WHERE namespace = $1 AND key = $2`,e)}catch(t){this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)}}async sweep(){const t=await this.pool();return(await t.query(`DELETE FROM ${this.quoted} WHERE expires_at <= $1`,[new Date])).rowCount??0}}export{PostgresStore as default};
1
+ import{AsyncResource as t}from"node:async_hooks";import e from"../../lib/storage/store.js";import{PROBE_TIMEOUT_MS as n}from"../../lib/health.js";import{outsideAnyRequest as a}from"../../lib/requestContext.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t),quote=t=>`"${t.replace(/"/g,'""')}"`;class PostgresStore extends e{constructor(t,e,n){super(t),this.name="storage/postgres",this.url=e,this.table=n,this.quoted=n.split(".").map(quote).join(".")}async driver(){return import("pg")}async open(){await this.pool()}async health(){const t=new((await this.driver()).Client)({connectionString:this.url,connectionTimeoutMillis:n,query_timeout:n});t.on("error",()=>{});try{return await t.connect(),await t.query("SELECT 1"),"ok"}finally{await t.end().catch(()=>{})}}async close(){await super.close();const t=this.connection;this.connection=void 0,t&&await((await t.catch(()=>{}))?.end())}pool(){return this.closed?Promise.reject(new Error(`${this.name}: closed`)):(this.connection??(this.connection=a(()=>this.connect()).catch(t=>{throw this.connection=void 0,t})),this.connection)}async connect(){let e;try{e=await this.driver()}catch(t){throw new Error(`${this.name}: requires the optional dependency "pg", which is not installed. Install it to keep the records in PostgreSQL, or choose another --storage-backend. (${errorText(t)})`)}const n=new e.Pool({connectionString:this.url,connectionTimeoutMillis:1e4,allowExitOnIdle:!0});n.on("error",t.bind(t=>this.logger.warn(`${this.name}: connection lost: ${errorText(t)}`)));try{await this.ensureTable(n)}catch(t){throw await n.end().catch(()=>{}),new Error(`${this.name}: cannot use table ${this.table}: ${errorText(t)}`)}return n}async ensureTable(t){const e=await t.query("SELECT to_regclass($1) AS t",[this.quoted]);if(e.rows[0]?.t)return;const n=this.quoted,a=quote(`${this.table.split(".").pop()}_expires_at`);await t.query(`CREATE TABLE IF NOT EXISTS ${n} (\n namespace text NOT NULL,\n key text NOT NULL,\n value text NOT NULL,\n expires_at timestamptz NOT NULL,\n PRIMARY KEY (namespace, key)\n )`),await t.query(`CREATE INDEX IF NOT EXISTS ${a} ON ${n} (expires_at)`),this.logger.info(`${this.name}: created table ${this.table}`)}static parts(t){const e=t.indexOf("\0"),n=t.slice(e+1);return n.includes("\0")?null:[t.slice(0,e),n]}async load(t){const e=PostgresStore.parts(t);if(!e)return null;try{const t=await this.pool(),n=(await t.query(`SELECT value, expires_at FROM ${this.quoted}\n WHERE namespace = $1 AND key = $2`,e)).rows[0];return n?{value:n.value,deadline:n.expires_at.getTime()}:null}catch(t){return this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,e){const n=PostgresStore.parts(t);if(!n)throw new Error(`${this.name}: cannot keep a key holding a NUL`);try{const t=await this.pool();await t.query(`INSERT INTO ${this.quoted}\n (namespace, key, value, expires_at) VALUES ($1, $2, $3, $4)\n ON CONFLICT (namespace, key) DO UPDATE\n SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,[...n,e.value,new Date(e.deadline)])}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),t}}async drop(t){const e=PostgresStore.parts(t);if(e)try{const t=await this.pool();await t.query(`DELETE FROM ${this.quoted}\n WHERE namespace = $1 AND key = $2`,e)}catch(t){this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)}}async sweep(){const t=await this.pool();return(await t.query(`DELETE FROM ${this.quoted} WHERE expires_at <= $1`,[new Date])).rowCount??0}}export{PostgresStore as default};
@@ -1 +1 @@
1
- import t from"../../lib/storage/store.js";import{outsideRequest as e}from"../../lib/requestContext.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t);class ValkeyStore extends t{constructor(t,e,n){super(t),this.name="storage/valkey",this.down=!1,this.url=e,this.prefix=n}async driver(){return import("iovalkey")}async open(){await this.client()}async close(){await super.close();const t=this.connection;this.connection=void 0;const e=await(t?.catch(()=>{}));e&&await e.quit().catch(()=>e.disconnect())}client(){return this.connection??(this.connection=e(()=>this.connect()).catch(t=>{throw this.connection=void 0,t})),this.connection}async connect(){let t;try{t=await this.driver()}catch(t){throw new Error(`${this.name}: requires the optional dependency "iovalkey", which is not installed. Install it to keep the records in Valkey, or choose another --storage-backend. (${errorText(t)})`)}const e=new t.Valkey(this.url,{lazyConnect:!0,enableOfflineQueue:!1,maxRetriesPerRequest:1,commandTimeout:5e3});let n,o=!1;e.on("error",t=>{n=t,o&&!this.down&&(this.down=!0,this.logger.warn(`${this.name}: connection lost: ${errorText(t)}`))}),e.on("ready",()=>{this.down&&this.logger.info(`${this.name}: connection back`),this.down=!1});try{await e.connect(),o=!0}catch(t){throw e.disconnect(),new Error(`${this.name}: cannot connect: ${errorText(n??t)}`)}return e}key(t){return`${this.prefix}${t}`}async load(t){try{const e=await(await this.client()).get(this.key(t));return null===e?null:ValkeyStore.decodeRecord(e)}catch(t){return this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,e){const n=Math.ceil(e.deadline-Date.now());try{const o=await this.client();if(n<=0)return void await o.del(this.key(t));await o.set(this.key(t),ValkeyStore.encodeRecord(t,e),"PX",n)}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),t}}async drop(t){try{await(await this.client()).del(this.key(t))}catch(t){this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)}}async sweep(){return 0}}export{ValkeyStore as default};
1
+ import{AsyncResource as t}from"node:async_hooks";import e from"../../lib/storage/store.js";import{outsideAnyRequest as n}from"../../lib/requestContext.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t);class ValkeyStore extends e{constructor(t,e,n){super(t),this.name="storage/valkey",this.down=!1,this.url=e,this.prefix=n}async driver(){return import("iovalkey")}async open(){await this.client()}async health(){return await(await this.client()).ping(),"ok"}async close(){await super.close();const t=this.connection;this.connection=void 0;const e=await(t?.catch(()=>{}));e&&await e.quit().catch(()=>e.disconnect())}client(){return this.closed?Promise.reject(new Error(`${this.name}: closed`)):(this.connection??(this.connection=n(()=>this.connect()).catch(t=>{throw this.connection=void 0,t})),this.connection)}async connect(){let e;try{e=await this.driver()}catch(t){throw new Error(`${this.name}: requires the optional dependency "iovalkey", which is not installed. Install it to keep the records in Valkey, or choose another --storage-backend. (${errorText(t)})`)}const n=new e.Valkey(this.url,{lazyConnect:!0,enableOfflineQueue:!1,maxRetriesPerRequest:1,commandTimeout:5e3});let o,i=!1;n.on("error",t.bind(t=>{o=t,i&&!this.down&&(this.down=!0,this.logger.warn(`${this.name}: connection lost: ${errorText(t)}`))})),n.on("ready",t.bind(()=>{this.down&&this.logger.info(`${this.name}: connection back`),this.down=!1}));try{await n.connect(),i=!0}catch(t){throw n.disconnect(),new Error(`${this.name}: cannot connect: ${errorText(o??t)}`)}return n}key(t){return`${this.prefix}${t}`}async load(t){try{const e=await(await this.client()).get(this.key(t));return null===e?null:ValkeyStore.decodeRecord(e)}catch(t){return this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,e){const n=Math.ceil(e.deadline-Date.now());try{const o=await this.client();if(n<=0)return void await o.del(this.key(t));await o.set(this.key(t),ValkeyStore.encodeRecord(t,e),"PX",n)}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),t}}async drop(t){try{await(await this.client()).del(this.key(t))}catch(t){this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)}}async sweep(){return 0}}export{ValkeyStore as default};
@@ -1 +1 @@
1
- import e from"../abstract/plugin.js";import r from"./storage/file.js";import s from"./storage/ldap.js";import t from"./storage/postgres.js";import o from"./storage/valkey.js";let a;const i="ldap, file, postgres, valkey";class Storage extends e{constructor(e){if(super(e),this.name="storage",a)throw new Error(`storage: an instance is already loaded, keeping its records in ${a.store.name}. A second one would put the same record in two places and leave registration order to decide which answers. Load one, and let its consumers share it.`);const n=e.config.storage_backend||"";switch(n){case"ldap":{const r=e.config.storage_ldap_base;if(!r)throw new Error("storage: the ldap backend needs --storage-ldap-base, the branch it writes its records to. It holds records and nothing else, so it belongs outside the branches the directory serves.");this.store=new s(e,r,e.config.storage_ldap_object_class||"applicationProcess");break}case"file":{const s=e.config.storage_file_directory;if(!s)throw new Error("storage: the file backend needs --storage-file-directory, the directory it writes its records to. It writes one file per record, so nothing else should be writing there.");this.store=new r(e.logger,s,e.config.storage_sweep_interval||600);break}case"postgres":{const r=e.config.storage_postgres_url;if(!r)throw new Error("storage: the postgres backend needs --storage-postgres-url, the database it writes its records to.");this.store=new t(e.logger,r,e.config.storage_postgres_table||"ldap_rest_storage");break}case"valkey":{const r=e.config.storage_valkey_url;if(!r)throw new Error("storage: the valkey backend needs --storage-valkey-url, the server it writes its records to.");this.store=new o(e.logger,r,e.config.storage_valkey_prefix??"");break}default:throw new Error(n?`storage: unknown backend "${n}". Known: ${i}.`:`storage: --storage-backend says which sub-plugin keeps the records. Known: ${i}.`)}a=this,this.logger.info(`storage: records kept by ${this.store.name}`),this.store.open().catch(e=>this.logger.error(e instanceof Error?e.message:String(e))),this.store.startSweeping(e.config.storage_sweep_interval||0)}static release(e){a===e&&(a=void 0)}}export{Storage as default};
1
+ import e from"../abstract/plugin.js";import r from"./storage/file.js";import s from"./storage/ldap.js";import t from"./storage/postgres.js";import o from"./storage/valkey.js";let a;const i="ldap, file, postgres, valkey";class Storage extends e{constructor(e){if(super(e),this.name="storage",this.degraded=!1,this.probes=0,a)throw new Error(`storage: an instance is already loaded, keeping its records in ${a.store.name}. A second one would put the same record in two places and leave registration order to decide which answers. Load one, and let its consumers share it.`);const n=e.config.storage_backend||"";switch(n){case"ldap":{const r=e.config.storage_ldap_base;if(!r)throw new Error("storage: the ldap backend needs --storage-ldap-base, the branch it writes its records to. It holds records and nothing else, so it belongs outside the branches the directory serves.");this.store=new s(e,r,e.config.storage_ldap_object_class||"applicationProcess");break}case"file":{const s=e.config.storage_file_directory;if(!s)throw new Error("storage: the file backend needs --storage-file-directory, the directory it writes its records to. It writes one file per record, so nothing else should be writing there.");this.store=new r(e.logger,s,e.config.storage_sweep_interval||600);break}case"postgres":{const r=e.config.storage_postgres_url;if(!r)throw new Error("storage: the postgres backend needs --storage-postgres-url, the database it writes its records to.");this.store=new t(e.logger,r,e.config.storage_postgres_table||"ldap_rest_storage");break}case"valkey":{const r=e.config.storage_valkey_url;if(!r)throw new Error("storage: the valkey backend needs --storage-valkey-url, the server it writes its records to.");this.store=new o(e.logger,r,e.config.storage_valkey_prefix??"");break}default:throw new Error(n?`storage: unknown backend "${n}". Known: ${i}.`:`storage: --storage-backend says which sub-plugin keeps the records. Known: ${i}.`)}a=this,this.logger.info(`storage: records kept by ${this.store.name}`),this.store.open().catch(e=>this.logger.error(e instanceof Error?e.message:String(e))),this.store.startSweeping(e.config.storage_sweep_interval||0)}async close(){await this.store.close()}async health(){const e=++this.probes;try{const r=await this.store.health();return e===this.probes&&(this.degraded&&this.logger.info(`${this.store.name}: answers again`),this.degraded=!1),r}catch(r){return e===this.probes&&(this.degraded||this.logger.warn(`${this.store.name}: does not answer: ${r instanceof Error?r.message:String(r)}`),this.degraded=!0),"degraded"}}static release(e){a===e&&(a=void 0)}}export{Storage as default};