ldap-rest 0.8.1 → 0.9.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 (79) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/bin/index.js +1 -1
  3. package/dist/config/args.js +1 -1
  4. package/dist/lib/auth/base.js +1 -1
  5. package/dist/lib/authz/base.js +1 -1
  6. package/dist/lib/authz/composition.js +1 -0
  7. package/dist/lib/expressFormatedResponses.js +1 -1
  8. package/dist/lib/ldapActions.js +1 -1
  9. package/dist/lib/ldapCodes.js +1 -0
  10. package/dist/lib/parseConfig.js +1 -1
  11. package/dist/lib/storage/store.js +1 -0
  12. package/dist/lib/utils.js +1 -1
  13. package/dist/plugins/auth/authzDynamic.js +1 -1
  14. package/dist/plugins/auth/authzPerBranch.js +1 -1
  15. package/dist/plugins/auth/authzPerRoute.js +1 -1
  16. package/dist/plugins/auth/authzScope.js +1 -1
  17. package/dist/plugins/auth/hmac.js +1 -1
  18. package/dist/plugins/auth/llng.js +1 -1
  19. package/dist/plugins/auth/openidconnect.js +1 -1
  20. package/dist/plugins/auth/token.js +1 -1
  21. package/dist/plugins/auth/totp.js +1 -1
  22. package/dist/plugins/bcl.js +1 -0
  23. package/dist/plugins/ldap/organizations.js +1 -1
  24. package/dist/plugins/priority.json.js +1 -1
  25. package/dist/plugins/scim/baseResolver.js +1 -1
  26. package/dist/plugins/scim/errors.js +1 -1
  27. package/dist/plugins/scim/groups.js +1 -1
  28. package/dist/plugins/scim/list.js +1 -1
  29. package/dist/plugins/scim/users.js +1 -1
  30. package/dist/plugins/storage/file.js +1 -0
  31. package/dist/plugins/storage/ldap.js +1 -0
  32. package/dist/plugins/storage.js +1 -0
  33. package/dist/src/abstract/ldapFlat.d.ts +11 -3
  34. package/dist/src/abstract/plugin.d.ts +20 -0
  35. package/dist/src/bin/index.d.ts +50 -8
  36. package/dist/src/config/args.d.ts +36 -0
  37. package/dist/src/hooks.d.ts +55 -0
  38. package/dist/src/lib/auth/base.d.ts +175 -9
  39. package/dist/src/lib/authz/base.d.ts +133 -4
  40. package/dist/src/lib/authz/composition.d.ts +145 -0
  41. package/dist/src/lib/expressFormatedResponses.d.ts +9 -2
  42. package/dist/src/lib/ldapActions.d.ts +30 -0
  43. package/dist/src/lib/ldapCodes.d.ts +28 -0
  44. package/dist/src/lib/storage/store.d.ts +84 -0
  45. package/dist/src/lib/utils.d.ts +19 -0
  46. package/dist/src/plugins/auth/authzDynamic.d.ts +76 -0
  47. package/dist/src/plugins/auth/authzPerBranch.d.ts +11 -0
  48. package/dist/src/plugins/auth/authzPerRoute.d.ts +26 -0
  49. package/dist/src/plugins/auth/authzScope.d.ts +44 -3
  50. package/dist/src/plugins/auth/hmac.d.ts +2 -0
  51. package/dist/src/plugins/auth/llng.d.ts +39 -0
  52. package/dist/src/plugins/auth/openidconnect.d.ts +66 -3
  53. package/dist/src/plugins/auth/rateLimit.d.ts +3 -1
  54. package/dist/src/plugins/auth/token.d.ts +2 -0
  55. package/dist/src/plugins/auth/totp.d.ts +2 -0
  56. package/dist/src/plugins/bcl.d.ts +65 -0
  57. package/dist/src/plugins/ldap/organizations.d.ts +90 -1
  58. package/dist/src/plugins/scim/baseResolver.d.ts +3 -1
  59. package/dist/src/plugins/scim/errors.d.ts +2 -10
  60. package/dist/src/plugins/storage/file.d.ts +19 -0
  61. package/dist/src/plugins/storage/ldap.d.ts +17 -0
  62. package/dist/src/plugins/storage.d.ts +44 -0
  63. package/docs/plugin-development/README.md +12 -1
  64. package/docs/plugin-development/dependencies.md +40 -15
  65. package/docs/usage/configuration.md +89 -13
  66. package/docs/usage/plugins/README.md +17 -12
  67. package/docs/usage/plugins/auth/README.md +129 -0
  68. package/docs/usage/plugins/auth/authz-dynamic.md +42 -0
  69. package/docs/usage/plugins/auth/authz-linid1.md +32 -0
  70. package/docs/usage/plugins/auth/authz-per-branch.md +8 -0
  71. package/docs/usage/plugins/auth/authz-per-route.md +6 -0
  72. package/docs/usage/plugins/auth/authz-scope.md +69 -16
  73. package/docs/usage/plugins/auth/back-channel-logout.md +86 -0
  74. package/docs/usage/plugins/auth/llng.md +29 -0
  75. package/docs/usage/plugins/auth/oidc.md +52 -0
  76. package/docs/usage/plugins/ldap/organizations.md +37 -0
  77. package/docs/usage/plugins/utilities/storage.md +94 -0
  78. package/docs/usage/upgrading.md +289 -0
  79. package/package.json +17 -1
@@ -1 +1 @@
1
- import{createHmac as t}from"crypto";import{unauthorized as e}from"../../lib/expressFormatedResponses.js";import s from"../../lib/auth/base.js";class AuthTotp extends s{constructor(...t){super(...t),this.name="authTotp",this.roles=["auth"],this.totpUsers=[];const e=this.config.auth_totp;this.window=this.config.auth_totp_window??1,this.step=this.config.auth_totp_step??30,e&&Array.isArray(e)&&e.forEach((t,e)=>{const s=t.split(":");if(s.length>=2){const t=s[0].trim(),r=s[1].trim(),i=s[2]?parseInt(s[2].trim(),10):6;if(!this.isValidBase32(t))return void this.logger.warn(`Invalid Base32 secret for TOTP user "${r}" (index ${e})`);if(i<6||i>10)return void this.logger.warn(`Invalid digits count for TOTP user "${r}": ${i} (must be 6-10)`);this.totpUsers.push({name:r,secret:t,digits:i})}else this.logger.warn(`Invalid TOTP config format at index ${e}: expected "secret:name[:digits]"`)}),0===this.totpUsers.length?this.logger.warn("No valid TOTP users configured"):this.logger.info(`TOTP authentication initialized with ${this.totpUsers.length} user(s)`)}authMethod(t,s,r){const i=t.headers.authorization;if(!i||!/^Bearer .+/.test(i))return this.logger.warn("Missing or invalid Authorization header"),e(s);const o=i.split(" ")[1];for(const e of this.totpUsers)if(this.verifyTotp(e.secret,o,e.digits))return this.logger.debug(`TOTP authentication successful for user: ${e.name}`),t.user=e.name,r();return this.logger.warn(`Unauthorized TOTP token: ${o}`),e(s)}verifyTotp(t,e,s){const r=Math.floor(Date.now()/1e3);for(let i=-this.window;i<=this.window;i++){const o=Math.floor(r/this.step)+i;if(this.generateTotp(t,o,s)===e)return!0}return!1}generateTotp(e,s,r){const i=this.base32Decode(e),o=Buffer.alloc(8);o.writeBigUInt64BE(BigInt(s));const n=t("sha1",i);n.update(o);const a=n.digest(),h=15&a[a.length-1];return(((127&a[h])<<24|(255&a[h+1])<<16|(255&a[h+2])<<8|255&a[h+3])%Math.pow(10,r)).toString().padStart(r,"0")}base32Decode(t){let e=t.toUpperCase();for(;e.endsWith("=");)e=e.slice(0,-1);let s=0,r=0;const i=[];for(let t=0;t<e.length;t++){const o="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(e[t]);if(-1===o)throw new Error(`Invalid Base32 character: ${e[t]}`);r=r<<5|o,s+=5,s>=8&&(i.push(r>>>s-8&255),s-=8)}return Buffer.from(i)}isValidBase32(t){return/^[A-Z2-7]+=*$/.test(t.toUpperCase())}}export{AuthTotp as default};
1
+ import{createHmac as t}from"crypto";import{unauthorized as e}from"../../lib/expressFormatedResponses.js";import s from"../../lib/auth/base.js";class AuthTotp extends s{knownIdentities(){return this.totpUsers.map(t=>t.name)}identitySource(){return"req.user and req.userName: the name given in --auth-totp"}constructor(...t){super(...t),this.name="authTotp",this.roles=["auth"],this.totpUsers=[];const e=this.config.auth_totp;this.window=this.config.auth_totp_window??1,this.step=this.config.auth_totp_step??30,e&&Array.isArray(e)&&e.forEach((t,e)=>{const s=t.split(":");if(s.length>=2){const t=s[0].trim(),r=s[1].trim(),i=s[2]?parseInt(s[2].trim(),10):6;if(!this.isValidBase32(t))return void this.logger.warn(`Invalid Base32 secret for TOTP user "${r}" (index ${e})`);if(i<6||i>10)return void this.logger.warn(`Invalid digits count for TOTP user "${r}": ${i} (must be 6-10)`);this.totpUsers.push({name:r,secret:t,digits:i})}else this.logger.warn(`Invalid TOTP config format at index ${e}: expected "secret:name[:digits]"`)}),0===this.totpUsers.length?this.logger.warn("No valid TOTP users configured"):this.logger.info(`TOTP authentication initialized with ${this.totpUsers.length} user(s)`)}authMethod(t,s,r){const i=t.headers.authorization;if(!i||!/^Bearer .+/.test(i))return this.logger.warn("Missing or invalid Authorization header"),e(s);const o=i.split(" ")[1];for(const e of this.totpUsers)if(this.verifyTotp(e.secret,o,e.digits))return this.logger.debug(`TOTP authentication successful for user: ${e.name}`),this.publishIdentity(t,e.name),r();return this.logger.warn(`Unauthorized TOTP token: ${o}`),e(s)}verifyTotp(t,e,s){const r=Math.floor(Date.now()/1e3);for(let i=-this.window;i<=this.window;i++){const o=Math.floor(r/this.step)+i;if(this.generateTotp(t,o,s)===e)return!0}return!1}generateTotp(e,s,r){const i=this.base32Decode(e),o=Buffer.alloc(8);o.writeBigUInt64BE(BigInt(s));const n=t("sha1",i);n.update(o);const a=n.digest(),h=15&a[a.length-1];return(((127&a[h])<<24|(255&a[h+1])<<16|(255&a[h+2])<<8|255&a[h+3])%Math.pow(10,r)).toString().padStart(r,"0")}base32Decode(t){let e=t.toUpperCase();for(;e.endsWith("=");)e=e.slice(0,-1);let s=0,r=0;const i=[];for(let t=0;t<e.length;t++){const o="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(e[t]);if(-1===o)throw new Error(`Invalid Base32 character: ${e[t]}`);r=r<<5|o,s+=5,s>=8&&(i.push(r>>>s-8&255),s-=8)}return Buffer.from(i)}isValidBase32(t){return/^[A-Z2-7]+=*$/.test(t.toUpperCase())}}export{AuthTotp as default};
@@ -0,0 +1 @@
1
+ import t from"../abstract/plugin.js";const e="bcl";class Bcl extends t{constructor(t){super(t),this.name="bcl",this.dependencies={storage:"core/storage"},this.hooks={oidclogouttoken:async t=>{const s=Bcl.keys(t);if(0===s.length)return void this.logger.warn(`${this.name}: a logout token carried no sid nor sub`);const o=Date.now()+this.retention;await Promise.all(s.map(t=>this.store.set(e,t,"1",o)))},oidclogin:async t=>{await Promise.all(Bcl.keys(t).map(t=>this.store.delete(e,t)))},oidcsessionvalid:async([t,s])=>{if(!s)return[t,s];for(const s of Bcl.keys(t))if(null!==await this.store.get(e,s))return[t,!1];return[t,!0]}},this.retention=1e3*(t.config.bcl_retention||604800)}api(){const t=this.requirePlugin("storage");if(!t)throw new Error("bcl: needs core/storage loaded, and --storage-backend set. Without somewhere to keep what a logout killed, every session would outlive the logout that closed it and nothing would say so.");this.storage=t}get store(){if(!this.storage)throw new Error("bcl: the store was asked for before api() took hold of it. A plugin built outside registerPlugin has to call api() as it does.");return this.storage.store}static keys(t){const e=[];return t.sid&&e.push(`${t.iss}|sid|${t.sid}`),t.sub&&e.push(`${t.iss}|sub|${t.sub}`),e}}export{Bcl as default};
@@ -1 +1 @@
1
- import t from"fs";import i from"../../abstract/plugin.js";import{tryMethodData as a,jsonBody as e,tryMethod as n,wantJson as s}from"../../lib/expressFormatedResponses.js";import{transformSchemas as r,asyncHandler as o,validateDnValue as h,escapeDnValue as c,getRdn as d,isDnInBranch as g,rdnValue as l,normalizeDn as p,getParentDn as f,escapeLdapFilter as u,launchHooksChained as w,isChildOf as m}from"../../lib/utils.js";import{BadRequestError as z,NotFoundError as $,ConflictError as b}from"../../lib/errors.js";import{assertClientMaySet as v,modifiedAttributeNames as y,missingRequiredAttribute as _,checkDnValues as O,matchesPattern as C}from"../../config/schema.js";class LdapOrganizations extends i{constructor(i){if(super(i),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.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 z("An organization link cannot be deleted");if(e)throw new z("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)/^ou=/.test(t)&&await this.isEmptyOrganization(t);return[t,i]},ldaprenamerequest:([t,i,a])=>[t,i,a]},!this.config.ldap_top_organization)throw new Error("Missing --ldap-top-organization");this.pathAttr=this.config.ldap_organization_path_attribute,this.linkAttr=this.config.ldap_organization_link_attribute,this.config.organization_schema&&t.readFile(this.config.organization_schema,(t,i)=>{if(t)this.logger.error(`Failed to load organization schema from ${this.config.organization_schema}: ${t}`);else try{this.schema=JSON.parse(r(i.toString(),this.config)),this.logger.debug("Organization schema loaded")}catch(t){this.logger.error(`Failed to parse organization schema: ${t}`)}})}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`,o(async(t,i)=>{const e=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)throw new z('query parameter "q" is required');await a(i,async()=>this.hideNeverReturn(await this.searchOrganisationSubnodes(e,n)))})),t.post(`${this.config.api_prefix}/v1/ldap/organizations`,o(async(t,i)=>this.apiAdd(t,i))),t.put(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,o(async(t,i)=>this.apiModify(t,i))),t.delete(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,o(async(t,i)=>this.apiDelete(t,i))),t.post(`${this.config.api_prefix}/v1/ldap/organizations/:dn/move`,o(async(t,i)=>this.apiMove(t,i)))}async apiAdd(t,i){const a=e(t,i,"ou");if(!a)return;v(this.schema,Object.keys(a),"ou"),h(a.ou,"ou");const s=a.parentDn||this.config.ldap_top_organization,r=`ou=${c(a.ou)},${s}`,o={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,o,t)}async apiModify(t,i){const a=e(t,i);if(!a)return;const s=decodeURIComponent(t.params.dn);if(!s)throw new z("dn is required");v(this.schema,y(a)),await n(i,this.modifyOrganization.bind(this),s,a)}async apiDelete(t,i){if(!s(t,i))return;const a=decodeURIComponent(t.params.dn);if(!a)throw new z("dn is required");await n(i,this.deleteOrganization.bind(this),a)}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 z("dn is required");const{targetOrgDn:o}=n;if(!o||"string"!=typeof o)throw new z("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.search({paged:!1,scope:"base"},a);if(0===e.searchEntries.length)throw new $(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new z(`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(d(i))&&g(i,s)?l(i):void 0,o=t.ou,h=(Array.isArray(o)?o[0]:o)||r;let c=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?p(s):"";if(!i||!t||!g(i,s)||p(i)===t||p(f(i))===t)return;throw new z(`Organization path "${e}" names no parent, but ${i} is not directly under ${s}`)}if(!e.endsWith(n+h))throw new z(`Organization path must end with its own name, preceded by separator "${n}"`);c=e.slice(0,e.length-n.length-h.length)}const w=await this.server.ldap.search({paged:!1,scope:"sub",filter:`(${this.pathAttr}=${u(c)})`,attributes:["dn"]},this.config.ldap_top_organization);if(!w.searchEntries||0===w.searchEntries.length)throw new z(`Invalid organization path ${e}: no organization holds "${c}"`)}}async isEmptyOrganization(t){if((await this.server.ldap.search({paged:!1,filter:`(${this.config.ldap_organization_link_attribute}=${t})`})).searchEntries.length>0)throw new b(`Organization ${t} is not empty`)}isOu(t){if(!t.objectClass)return!1;const i=t.objectClass.map(t=>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 z("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 $("Top organization not found");const[,a]=await w(this.registeredHooks.getOrganisationTop,[t,i.searchEntries[0]]);return a}async getOrganisationByDn(t,i){const a=await this.server.ldap.search({paged:!1,scope:"base",filter:"(objectClass=organizationalUnit)"},t,i);if(1!==a.searchEntries.length)throw new $(`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;if(!n||"organizationalUnit"===n)try{const e=await this.server.ldap.search({paged:!1,scope:"one",filter:"(objectClass=organizationalUnit)"},t,i);this.server.logger.debug(`Found ${e.searchEntries.length} sub-OUs for ${t}`),a.push(...e.searchEntries)}catch(i){this.server.logger.debug(`No sub-OUs found for ${t}: ${i}`)}const s=this.config.ldap_top_organization,r=f(s);let o=`(${this.config.ldap_organization_link_attribute}=${u(t)})`;n&&(o=`(&${o}(objectClass=${u(n)}))`),this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${r}`);const h=await this.server.ldap.search({paged:!0,filter:o},r,i);let c=0,d=0;for await(const t of h){d+=t.searchEntries.length;const i=e-c;if(i>0){const e=t.searchEntries.slice(0,i);a.push(...e),c+=e.length}}return this.server.logger.debug(`Found ${d} linked entities for ${t}, returning ${c}`),d>e&&a.push({dn:`more-${t}`,cn:[`... ${d-e} more elements`],objectClass:["moreIndicator"],_isMoreIndicator:"true",_totalCount:d.toString(),_displayedCount:e.toString()}),a}async addOrganization(t,i,a){return await this.validateNewOrganization(t,i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i){return await this.validateChanges(t,i),await this.server.ldap.modify(t,i)}async validateNewOrganization(t,i){if(!this.schema)return!0;for(const[t,a]of Object.entries(i))if(!await this._validateOneChange(t,a))throw new z(this.invalidValueMessage(t));const a=_(this.schema,i,this.server.loadedPlugins);if(a)throw new z(`Missing required field ${a}`);return!0}invalidValueMessage(t){const i=this.schema?.attributes[t],a=i?.hint||i?.items?.hint;return a?`Invalid value for field ${t}: ${a}`:`Invalid value for field ${t}`}async validateChanges(t,i){if(!this.schema)return!0;if(i.add)for(const[t,a]of Object.entries(i.add))if(!await this._validateOneChange(t,a))throw new z(this.invalidValueMessage(t));if(i.replace)for(const[t,a]of Object.entries(i.replace))if(!await this._validateOneChange(t,a))throw new z(this.invalidValueMessage(t));return!0}async _validateOneChange(t,i){if(!this.schema)return!0;const a=this.schema.attributes[t];if(!a){if(this.schema.strict)throw new z(`Field ${t} is not allowed`);return!0}if(null==i){if(a.required)throw new z(`Field ${t} is required`);return!0}if("string"===a.type&&Array.isArray(i))throw new z(`Field ${t} must be a single value`);return await O(t,a,i,t=>this.entryExists(t)),C(a,i)}async entryExists(t){try{return(await this.server.ldap.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}}async moveOrganization(t,i,a){let e;try{e=await this.server.ldap.search({paged:!1,scope:"base"},i)}catch(t){throw new $(`Target organization ${i} not found: ${t instanceof Error?t.message:String(t)}`)}if(!e.searchEntries||0===e.searchEntries.length)throw new $(`Target organization ${i} not found`);if(!this.isOu(e.searchEntries[0]))throw new z(`Target ${i} is not an organizational unit`);const n=`${d(t)},${i}`;let s;try{s=await this.server.ldap.search({paged:!1,scope:"base"},t)}catch(t){throw new $(`Source organization not found: ${t}`)}if(!s.searchEntries||0===s.searchEntries.length)throw new $(`Source organization ${t} not found`);if(n===t)throw new z("Cannot move organization to its current location");if(i.toLowerCase()===t.toLowerCase()||m(i,t))throw new z("Cannot move organization into itself or its descendant");try{await this.server.ldap.rename(t,n,a),this.logger.info(`Moved organization from ${t} to ${n}`)}catch(t){throw new Error(`Failed to move organization: ${t}`)}return{newDn:n}}async deleteOrganization(t){return await this.server.ldap.delete(t)}async searchOrganisationSubnodes(t,i){const a=[],e=u(i);try{const n=await this.server.ldap.search({paged:!1,scope:"one",filter:`(&(objectClass=organizationalUnit)(|(ou=*${e}*)(description=*${e}*)))`},t);this.server.logger.debug(`Found ${n.searchEntries.length} sub-OUs matching "${i}" for ${t}`),a.push(...n.searchEntries)}catch(a){this.server.logger.debug(`No sub-OUs found matching "${i}" for ${t}: ${a}`)}const n=this.config.ldap_top_organization,s=f(n),r=u(t),o=`(&(${this.config.ldap_organization_link_attribute}=${r})(|(uid=*${e}*)(cn=*${e}*)(mail=*${e}*)(sn=*${e}*)(givenName=*${e}*)))`;this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${s}`);const h=await this.server.ldap.search({paged:!0,filter:o},s);let c=0;for await(const t of h)c+=t.searchEntries.length,a.push(...t.searchEntries);return this.server.logger.debug(`Found ${c} linked entities matching "${i}" for ${t}`),a}getConfigApiData(){const t=this.config.api_prefix||"/api";let i;if(this.server.loadedPlugins.static&&this.config.organization_schema){const t=this.config.static_name||"static",a=this.config.organization_schema.indexOf("/schemas/");if(-1!==a){i=`/${t}${this.config.organization_schema.substring(a)}`}}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`,create:`${t}/v1/ldap/organizations`,update:`${t}/v1/ldap/organizations/:dn`,move:`${t}/v1/ldap/organizations/:dn/move`,delete:`${t}/v1/ldap/organizations/:dn`}}}}export{LdapOrganizations as default};
1
+ import t from"fs";import i from"../../abstract/plugin.js";import{tryMethodData as a,jsonBody as e,tryMethod as n,wantJson as s}from"../../lib/expressFormatedResponses.js";import{transformSchemas as r,asyncHandler as o,validateDnValue as h,escapeDnValue as c,getRdn as d,isDnInBranch as l,rdnValue as g,normalizeDn as p,getParentDn as f,escapeLdapFilter as u,launchHooksChained as w,isChildOf as m}from"../../lib/utils.js";import{BadRequestError as z,NotFoundError as y,ConflictError as b}from"../../lib/errors.js";import{extractLdapCode as _}from"../../lib/ldapCodes.js";import{assertClientMaySet as $,modifiedAttributeNames as v,missingRequiredAttribute as O,checkDnValues as C,matchesPattern as E}from"../../config/schema.js";class LdapOrganizations extends i{constructor(i){if(super(i),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.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 z("An organization link cannot be deleted");if(e)throw new z("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)/^ou=/.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");this.pathAttr=this.config.ldap_organization_path_attribute,this.linkAttr=this.config.ldap_organization_link_attribute,this.config.organization_schema&&t.readFile(this.config.organization_schema,(t,i)=>{if(t)this.logger.error(`Failed to load organization schema from ${this.config.organization_schema}: ${t}`);else try{this.schema=JSON.parse(r(i.toString(),this.config)),this.logger.debug("Organization schema loaded")}catch(t){this.logger.error(`Failed to parse organization schema: ${t}`)}})}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`,o(async(t,i)=>{const e=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)throw new z('query parameter "q" is required');await a(i,async()=>this.hideNeverReturn(await this.searchOrganisationSubnodes(e,n,t)))})),t.post(`${this.config.api_prefix}/v1/ldap/organizations`,o(async(t,i)=>this.apiAdd(t,i))),t.put(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,o(async(t,i)=>this.apiModify(t,i))),t.delete(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,o(async(t,i)=>this.apiDelete(t,i))),t.post(`${this.config.api_prefix}/v1/ldap/organizations/:dn/move`,o(async(t,i)=>this.apiMove(t,i)))}async apiAdd(t,i){const a=e(t,i,"ou");if(!a)return;$(this.schema,Object.keys(a),"ou"),h(a.ou,"ou");const s=a.parentDn||this.config.ldap_top_organization,r=`ou=${c(a.ou)},${s}`,o={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,o,t)}async apiModify(t,i){const a=e(t,i);if(!a)return;const s=decodeURIComponent(t.params.dn);if(!s)throw new z("dn is required");$(this.schema,v(a)),await n(i,this.modifyOrganization.bind(this),s,a)}async apiDelete(t,i){if(!s(t,i))return;const a=decodeURIComponent(t.params.dn);if(!a)throw new z("dn is required");await n(i,this.deleteOrganization.bind(this),a)}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 z("dn is required");const{targetOrgDn:o}=n;if(!o||"string"!=typeof o)throw new z("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.search({paged:!1,scope:"base"},a);if(0===e.searchEntries.length)throw new y(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new z(`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(d(i))&&l(i,s)?g(i):void 0,o=t.ou,h=(Array.isArray(o)?o[0]:o)||r;let c=e;if((this.isOu(t)||void 0!==r)&&h){const t=s?g(s):"";if(t&&e.startsWith(h+n)&&e.endsWith(n+t))return;if(e===h){const t=s?p(s):"";if(!i||!t||!l(i,s)||p(i)===t||p(f(i))===t)return;throw new z(`Organization path "${e}" names no parent, but ${i} is not directly under ${s}`)}if(!e.endsWith(n+h))throw new z(`Organization path must end with its own name, preceded by separator "${n}"`);c=e.slice(0,e.length-n.length-h.length)}const w=await this.server.ldap.search({paged:!1,scope:"sub",filter:`(${this.pathAttr}=${u(c)})`,attributes:["dn"]},this.config.ldap_top_organization);if(!w.searchEntries||0===w.searchEntries.length)throw new z(`Invalid organization path ${e}: no organization holds "${c}"`)}}async isEmptyOrganization(t){if((await this.server.ldap.search({paged:!1,filter:`(${this.config.ldap_organization_link_attribute}=${t})`})).searchEntries.length>0)throw new b(`Organization ${t} is not empty`)}isOu(t){if(!t.objectClass)return!1;const i=t.objectClass.map(t=>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 z("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 y("Top organization not found");const[,a]=await w(this.registeredHooks.getOrganisationTop,[t,i.searchEntries[0]]);return a}async getOrganisationByDn(t,i){const a=await this.server.ldap.search({paged:!1,scope:"base",filter:"(objectClass=organizationalUnit)"},t,i);if(1!==a.searchEntries.length)throw new y(`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=f(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))}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(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,{dn:`more-organizations-${t}`,cn:["... more organizations than the directory will list"],objectClass:["moreIndicator"],_isMoreIndicator:"true",_displayedCount:n.length.toString()}]}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(4!==_(s))throw s;const r=await this.server.ldap.search({paged:!1,filter:i,sizeLimit:a+1},t,n);return 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.`),[...r.searchEntries.slice(0,a),{dn:`more-${e}`,cn:["... more elements than the directory will list"],objectClass:["moreIndicator"],_isMoreIndicator:"true",_displayedCount:Math.min(r.searchEntries.length,a).toString()}]}return this.server.logger.debug(`Found ${r} linked entities for ${e}, returning ${s.length}`),r<=a?s:[...s,{dn:`more-${e}`,cn:[`... ${r-a} more elements`],objectClass:["moreIndicator"],_isMoreIndicator:"true",_totalCount:r.toString(),_displayedCount:a.toString()}]}async addOrganization(t,i,a){return await this.validateNewOrganization(t,i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i){return await this.validateChanges(t,i),await this.server.ldap.modify(t,i)}async validateNewOrganization(t,i){if(!this.schema)return!0;for(const[t,a]of Object.entries(i))if(!await this._validateOneChange(t,a))throw new z(this.invalidValueMessage(t));const a=O(this.schema,i,this.server.loadedPlugins);if(a)throw new z(`Missing required field ${a}`);return!0}invalidValueMessage(t){const i=this.schema?.attributes[t],a=i?.hint||i?.items?.hint;return a?`Invalid value for field ${t}: ${a}`:`Invalid value for field ${t}`}async validateChanges(t,i){if(!this.schema)return!0;if(i.add)for(const[t,a]of Object.entries(i.add))if(!await this._validateOneChange(t,a))throw new z(this.invalidValueMessage(t));if(i.replace)for(const[t,a]of Object.entries(i.replace))if(!await this._validateOneChange(t,a))throw new z(this.invalidValueMessage(t));return!0}async _validateOneChange(t,i){if(!this.schema)return!0;const a=this.schema.attributes[t];if(!a){if(this.schema.strict)throw new z(`Field ${t} is not allowed`);return!0}if(null==i){if(a.required)throw new z(`Field ${t} is required`);return!0}if("string"===a.type&&Array.isArray(i))throw new z(`Field ${t} must be a single value`);return await C(t,a,i,t=>this.entryExists(t)),E(a,i)}async entryExists(t){try{return(await this.server.ldap.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}}async moveOrganization(t,i,a){let e;try{e=await this.server.ldap.search({paged:!1,scope:"base"},i)}catch(t){throw new y(`Target organization ${i} not found: ${t instanceof Error?t.message:String(t)}`)}if(!e.searchEntries||0===e.searchEntries.length)throw new y(`Target organization ${i} not found`);if(!this.isOu(e.searchEntries[0]))throw new z(`Target ${i} is not an organizational unit`);const n=`${d(t)},${i}`;let s;try{s=await this.server.ldap.search({paged:!1,scope:"base"},t)}catch(t){throw new y(`Source organization not found: ${t}`)}if(!s.searchEntries||0===s.searchEntries.length)throw new y(`Source organization ${t} not found`);if(n===t)throw new z("Cannot move organization to its current location");if(i.toLowerCase()===t.toLowerCase()||m(i,t))throw new z("Cannot move organization into itself or its descendant");try{await this.server.ldap.rename(t,n,a),this.logger.info(`Moved organization from ${t} to ${n}`)}catch(t){throw new Error(`Failed to move organization: ${t}`)}return{newDn:n}}async deleteOrganization(t){return await this.server.ldap.delete(t)}async searchOrganisationSubnodes(t,i,a){const e=[],n=this.config.ldap_organization_max_subnodes||50,s=u(i);e.push(...await this.childOrganizations(t,`(&(objectClass=organizationalUnit)(|(ou=*${s}*)(description=*${s}*)))`,n,a));const r=this.config.ldap_top_organization,o=f(r),h=u(t),c=`(&(${this.config.ldap_organization_link_attribute}=${h})(|(uid=*${s}*)(cn=*${s}*)(mail=*${s}*)(sn=*${s}*)(givenName=*${s}*)))`;return this.server.logger.debug(`Searching for linked entities with filter: ${c} in ${o}`),e.push(...await this.linkedEntities(o,c,n,t,a)),e}getConfigApiData(){const t=this.config.api_prefix||"/api";let i;if(this.server.loadedPlugins.static&&this.config.organization_schema){const t=this.config.static_name||"static",a=this.config.organization_schema.indexOf("/schemas/");if(-1!==a){i=`/${t}${this.config.organization_schema.substring(a)}`}}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`,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 +1 @@
1
- var e=["core/auth/trustedProxy","core/weblogs","core/auth/crowdsec","core/auth/rateLimit","core/auth/token","core/auth/llng","core/auth/openidconnect","core/auth/authzPerRoute"];export{e as default};
1
+ var e=["core/storage","core/auth/trustedProxy","core/weblogs","core/auth/crowdsec","core/auth/rateLimit","core/auth/token","core/auth/llng","core/auth/openidconnect","core/auth/authzPerRoute"];export{e as default};
@@ -1 +1 @@
1
- import e from"fs";import{escapeDnValue as s,isChildOf as r}from"../../lib/utils.js";class BaseResolver{constructor(s){const r=s.ldap_base||"";this.defaultUserBase=s.scim_user_base||r,this.defaultGroupBase=s.scim_group_base||r,this.userTemplate=s.scim_user_base_template||"",this.groupTemplate=s.scim_group_base_template||"",this.userBaseHeader=(s.scim_user_base_header||"").toLowerCase(),this.groupBaseHeader=(s.scim_group_base_header||"").toLowerCase(),this.headerRoot=s.scim_base_header_root||"";const t=s.scim_base_map||"";if(t)try{const s=e.readFileSync(t,"utf8");this.map=JSON.parse(s)}catch(e){throw new Error(`Failed to load SCIM base map from ${t}: ${e instanceof Error?e.message:String(e)}`)}}applyTemplate(e,r){const t=r?s(r):"";return e.replace(/\{user\}/g,t)}readHeader(e,s){if(!e||"object"!=typeof e)return;const r=e;if("function"==typeof r.get){const e=r.get(s);if("string"==typeof e&&e.length>0)return e}const t=r.headers?.[s];return"string"==typeof t&&t.length>0?t:Array.isArray(t)&&"string"==typeof t[0]&&t[0].length>0?t[0]:void 0}resolve(e,s){const t=s&&"object"==typeof s&&"user"in s?s.user:void 0;if(this.map&&t&&this.map[t]){const s=this.map[t];if("user"===e&&s.userBase)return s.userBase;if("group"===e&&s.groupBase)return s.groupBase}const a="user"===e?this.userBaseHeader:this.groupBaseHeader;if(a&&this.headerRoot){const e=this.readHeader(s,a)?.trim();if(e&&!/[\u0000-\u001f\u007f]/.test(e)&&(e.toLowerCase()===this.headerRoot.toLowerCase()||r(e,this.headerRoot)))return e}if(this.map&&this.map["*"]){const s=this.map["*"];if("user"===e&&s.userBase)return this.applyTemplate(s.userBase,t);if("group"===e&&s.groupBase)return this.applyTemplate(s.groupBase,t)}const o="user"===e?this.userTemplate:this.groupTemplate;return o?this.applyTemplate(o,t):"user"===e?this.defaultUserBase:this.defaultGroupBase}userBase(e){return this.resolve("user",e)}groupBase(e){return this.resolve("group",e)}}export{BaseResolver};
1
+ import e from"fs";import{assertIdentityMode as s,identityFor as r}from"../../lib/auth/base.js";import{escapeDnValue as t,isChildOf as a}from"../../lib/utils.js";class BaseResolver{constructor(r){s(r,"scim base resolver"),this.config=r;const t=r.ldap_base||"";this.defaultUserBase=r.scim_user_base||t,this.defaultGroupBase=r.scim_group_base||t,this.userTemplate=r.scim_user_base_template||"",this.groupTemplate=r.scim_group_base_template||"",this.userBaseHeader=(r.scim_user_base_header||"").toLowerCase(),this.groupBaseHeader=(r.scim_group_base_header||"").toLowerCase(),this.headerRoot=r.scim_base_header_root||"";const a=r.scim_base_map||"";if(a)try{const s=e.readFileSync(a,"utf8");this.map=JSON.parse(s)}catch(e){throw new Error(`Failed to load SCIM base map from ${a}: ${e instanceof Error?e.message:String(e)}`)}}applyTemplate(e,s){const r=s?t(s):"";return e.replace(/\{user\}/g,r)}readHeader(e,s){if(!e||"object"!=typeof e)return;const r=e;if("function"==typeof r.get){const e=r.get(s);if("string"==typeof e&&e.length>0)return e}const t=r.headers?.[s];return"string"==typeof t&&t.length>0?t:Array.isArray(t)&&"string"==typeof t[0]&&t[0].length>0?t[0]:void 0}resolve(e,s){const t=s&&"object"==typeof s?r(s,this.config).value:void 0;if(this.map&&t&&this.map[t]){const s=this.map[t];if("user"===e&&s.userBase)return s.userBase;if("group"===e&&s.groupBase)return s.groupBase}const o="user"===e?this.userBaseHeader:this.groupBaseHeader;if(o&&this.headerRoot){const e=this.readHeader(s,o)?.trim();if(e&&!/[\u0000-\u001f\u007f]/.test(e)&&(e.toLowerCase()===this.headerRoot.toLowerCase()||a(e,this.headerRoot)))return e}if(this.map&&this.map["*"]){const s=this.map["*"];if("user"===e&&s.userBase)return this.applyTemplate(s.userBase,t);if("group"===e&&s.groupBase)return this.applyTemplate(s.groupBase,t)}const i="user"===e?this.userTemplate:this.groupTemplate;return i?this.applyTemplate(i,t):"user"===e?this.defaultUserBase:this.defaultGroupBase}userBase(e){return this.resolve("user",e)}groupBase(e){return this.resolve("group",e)}}export{BaseResolver};
@@ -1 +1 @@
1
- import{HttpError as e}from"../../lib/errors.js";import{SCHEMA_ERROR as t}from"./types.js";const r="application/scim+json";class ScimError extends e{constructor(e,t,r){super(t,e),this.name="ScimError",this.scimType=r}}const scimInvalidFilter=(e="Invalid filter")=>new ScimError(400,e,"invalidFilter"),scimInvalidPath=(e="Invalid path")=>new ScimError(400,e,"invalidPath"),scimInvalidValue=(e="Invalid value")=>new ScimError(400,e,"invalidValue"),scimInvalidSyntax=(e="Invalid syntax")=>new ScimError(400,e,"invalidSyntax"),scimNoTarget=(e="No target")=>new ScimError(400,e,"noTarget"),scimMutability=(e="Immutable attribute")=>new ScimError(400,e,"mutability"),scimUniqueness=(e="Uniqueness violation")=>new ScimError(409,e,"uniqueness"),scimTooMany=(e="Too many results")=>new ScimError(400,e,"tooMany"),scimNotFound=(e="Resource not found")=>new ScimError(404,e);function writeScimError(e,i,o,n){const s={schemas:[t],status:String(i),...n?{scimType:n}:{},detail:o};e.status(i).type(r).json(s)}function extractLdapCode(e){if(null==e)return;if("object"==typeof e&&"code"in e){const t=e.code;if("number"==typeof t)return t}const t=e instanceof Error?e.message:"string"==typeof e?e:"";return/noSuchObject|No such object|code:?\s*(32|0x20)/i.test(t)?32:/entryAlreadyExists|Already[_ ]?Exists|code:?\s*(68|0x44)/i.test(t)?68:/noSuchAttribute|No such attribute|code:?\s*(16|0x10)/i.test(t)?16:/sizeLimitExceeded|Size Limit Exceeded/i.test(t)?4:/undefinedAttributeType|Undefined attribute type|code:?\s*(17|0x11)/i.test(t)?17:/objectClassViolation|Object Class Violation|code:?\s*(65|0x41)/i.test(t)?65:void 0}function scimErrorFromException(r,i=500){const sanitize=e=>/\[authz-forbidden\]/.test(e)?"Token does not have permission on this branch":e,body=(e,r,i)=>({schemas:[t],status:String(e),...i?{scimType:i}:{},detail:r});if(r instanceof ScimError)return body(r.statusCode,sanitize(r.message),r.scimType);if(r instanceof e)return body(r.statusCode,sanitize(r.message));const o=r instanceof Error?r.message:String(r);if(/\[authz-forbidden\]/.test(o))return body(403,"Token does not have permission on this branch");const n=extractLdapCode(r);return 32===n?body(404,"Resource not found"):68===n?body(409,sanitize(o),"uniqueness"):17===n||65===n?body(400,`The directory rejected this write against its schema: ${sanitize(o)}`,"invalidValue"):body(i,sanitize(o)||"Internal error")}function writeScimErrorFromException(e,t,i=500){const o=scimErrorFromException(t,i);e.status(Number(o.status)).type(r).json(o)}const scimAsyncHandler=e=>(t,r,i)=>{Promise.resolve().then(()=>e(t,r,i)).catch(e=>{r.headersSent?i(e):writeScimErrorFromException(r,e)})};export{r as SCIM_CONTENT_TYPE,ScimError,extractLdapCode,scimAsyncHandler,scimErrorFromException,scimInvalidFilter,scimInvalidPath,scimInvalidSyntax,scimInvalidValue,scimMutability,scimNoTarget,scimNotFound,scimTooMany,scimUniqueness,writeScimError,writeScimErrorFromException};
1
+ import{HttpError as r}from"../../lib/errors.js";import{extractLdapCode as e}from"../../lib/ldapCodes.js";import{SCHEMA_ERROR as t}from"./types.js";const i="application/scim+json";class ScimError extends r{constructor(r,e,t){super(e,r),this.name="ScimError",this.scimType=t}}const scimInvalidFilter=(r="Invalid filter")=>new ScimError(400,r,"invalidFilter"),scimInvalidPath=(r="Invalid path")=>new ScimError(400,r,"invalidPath"),scimInvalidValue=(r="Invalid value")=>new ScimError(400,r,"invalidValue"),scimInvalidSyntax=(r="Invalid syntax")=>new ScimError(400,r,"invalidSyntax"),scimNoTarget=(r="No target")=>new ScimError(400,r,"noTarget"),scimMutability=(r="Immutable attribute")=>new ScimError(400,r,"mutability"),scimUniqueness=(r="Uniqueness violation")=>new ScimError(409,r,"uniqueness"),scimTooMany=(r="Too many results")=>new ScimError(400,r,"tooMany"),scimNotFound=(r="Resource not found")=>new ScimError(404,r);function writeScimError(r,e,o,n){const s={schemas:[t],status:String(e),...n?{scimType:n}:{},detail:o};r.status(e).type(i).json(s)}function scimErrorFromException(i,o=500){const sanitize=r=>/\[authz-forbidden\]/.test(r)?"Token does not have permission on this branch":r,body=(r,e,i)=>({schemas:[t],status:String(r),...i?{scimType:i}:{},detail:e});if(i instanceof ScimError)return body(i.statusCode,sanitize(i.message),i.scimType);if(i instanceof r)return body(i.statusCode,sanitize(i.message));const n=i instanceof Error?i.message:String(i);if(/\[authz-forbidden\]/.test(n))return body(403,"Token does not have permission on this branch");const s=e(i);return 32===s?body(404,"Resource not found"):68===s?body(409,sanitize(n),"uniqueness"):17===s||65===s?body(400,`The directory rejected this write against its schema: ${sanitize(n)}`,"invalidValue"):body(o,sanitize(n)||"Internal error")}function writeScimErrorFromException(r,e,t=500){const o=scimErrorFromException(e,t);r.status(Number(o.status)).type(i).json(o)}const scimAsyncHandler=r=>(e,t,i)=>{Promise.resolve().then(()=>r(e,t,i)).catch(r=>{t.headersSent?i(r):writeScimErrorFromException(t,r)})};export{i as SCIM_CONTENT_TYPE,ScimError,e as extractLdapCode,scimAsyncHandler,scimErrorFromException,scimInvalidFilter,scimInvalidPath,scimInvalidSyntax,scimInvalidValue,scimMutability,scimNoTarget,scimNotFound,scimTooMany,scimUniqueness,writeScimError,writeScimErrorFromException};
@@ -1 +1 @@
1
- import{escapeDnValue as t,isDummyMemberDn as e,launchHooksChained as s,validateDnValue as r,launchHooks as i,isChildOf as o,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as n}from"./types.js";import{withExternalId as c,mergeMapping as h,loadMappingFile as u,DEFAULT_GROUP_MAPPING as p,requiredLdapAttributes as m,ldapToScimGroup as l,scimGroupToLdap as d}from"./mapping.js";import{scimFilterToLdap as f}from"./filter.js";import{pagedSearch as g}from"./list.js";import{patchToModifyRequest as b}from"./patch.js";import{extractLdapCode as w,scimNotFound as y,ScimError as R,scimInvalidValue as _,scimUniqueness as $}from"./errors.js";class ScimGroups{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_group_rdn_attribute||"cn",this.objectClass=this.config.scim_group_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=this.config.scim_group_mapping||"";this.mapping=c(h(p,e?u(e):void 0),this.config.scim_group_external_id_attribute||"")}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"Group",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:"",lockValue:""}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const s=this.baseResolver.userBase(t),r=this.baseResolver.groupBase(t),i=this.scimPrefix,o=this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),a=this.config.scim_user_rdn_attribute||"uid",n=this.rdnAttribute,c=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(e(t,c))return;const h=/^([^=]+)=((?:\\.|[^,])+)/.exec(t);if(!h)return;const u=h[2].replace(/\\(.)/g,"$1"),p=t.toLowerCase();let m;s&&p.endsWith(s.toLowerCase())?m="User":r&&p.endsWith(r.toLowerCase())?m="Group":h[1]===a?m="User":h[1]===n&&(m="Group");const l={value:u};if(m&&(l.type=m,o)){const t="User"===m?"Users":"Groups";l.$ref=`${o.replace(/\/$/,"")}${i}/${t}/${encodeURIComponent(u)}`}return l}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:[...m(this.mapping),"member"]},s)}catch(t){if(32===w(t))throw y(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw y(`Group ${e} not found`);return l(r.searchEntries[0],this.mapping,this.ctx(t),this.buildMemberResolver(t))}async list(t,e){const s=this.baseResolver.groupBase(t),r=Math.max(1,e.startIndex||1),i=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let o,a=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=f(e.filter,this.mapping);t.idEquals?o=t.idEquals:a=`(&${a}${t.ldapFilter})`}if(o)try{const e=await this.get(t,o);return{schemas:[n],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof R&&404===t.statusCode)return{schemas:[n],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:c,totalResults:h}=await g({directory:this.ldap.forRequest(t),base:s,filter:a,attributes:[...m(this.mapping),"member"],startIndex:r,count:i,maxScanned:this.maxScanned}),u=this.buildMemberResolver(t),p=c.map(e=>l(e,this.mapping,this.ctx(t),u));return{schemas:[n],totalResults:h,startIndex:r,itemsPerPage:p.length,Resources:p}}async create(e,o){if(!o.displayName)throw _("displayName is required");const a=(await s(this.hooks.scimgroupcreate,[o,e]))[0],{rdn:n,attributes:c}=d(a,this.mapping,this.objectClass);if(!n)throw _("displayName is required");r(n,this.rdnAttribute),c[this.rdnAttribute]=n;const h=[];if(a.members&&Array.isArray(a.members))for(const t of a.members){if(!t||!t.value)continue;const s=await this.users.resolveRef(e,t.value);s&&h.push(s)}if(0===h.length){const t=this.config.group_dummy_user||"cn=fakeuser";h.push(t)}c.member=h;const u=this.baseResolver.groupBase(e),p=`${this.rdnAttribute}=${t(n)},${u}`;try{await this.ldap.forRequest(e).add(p,c)}catch(t){if(68===w(t))throw $(`Group ${n} already exists`);throw t}const m=await this.get(e,n);return i(this.hooks.scimgroupcreatedone,m),m}async replace(t,e,r){await this.get(t,e);const o=(await s(this.hooks.scimgroupupdate,[e,r,t]))[1],{attributes:a}=d(o,this.mapping,this.objectClass),n=[];if(o.members&&Array.isArray(o.members))for(const e of o.members){if(!e||!e.value)continue;const s=await this.users.resolveRef(t,e.value);s&&n.push(s)}if(0===n.length){const t=this.config.group_dummy_user||"cn=fakeuser";n.push(t)}const c=this.dnForId(e,t),h={replace:{}};for(const[t,e]of Object.entries(a))"objectClass"!==t&&t!==this.rdnAttribute&&(h.replace[t]=e);h.replace.member=n,await this.ldap.forRequest(t).modify(c,h);const u=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,u),u}async patch(t,e,s){const r=this.dnForId(e,t),o=await this.currentEntry(t,e),a=await b(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e),current:o});if(a.delete&&!Array.isArray(a.delete)&&"member"in a.delete){const t=this.config.group_dummy_user||"cn=fakeuser";delete a.delete.member,0===Object.keys(a.delete).length&&delete a.delete,a.replace||(a.replace={}),a.replace.member=[t]}await this.ldap.forRequest(t).modify(r,a);const n=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,n),n}async currentEntry(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:[...m(this.mapping),"member"]},s)}catch(t){if(32===w(t))throw y(`Group ${e} not found`);throw t}if(!r.searchEntries?.length)throw y(`Group ${e} not found`);return r.searchEntries[0]}async delete(t,e){await this.get(t,e);const r=(await s(this.hooks.scimgroupdelete,[e,t]))[0],o=this.dnForId(r,t);await this.ldap.forRequest(t).delete(o),i(this.hooks.scimgroupdeletedone,r)}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e);if(s.includes("=")&&s.includes(","))return o(s,r)?s:void 0;const i=`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==w(t))throw t}try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},r);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==w(t))throw t}}get groupMapping(){return this.mapping}}export{ScimGroups};
1
+ import{escapeDnValue as t,isDummyMemberDn as e,launchHooksChained as s,validateDnValue as r,launchHooks as i,isChildOf as o,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as n}from"./types.js";import{withExternalId as c,mergeMapping as h,loadMappingFile as u,DEFAULT_GROUP_MAPPING as p,requiredLdapAttributes as m,ldapToScimGroup as l,scimGroupToLdap as d}from"./mapping.js";import{scimFilterToLdap as f}from"./filter.js";import{pagedSearch as g}from"./list.js";import{patchToModifyRequest as b}from"./patch.js";import{scimNotFound as w,ScimError as y,scimInvalidValue as R,scimUniqueness as _}from"./errors.js";import{extractLdapCode as $}from"../../lib/ldapCodes.js";class ScimGroups{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_group_rdn_attribute||"cn",this.objectClass=this.config.scim_group_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=this.config.scim_group_mapping||"";this.mapping=c(h(p,e?u(e):void 0),this.config.scim_group_external_id_attribute||"")}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"Group",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:"",lockValue:""}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const s=this.baseResolver.userBase(t),r=this.baseResolver.groupBase(t),i=this.scimPrefix,o=this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),a=this.config.scim_user_rdn_attribute||"uid",n=this.rdnAttribute,c=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(e(t,c))return;const h=/^([^=]+)=((?:\\.|[^,])+)/.exec(t);if(!h)return;const u=h[2].replace(/\\(.)/g,"$1"),p=t.toLowerCase();let m;s&&p.endsWith(s.toLowerCase())?m="User":r&&p.endsWith(r.toLowerCase())?m="Group":h[1]===a?m="User":h[1]===n&&(m="Group");const l={value:u};if(m&&(l.type=m,o)){const t="User"===m?"Users":"Groups";l.$ref=`${o.replace(/\/$/,"")}${i}/${t}/${encodeURIComponent(u)}`}return l}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:[...m(this.mapping),"member"]},s)}catch(t){if(32===$(t))throw w(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw w(`Group ${e} not found`);return l(r.searchEntries[0],this.mapping,this.ctx(t),this.buildMemberResolver(t))}async list(t,e){const s=this.baseResolver.groupBase(t),r=Math.max(1,e.startIndex||1),i=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let o,a=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=f(e.filter,this.mapping);t.idEquals?o=t.idEquals:a=`(&${a}${t.ldapFilter})`}if(o)try{const e=await this.get(t,o);return{schemas:[n],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof y&&404===t.statusCode)return{schemas:[n],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:c,totalResults:h}=await g({directory:this.ldap.forRequest(t),base:s,filter:a,attributes:[...m(this.mapping),"member"],startIndex:r,count:i,maxScanned:this.maxScanned}),u=this.buildMemberResolver(t),p=c.map(e=>l(e,this.mapping,this.ctx(t),u));return{schemas:[n],totalResults:h,startIndex:r,itemsPerPage:p.length,Resources:p}}async create(e,o){if(!o.displayName)throw R("displayName is required");const a=(await s(this.hooks.scimgroupcreate,[o,e]))[0],{rdn:n,attributes:c}=d(a,this.mapping,this.objectClass);if(!n)throw R("displayName is required");r(n,this.rdnAttribute),c[this.rdnAttribute]=n;const h=[];if(a.members&&Array.isArray(a.members))for(const t of a.members){if(!t||!t.value)continue;const s=await this.users.resolveRef(e,t.value);s&&h.push(s)}if(0===h.length){const t=this.config.group_dummy_user||"cn=fakeuser";h.push(t)}c.member=h;const u=this.baseResolver.groupBase(e),p=`${this.rdnAttribute}=${t(n)},${u}`;try{await this.ldap.forRequest(e).add(p,c)}catch(t){if(68===$(t))throw _(`Group ${n} already exists`);throw t}const m=await this.get(e,n);return i(this.hooks.scimgroupcreatedone,m),m}async replace(t,e,r){await this.get(t,e);const o=(await s(this.hooks.scimgroupupdate,[e,r,t]))[1],{attributes:a}=d(o,this.mapping,this.objectClass),n=[];if(o.members&&Array.isArray(o.members))for(const e of o.members){if(!e||!e.value)continue;const s=await this.users.resolveRef(t,e.value);s&&n.push(s)}if(0===n.length){const t=this.config.group_dummy_user||"cn=fakeuser";n.push(t)}const c=this.dnForId(e,t),h={replace:{}};for(const[t,e]of Object.entries(a))"objectClass"!==t&&t!==this.rdnAttribute&&(h.replace[t]=e);h.replace.member=n,await this.ldap.forRequest(t).modify(c,h);const u=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,u),u}async patch(t,e,s){const r=this.dnForId(e,t),o=await this.currentEntry(t,e),a=await b(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e),current:o});if(a.delete&&!Array.isArray(a.delete)&&"member"in a.delete){const t=this.config.group_dummy_user||"cn=fakeuser";delete a.delete.member,0===Object.keys(a.delete).length&&delete a.delete,a.replace||(a.replace={}),a.replace.member=[t]}await this.ldap.forRequest(t).modify(r,a);const n=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,n),n}async currentEntry(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:[...m(this.mapping),"member"]},s)}catch(t){if(32===$(t))throw w(`Group ${e} not found`);throw t}if(!r.searchEntries?.length)throw w(`Group ${e} not found`);return r.searchEntries[0]}async delete(t,e){await this.get(t,e);const r=(await s(this.hooks.scimgroupdelete,[e,t]))[0],o=this.dnForId(r,t);await this.ldap.forRequest(t).delete(o),i(this.hooks.scimgroupdeletedone,r)}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e);if(s.includes("=")&&s.includes(","))return o(s,r)?s:void 0;const i=`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==$(t))throw t}try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},r);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==$(t))throw t}}get groupMapping(){return this.mapping}}export{ScimGroups};
@@ -1 +1 @@
1
- import{scimTooMany as t,ScimError as e,extractLdapCode as r}from"./errors.js";async function pagedSearch(a){const i=Math.max(0,a.startIndex-1),s=[];let o=0;try{const e=await a.directory.search({filter:a.filter,scope:"sub",paged:{pageSize:(n=a.count,Math.min(500,Math.max(n,100)))},attributes:a.attributes,sizeLimit:0},a.base);for await(const r of e)for(const e of r.searchEntries||[])if(o>=i&&s.length<a.count&&s.push(e),o++,o>a.maxScanned)throw t(`Query matched more than ${a.maxScanned} resources; narrow it with a filter or raise --scim-max-scanned`)}catch(a){if(a instanceof e)throw a;const i=r(a);if(32===i)return{entries:[],totalResults:0};if(4===i)throw t("The directory refused the query with sizeLimitExceeded; narrow it with a filter or raise the directory size limit");throw a}var n;return{entries:s,totalResults:o}}export{pagedSearch};
1
+ import{scimTooMany as t,ScimError as e}from"./errors.js";import{extractLdapCode as r}from"../../lib/ldapCodes.js";async function pagedSearch(a){const i=Math.max(0,a.startIndex-1),s=[];let o=0;try{const e=await a.directory.search({filter:a.filter,scope:"sub",paged:{pageSize:(n=a.count,Math.min(500,Math.max(n,100)))},attributes:a.attributes,sizeLimit:0},a.base);for await(const r of e)for(const e of r.searchEntries||[])if(o>=i&&s.length<a.count&&s.push(e),o++,o>a.maxScanned)throw t(`Query matched more than ${a.maxScanned} resources; narrow it with a filter or raise --scim-max-scanned`)}catch(a){if(a instanceof e)throw a;const i=r(a);if(32===i)return{entries:[],totalResults:0};if(4===i)throw t("The directory refused the query with sizeLimitExceeded; narrow it with a filter or raise the directory size limit");throw a}var n;return{entries:s,totalResults:o}}export{pagedSearch};
@@ -1 +1 @@
1
- import{escapeDnValue as t,launchHooksChained as e,validateDnValue as s,launchHooks as i,isChildOf as r,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as o}from"./types.js";import{resolveLockConfig as c,mergeMapping as n,loadMappingFile as h,DEFAULT_USER_MAPPING as u,requiredLdapAttributes as l,ldapToScimUser as d,scimUserToLdap as p}from"./mapping.js";import{scimFilterToLdap as m}from"./filter.js";import{pagedSearch as f}from"./list.js";import{patchToModifyRequest as b}from"./patch.js";import{extractLdapCode as g,scimNotFound as w,ScimError as k,scimInvalidValue as A,scimUniqueness as y}from"./errors.js";class ScimUsers{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_user_rdn_attribute||"uid",this.objectClass=this.config.scim_user_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=c(this.config.scim_user_lock_attribute||"",this.config.scim_user_lock_value||"");this.lockAttribute=e.attribute,this.lockValue=e.value;const s=this.config.scim_user_mapping||"";this.mapping=n(u,s?h(s):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"User",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:this.lockAttribute,lockValue:this.lockValue}}ldapAttributes(){return l(this.mapping,[this.lockAttribute])}dnForId(e,s){const i=this.baseResolver.userBase(s);return`${this.rdnAttribute}=${t(e)},${i}`}async get(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===g(t))throw w(`User ${e} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw w(`User ${e} not found`);return d(i.searchEntries[0],this.mapping,this.ctx(t))}async list(t,e){const s=this.baseResolver.userBase(t),i=Math.max(1,e.startIndex||1),r=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let a,c=`(${this.objectClass.includes("inetOrgPerson")?"objectClass=inetOrgPerson":`objectClass=${this.objectClass[0]}`})`;if(e.filter){const t=m(e.filter,this.mapping,{lockAttribute:this.lockAttribute,supportsActive:!0});t.idEquals?a=t.idEquals:c=`(&${c}${t.ldapFilter})`}if(a)try{const e=await this.get(t,a);return{schemas:[o],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof k&&404===t.statusCode)return{schemas:[o],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:n,totalResults:h}=await f({directory:this.ldap.forRequest(t),base:s,filter:c,attributes:this.ldapAttributes(),startIndex:i,count:r,maxScanned:this.maxScanned});e.sortBy,e.sortOrder;const u=n.map(e=>d(e,this.mapping,this.ctx(t)));return{schemas:[o],totalResults:h,startIndex:i,itemsPerPage:u.length,Resources:u}}async create(r,a){if(!a.userName)throw A("userName is required");const o=(await e(this.hooks.scimusercreate,[a,r]))[0],{rdn:c,attributes:n}=p(o,this.mapping,this.ctx(r),this.objectClass);if(!c)throw A("userName is required");s(c,this.rdnAttribute),n[this.rdnAttribute]=c;const h=this.baseResolver.userBase(r),u=`${this.rdnAttribute}=${t(c)},${h}`;try{await this.ldap.forRequest(r).add(u,n)}catch(t){if(68===g(t))throw y(`User ${c} already exists`);if(void 0!==n[this.lockAttribute])throw this.lockSchemaError(t);throw t}const l=await this.get(r,c);return i(this.hooks.scimusercreatedone,l),l}async replace(t,s,r){const a=this.dnForId(s,t),o=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},a);if(!o.searchEntries?.length)throw w(`User ${s} not found`);const c=o.searchEntries[0],n=(await e(this.hooks.scimuserupdate,[s,r,t]))[1],{attributes:h,active:u}=p(n,this.mapping,this.ctx(t),this.objectClass),l={replace:{},delete:[]},d=new Set(["objectClass","entryUUID","createTimestamp","modifyTimestamp",this.rdnAttribute,this.lockAttribute]),hasAttrValue=t=>null!=t&&("string"==typeof t?t.length>0:!Array.isArray(t)||t.length>0&&t.some(t=>null!=t));for(const t of this.ldapAttributes())d.has(t)||(null!=h[t]?l.replace[t]=h[t]:hasAttrValue(c[t])&&l.delete.push(t));if(!1===u?l.replace[this.lockAttribute]=this.lockValue:!0===u&&hasAttrValue(c[this.lockAttribute])&&l.delete.push(this.lockAttribute),0===Object.keys(l.replace||{}).length&&delete l.replace,0===l.delete.length&&delete l.delete,l.replace||l.delete)try{await this.ldap.forRequest(t).modify(a,l)}catch(t){if(void 0!==u)throw this.lockSchemaError(t);if(16!==g(t))throw t;throw new k(409,`${a} changed while this PUT was being applied; nothing was written. Retry the request.`)}const m=await this.get(t,s);return i(this.hooks.scimuserupdatedone,s,m),m}async patch(t,e,s){const r=this.dnForId(e,t),a=await this.currentEntry(t,e),o=await b(s,{mapping:this.mapping,lockAttribute:this.lockAttribute,lockValue:this.lockValue,supportsActive:!0,current:a});try{await this.ldap.forRequest(t).modify(r,o)}catch(t){if(function(t,e){if(t.replace&&e in t.replace)return!0;if(t.add&&e in t.add)return!0;const s=t.delete;return Array.isArray(s)?s.includes(e):!(!s||"object"!=typeof s)&&e in s}(o,this.lockAttribute))throw this.lockSchemaError(t);throw t}const c=await this.get(t,e);return i(this.hooks.scimuserupdatedone,e,c),c}lockSchemaError(t){const e=g(t);if(17!==e&&65!==e)return t;const s=t instanceof Error?t.message:String(t);return A(`The directory rejected '${this.lockAttribute}', the attribute backing 'active': ${s}. Point --scim-user-lock-attribute and --scim-user-lock-value at an attribute this directory defines (nsAccountLock / TRUE on 389-ds); the default needs the ppolicy overlay.`)}async currentEntry(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===g(t))throw w(`User ${e} not found`);throw t}if(!i.searchEntries?.length)throw w(`User ${e} not found`);return i.searchEntries[0]}async delete(t,s){await this.get(t,s);const r=(await e(this.hooks.scimuserdelete,[s,t]))[0],a=this.dnForId(r,t);await this.ldap.forRequest(t).delete(a),i(this.hooks.scimuserdeletedone,r)}async resolveRef(e,s){if(!s)return;const i=this.baseResolver.userBase(e);if(s.includes("=")&&s.includes(","))return r(s,i)?s:void 0;const o=`${this.rdnAttribute}=${t(s)},${i}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},o);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==g(t))throw t}try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==g(t))throw t}}get userMapping(){return this.mapping}}export{ScimUsers};
1
+ import{escapeDnValue as t,launchHooksChained as e,validateDnValue as s,launchHooks as i,isChildOf as r,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as o}from"./types.js";import{resolveLockConfig as c,mergeMapping as n,loadMappingFile as h,DEFAULT_USER_MAPPING as u,requiredLdapAttributes as l,ldapToScimUser as d,scimUserToLdap as p}from"./mapping.js";import{scimFilterToLdap as m}from"./filter.js";import{pagedSearch as f}from"./list.js";import{patchToModifyRequest as b}from"./patch.js";import{scimNotFound as g,ScimError as w,scimInvalidValue as k,scimUniqueness as A}from"./errors.js";import{extractLdapCode as y}from"../../lib/ldapCodes.js";class ScimUsers{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_user_rdn_attribute||"uid",this.objectClass=this.config.scim_user_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=c(this.config.scim_user_lock_attribute||"",this.config.scim_user_lock_value||"");this.lockAttribute=e.attribute,this.lockValue=e.value;const s=this.config.scim_user_mapping||"";this.mapping=n(u,s?h(s):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"User",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:this.lockAttribute,lockValue:this.lockValue}}ldapAttributes(){return l(this.mapping,[this.lockAttribute])}dnForId(e,s){const i=this.baseResolver.userBase(s);return`${this.rdnAttribute}=${t(e)},${i}`}async get(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===y(t))throw g(`User ${e} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw g(`User ${e} not found`);return d(i.searchEntries[0],this.mapping,this.ctx(t))}async list(t,e){const s=this.baseResolver.userBase(t),i=Math.max(1,e.startIndex||1),r=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let a,c=`(${this.objectClass.includes("inetOrgPerson")?"objectClass=inetOrgPerson":`objectClass=${this.objectClass[0]}`})`;if(e.filter){const t=m(e.filter,this.mapping,{lockAttribute:this.lockAttribute,supportsActive:!0});t.idEquals?a=t.idEquals:c=`(&${c}${t.ldapFilter})`}if(a)try{const e=await this.get(t,a);return{schemas:[o],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof w&&404===t.statusCode)return{schemas:[o],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:n,totalResults:h}=await f({directory:this.ldap.forRequest(t),base:s,filter:c,attributes:this.ldapAttributes(),startIndex:i,count:r,maxScanned:this.maxScanned});e.sortBy,e.sortOrder;const u=n.map(e=>d(e,this.mapping,this.ctx(t)));return{schemas:[o],totalResults:h,startIndex:i,itemsPerPage:u.length,Resources:u}}async create(r,a){if(!a.userName)throw k("userName is required");const o=(await e(this.hooks.scimusercreate,[a,r]))[0],{rdn:c,attributes:n}=p(o,this.mapping,this.ctx(r),this.objectClass);if(!c)throw k("userName is required");s(c,this.rdnAttribute),n[this.rdnAttribute]=c;const h=this.baseResolver.userBase(r),u=`${this.rdnAttribute}=${t(c)},${h}`;try{await this.ldap.forRequest(r).add(u,n)}catch(t){if(68===y(t))throw A(`User ${c} already exists`);if(void 0!==n[this.lockAttribute])throw this.lockSchemaError(t);throw t}const l=await this.get(r,c);return i(this.hooks.scimusercreatedone,l),l}async replace(t,s,r){const a=this.dnForId(s,t),o=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},a);if(!o.searchEntries?.length)throw g(`User ${s} not found`);const c=o.searchEntries[0],n=(await e(this.hooks.scimuserupdate,[s,r,t]))[1],{attributes:h,active:u}=p(n,this.mapping,this.ctx(t),this.objectClass),l={replace:{},delete:[]},d=new Set(["objectClass","entryUUID","createTimestamp","modifyTimestamp",this.rdnAttribute,this.lockAttribute]),hasAttrValue=t=>null!=t&&("string"==typeof t?t.length>0:!Array.isArray(t)||t.length>0&&t.some(t=>null!=t));for(const t of this.ldapAttributes())d.has(t)||(null!=h[t]?l.replace[t]=h[t]:hasAttrValue(c[t])&&l.delete.push(t));if(!1===u?l.replace[this.lockAttribute]=this.lockValue:!0===u&&hasAttrValue(c[this.lockAttribute])&&l.delete.push(this.lockAttribute),0===Object.keys(l.replace||{}).length&&delete l.replace,0===l.delete.length&&delete l.delete,l.replace||l.delete)try{await this.ldap.forRequest(t).modify(a,l)}catch(t){if(void 0!==u)throw this.lockSchemaError(t);if(16!==y(t))throw t;throw new w(409,`${a} changed while this PUT was being applied; nothing was written. Retry the request.`)}const m=await this.get(t,s);return i(this.hooks.scimuserupdatedone,s,m),m}async patch(t,e,s){const r=this.dnForId(e,t),a=await this.currentEntry(t,e),o=await b(s,{mapping:this.mapping,lockAttribute:this.lockAttribute,lockValue:this.lockValue,supportsActive:!0,current:a});try{await this.ldap.forRequest(t).modify(r,o)}catch(t){if(function(t,e){if(t.replace&&e in t.replace)return!0;if(t.add&&e in t.add)return!0;const s=t.delete;return Array.isArray(s)?s.includes(e):!(!s||"object"!=typeof s)&&e in s}(o,this.lockAttribute))throw this.lockSchemaError(t);throw t}const c=await this.get(t,e);return i(this.hooks.scimuserupdatedone,e,c),c}lockSchemaError(t){const e=y(t);if(17!==e&&65!==e)return t;const s=t instanceof Error?t.message:String(t);return k(`The directory rejected '${this.lockAttribute}', the attribute backing 'active': ${s}. Point --scim-user-lock-attribute and --scim-user-lock-value at an attribute this directory defines (nsAccountLock / TRUE on 389-ds); the default needs the ppolicy overlay.`)}async currentEntry(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===y(t))throw g(`User ${e} not found`);throw t}if(!i.searchEntries?.length)throw g(`User ${e} not found`);return i.searchEntries[0]}async delete(t,s){await this.get(t,s);const r=(await e(this.hooks.scimuserdelete,[s,t]))[0],a=this.dnForId(r,t);await this.ldap.forRequest(t).delete(a),i(this.hooks.scimuserdeletedone,r)}async resolveRef(e,s){if(!s)return;const i=this.baseResolver.userBase(e);if(s.includes("=")&&s.includes(","))return r(s,i)?s:void 0;const o=`${this.rdnAttribute}=${t(s)},${i}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},o);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==y(t))throw t}try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==y(t))throw t}}get userMapping(){return this.mapping}}export{ScimUsers};
@@ -0,0 +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};
@@ -0,0 +1 @@
1
+ import{createHash as e}from"node:crypto";import t from"../../lib/storage/store.js";const errorText=e=>e instanceof Error?e.message:JSON.stringify(e);class LdapStore extends t{constructor(e,t,s){super(e.logger),this.name="storage/ldap",this.server=e,this.base=t,this.objectClass=s}dn(t){return`cn=${e("sha256").update(t).digest("hex")},${this.base}`}static encode(e,t){return`${t.deadline} ${LdapStore.readable(e)}\n${t.value}`}static decode(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 load(e){try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:["description"]},this.dn(e)),s=t.searchEntries?.[0];return s?LdapStore.decode(s.description):null}catch(e){return 32!==e?.code&&this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(e)}`),null}}async save(e,t){const s=this.dn(e),r=LdapStore.encode(e,t);try{await this.server.ldap.add(s,{objectClass:["top",this.objectClass],cn:s.slice(3,s.indexOf(",")),description:r})}catch(e){if(68!==e?.code)throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(e)}`),e;await this.server.ldap.modify(s,{replace:{description:r}})}}async drop(e){await this.server.ldap.delete(this.dn(e)).catch(e=>{32!==e?.code&&this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(e)}`)})}async sweep(){const e=Date.now();let t=0;const s=await this.server.ldap.search({paged:!1,scope:"one",filter:`(objectClass=${this.objectClass})`,attributes:["description"]},this.base);for(const r of s.searchEntries||[]){const s=LdapStore.decode(r.description);null!==s&&s.deadline>e||(await this.server.ldap.delete(String(r.dn)).catch(()=>{}),t++)}return t}}export{LdapStore as default};
@@ -0,0 +1 @@
1
+ import e from"../abstract/plugin.js";import r from"./storage/file.js";import t from"./storage/ldap.js";let s;class Storage extends e{constructor(e){if(super(e),this.name="storage",s)throw new Error(`storage: an instance is already loaded, keeping its records in ${s.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 o=e.config.storage_backend||"";switch(o){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 t(e,r,e.config.storage_ldap_object_class||"applicationProcess");break}case"file":{const t=e.config.storage_file_directory;if(!t)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,t,e.config.storage_sweep_interval||600);break}default:throw new Error(o?`storage: unknown backend "${o}". Known: ldap, file.`:"storage: --storage-backend says which sub-plugin keeps the records. Known: ldap, file.")}s=this,this.logger.info(`storage: records kept by ${this.store.name}`),this.store.startSweeping(e.config.storage_sweep_interval||0)}static release(e){s===e&&(s=void 0)}}export{Storage as default};
@@ -263,6 +263,13 @@ export default abstract class LdapFlat extends DmPlugin {
263
263
  /**
264
264
  * Tell whether an entry already uses this RDN value in the branch.
265
265
  *
266
+ * Asked as the server, not as the caller. A uniqueness check has to see
267
+ * the whole branch to answer at all: a caller who cannot read
268
+ * `uid=jdoe` must not be handed `jdoe` as a free identifier, which is
269
+ * what a request-bound read would answer. What it can leak is bounded by
270
+ * the same reasoning — whether a name is taken, through the identifier
271
+ * the server then generates.
272
+ *
266
273
  * @param value candidate RDN value
267
274
  * @returns true when the DN is taken
268
275
  */
@@ -329,7 +336,7 @@ export default abstract class LdapFlat extends DmPlugin {
329
336
  */
330
337
  protected resolveDn(id: string): string;
331
338
  addEntry(id: string, additional?: AttributesList, req?: Request): Promise<boolean>;
332
- modifyEntry(id: string, changes: ModifyRequest): Promise<boolean>;
339
+ modifyEntry(id: string, changes: ModifyRequest, req?: Request): Promise<boolean>;
333
340
  /**
334
341
  * Change the RDN of an entry.
335
342
  *
@@ -346,7 +353,7 @@ export default abstract class LdapFlat extends DmPlugin {
346
353
  * @returns true when the directory accepted the rename
347
354
  */
348
355
  renameEntry(id: string, newId: string, req?: Request): Promise<boolean>;
349
- deleteEntry(id: string): Promise<boolean>;
356
+ deleteEntry(id: string, req?: Request): Promise<boolean>;
350
357
  /**
351
358
  * Move entry to a different organization
352
359
  * Updates department link and path attributes
@@ -363,9 +370,10 @@ export default abstract class LdapFlat extends DmPlugin {
363
370
  /**
364
371
  * List entries from LDAP
365
372
  */
366
- listEntries({ filter, attributes, }: {
373
+ listEntries({ filter, attributes, req, }: {
367
374
  filter?: string;
368
375
  attributes?: string[];
376
+ req?: Request;
369
377
  }): Promise<LdapList>;
370
378
  searchEntriesByName(name: string, partial?: boolean, attrs?: string[]): Promise<LdapList>;
371
379
  validateNewEntry(dn: string, entry: AttributesList): Promise<boolean>;
@@ -27,6 +27,26 @@ export default abstract class DmPlugin {
27
27
  roles?: Role[] | undefined;
28
28
  api?(app: Express): MaybePromise<void>;
29
29
  getConfigApiData?(): Record<string, unknown> | undefined;
30
+ /**
31
+ * Called once every plugin is loaded, before the first request.
32
+ *
33
+ * A constructor and `api()` run while the others are still loading, so
34
+ * neither can see what a configuration amounts to as a whole — which
35
+ * authenticators are loaded beside this one, whether two plugins claim the
36
+ * same thing. This is where a plugin says, once, that a combination is
37
+ * ambiguous: the check that catches a mispairing before a request does
38
+ * rather than after.
39
+ */
40
+ afterLoad?(): void;
41
+ /**
42
+ * Called once every plugin is loaded, before `afterLoad`: refuse a
43
+ * configuration this plugin cannot serve.
44
+ *
45
+ * `afterLoad` is an opinion, logged and served anyway. Throwing here stops
46
+ * the server from starting — for a setting that would otherwise make the
47
+ * plugin answer on behalf of something that is not there.
48
+ */
49
+ assertComposition?(): void;
30
50
  abstract name: string;
31
51
  /**
32
52
  * Constructor
@@ -52,6 +52,12 @@ export declare class DM {
52
52
  */
53
53
  private _authState;
54
54
  private _errorMiddlewareSetup;
55
+ /**
56
+ * Whether the plugins of the configuration are loaded and their
57
+ * composition checked. A plugin registered after that — a server
58
+ * assembled by hand — is checked on its own as it arrives.
59
+ */
60
+ private _composed;
55
61
  private _errorMiddleware?;
56
62
  constructor();
57
63
  /**
@@ -128,16 +134,14 @@ export declare class DM {
128
134
  */
129
135
  dispatchAuth(req: Request, res: Response, next: () => void): void;
130
136
  /**
131
- * Path prefixes claimed by scoped authentication plugins.
137
+ * Let every loaded plugin look at the configuration as a whole.
132
138
  *
133
- * An unscoped plugin subtracts these from what it guards, so a
134
- * configuration can say "OIDC on /api/admin, token everywhere else"
135
- * without anyone maintaining the list of everywhere else.
136
- *
137
- * @param except name of the plugin asking, excluded from the result
138
- * @returns the prefixes other authentication plugins guard
139
+ * Constructors and `api()` run while the rest is still loading, so a
140
+ * plugin cannot see there what it is loaded *beside*. A failure here is
141
+ * reported and does not stop the server: it is a plugin's opinion about a
142
+ * configuration, not a condition for serving it.
139
143
  */
140
- claimedAuthPrefixes(except?: string): string[];
144
+ afterLoad(): void;
141
145
  /**
142
146
  * List the routes no authentication plugin guards, once every plugin is
143
147
  * loaded.
@@ -156,6 +160,44 @@ export declare class DM {
156
160
  setupErrorMiddleware(): void;
157
161
  run(): Promise<void>;
158
162
  stop(): void;
163
+ /**
164
+ * The module part of a `--plugin` entry.
165
+ *
166
+ * An entry is `module[:name[:{overrides}]]`, and the module is what
167
+ * decides where it loads from — and, since the priority list names
168
+ * modules, which pass loads it. One rule, used by the loader and by that
169
+ * list, rather than two that can drift.
170
+ *
171
+ * @param entry a `--plugin` value
172
+ * @returns the module it names
173
+ */
174
+ private static moduleOf;
175
+ /**
176
+ * Where a module name resolves, as `import()` would resolve it.
177
+ *
178
+ * `core/x` is this build's `plugins/x.js`, and a relative path is relative
179
+ * to this file — so the two spellings of one plugin compare equal, which
180
+ * is what lets the priority list recognise a plugin configured by path.
181
+ *
182
+ * @param module module part of a `--plugin` entry
183
+ * @returns an absolute path for what can be resolved, the name itself
184
+ * otherwise (a package, resolved by node)
185
+ */
186
+ private resolveModule;
159
187
  loadPlugin(pluginName: string): Promise<boolean>;
188
+ /**
189
+ * Refuse a plugin whose arrival makes the authorization composition
190
+ * ambiguous, before any of it is registered.
191
+ *
192
+ * The configuration's plugins are checked together once loaded; a plugin
193
+ * registered afterwards — the way an embedding host assembles a server —
194
+ * would otherwise never be, and two branch models registered by hand would
195
+ * compose as the AND the startup check exists to refuse. Checked before
196
+ * its routes and hooks exist, so a refused plugin leaves nothing behind.
197
+ *
198
+ * @param obj the plugin about to be registered
199
+ * @throws Error when the composition would be refused at startup
200
+ */
201
+ private admitToComposition;
160
202
  registerPlugin(pluginName: string, obj: DmPlugin, name?: string): Promise<boolean>;
161
203
  }
@@ -50,6 +50,17 @@ export interface Config {
50
50
  groups_allow_unexistent_members?: boolean;
51
51
  group_dummy_user?: string;
52
52
  group_schema?: string;
53
+ /**
54
+ * Keyed storage, shared by whoever needs to keep something with a
55
+ * deadline. `storage_backend` names the sub-plugin that keeps it; the rest
56
+ * configures that one.
57
+ */
58
+ storage_backend?: string;
59
+ storage_sweep_interval?: number;
60
+ storage_ldap_base?: string;
61
+ storage_ldap_object_class?: string;
62
+ storage_file_directory?: string;
63
+ bcl_retention?: number;
53
64
  ldap_top_organization?: string;
54
65
  ldap_organization_class?: string[];
55
66
  ldap_organization_link_attribute?: string;
@@ -74,6 +85,7 @@ export interface Config {
74
85
  static_name?: string;
75
86
  auth_path_prefix?: string | string[];
76
87
  llng_ini?: string;
88
+ llng_username_header?: string;
77
89
  auth_token?: string[];
78
90
  auth_totp?: string[];
79
91
  auth_totp_window?: number;
@@ -81,10 +93,33 @@ export interface Config {
81
93
  auth_hmac?: string[];
82
94
  auth_hmac_window?: number;
83
95
  oidc_server?: string;
96
+ oidc_username_claim?: string;
84
97
  oidc_client_id?: string;
85
98
  oidc_client_secret?: string;
86
99
  base_url?: string;
87
100
  authz_per_route?: string[];
101
+ /**
102
+ * Judge an attached entry by the organization it hangs off rather than by
103
+ * the branch it is stored in, and filter listings accordingly. Off by
104
+ * default: where a branch is a tenant rather than a department, opening a
105
+ * listing across branches is opening a door between customers.
106
+ *
107
+ * Honoured by the plugins built on `AuthzBase` — `authzPerBranch` and
108
+ * `authzLinid1`. `authzDynamic` extends `AuthBase` and registers its own
109
+ * hooks, so it never sees the filter and this option does nothing there.
110
+ */
111
+ authz_filter_attached_entries?: boolean;
112
+ authz_unresolved_user?: string;
113
+ authz_identity?: string;
114
+ /**
115
+ * The authentication plugins, by instance name, whose requests an
116
+ * authorization plugin judges. Empty means every one of them.
117
+ */
118
+ authz_for?: string[];
119
+ /** Let two LDAP judges whose populations overlap compose as an AND. */
120
+ authz_combine?: boolean;
121
+ /** The branch-level plugin `authzScope` answers with, by instance name. */
122
+ authz_scope_source?: string;
88
123
  authz_per_branch_config?: AuthConfig;
89
124
  authz_per_branch_cache_ttl?: number;
90
125
  authz_dynamic_base?: string;
@@ -93,6 +128,7 @@ export interface Config {
93
128
  authz_dynamic_config_attribute?: string;
94
129
  authz_dynamic_tenant_attribute?: string;
95
130
  authz_dynamic_reload_endpoint?: boolean;
131
+ authz_dynamic_bypass?: string[];
96
132
  authz_local_admin_attribute?: string;
97
133
  rate_limit_window_ms?: number;
98
134
  rate_limit_max?: number;
@@ -9,6 +9,18 @@ import type { ChangesToNotify } from './plugins/ldap/onChange';
9
9
  import * as utils from './lib/utils';
10
10
  export type MaybePromise<T> = Promise<T> | T;
11
11
  export type ChainedHook<T> = (arg: T) => MaybePromise<T>;
12
+ /**
13
+ * What identifies an OpenID Connect session on both sides of a Back-Channel
14
+ * Logout: the issuer, plus whichever of `sid` and `sub` the provider sends.
15
+ * At least one of the two is present — the specification requires it, and
16
+ * `express-openid-connect` refuses the token otherwise.
17
+ */
18
+ export interface OidcSessionClaims {
19
+ iss: string;
20
+ sid?: string;
21
+ sub?: string;
22
+ }
23
+ export type OidcLogoutToken = OidcSessionClaims;
12
24
  export type VoidHook<T extends unknown[]> = (...args: T) => MaybePromise<void>;
13
25
  export type OtherHook = Function;
14
26
  export { utils };
@@ -22,6 +34,20 @@ export interface Hooks {
22
34
  ldapsearchopts?: ChainedHook<SearchOptions>;
23
35
  ldapsearchrequest?: ChainedHook<[string, SearchOptions, Request?]>;
24
36
  ldapsearchresult?: ChainedHook<SearchResult>;
37
+ /**
38
+ * What a given caller is allowed to see of a search that already ran.
39
+ *
40
+ * Distinct from `ldapsearchresult`, and deliberately so: that one fires
41
+ * before the result is cached and is handed no request, so anything it
42
+ * removed for one caller would be served from the cache to the next. This
43
+ * one runs after the cache, on every return path, and carries the request —
44
+ * so a subscriber may drop entries per caller without the answer outliving
45
+ * them. On a paginated search it is called once per chunk, and a chunk may
46
+ * come back with every entry dropped and none left, a page whose entries
47
+ * all belong elsewhere: the search goes on, so a consumer that reads an
48
+ * empty chunk as the end of the results would stop early.
49
+ */
50
+ ldapsearchfilter?: ChainedHook<[SearchResult, Request?, SearchOptions?]>;
25
51
  ldapaddrequest?: ChainedHook<[string, AttributesList, Request?]>;
26
52
  ldapadddone?: (args: [string, AttributesList]) => MaybePromise<void>;
27
53
  ldapmodifyrequest?: ChainedHook<[string, ModifyRequest, number, Request?]>;
@@ -35,6 +61,35 @@ export interface Hooks {
35
61
  */
36
62
  /** Demo plugin */
37
63
  hello?: () => string;
64
+ /**
65
+ * OpenID Connect session validity
66
+ *
67
+ * `core/auth/openidconnect` knows how to receive a logout token and how to
68
+ * ask whether the session in front of it is still alive; it knows nothing
69
+ * about where that answer is kept. A Back-Channel Logout plugin subscribes
70
+ * to these two and supplies it.
71
+ *
72
+ * `oidcsessionvalid` carries the claims and the verdict so far: a
73
+ * subscriber that has nothing to say passes the pair along unchanged, and
74
+ * one that knows the session is dead answers `false`. Chained, so a refusal
75
+ * already given is never turned back into an acceptance.
76
+ */
77
+ oidcsessionvalid?: ChainedHook<[OidcSessionClaims, boolean]>;
78
+ /** A logout token arrived and verified; subscribers record what it kills. */
79
+ oidclogouttoken?: VoidHook<[OidcLogoutToken]>;
80
+ /**
81
+ * A session was just established; subscribers forget what would kill it.
82
+ *
83
+ * A logout token names a `sid`, a `sub`, or both, and a mark on the `sub`
84
+ * kills every session of that person — including the ones created
85
+ * afterwards, which is not what logging out means. Clearing it here is what
86
+ * keeps an old logout from reaching a session younger than itself.
87
+ *
88
+ * It has a consequence worth knowing: clearing the `sub` mark also revives
89
+ * sessions killed by a "log out everywhere" token that carried no `sid`.
90
+ * The library's own default hook behaves the same way.
91
+ */
92
+ oidclogin?: VoidHook<[OidcSessionClaims]>;
38
93
  /** LdapGroups plugin */
39
94
  ldapgroupvalidatemembers?: ChainedHook<[string, string[]]>;
40
95
  ldapgroupadd?: ChainedHook<[string, AttributesList]>;