ldap-rest 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/bin/index.mjs +8 -7
  2. package/dist/abstract/ldapFlat.js +1 -1
  3. package/dist/abstract/plugin.js +1 -1
  4. package/dist/abstract/twakePlugin.js +1 -1
  5. package/dist/bin/index.js +1 -1
  6. package/dist/config/args.js +1 -1
  7. package/dist/config/schema.js +1 -1
  8. package/dist/lib/changeContext.js +1 -1
  9. package/dist/lib/entitySchema/validator.js +1 -0
  10. package/dist/lib/expressFormatedResponses.js +1 -1
  11. package/dist/lib/health.js +1 -0
  12. package/dist/lib/instanceProviders.js +1 -1
  13. package/dist/lib/ldapActions.js +1 -1
  14. package/dist/lib/ldapSchema.js +1 -1
  15. package/dist/lib/metrics.js +1 -0
  16. package/dist/lib/requestContext.js +1 -1
  17. package/dist/lib/shutdown.js +1 -0
  18. package/dist/lib/staticFiles.js +1 -0
  19. package/dist/lib/storage/store.js +1 -1
  20. package/dist/lib/utils.js +1 -1
  21. package/dist/logger/winston.js +1 -1
  22. package/dist/plugins/auth/crowdsec.js +1 -1
  23. package/dist/plugins/auth/openidconnect.js +1 -1
  24. package/dist/plugins/demo/helloworld.js +1 -1
  25. package/dist/plugins/ldap/bulkImport.js +1 -1
  26. package/dist/plugins/ldap/departmentSync.js +1 -1
  27. package/dist/plugins/ldap/flatGeneric.js +1 -1
  28. package/dist/plugins/ldap/groups.js +1 -1
  29. package/dist/plugins/ldap/organizations.js +1 -1
  30. package/dist/plugins/metrics.js +1 -0
  31. package/dist/plugins/rabbitmq.js +1 -1
  32. package/dist/plugins/scim/baseResolver.js +1 -1
  33. package/dist/plugins/scim/bulk.js +1 -1
  34. package/dist/plugins/scim/errors.js +1 -1
  35. package/dist/plugins/scim/filter.js +1 -1
  36. package/dist/plugins/scim/groups.js +1 -1
  37. package/dist/plugins/scim/mapping.js +1 -1
  38. package/dist/plugins/scim/patch.js +1 -1
  39. package/dist/plugins/scim/projection.js +1 -1
  40. package/dist/plugins/scim/scim.js +1 -1
  41. package/dist/plugins/scim/users.js +1 -1
  42. package/dist/plugins/static.js +1 -1
  43. package/dist/plugins/storage/file.js +1 -1
  44. package/dist/plugins/storage/postgres.js +1 -1
  45. package/dist/plugins/storage/valkey.js +1 -1
  46. package/dist/plugins/storage.js +1 -1
  47. package/dist/plugins/twake/calendar.js +1 -1
  48. package/dist/plugins/twake/clouderyProvision.js +1 -1
  49. package/dist/plugins/twake/cozyProvision.js +1 -1
  50. package/dist/plugins/twake/drive.js +1 -1
  51. package/dist/plugins/twake/groups.js +1 -1
  52. package/dist/plugins/twake/james.js +1 -1
  53. package/dist/plugins/twake/migrationProxy.js +1 -0
  54. package/dist/plugins/twake/spaces.js +1 -1
  55. package/dist/plugins/weblogs.js +1 -1
  56. package/dist/src/abstract/ldapFlat.d.ts +3 -13
  57. package/dist/src/abstract/plugin.d.ts +20 -0
  58. package/dist/src/abstract/twakePlugin.d.ts +23 -1
  59. package/dist/src/bin/index.d.ts +46 -1
  60. package/dist/src/config/args.d.ts +12 -1
  61. package/dist/src/lib/changeContext.d.ts +11 -0
  62. package/dist/src/lib/entitySchema/validator.d.ts +64 -0
  63. package/dist/src/lib/expressFormatedResponses.d.ts +19 -0
  64. package/dist/src/lib/health.d.ts +63 -0
  65. package/dist/src/lib/instanceProviders.d.ts +4 -0
  66. package/dist/src/lib/ldapActions.d.ts +32 -2
  67. package/dist/src/lib/metrics.d.ts +44 -0
  68. package/dist/src/lib/requestContext.d.ts +40 -0
  69. package/dist/src/lib/shutdown.d.ts +20 -0
  70. package/dist/src/lib/staticFiles.d.ts +33 -0
  71. package/dist/src/lib/storage/store.d.ts +8 -0
  72. package/dist/src/lib/utils.d.ts +15 -0
  73. package/dist/src/logger/winston.d.ts +4 -0
  74. package/dist/src/plugins/ldap/groups.d.ts +18 -3
  75. package/dist/src/plugins/ldap/organizations.d.ts +3 -5
  76. package/dist/src/plugins/metrics.d.ts +17 -0
  77. package/dist/src/plugins/rabbitmq.d.ts +37 -1
  78. package/dist/src/plugins/scim/errors.d.ts +2 -0
  79. package/dist/src/plugins/scim/types.d.ts +2 -1
  80. package/dist/src/plugins/static.d.ts +12 -0
  81. package/dist/src/plugins/storage/file.d.ts +4 -0
  82. package/dist/src/plugins/storage/postgres.d.ts +7 -14
  83. package/dist/src/plugins/storage/valkey.d.ts +2 -15
  84. package/dist/src/plugins/storage.d.ts +8 -0
  85. package/dist/src/plugins/twake/drive.d.ts +8 -0
  86. package/dist/src/plugins/twake/groups.d.ts +1 -2
  87. package/dist/src/plugins/twake/james.d.ts +2 -0
  88. package/dist/src/plugins/twake/migrationProxy.d.ts +71 -0
  89. package/docs/client-development/api/reference.md +5 -9
  90. package/docs/client-development/api/rest-api.md +4 -10
  91. package/docs/client-development/schemas/README.md +60 -21
  92. package/docs/plugin-development/README.md +15 -13
  93. package/docs/plugin-development/dependencies.md +8 -0
  94. package/docs/plugin-development/hooks.md +1 -1
  95. package/docs/usage/README.md +1 -0
  96. package/docs/usage/configuration.md +62 -24
  97. package/docs/usage/operations.md +193 -0
  98. package/docs/usage/plugins/README.md +2 -0
  99. package/docs/usage/plugins/auth/authz-per-branch.md +2 -1
  100. package/docs/usage/plugins/integrations/README.md +8 -4
  101. package/docs/usage/plugins/integrations/groups.md +2 -2
  102. package/docs/usage/plugins/integrations/mail-migration.md +102 -0
  103. package/docs/usage/plugins/integrations/scim.md +16 -3
  104. package/docs/usage/plugins/integrations/spaces.md +1 -1
  105. package/docs/usage/plugins/ldap/bulk-import.md +3 -1
  106. package/docs/usage/plugins/ldap/flat-generic.md +5 -2
  107. package/docs/usage/plugins/ldap/on-change.md +2 -1
  108. package/docs/usage/plugins/ldap/organizations.md +7 -7
  109. package/docs/usage/plugins/utilities/metrics.md +21 -0
  110. package/docs/usage/plugins/utilities/static.md +16 -8
  111. package/docs/usage/plugins/utilities/storage.md +7 -0
  112. package/docs/usage/plugins/utilities/weblogs.md +51 -30
  113. package/docs/usage/upgrading.md +84 -0
  114. package/package.json +11 -2
  115. package/static/schemas/example/README.md +1 -0
@@ -1 +1 @@
1
- import{escapeDnValue as t,isDummyMemberDn as e,launchHooksChained as s,validateDnValue as r,launchHooks as i,isChildOf as o,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as n}from"./types.js";import{withExternalId as c,mergeMapping as h,loadMappingFile as u,DEFAULT_GROUP_MAPPING as d,requiredLdapAttributes as l,ldapToScimGroup as m,scimGroupToLdap as p}from"./mapping.js";import{scimFilterToLdap as f}from"./filter.js";import{pagedSearch as g}from"./list.js";import{writePatch as b,patchToModifyRequest as w}from"./patch.js";import{scimNotFound as y,ScimError as R,scimInvalidValue as _,scimUniqueness as $}from"./errors.js";import{extractLdapCode as v}from"../../lib/ldapCodes.js";class ScimGroups{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_group_rdn_attribute||"cn",this.objectClass=this.config.scim_group_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=this.config.scim_group_mapping||"";this.mapping=c(h(d,e?u(e):void 0),this.config.scim_group_external_id_attribute||""),this.attributes=l(this.mapping,[this.rdnAttribute,"member"])}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"Group",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:"",lockValue:""}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const s=this.baseResolver.userBase(t),r=this.baseResolver.groupBase(t),i=this.scimPrefix,o=this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),a=this.config.scim_user_rdn_attribute||"uid",n=this.rdnAttribute,c=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(e(t,c))return;const h=/^([^=]+)=((?:\\.|[^,])+)/.exec(t);if(!h)return;const u=h[2].replace(/\\(.)/g,"$1"),d=t.toLowerCase();let l;s&&d.endsWith(s.toLowerCase())?l="User":r&&d.endsWith(r.toLowerCase())?l="Group":h[1]===a?l="User":h[1]===n&&(l="Group");const m={value:u};if(l&&(m.type=l,o)){const t="User"===l?"Users":"Groups";m.$ref=`${o.replace(/\/$/,"")}${i}/${t}/${encodeURIComponent(u)}`}return m}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.attributes},s)}catch(t){if(32===v(t))throw y(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw y(`Group ${e} not found`);return m(r.searchEntries[0],this.mapping,this.ctx(t),this.buildMemberResolver(t))}async list(t,e){const s=this.baseResolver.groupBase(t),r=Math.max(1,e.startIndex||1),i=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let o,a=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=f(e.filter,this.mapping);t.idEquals?o=t.idEquals:a=`(&${a}${t.ldapFilter})`}if(o)try{const e=await this.get(t,o);return{schemas:[n],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof R&&404===t.statusCode)return{schemas:[n],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:c,totalResults:h}=await g({directory:this.ldap.forRequest(t),base:s,filter:a,attributes:this.attributes,startIndex:r,count:i,maxScanned:this.maxScanned}),u=this.buildMemberResolver(t),d=c.map(e=>m(e,this.mapping,this.ctx(t),u));return{schemas:[n],totalResults:h,startIndex:r,itemsPerPage:d.length,Resources:d}}async create(e,o){if(!o.displayName)throw _("displayName is required");const a=this.baseResolver.groupBase(e),n=(await s(this.hooks.scimgroupcreate,[{...o,id:void 0},e,a]))[0],{rdn:c,attributes:h}=p(n,this.mapping,this.objectClass),u=!h[this.rdnAttribute]&&n.id||c;if(!u)throw _("displayName is required");r(u,this.rdnAttribute),h[this.rdnAttribute]=u;const d=[];if(n.members&&Array.isArray(n.members))for(const t of n.members){if(!t||!t.value)continue;const s=await this.users.resolveRef(e,t.value);s&&d.push(s)}if(0===d.length){const t=this.config.group_dummy_user||"cn=fakeuser";d.push(t)}h.member=d;const l=`${this.rdnAttribute}=${t(u)},${a}`;try{await this.ldap.forRequest(e).add(l,h)}catch(t){if(68===v(t))throw $(`Group ${u} already exists`);throw t}const m=await this.get(e,u);return i(this.hooks.scimgroupcreatedone,m),m}async replace(t,e,r){await this.get(t,e);const o=(await s(this.hooks.scimgroupupdate,[e,r,t]))[1],{attributes:a}=p(o,this.mapping,this.objectClass),n=[];if(o.members&&Array.isArray(o.members))for(const e of o.members){if(!e||!e.value)continue;const s=await this.users.resolveRef(t,e.value);s&&n.push(s)}if(0===n.length){const t=this.config.group_dummy_user||"cn=fakeuser";n.push(t)}const c=this.dnForId(e,t),h={replace:{}};for(const[t,e]of Object.entries(a))"objectClass"!==t&&t!==this.rdnAttribute&&(h.replace[t]=e);h.replace.member=n,await this.ldap.forRequest(t).modify(c,h);const u=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,u),u}async patch(t,e,s){const r=this.dnForId(e,t);await b(r,async()=>{const i=await this.currentEntry(t,e),o=await w(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e),current:i}),a=o.delete&&!Array.isArray(o.delete)?o.delete.member:void 0;if(void 0!==a){const t=this.config.group_dummy_user||"cn=fakeuser";if(""===a)delete o.delete.member,0===Object.keys(o.delete).length&&delete o.delete,o.replace||(o.replace={}),o.replace.member=[t];else{const e=[].concat(a);0===[].concat(i.member??[],o.add?.member??[]).map(String).filter(t=>!e.includes(t)).length&&(o.add||(o.add={}),o.add.member=t)}}await this.ldap.forRequest(t).modify(r,o,{permissive:!0})});const o=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,o),o}async currentEntry(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.attributes},s)}catch(t){if(32===v(t))throw y(`Group ${e} not found`);throw t}if(!r.searchEntries?.length)throw y(`Group ${e} not found`);return r.searchEntries[0]}async delete(t,e){await this.get(t,e);const r=(await s(this.hooks.scimgroupdelete,[e,t]))[0],o=this.dnForId(r,t);await this.ldap.forRequest(t).delete(o),i(this.hooks.scimgroupdeletedone,r)}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e),i=s.includes("=")&&s.includes(",");if(i&&!o(s,r))return;const n=i?s:`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},n);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==v(t))throw t}if(!i)try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},r);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==v(t))throw t}}get groupMapping(){return this.mapping}}export{ScimGroups};
1
+ import{escapeDnValue as t,isDummyMemberDn as e,parseDn as s,rdnValue as r,launchHooksChained as i,validateDnValue as o,launchHooks as a,isChildOf as n,escapeLdapFilter as c}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as u}from"./types.js";import{withExternalId as h,mergeMapping as l,loadMappingFile as d,DEFAULT_GROUP_MAPPING as m,requiredLdapAttributes as p,ldapToScimGroup as f,scimGroupToLdap as g}from"./mapping.js";import{scimFilterToLdap as b}from"./filter.js";import{pagedSearch as w}from"./list.js";import{writePatch as y,patchToModifyRequest as R}from"./patch.js";import{scimNotFound as v,ScimError as _,scimInvalidValue as $,kindOf as x,scimUniqueness as A}from"./errors.js";import{extractLdapCode as j}from"../../lib/ldapCodes.js";class ScimGroups{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_group_rdn_attribute||"cn",this.objectClass=this.config.scim_group_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=this.config.scim_group_mapping||"";this.mapping=h(l(m,e?d(e):void 0),this.config.scim_group_external_id_attribute||""),this.attributes=p(this.mapping,[this.rdnAttribute,"member"])}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"Group",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:"",lockValue:""}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const i=this.baseResolver.userBase(t),o=this.baseResolver.groupBase(t),a=this.scimPrefix,n=this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),c=this.config.scim_user_rdn_attribute||"uid",u=this.rdnAttribute,h=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(e(t,h))return;const[l]=s(t),d=l?l.indexOf("="):-1;if(d<=0)return;const m=l.slice(0,d).trim(),p=r(t),f=t.toLowerCase();let g;i&&f.endsWith(i.toLowerCase())?g="User":o&&f.endsWith(o.toLowerCase())?g="Group":m===c?g="User":m===u&&(g="Group");const b={value:p};if(g&&(b.type=g,n)){const t="User"===g?"Users":"Groups";b.$ref=`${n.replace(/\/$/,"")}${a}/${t}/${encodeURIComponent(p)}`}return b}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.attributes},s)}catch(t){if(32===j(t))throw v(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw v(`Group ${e} not found`);return f(r.searchEntries[0],this.mapping,this.ctx(t),this.buildMemberResolver(t))}async list(t,e){const s=this.baseResolver.groupBase(t),r=Math.max(1,e.startIndex||1),i=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let o,a=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=b(e.filter,this.mapping);t.idEquals?o=t.idEquals:a=`(&${a}${t.ldapFilter})`}if(o)try{const e=await this.get(t,o);return{schemas:[u],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof _&&404===t.statusCode)return{schemas:[u],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:n,totalResults:c}=await w({directory:this.ldap.forRequest(t),base:s,filter:a,attributes:this.attributes,startIndex:r,count:i,maxScanned:this.maxScanned}),h=this.buildMemberResolver(t),l=n.map(e=>f(e,this.mapping,this.ctx(t),h));return{schemas:[u],totalResults:c,startIndex:r,itemsPerPage:l.length,Resources:l}}async create(e,s){if(!s.displayName)throw $("displayName is required");const r=this.baseResolver.groupBase(e);g({...s,id:void 0},this.mapping,this.objectClass);const n=(await i(this.hooks.scimgroupcreate,[{...s,id:void 0},e,r]))[0],{rdn:c,attributes:u}=g(n,this.mapping,this.objectClass),h=!u[this.rdnAttribute]&&n.id||c;if(!h)throw $("displayName is required");o(h,this.rdnAttribute),u[this.rdnAttribute]=h;const l=[];if(n.members&&Array.isArray(n.members))for(const t of n.members){if(!t||null==t.value)continue;if("string"!=typeof t.value)throw $(`Each member value must be a string, got ${x(t.value)}`);const s=await this.users.resolveRef(e,t.value);s&&l.push(s)}if(0===l.length){const t=this.config.group_dummy_user||"cn=fakeuser";l.push(t)}u.member=l;const d=`${this.rdnAttribute}=${t(h)},${r}`;try{await this.ldap.forRequest(e).add(d,u)}catch(t){if(68===j(t))throw A(`Group ${h} already exists`);throw t}const m=await this.get(e,h);return a(this.hooks.scimgroupcreatedone,m),m}async replace(t,e,s){await this.get(t,e);const r=(await i(this.hooks.scimgroupupdate,[e,s,t]))[1],{attributes:o}=g(r,this.mapping,this.objectClass),n=[];if(r.members&&Array.isArray(r.members))for(const e of r.members){if(!e||null==e.value)continue;if("string"!=typeof e.value)throw $(`Each member value must be a string, got ${x(e.value)}`);const s=await this.users.resolveRef(t,e.value);s&&n.push(s)}if(0===n.length){const t=this.config.group_dummy_user||"cn=fakeuser";n.push(t)}const c=this.dnForId(e,t),u={replace:{}};for(const[t,e]of Object.entries(o))"objectClass"!==t&&t!==this.rdnAttribute&&(u.replace[t]=e);u.replace.member=n,await this.ldap.forRequest(t).modify(c,u);const h=await this.get(t,e);return a(this.hooks.scimgroupupdatedone,e,h),h}async patch(t,e,s){const r=this.dnForId(e,t);await y(r,async()=>{const i=await this.currentEntry(t,e),o=await R(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e),current:i}),a=o.delete&&!Array.isArray(o.delete)?o.delete.member:void 0;if(void 0!==a){const t=this.config.group_dummy_user||"cn=fakeuser";if(""===a)delete o.delete.member,0===Object.keys(o.delete).length&&delete o.delete,o.replace||(o.replace={}),o.replace.member=[t];else{const e=[].concat(a);0===[].concat(i.member??[],o.add?.member??[]).map(String).filter(t=>!e.includes(t)).length&&(o.add||(o.add={}),o.add.member=t)}}await this.ldap.forRequest(t).modify(r,o,{permissive:!0})});const i=await this.get(t,e);return a(this.hooks.scimgroupupdatedone,e,i),i}async currentEntry(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.attributes},s)}catch(t){if(32===j(t))throw v(`Group ${e} not found`);throw t}if(!r.searchEntries?.length)throw v(`Group ${e} not found`);return r.searchEntries[0]}async delete(t,e){await this.get(t,e);const s=(await i(this.hooks.scimgroupdelete,[e,t]))[0],r=this.dnForId(s,t);await this.ldap.forRequest(t).delete(r),a(this.hooks.scimgroupdeletedone,s)}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e),i=s.includes("=")&&s.includes(",");if(i&&!n(s,r))return;const o=i?s:`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},o);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==j(t))throw t}if(!i)try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${c(s)})`,scope:"sub",paged:!1,attributes:["dn"]},r);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==j(t))throw t}}get groupMapping(){return this.mapping}}export{ScimGroups};
@@ -1 +1 @@
1
- import e from"fs";import{scimInvalidValue as r}from"./errors.js";import{SCHEMA_GROUP as t,SCHEMA_USER as i}from"./types.js";const a=["createTimestamp","modifyTimestamp","entryUUID"],n={resourceType:"User",schemas:[i],entries:[{scim:"userName",ldap:"uid"},{scim:"externalId",ldap:"employeeNumber"},{scim:"name",sub:{familyName:"sn",givenName:"givenName",formatted:"cn",middleName:"initials"}},{scim:"displayName",ldap:"displayName"},{scim:"nickName",ldap:"displayName"},{scim:"title",ldap:"title"},{scim:"preferredLanguage",ldap:"preferredLanguage"},{scim:"emails",ldapPrimary:"mail",ldapSecondary:"mailAlternateAddress",multi:"array"},{scim:"phoneNumbers",ldapPrimary:"telephoneNumber",ldapSecondary:"mobile",multi:"array"}]},o={resourceType:"Group",schemas:[t],entries:[{scim:"displayName",ldap:"cn"}]};function withExternalId(e,r){const t=(r||"").trim();return t?e.entries.some(e=>"externalId"===e.scim)?e:{...e,entries:[...e.entries,{scim:"externalId",ldap:t}]}:e}function asString(e){if(null!=e){if(Array.isArray(e)){const r=e[0];return null==r?void 0:String(r)}return Buffer.isBuffer(e)?e.toString():String(e)}}function ldapTimeToIso(e){if(!e)return;const r=/^(\d{4})(\d{2})(\d{2})(\d{2})(?:(\d{2})(?:(\d{2}))?)?(?:[.,](\d+))?(Z|[+-]\d{2}(?:\d{2})?)?$/.exec(e);if(!r)return e;const[,t,i,a,n,o,s,c,l]=r;let d=0;if(c){const e=s?1e3:o?6e4:36e5;d=Math.round(Number(`0.${c}`)*e)}let u=1e3*(60*(60*Number(n)+Number(o||0))+Number(s||0))+d;const m=Math.floor(u/36e5);u-=36e5*m;const f=Math.floor(u/6e4);u-=6e4*f;const p=Math.floor(u/1e3),y=u-1e3*p;let b="Z";l&&"Z"!==l&&(b=`${l[0]}${l.slice(1,3)}:${l.slice(3,5)||"00"}`);const pad=e=>String(e).padStart(2,"0"),g=`${t}-${i}-${a}T${pad(m)}:${pad(f)}:${pad(p)}${y?`.${String(y).padStart(3,"0")}`:""}${b}`;return Number.isNaN(Date.parse(g))?e:g}function asArray(e){return null==e?[]:Array.isArray(e)?e.map(e=>Buffer.isBuffer(e)?e.toString():String(e)):Buffer.isBuffer(e)?[e.toString()]:[String(e)]}function loadMappingFile(r){if(!r)return;const t=e.readFileSync(r,"utf8");return JSON.parse(t)}function mergeMapping(e,r){if(!r)return e;const t=new Map;for(const r of e.entries)t.set(r.scim,r);for(const e of r.entries)t.set(e.scim,e);return{...e,entries:Array.from(t.values())}}const s="pwdAccountLockedTime",c="000001010000Z",l=/^\d{10,14}(\.\d+)?(Z|[+-]\d{2,4})$/;let warn=e=>{console.warn(`WARNING: ${e}`)};function setLockConfigWarn(e){const r=warn;return warn=e,r}const d=/^([A-Za-z][A-Za-z0-9-]*|\d+(\.\d+)+)$/;function resolveLockConfig(e,r,t={attribute:"--scim-user-lock-attribute",value:"--scim-user-lock-value"}){const i=(e||"").trim()||s;if(!d.test(i))throw new Error(`${t.attribute} must be an LDAP attribute name, got '${i}'`);const a=i.toLowerCase()===s.toLowerCase(),n=(r||"").trim();if(!n){if(a)return{attribute:i,value:c};throw new Error(`${t.attribute} is '${i}', so ${t.value} must say what marks an account locked (for nsAccountLock on 389-ds, 'TRUE'). The default '${c}' only means anything to ${s}.`)}return a||n!==c||warn(`${t.value} is the ppolicy convention '${c}' but ${t.attribute} is '${i}', not ${s}. Unless '${i}' is a GeneralizedTime your directory reads as a lock, every deactivation will answer 200 and read back as active:false while the account keeps binding. Verify a deactivation actually prevents a bind.`),a&&!l.test(n)&&warn(`${t.value} '${n}' is not a GeneralizedTime, and ${s} holds one. The directory will refuse the write, or — where the attribute was redefined locally — store a value it does not read as a lock. The ppolicy convention is '${c}'.`),{attribute:i,value:n}}function readActive(e){if(void 0!==e){if("boolean"==typeof e)return e;if("string"==typeof e){const r=e.trim().toLowerCase();if("true"===r)return!0;if("false"===r)return!1}throw r(`Cannot read ${JSON.stringify(e)} as active`)}}function isLocked(e,r){return holdsValue(e,r.lockAttribute,r.lockValue)}function holdsValue(e,r,t){if(!r)return!1;const i=r.toLowerCase(),a=Object.keys(e).find(e=>e.toLowerCase()===i),n=void 0===a?void 0:e[a];if(null==n)return!1;const o=t.toLowerCase();return(Array.isArray(n)?n:[n]).some(e=>null!=e&&e.toString().toLowerCase()===o)}function resolveScimId(e,r){if("rdn"===r.idAttribute)return asString(e[r.rdnAttribute]);return asString(e[r.idAttribute])??asString(e[r.rdnAttribute])}function buildLocation(e,r){if(!r.baseUrl)return;const t="User"===r.resourceType?"Users":"Groups";return`${r.baseUrl.replace(/\/$/,"")}${r.scimPrefix}/${t}/${encodeURIComponent(e)}`}function ldapToScimUser(e,r,t){const a={schemas:[i]},n=resolveScimId(e,t);n&&(a.id=n);for(const t of r.entries){if(t.sub){const r={};for(const[i,a]of Object.entries(t.sub)){const t=asString(e[a]);null!=t&&(r[i]=t)}Object.keys(r).length>0&&(a[t.scim]=r);continue}if(t.ldapPrimary||t.ldapSecondary){const r=[];if(t.ldapPrimary){const i=asString(e[t.ldapPrimary]);null!=i&&r.push({value:i,primary:!0})}if(t.ldapSecondary)for(const i of asArray(e[t.ldapSecondary]))r.push({value:i});r.length>0&&(a[t.scim]=r);continue}if(t.ldap)if("array"===t.multi){const r=asArray(e[t.ldap]);r.length>0&&(a[t.scim]=r)}else{const r=asString(e[t.ldap]);null!=r&&(a[t.scim]=r)}}return a.active=!isLocked(e,t),n&&(a.meta={resourceType:"User",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(n,t)}),a}function scimUserToLdap(e,r,t,i){const a={objectClass:i},n=e.userName||e.id||"";for(const t of r.entries){if(t.readOnly||t.operational)continue;const r=e[t.scim];if(null!=r)if(t.sub&&"object"==typeof r)for(const[e,i]of Object.entries(t.sub)){const t=r[e];"string"==typeof t&&t.length>0&&(a[i]=t)}else{if(t.ldapPrimary||t.ldapSecondary){if(!Array.isArray(r))continue;const e=r,i=e.find(e=>!0===e.primary)||e[0],n=e.filter(e=>e!==i);t.ldapPrimary&&i&&i.value&&(a[t.ldapPrimary]=i.value),t.ldapSecondary&&n.length>0&&(a[t.ldapSecondary]=n.map(e=>e.value).filter(Boolean));continue}t.ldap&&(Array.isArray(r)?a[t.ldap]=r.map(e=>String(e)):"string"!=typeof r&&"number"!=typeof r||(a[t.ldap]=String(r)))}}const o=readActive(e.active);if(!1===o&&(a[t.lockAttribute]=t.lockValue||c),!a.cn&&n&&(a.cn=n),!a.sn){const r=e.name?.familyName||e.displayName||n;r&&(a.sn=r)}return{rdn:n,attributes:a,active:o}}function ldapToScimGroup(e,r,i,a){const n={schemas:[t]},o=resolveScimId(e,i);o&&(n.id=o);for(const t of r.entries){if(!t.ldap)continue;const r=e[t.ldap];if(null!=r)if("array"===t.multi){const e=asArray(r);e.length>0&&(n[t.scim]=e)}else{const e=asString(r);null!=e&&(n[t.scim]=e)}}const s=asArray(e.member),c=[];for(const e of s)if(e)if(a){const r=a(e);r&&c.push(r)}else c.push({value:e,type:"User"});return c.length>0&&(n.members=c),o&&(n.meta={resourceType:"Group",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(o,i)}),n}function scimGroupToLdap(e,r,t){const i={objectClass:t},a=e.displayName||e.id||"";for(const t of r.entries){if(t.readOnly||t.operational)continue;if(!t.ldap)continue;const r=e[t.scim];null!=r&&(Array.isArray(r)?i[t.ldap]=r.map(e=>"string"==typeof e?e:JSON.stringify(e)):"string"!=typeof r&&"number"!=typeof r||(i[t.ldap]=String(r)))}return{rdn:a,attributes:i}}function requiredLdapAttributes(e,r=[]){const t=new Set(["objectClass",...a,...r]);for(const r of e.entries)if(r.ldap&&t.add(r.ldap),r.ldapPrimary&&t.add(r.ldapPrimary),r.ldapSecondary&&t.add(r.ldapSecondary),r.sub)for(const e of Object.values(r.sub))t.add(e);return Array.from(t)}function scimPathToLdapAttribute(e,r){const t=e.split(".")[0],i=e.includes(".")?e.split(".").slice(1).join("."):"";for(const e of r.entries)if(e.scim===t){if(e.sub&&i)return e.sub[i];if((e.ldapPrimary||e.ldapSecondary)&&("value"===i||!i))return e.ldapPrimary||e.ldapSecondary;if(e.ldap)return e.ldap}}export{o as DEFAULT_GROUP_MAPPING,s as DEFAULT_LOCK_ATTRIBUTE,c as DEFAULT_LOCK_VALUE,n as DEFAULT_USER_MAPPING,buildLocation,holdsValue,isLocked,ldapTimeToIso,ldapToScimGroup,ldapToScimUser,loadMappingFile,mergeMapping,readActive,requiredLdapAttributes,resolveLockConfig,resolveScimId,scimGroupToLdap,scimPathToLdapAttribute,scimUserToLdap,setLockConfigWarn,warn,withExternalId};
1
+ import e from"fs";import{scimInvalidValue as r,kindOf as t}from"./errors.js";import{SCHEMA_GROUP as a,SCHEMA_USER as i}from"./types.js";const n=["createTimestamp","modifyTimestamp","entryUUID"],o={resourceType:"User",schemas:[i],entries:[{scim:"userName",ldap:"uid"},{scim:"externalId",ldap:"employeeNumber"},{scim:"name",sub:{familyName:"sn",givenName:"givenName",formatted:"cn",middleName:"initials"}},{scim:"displayName",ldap:"displayName"},{scim:"nickName",ldap:"displayName"},{scim:"title",ldap:"title"},{scim:"preferredLanguage",ldap:"preferredLanguage"},{scim:"emails",ldapPrimary:"mail",ldapSecondary:"mailAlternateAddress",multi:"array"},{scim:"phoneNumbers",ldapPrimary:"telephoneNumber",ldapSecondary:"mobile",multi:"array"}]},s={resourceType:"Group",schemas:[a],entries:[{scim:"displayName",ldap:"cn"}]};function withExternalId(e,r){const t=(r||"").trim();return t?e.entries.some(e=>"externalId"===e.scim)?e:{...e,entries:[...e.entries,{scim:"externalId",ldap:t}]}:e}function asString(e){if(null!=e){if(Array.isArray(e)){const r=e[0];return null==r?void 0:String(r)}return Buffer.isBuffer(e)?e.toString():String(e)}}function ldapTimeToIso(e){if(!e)return;const r=/^(\d{4})(\d{2})(\d{2})(\d{2})(?:(\d{2})(?:(\d{2}))?)?(?:[.,](\d+))?(Z|[+-]\d{2}(?:\d{2})?)?$/.exec(e);if(!r)return e;const[,t,a,i,n,o,s,l,c]=r;let u=0;if(l){const e=s?1e3:o?6e4:36e5;u=Math.round(Number(`0.${l}`)*e)}let d=1e3*(60*(60*Number(n)+Number(o||0))+Number(s||0))+u;const m=Math.floor(d/36e5);d-=36e5*m;const f=Math.floor(d/6e4);d-=6e4*f;const p=Math.floor(d/1e3),y=d-1e3*p;let b="Z";c&&"Z"!==c&&(b=`${c[0]}${c.slice(1,3)}:${c.slice(3,5)||"00"}`);const pad=e=>String(e).padStart(2,"0"),g=`${t}-${a}-${i}T${pad(m)}:${pad(f)}:${pad(p)}${y?`.${String(y).padStart(3,"0")}`:""}${b}`;return Number.isNaN(Date.parse(g))?e:g}function asArray(e){return null==e?[]:Array.isArray(e)?e.map(e=>Buffer.isBuffer(e)?e.toString():String(e)):Buffer.isBuffer(e)?[e.toString()]:[String(e)]}function loadMappingFile(r){if(!r)return;const t=e.readFileSync(r,"utf8");return JSON.parse(t)}function mergeMapping(e,r){if(!r)return e;const t=new Map;for(const r of e.entries)t.set(r.scim,r);for(const e of r.entries)t.set(e.scim,e);return{...e,entries:Array.from(t.values())}}const l="pwdAccountLockedTime",c="000001010000Z",u=/^\d{10,14}(\.\d+)?(Z|[+-]\d{2,4})$/;let warn=e=>{console.warn(`WARNING: ${e}`)};function setLockConfigWarn(e){const r=warn;return warn=e,r}const d=/^([A-Za-z][A-Za-z0-9-]*|\d+(\.\d+)+)$/;function resolveLockConfig(e,r,t={attribute:"--scim-user-lock-attribute",value:"--scim-user-lock-value"}){const a=(e||"").trim()||l;if(!d.test(a))throw new Error(`${t.attribute} must be an LDAP attribute name, got '${a}'`);const i=a.toLowerCase()===l.toLowerCase(),n=(r||"").trim();if(!n){if(i)return{attribute:a,value:c};throw new Error(`${t.attribute} is '${a}', so ${t.value} must say what marks an account locked (for nsAccountLock on 389-ds, 'TRUE'). The default '${c}' only means anything to ${l}.`)}return i||n!==c||warn(`${t.value} is the ppolicy convention '${c}' but ${t.attribute} is '${a}', not ${l}. Unless '${a}' is a GeneralizedTime your directory reads as a lock, every deactivation will answer 200 and read back as active:false while the account keeps binding. Verify a deactivation actually prevents a bind.`),i&&!u.test(n)&&warn(`${t.value} '${n}' is not a GeneralizedTime, and ${l} holds one. The directory will refuse the write, or — where the attribute was redefined locally — store a value it does not read as a lock. The ppolicy convention is '${c}'.`),{attribute:a,value:n}}function readActive(e){if(void 0!==e){if("boolean"==typeof e)return e;if("string"==typeof e){const r=e.trim().toLowerCase();if("true"===r)return!0;if("false"===r)return!1}throw r(`Cannot read ${t(e)} as active`)}}function isLocked(e,r){return holdsValue(e,r.lockAttribute,r.lockValue)}function holdsValue(e,r,t){if(!r)return!1;const a=r.toLowerCase(),i=Object.keys(e).find(e=>e.toLowerCase()===a),n=void 0===i?void 0:e[i];if(null==n)return!1;const o=t.toLowerCase();return(Array.isArray(n)?n:[n]).some(e=>null!=e&&e.toString().toLowerCase()===o)}function resolveScimId(e,r){if("rdn"===r.idAttribute)return asString(e[r.rdnAttribute]);return asString(e[r.idAttribute])??asString(e[r.rdnAttribute])}function buildLocation(e,r){if(!r.baseUrl)return;const t="User"===r.resourceType?"Users":"Groups";return`${r.baseUrl.replace(/\/$/,"")}${r.scimPrefix}/${t}/${encodeURIComponent(e)}`}function ldapToScimUser(e,r,t){const a={schemas:[i]},n=resolveScimId(e,t);n&&(a.id=n);for(const t of r.entries){if(t.sub){const r={};for(const[a,i]of Object.entries(t.sub)){const t=asString(e[i]);null!=t&&(r[a]=t)}Object.keys(r).length>0&&(a[t.scim]=r);continue}if(t.ldapPrimary||t.ldapSecondary){const r=[];if(t.ldapPrimary){const a=asString(e[t.ldapPrimary]);null!=a&&r.push({value:a,primary:!0})}if(t.ldapSecondary)for(const a of asArray(e[t.ldapSecondary]))r.push({value:a});r.length>0&&(a[t.scim]=r);continue}if(t.ldap)if("array"===t.multi){const r=asArray(e[t.ldap]);r.length>0&&(a[t.scim]=r)}else{const r=asString(e[t.ldap]);null!=r&&(a[t.scim]=r)}}return a.active=!isLocked(e,t),n&&(a.meta={resourceType:"User",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(n,t)}),a}function scimUserToLdap(e,a,i,n){const o={objectClass:n},s=namingValue(e,"userName")||namingValue(e,"id");for(const i of a.entries){if(i.readOnly||i.operational)continue;const a=e[i.scim];if(null!=a)if(i.sub){if(!a||"object"!=typeof a||Array.isArray(a))throw r(`${i.scim} must be an object, got ${t(a)}`);for(const[e,r]of Object.entries(i.sub)){const t=scalarOrValue(a[e],`${i.scim}.${e}`);void 0!==t&&""!==t&&(o[r]=t)}}else{if(i.ldapPrimary||i.ldapSecondary){if(!Array.isArray(a))throw r(`${i.scim} must be a list, got ${t(a)}`);const e=a.map(e=>multiValued(e,i.scim)).filter(e=>null!=e.value),n=e.find(e=>!0===e.primary)||e[0],s=e.filter(e=>e!==n);i.ldapPrimary&&n&&n.value&&(o[i.ldapPrimary]=n.value),i.ldapSecondary&&s.length>0&&(o[i.ldapSecondary]=s.map(e=>e.value).filter(Boolean));continue}i.ldap&&(o[i.ldap]=scalarValues(a,i.scim))}}const l=readActive(e.active);if(!1===l&&(o[i.lockAttribute]=i.lockValue||c),!o.cn&&s&&(o.cn=s),!o.sn){const r=[e.name?.familyName,e.displayName,s].find(e=>"string"==typeof e&&e.length>0);r&&(o.sn=r)}return{rdn:s,attributes:o,active:l}}function ldapToScimGroup(e,r,t,i){const n={schemas:[a]},o=resolveScimId(e,t);o&&(n.id=o);for(const t of r.entries){if(!t.ldap)continue;const r=e[t.ldap];if(null!=r)if("array"===t.multi){const e=asArray(r);e.length>0&&(n[t.scim]=e)}else{const e=asString(r);null!=e&&(n[t.scim]=e)}}const s=asArray(e.member),l=[];for(const e of s)if(e)if(i){const r=i(e);r&&l.push(r)}else l.push({value:e,type:"User"});return l.length>0&&(n.members=l),o&&(n.meta={resourceType:"Group",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(o,t)}),n}function scimGroupToLdap(e,r,t){const a={objectClass:t},i=namingValue(e,"displayName")||namingValue(e,"id");for(const t of r.entries){if(t.readOnly||t.operational)continue;if(!t.ldap)continue;const r=e[t.scim];null!=r&&(a[t.ldap]=scalarValues(r,t.scim))}return{rdn:i,attributes:a}}function namingValue(e,a){const i=e[a];if(null==i)return"";if("string"!=typeof i)throw r(`${a} must be a string, got ${t(i)}`);return i}function scalarOrValue(e,a){const i=e&&"object"==typeof e&&!Array.isArray(e)&&"value"in e?e.value:e;if(null!=i){if(isScalar(i))return String(i);throw r(`${a} must be a string, got ${t(i)}`)}}const isScalar=e=>"string"==typeof e||"number"==typeof e||"boolean"==typeof e;function scalarValues(e,a){if(isScalar(e))return String(e);if(Array.isArray(e)&&e.every(isScalar))return e.map(String);throw r(`${a} must be a string, a number, a boolean or a list of them, got ${t(e)}`)}function multiValued(e,a){if(e&&"object"==typeof e&&!Array.isArray(e)){const{value:r}=e;if(null==r)return e;if(isScalar(r))return{...e,value:String(r)}}throw r(`Each element of ${a} must be an object whose value is a scalar, got ${t(e)}`)}function requiredLdapAttributes(e,r=[]){const t=new Set(["objectClass",...n,...r]);for(const r of e.entries)if(r.ldap&&t.add(r.ldap),r.ldapPrimary&&t.add(r.ldapPrimary),r.ldapSecondary&&t.add(r.ldapSecondary),r.sub)for(const e of Object.values(r.sub))t.add(e);return Array.from(t)}function scimPathToLdapAttribute(e,r){if("string"!=typeof e)return;const t=e.split(".")[0],a=e.includes(".")?e.split(".").slice(1).join("."):"";for(const e of r.entries)if(e.scim===t){if(e.sub&&a)return Object.prototype.hasOwnProperty.call(e.sub,a)?e.sub[a]:void 0;if((e.ldapPrimary||e.ldapSecondary)&&("value"===a||!a))return e.ldapPrimary||e.ldapSecondary;if(e.ldap)return e.ldap}}export{s as DEFAULT_GROUP_MAPPING,l as DEFAULT_LOCK_ATTRIBUTE,c as DEFAULT_LOCK_VALUE,o as DEFAULT_USER_MAPPING,buildLocation,holdsValue,isLocked,ldapTimeToIso,ldapToScimGroup,ldapToScimUser,loadMappingFile,mergeMapping,readActive,requiredLdapAttributes,resolveLockConfig,resolveScimId,scimGroupToLdap,scimPathToLdapAttribute,scimUserToLdap,setLockConfigWarn,warn,withExternalId};
@@ -1 +1 @@
1
- import{extractLdapCode as e}from"../../lib/ldapCodes.js";import{scimInvalidValue as t,ScimError as r,scimInvalidPath as o,scimNoTarget as n}from"./errors.js";import{DEFAULT_LOCK_ATTRIBUTE as i,DEFAULT_LOCK_VALUE as a,holdsValue as s,scimPathToLdapAttribute as l,readActive as u}from"./mapping.js";function asValues(e){if(null==e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString():String(e)).filter(e=>e.length>0)}class WorkingEntry{constructor(e){this.current=e,this.values=new Map,this.touched=new Set,this.whole=new Set}get(e){return this.values.has(e)||this.values.set(e,asValues(this.current[e])),this.values.get(e)}set(e,t){this.whole.add(e),this.write(e,t)}append(e,t){const r=this.get(e);this.write(e,[...r,...t.filter(e=>!r.includes(e))])}drop(e,t){if(!t)return this.set(e,[]);this.write(e,this.get(e).filter(e=>!t.includes(e)))}write(e,t){this.touched.add(e),this.values.set(e,t)}diff(){const e={};for(const t of this.touched){const r=asValues(this.current[t]),o=this.get(t),n=o.filter(e=>!r.includes(e)),i=r.filter(e=>!o.includes(e));if(0!==n.length||0!==i.length)if(this.whole.has(t))if(0!==o.length)0===i.length&&r.length>0?(e.add||(e.add={}),e.add[t]=1===n.length?n[0]:n):(e.replace||(e.replace={}),e.replace[t]=1===o.length?o[0]:o);else{if(0===r.length)continue;e.delete||(e.delete={}),e.delete[t]=""}else n.length>0&&(e.add||(e.add={}),e.add[t]=1===n.length?n[0]:n),i.length>0&&(e.delete||(e.delete={}),e.delete[t]=1===i.length?i[0]:i)}return e}}function normalizeOp(e){const r=e.toLowerCase();if("add"===r||"remove"===r||"replace"===r)return r;throw t(`Unknown patch op '${e}'`)}const f=new Set(["__proto__","constructor","prototype"]),c=/^[A-Za-z_$:][\w$:]*$/;function assertSafeKey(e){if(f.has(e))throw o(`Forbidden attribute name '${e}'`)}function parsePath(e){if("string"!=typeof e||0===e.length||e.length>512)throw o(`Malformed path '${String(e)}'`);let t,r,n;const i=e.indexOf("[");if(i>=0){const a=e.lastIndexOf("]");if(a<=i)throw o(`Malformed path '${e}'`);t=e.slice(0,i),r=e.slice(i+1,a);const s=e.slice(a+1);if(s.length>0){if("."!==s[0])throw o(`Malformed path '${e}'`);n=s.slice(1)}}else{const r=e.indexOf(".");r>=0?(t=e.slice(0,r),n=e.slice(r+1)):t=e}if(!c.test(t))throw o(`Malformed path '${e}'`);if(assertSafeKey(t),null!=n){if(!c.test(n))throw o(`Malformed sub-attribute in '${e}'`);assertSafeKey(n)}return{top:t,filter:r,sub:n}}async function applyOperation(e,r,f){const c=normalizeOp(e.op),p=f.memberAttribute||"member";if(!e.path){if("remove"===c)throw n("PATCH 'remove' requires a path");if(null==e.value||"object"!=typeof e.value||Array.isArray(e.value))throw n("PATCH without path requires an object value");const t=e.value;for(const[o,n]of Object.entries(t))await applyOperation({op:e.op,path:o,value:n},r,f);return}const{top:h,sub:d,filter:y}=parsePath(e.path);if("members"===h){const t=f.resolveMemberRef;if(!t)throw n("Member operations require a member resolver");const asMemberValues=async e=>{const r=Array.isArray(e)?e:[e],o=[];for(const e of r){const r="string"==typeof e?e:"object"==typeof e&&null!=e&&"value"in e?String(e.value):"";if(!r)continue;const n=await t(r);n&&o.push(n)}return o};if("add"===c)return void r.append(p,await asMemberValues(e.value));if("replace"===c){const t=await asMemberValues(e.value);if(0===t.length)return;return void r.set(p,t)}if(!y&&null==e.value)return void r.drop(p);const o=y?/value\s+eq\s+"([^"]+)"/i.exec(y)?.[1]??"":"",i=[...o?await asMemberValues(o):[],...null!=e.value?await asMemberValues(e.value):[]];return void r.drop(p,i)}if("active"===h&&f.supportsActive){if(d||y)throw o(`'active' has no sub-attribute (got '${e.path}')`);const n=f.lockAttribute||i,l=Object.keys(f.current).find(e=>e.toLowerCase()===n.toLowerCase())??n,p="remove"===c||function(e){const r=u(e);if(void 0===r)throw t("PATCH on 'active' requires a value");return r}(e.value),h=f.lockValue||a;return void(p?s({[l]:r.get(l)},l,h)&&r.drop(l):r.set(l,[h]))}if(y)throw o(`Complex multi-valued filters are only supported on 'members' (got '${e.path}')`);const v=l(d?`${h}.${d}`:h,f.mapping);if(!v)throw o(`Unknown SCIM attribute path '${e.path}'`);if("remove"===c)return void r.drop(v);const w=function(e){if(null!=e)return"string"==typeof e?e:"number"==typeof e||"boolean"==typeof e?String(e):Array.isArray(e)?e.map(e=>{if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);if(e&&"object"==typeof e&&!Array.isArray(e)&&"value"in e&&("string"==typeof e.value||"number"==typeof e.value||"boolean"==typeof e.value))return String(e.value);throw t(`Unsupported array element in PATCH value: ${JSON.stringify(e)}`)}):void 0}(e.value);if(null==w)throw t(`PATCH ${e.op} ${e.path} missing value`);const g=Array.isArray(w)?w:[w];"add"===c?r.append(v,g):r.set(v,g)}async function patchToModifyRequest(e,r){if(!e.Operations||!Array.isArray(e.Operations))throw t("Missing Operations array");const o=new WorkingEntry(r.current);for(const t of e.Operations)await applyOperation(t,o,r);return o.diff()}async function writePatch(t,o){for(let n=1;;n++)try{return await o()}catch(o){if(16!==e(o))throw o;if(2===n)throw new r(409,`${t} changed while this PATCH was being applied; nothing was written. Retry the request.`)}}function applyPatchToResource(e,t){const r={...e};for(const e of t.Operations){const t=normalizeOp(e.op);if(!e.path){if(e.value&&"object"==typeof e.value&&!Array.isArray(e.value))for(const[t,o]of Object.entries(e.value))assertSafeKey(t),r[t]=o;continue}const{top:o,sub:n}=parsePath(e.path);if("remove"!==t)if(n){const t=r[o],i=t&&"object"==typeof t&&!Array.isArray(t)?{...t}:Object.create(null);i[n]=e.value,r[o]=i}else r[o]=e.value;else if(n){const e=r[o];e&&"object"==typeof e&&delete e[n]}else delete r[o]}return r}export{applyPatchToResource,patchToModifyRequest,writePatch};
1
+ import{extractLdapCode as e}from"../../lib/ldapCodes.js";import{scimInvalidValue as t,kindOf as r,ScimError as o,scimInvalidPath as a,scimNoTarget as n}from"./errors.js";import{DEFAULT_LOCK_ATTRIBUTE as i,DEFAULT_LOCK_VALUE as s,holdsValue as l,scimPathToLdapAttribute as u,readActive as f}from"./mapping.js";function asValues(e){if(null==e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString():String(e)).filter(e=>e.length>0)}class WorkingEntry{constructor(e){this.current=e,this.values=new Map,this.touched=new Set,this.whole=new Set}get(e){return this.values.has(e)||this.values.set(e,asValues(this.current[e])),this.values.get(e)}set(e,t){this.whole.add(e),this.write(e,t)}append(e,t){const r=this.get(e);this.write(e,[...r,...t.filter(e=>!r.includes(e))])}drop(e,t){if(!t)return this.set(e,[]);this.write(e,this.get(e).filter(e=>!t.includes(e)))}write(e,t){this.touched.add(e),this.values.set(e,t)}diff(){const e={};for(const t of this.touched){const r=asValues(this.current[t]),o=this.get(t),a=o.filter(e=>!r.includes(e)),n=r.filter(e=>!o.includes(e));if(0!==a.length||0!==n.length)if(this.whole.has(t))if(0!==o.length)0===n.length&&r.length>0?(e.add||(e.add={}),e.add[t]=1===a.length?a[0]:a):(e.replace||(e.replace={}),e.replace[t]=1===o.length?o[0]:o);else{if(0===r.length)continue;e.delete||(e.delete={}),e.delete[t]=""}else a.length>0&&(e.add||(e.add={}),e.add[t]=1===a.length?a[0]:a),n.length>0&&(e.delete||(e.delete={}),e.delete[t]=1===n.length?n[0]:n)}return e}}function normalizeOp(e){if("string"!=typeof e)throw t(`Patch op must be a string, got ${r(e)}`);const o=e.toLowerCase();if("add"===o||"remove"===o||"replace"===o)return o;throw t(`Unknown patch op '${e}'`)}const c=new Set(["__proto__","constructor","prototype"]),p=/^[A-Za-z_$:][\w$:]*$/;function assertSafeKey(e){if(c.has(e))throw a(`Forbidden attribute name '${e}'`)}function parsePath(e){if("string"!=typeof e||0===e.length||e.length>512)throw a(`Malformed path '${String(e)}'`);let t,r,o;const n=e.indexOf("[");if(n>=0){const i=e.lastIndexOf("]");if(i<=n)throw a(`Malformed path '${e}'`);t=e.slice(0,n),r=e.slice(n+1,i);const s=e.slice(i+1);if(s.length>0){if("."!==s[0])throw a(`Malformed path '${e}'`);o=s.slice(1)}}else{const r=e.indexOf(".");r>=0?(t=e.slice(0,r),o=e.slice(r+1)):t=e}if(!p.test(t))throw a(`Malformed path '${e}'`);if(assertSafeKey(t),null!=o){if(!p.test(o))throw a(`Malformed sub-attribute in '${e}'`);assertSafeKey(o)}return{top:t,filter:r,sub:o}}function memberRef(e){const o=e&&"object"==typeof e&&!Array.isArray(e)?e.value:e;if(null==o)return"";if("string"==typeof o||"number"==typeof o)return String(o);throw t(`Each member must be a string or an object whose value is one, got ${r(o)}`)}async function applyOperation(e,o,c){const p=normalizeOp(e.op),h=c.memberAttribute||"member";if(!e.path){if("remove"===p)throw n("PATCH 'remove' requires a path");if(null==e.value||"object"!=typeof e.value||Array.isArray(e.value))throw n("PATCH without path requires an object value");const t=e.value;for(const[r,n]of Object.entries(t)){if(!r)throw a("Empty attribute name");await applyOperation({op:e.op,path:r,value:n},o,c)}return}if("string"!=typeof e.path)throw a(`Path must be a string, got ${r(e.path)}`);const{top:d,sub:y,filter:w}=parsePath(e.path);if("members"===d){const t=c.resolveMemberRef;if(!t)throw n("Member operations require a member resolver");const asMemberValues=async e=>{const r=Array.isArray(e)?e:[e],o=[];for(const e of r){const r=memberRef(e);if(!r)continue;const a=await t(r);a&&o.push(a)}return o};if("add"===p)return void o.append(h,await asMemberValues(e.value));if("replace"===p){const t=await asMemberValues(e.value);if(0===t.length)return;return void o.set(h,t)}if(!w&&null==e.value)return void o.drop(h);const r=w?/value\s+eq\s+"([^"]+)"/i.exec(w)?.[1]??"":"",a=[...r?await asMemberValues(r):[],...null!=e.value?await asMemberValues(e.value):[]];return void o.drop(h,a)}if("active"===d&&c.supportsActive){if(y||w)throw a(`'active' has no sub-attribute (got '${e.path}')`);const r=c.lockAttribute||i,n=Object.keys(c.current).find(e=>e.toLowerCase()===r.toLowerCase())??r,u="remove"===p||function(e){const r=f(e);if(void 0===r)throw t("PATCH on 'active' requires a value");return r}(e.value),h=c.lockValue||s;return void(u?l({[n]:o.get(n)},n,h)&&o.drop(n):o.set(n,[h]))}if(w)throw a(`Complex multi-valued filters are only supported on 'members' (got '${e.path}')`);const m=u(y?`${d}.${y}`:d,c.mapping);if(!m)throw a(`Unknown SCIM attribute path '${e.path}'`);if("remove"===p)return void o.drop(m);const v=function(e){if(null!=e)return"string"==typeof e?e:"number"==typeof e||"boolean"==typeof e?String(e):Array.isArray(e)?e.map(e=>{if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);if(e&&"object"==typeof e&&!Array.isArray(e)&&"value"in e&&("string"==typeof e.value||"number"==typeof e.value||"boolean"==typeof e.value))return String(e.value);throw t(`Unsupported array element in PATCH value: ${r(e)}`)}):void 0}(e.value);if(null==v)throw t(`PATCH ${e.op} ${e.path} missing value`);const g=Array.isArray(v)?v:[v];"add"===p?o.append(m,g):o.set(m,g)}async function patchToModifyRequest(e,o){if(!e.Operations||!Array.isArray(e.Operations))throw t("Missing Operations array");const a=new WorkingEntry(o.current);for(const n of e.Operations){if(!n||"object"!=typeof n||Array.isArray(n))throw t(`Each operation must be an object, got ${r(n)}`);await applyOperation(n,a,o)}return a.diff()}async function writePatch(t,r){for(let a=1;;a++)try{return await r()}catch(r){if(16!==e(r))throw r;if(2===a)throw new o(409,`${t} changed while this PATCH was being applied; nothing was written. Retry the request.`)}}function applyPatchToResource(e,t){const r={...e};for(const e of t.Operations){const t=normalizeOp(e.op);if(!e.path){if(e.value&&"object"==typeof e.value&&!Array.isArray(e.value))for(const[t,o]of Object.entries(e.value))assertSafeKey(t),r[t]=o;continue}const{top:o,sub:a}=parsePath(e.path);if("remove"!==t)if(a){const t=r[o],n=t&&"object"==typeof t&&!Array.isArray(t)?{...t}:Object.create(null);n[a]=e.value,r[o]=n}else r[o]=e.value;else if(a){const e=r[o];e&&"object"==typeof e&&delete e[a]}else delete r[o]}return r}export{applyPatchToResource,patchToModifyRequest,writePatch};
@@ -1 +1 @@
1
- import{scimInvalidValue as t}from"./errors.js";import{SCHEMA_USER as e,SCHEMA_GROUP as r}from"./types.js";const o=["schemas","id"];function stripCoreUrn(t){for(const o of[e,r])if(t.toLowerCase().startsWith(`${o.toLowerCase()}:`))return t.slice(o.length+1);return t}function groupPaths(t){const e=new Map;for(const r of t){const t=stripCoreUrn(r.trim());if(!t)continue;const o=t.startsWith("urn:")?t.indexOf(".",t.lastIndexOf(":")):t.indexOf("."),n=(o>0?t.slice(0,o):t).toLowerCase(),s=o>0?t.slice(o+1).toLowerCase():"";if(!s){e.set(n,null);continue}const c=e.get(n);null!==c&&(c?c.add(s):e.set(n,new Set([s])))}return e}function narrow(t,e){const pick=t=>{const r={};for(const[o,n]of Object.entries(t))e.has(o.toLowerCase())&&(r[o]=n);return r};if(Array.isArray(t)){return t.map(t=>t&&"object"==typeof t&&!Array.isArray(t)?pick(t):t).filter(t=>!(t&&"object"==typeof t&&0===Object.keys(t).length))}return t&&"object"==typeof t?pick(t):t}function without(t,e){const drop=t=>{const r={};for(const[o,n]of Object.entries(t))e.has(o.toLowerCase())||(r[o]=n);return r};return Array.isArray(t)?t.map(t=>t&&"object"==typeof t&&!Array.isArray(t)?drop(t):t):t&&"object"==typeof t?drop(t):t}function assertProjection(e){if(e.attributes?.length&&e.excludedAttributes?.length)throw t("'attributes' and 'excludedAttributes' are mutually exclusive (RFC 7644 section 3.9)")}function projectedToNothing(t){return Array.isArray(t)?0===t.length:"object"==typeof t&&null!==t&&0===Object.keys(t).length}function projectResource(t,e){if(e.attributes?.length){const r=groupPaths([...o,...e.attributes]),n={};for(const[e,o]of Object.entries(t)){if(!r.has(e.toLowerCase()))continue;const t=r.get(e.toLowerCase());if(!t){n[e]=o;continue}const s=narrow(o,t);projectedToNothing(s)||(n[e]=s)}return n}if(e.excludedAttributes?.length){const r=groupPaths(e.excludedAttributes),n=new Set(o),s={};for(const[e,o]of Object.entries(t)){const t=e.toLowerCase(),c=r.get(t);if(void 0===c){s[e]=o;continue}if(null===c){n.has(t)&&(s[e]=o);continue}const i=without(o,c);projectedToNothing(i)||(s[e]=i)}return s}return t}function projectList(t,e){return e.attributes?.length||e.excludedAttributes?.length?{...t,Resources:t.Resources.map(t=>projectResource(t,e))}:t}export{assertProjection,projectList,projectResource};
1
+ import{scimInvalidValue as t}from"./errors.js";import{SCHEMA_USER as e,SCHEMA_GROUP as o}from"./types.js";const r=["schemas","id"];function stripCoreUrn(t){for(const r of[e,o])if(t.toLowerCase().startsWith(`${r.toLowerCase()}:`))return t.slice(r.length+1);return t}function groupPaths(t){const e=new Map;for(const o of t){const t=stripCoreUrn(o.trim());if(!t)continue;const r=t.startsWith("urn:")?t.indexOf(".",t.lastIndexOf(":")):t.indexOf("."),n=(r>0?t.slice(0,r):t).toLowerCase(),s=r>0?t.slice(r+1).toLowerCase():"";if(!s){e.set(n,null);continue}const c=e.get(n);null!==c&&(c?c.add(s):e.set(n,new Set([s])))}return e}const nonEmpty=t=>t.filter(t=>!(t&&"object"==typeof t&&0===Object.keys(t).length));function narrow(t,e){const pick=t=>{const o={};for(const[r,n]of Object.entries(t))e.has(r.toLowerCase())&&(o[r]=n);return o};return Array.isArray(t)?nonEmpty(t.map(t=>t&&"object"==typeof t&&!Array.isArray(t)?pick(t):t)):t&&"object"==typeof t?pick(t):t}function without(t,e){const drop=t=>{const o={};for(const[r,n]of Object.entries(t))e.has(r.toLowerCase())||(o[r]=n);return o};return Array.isArray(t)?nonEmpty(t.map(t=>t&&"object"==typeof t&&!Array.isArray(t)?drop(t):t)):t&&"object"==typeof t?drop(t):t}function assertProjection(e){if(e.attributes?.length&&e.excludedAttributes?.length)throw t("'attributes' and 'excludedAttributes' are mutually exclusive (RFC 7644 section 3.9)")}function projectedToNothing(t){return Array.isArray(t)?0===t.length:"object"==typeof t&&null!==t&&0===Object.keys(t).length}function projectResource(t,e){if(e.attributes?.length){const o=groupPaths([...r,...e.attributes]),n={};for(const[e,r]of Object.entries(t)){if(!o.has(e.toLowerCase()))continue;const t=o.get(e.toLowerCase());if(!t){n[e]=r;continue}const s=narrow(r,t);projectedToNothing(s)||(n[e]=s)}return n}if(e.excludedAttributes?.length){const o=groupPaths(e.excludedAttributes),n=new Set(r),s={};for(const[e,r]of Object.entries(t)){const t=e.toLowerCase(),c=o.get(t);if(void 0===c){s[e]=r;continue}if(null===c){n.has(t)&&(s[e]=r);continue}const i=without(r,c);projectedToNothing(i)||(s[e]=i)}return s}return t}function projectList(t,e){return e.attributes?.length||e.excludedAttributes?.length?{...t,Resources:t.Resources.map(t=>projectResource(t,e))}:t}export{assertProjection,projectList,projectResource};
@@ -1 +1 @@
1
- import{fileURLToPath as s}from"url";import{join as e,dirname as i}from"path";import t from"../../abstract/plugin.js";import{setChangeSource as o}from"../../lib/changeContext.js";import{jsonBodyParser as r}from"../../lib/rawBody.js";import{BaseResolver as a}from"./baseResolver.js";import{ScimUsers as n}from"./users.js";import{ScimGroups as c}from"./groups.js";import{ScimBulk as p}from"./bulk.js";import{ScimDiscovery as m}from"./discovery.js";import{SCIM_CONTENT_TYPE as u,scimAsyncHandler as d,writeScimError as h}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";import{projectResource as l,assertProjection as g,projectList as f}from"./projection.js";const isStaticFiles=s=>"function"==typeof s.urlOf&&"function"==typeof s.servedPath;function scimBodyParser(s){return r({type:["application/json","application/scim+json"],limit:`${s.scim_bulk_max_payload_size||1048576}b`})}class Scim extends t{constructor(t){super(t),this.name="scim",this.roles=["api","configurable"],this.ldap=t.ldap,this.scimPrefix=this.config.scim_prefix||"/scim/v2",this.baseResolver=new a(this.config),this.users=new n({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,hooks:this.registeredHooks}),this.groups=new c({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,users:this.users,hooks:this.registeredHooks}),this.bulk=new p({config:this.config,logger:this.logger,users:this.users,groups:this.groups,scimPrefix:this.scimPrefix,hooks:this.registeredHooks});const o=this.config.schemas_path||e(i(s(import.meta.url)),"..","..","..","static","schemas");this.scimSchemaDir=e(o,"scim"),this.discovery=new m({config:this.config,schemaDir:this.scimSchemaDir,scimPrefix:this.scimPrefix,loadedPlugins:t.loadedPlugins})}scimJson(s,e,i){s.status(e).type(u).json(i)}scimResource(s,e,i,t,o={}){const r=i.id?`${this.scimPrefix}/${t}/${encodeURIComponent(i.id)}`:void 0,a=(this.config.scim_base_url||"")&&i.meta?.location?i.meta.location:r;a&&s.set("Location",a),this.scimJson(s,e,l(i,o))}parseProjection(s){const toCsv=s=>{if("string"!=typeof s)return;const e=s.split(",").map(s=>s.trim()).filter(Boolean);return e.length>0?e:void 0},e={attributes:toCsv(s.query.attributes),excludedAttributes:toCsv(s.query.excludedAttributes)};return g(e),e}readScimBody(s){const e=String(s.headers["content-type"]||"");if(e.startsWith("application/scim+json")||e.startsWith("application/json"))return s.body}api(s){const e=this.scimPrefix;this.server.registerBodyParser(e,scimBodyParser(this.config)),s.use(e,(s,e,i)=>{o(s,"scim"),i()}),s.use(e,d((s,e,i)=>{this.parseProjection(s),i()})),s.get(`${e}/Users`,d(async(s,e)=>{const i=this.parseListQuery(s);g(i);const t=await this.users.list(s,i);this.scimJson(e,200,f(t,i))})),s.get(`${e}/Users/:id`,d(async(s,e)=>{const i=this.parseProjection(s),t=await this.users.get(s,decodeURIComponent(s.params.id));this.scimJson(e,200,l(t,i))})),s.post(`${e}/Users`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.users.create(s,i);this.scimResource(e,201,t,"Users",this.parseProjection(s))})),s.put(`${e}/Users/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.users.replace(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Users",this.parseProjection(s))})),s.patch(`${e}/Users/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.users.patch(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Users",this.parseProjection(s))})),s.delete(`${e}/Users/:id`,d(async(s,e)=>{await this.users.delete(s,decodeURIComponent(s.params.id)),e.status(204).end()})),s.get(`${e}/Groups`,d(async(s,e)=>{const i=this.parseListQuery(s);g(i);const t=await this.groups.list(s,i);this.scimJson(e,200,f(t,i))})),s.get(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.parseProjection(s),t=await this.groups.get(s,decodeURIComponent(s.params.id));this.scimJson(e,200,l(t,i))})),s.post(`${e}/Groups`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.groups.create(s,i);this.scimResource(e,201,t,"Groups",this.parseProjection(s))})),s.put(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.groups.replace(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Groups",this.parseProjection(s))})),s.patch(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.groups.patch(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Groups",this.parseProjection(s))})),s.delete(`${e}/Groups/:id`,d(async(s,e)=>{await this.groups.delete(s,decodeURIComponent(s.params.id)),e.status(204).end()})),s.post(`${e}/Bulk`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=this.config.scim_bulk_max_payload_size||1048576,o=JSON.stringify(i).length;if(o>t)return h(e,413,`Bulk payload too large: ${o} > ${t}`,"invalidValue");const r=await this.bulk.execute(s,i);this.scimJson(e,200,r)})),s.get(`${e}/ServiceProviderConfig`,d((s,e)=>{this.scimJson(e,200,this.discovery.serviceProviderConfig(s))})),s.get(`${e}/ResourceTypes`,d((s,e)=>{const i=this.discovery.resourceTypes(s);this.scimJson(e,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:i.length,Resources:i,startIndex:1,itemsPerPage:i.length})})),s.get(`${e}/ResourceTypes/:name`,d((s,e)=>{const i=this.discovery.resourceType(s.params.name,s);if(!i)return h(e,404,"ResourceType not found");this.scimJson(e,200,i)})),s.get(`${e}/Schemas`,d((s,e)=>{const i=this.discovery.schemas(s);this.scimJson(e,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:i.length,Resources:i,startIndex:1,itemsPerPage:i.length})})),s.get(`${e}/Schemas/:id`,d((s,e)=>{const i=this.discovery.schema(decodeURIComponent(s.params.id),s);if(!i)return h(e,404,"Schema not found");this.scimJson(e,200,i)}))}parseListQuery(s){const e=s.query,toInt=s=>{if("string"!=typeof s)return;const e=parseInt(s,10);return Number.isFinite(e)?e:void 0},toCsv=s=>{if("string"==typeof s)return s.split(",").map(s=>s.trim()).filter(Boolean)},i="string"==typeof e.sortOrder?e.sortOrder:void 0;return{filter:"string"==typeof e.filter?e.filter:void 0,startIndex:toInt(e.startIndex),count:toInt(e.count),attributes:toCsv(e.attributes),excludedAttributes:toCsv(e.excludedAttributes),sortBy:"string"==typeof e.sortBy?e.sortBy:void 0,sortOrder:"ascending"===i||"descending"===i?i:void 0}}getConfigApiData(){const s=this.scimPrefix,i=["User.json","Group.json","default-mapping.json"].map(s=>e(this.scimSchemaDir,s)),t=new Set,o=[];for(const s of Object.values(this.server.loadedPlugins)){if(!isStaticFiles(s))continue;const e=s.servedPath().toLowerCase();t.has(e)||(t.add(e),o.push(s))}let r;o.sort((s,e)=>Number("static"===e.name)-Number("static"===s.name));for(const s of o){const[e,t,o]=i.map(e=>s.urlOf(e));if(e&&t){r={user:e,group:t,...o?{defaultMapping:o}:{}};break}}const a=this.users.userMapping,n=this.groups.groupMapping,c=a.entries.map(s=>s.scim).concat(["id","active"]),p=n.entries.map(s=>s.scim).concat(["id"]);return{enabled:!0,version:"2.0",prefix:s,endpoints:{users:`${s}/Users`,user:`${s}/Users/:id`,groups:`${s}/Groups`,group:`${s}/Groups/:id`,bulk:`${s}/Bulk`,serviceProviderConfig:`${s}/ServiceProviderConfig`,resourceTypes:`${s}/ResourceTypes`,schemas:`${s}/Schemas`},schemaUrls:r,capabilities:{patch:!0,bulk:!0,filter:!0,sort:!0,etag:Boolean(this.config.scim_etag),changePassword:!1,maxResults:this.config.scim_max_results||200,maxScanned:this.config.scim_max_scanned||1e4,bulkMaxOperations:this.config.scim_bulk_max_operations||100,bulkMaxPayloadSize:this.config.scim_bulk_max_payload_size||1048576},resourceTypes:[{id:"User",endpoint:`${s}/Users`,schema:"urn:ietf:params:scim:schemas:core:2.0:User",rdnAttribute:this.config.scim_user_rdn_attribute||"uid",objectClass:this.config.scim_user_object_class,filterableAttributes:c,mapping:a.entries},{id:"Group",endpoint:`${s}/Groups`,schema:"urn:ietf:params:scim:schemas:core:2.0:Group",rdnAttribute:this.config.scim_group_rdn_attribute||"cn",objectClass:this.config.scim_group_object_class,filterableAttributes:p,mapping:n.entries}],idStrategy:this.config.scim_id_attribute||"rdn",baseResolution:{userBaseTemplate:this.config.scim_user_base_template||void 0,groupBaseTemplate:this.config.scim_group_base_template||void 0,hasBaseMap:Boolean(this.config.scim_base_map)}}}}export{Scim as default,scimBodyParser};
1
+ import{fileURLToPath as s}from"url";import{join as i,dirname as e}from"path";import t from"../../abstract/plugin.js";import{setChangeSource as r}from"../../lib/changeContext.js";import{jsonBodyParser as o}from"../../lib/rawBody.js";import{staticPlugins as a}from"../../lib/staticFiles.js";import{BaseResolver as c}from"./baseResolver.js";import{ScimUsers as n}from"./users.js";import{ScimGroups as p}from"./groups.js";import{ScimBulk as m}from"./bulk.js";import{ScimDiscovery as u}from"./discovery.js";import{SCIM_CONTENT_TYPE as h,scimAsyncHandler as d,writeScimError as l}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";import{projectResource as g,assertProjection as f,projectList as y}from"./projection.js";function scimBodyParser(s){return o({type:["application/json","application/scim+json"],limit:`${s.scim_bulk_max_payload_size||1048576}b`})}class Scim extends t{constructor(t){super(t),this.name="scim",this.roles=["api","configurable"],this.ldap=t.ldap,this.scimPrefix=this.config.scim_prefix||"/scim/v2",this.baseResolver=new c(this.config),this.users=new n({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,hooks:this.registeredHooks}),this.groups=new p({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,users:this.users,hooks:this.registeredHooks}),this.bulk=new m({config:this.config,logger:this.logger,users:this.users,groups:this.groups,scimPrefix:this.scimPrefix,hooks:this.registeredHooks});const r=this.config.schemas_path||i(e(s(import.meta.url)),"..","..","..","static","schemas");this.scimSchemaDir=i(r,"scim"),this.discovery=new u({config:this.config,schemaDir:this.scimSchemaDir,scimPrefix:this.scimPrefix,loadedPlugins:t.loadedPlugins})}scimJson(s,i,e){s.status(i).type(h).json(e)}scimResource(s,i,e,t,r={}){const o=e.id?`${this.scimPrefix}/${t}/${encodeURIComponent(e.id)}`:void 0,a=(this.config.scim_base_url||"")&&e.meta?.location?e.meta.location:o;a&&s.set("Location",a),this.scimJson(s,i,g(e,r))}parseProjection(s){const toCsv=s=>{if("string"!=typeof s)return;const i=s.split(",").map(s=>s.trim()).filter(Boolean);return i.length>0?i:void 0},i={attributes:toCsv(s.query.attributes),excludedAttributes:toCsv(s.query.excludedAttributes)};return f(i),i}readScimBody(s){const i=String(s.headers["content-type"]||"");if(i.startsWith("application/scim+json")||i.startsWith("application/json"))return s.body}api(s){const i=this.scimPrefix;this.server.registerBodyParser(i,scimBodyParser(this.config)),s.use(i,(s,i,e)=>{r(s,"scim"),e()}),s.use(i,d((s,i,e)=>{this.parseProjection(s),e()})),s.get(`${i}/Users`,d(async(s,i)=>{const e=this.parseListQuery(s);f(e);const t=await this.users.list(s,e);this.scimJson(i,200,y(t,e))})),s.get(`${i}/Users/:id`,d(async(s,i)=>{const e=this.parseProjection(s),t=await this.users.get(s,s.params.id);this.scimJson(i,200,g(t,e))})),s.post(`${i}/Users`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return l(i,400,"Missing body","invalidSyntax");const t=await this.users.create(s,e);this.scimResource(i,201,t,"Users",this.parseProjection(s))})),s.put(`${i}/Users/:id`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return l(i,400,"Missing body","invalidSyntax");const t=await this.users.replace(s,s.params.id,e);this.scimResource(i,200,t,"Users",this.parseProjection(s))})),s.patch(`${i}/Users/:id`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return l(i,400,"Missing body","invalidSyntax");const t=await this.users.patch(s,s.params.id,e);this.scimResource(i,200,t,"Users",this.parseProjection(s))})),s.delete(`${i}/Users/:id`,d(async(s,i)=>{await this.users.delete(s,s.params.id),i.status(204).end()})),s.get(`${i}/Groups`,d(async(s,i)=>{const e=this.parseListQuery(s);f(e);const t=await this.groups.list(s,e);this.scimJson(i,200,y(t,e))})),s.get(`${i}/Groups/:id`,d(async(s,i)=>{const e=this.parseProjection(s),t=await this.groups.get(s,s.params.id);this.scimJson(i,200,g(t,e))})),s.post(`${i}/Groups`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return l(i,400,"Missing body","invalidSyntax");const t=await this.groups.create(s,e);this.scimResource(i,201,t,"Groups",this.parseProjection(s))})),s.put(`${i}/Groups/:id`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return l(i,400,"Missing body","invalidSyntax");const t=await this.groups.replace(s,s.params.id,e);this.scimResource(i,200,t,"Groups",this.parseProjection(s))})),s.patch(`${i}/Groups/:id`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return l(i,400,"Missing body","invalidSyntax");const t=await this.groups.patch(s,s.params.id,e);this.scimResource(i,200,t,"Groups",this.parseProjection(s))})),s.delete(`${i}/Groups/:id`,d(async(s,i)=>{await this.groups.delete(s,s.params.id),i.status(204).end()})),s.post(`${i}/Bulk`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return l(i,400,"Missing body","invalidSyntax");const t=this.config.scim_bulk_max_payload_size||1048576;let r;try{r=JSON.stringify(e).length}catch{return l(i,400,"Body nested too deeply","invalidValue")}if(r>t)return l(i,413,`Bulk payload too large: ${r} > ${t}`,"invalidValue");const o=await this.bulk.execute(s,e);this.scimJson(i,200,o)})),s.get(`${i}/ServiceProviderConfig`,d((s,i)=>{this.scimJson(i,200,this.discovery.serviceProviderConfig(s))})),s.get(`${i}/ResourceTypes`,d((s,i)=>{const e=this.discovery.resourceTypes(s);this.scimJson(i,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:e.length,Resources:e,startIndex:1,itemsPerPage:e.length})})),s.get(`${i}/ResourceTypes/:name`,d((s,i)=>{const e=this.discovery.resourceType(s.params.name,s);if(!e)return l(i,404,"ResourceType not found");this.scimJson(i,200,e)})),s.get(`${i}/Schemas`,d((s,i)=>{const e=this.discovery.schemas(s);this.scimJson(i,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:e.length,Resources:e,startIndex:1,itemsPerPage:e.length})})),s.get(`${i}/Schemas/:id`,d((s,i)=>{const e=this.discovery.schema(s.params.id,s);if(!e)return l(i,404,"Schema not found");this.scimJson(i,200,e)}))}parseListQuery(s){const i=s.query,toInt=s=>{if("string"!=typeof s)return;const i=parseInt(s,10);return Number.isFinite(i)?i:void 0},toCsv=s=>{if("string"==typeof s)return s.split(",").map(s=>s.trim()).filter(Boolean)},e="string"==typeof i.sortOrder?i.sortOrder:void 0;return{filter:"string"==typeof i.filter?i.filter:void 0,startIndex:toInt(i.startIndex),count:toInt(i.count),attributes:toCsv(i.attributes),excludedAttributes:toCsv(i.excludedAttributes),sortBy:"string"==typeof i.sortBy?i.sortBy:void 0,sortOrder:"ascending"===e||"descending"===e?e:void 0}}getConfigApiData(){const s=this.scimPrefix,e=["User.json","Group.json","default-mapping.json"].map(s=>i(this.scimSchemaDir,s));let t;for(const s of a(this.server)){const[i,r,o]=e.map(i=>s.schemaUrlOf(i));if(i&&r){t={user:i,group:r,...o?{defaultMapping:o}:{}};break}}const r=this.users.userMapping,o=this.groups.groupMapping,c=r.entries.map(s=>s.scim).concat(["id","active"]),n=o.entries.map(s=>s.scim).concat(["id"]);return{enabled:!0,version:"2.0",prefix:s,endpoints:{users:`${s}/Users`,user:`${s}/Users/:id`,groups:`${s}/Groups`,group:`${s}/Groups/:id`,bulk:`${s}/Bulk`,serviceProviderConfig:`${s}/ServiceProviderConfig`,resourceTypes:`${s}/ResourceTypes`,schemas:`${s}/Schemas`},schemaUrls:t,capabilities:{patch:!0,bulk:!0,filter:!0,sort:!0,etag:Boolean(this.config.scim_etag),changePassword:!1,maxResults:this.config.scim_max_results||200,maxScanned:this.config.scim_max_scanned||1e4,bulkMaxOperations:this.config.scim_bulk_max_operations||100,bulkMaxPayloadSize:this.config.scim_bulk_max_payload_size||1048576},resourceTypes:[{id:"User",endpoint:`${s}/Users`,schema:"urn:ietf:params:scim:schemas:core:2.0:User",rdnAttribute:this.config.scim_user_rdn_attribute||"uid",objectClass:this.config.scim_user_object_class,filterableAttributes:c,mapping:r.entries},{id:"Group",endpoint:`${s}/Groups`,schema:"urn:ietf:params:scim:schemas:core:2.0:Group",rdnAttribute:this.config.scim_group_rdn_attribute||"cn",objectClass:this.config.scim_group_object_class,filterableAttributes:n,mapping:o.entries}],idStrategy:this.config.scim_id_attribute||"rdn",baseResolution:{userBaseTemplate:this.config.scim_user_base_template||void 0,groupBaseTemplate:this.config.scim_group_base_template||void 0,hasBaseMap:Boolean(this.config.scim_base_map)}}}}export{Scim as default,scimBodyParser};
@@ -1 +1 @@
1
- import{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 h,loadMappingFile as n,DEFAULT_USER_MAPPING as l,requiredLdapAttributes as u,ldapToScimUser as d,scimUserToLdap as p,isLocked as m}from"./mapping.js";import{scimFilterToLdap as f}from"./filter.js";import{pagedSearch as b}from"./list.js";import{writePatch as g,patchToModifyRequest as w}from"./patch.js";import{scimNotFound as k,ScimError as y,scimInvalidValue as A,scimUniqueness as R}from"./errors.js";import{extractLdapCode as $}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=h(l,s?n(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 u(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===$(t))throw k(`User ${e} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw k(`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=f(e.filter,this.mapping,{lockAttribute:this.lockAttribute,lockValue:this.lockValue,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 y&&404===t.statusCode)return{schemas:[o],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:h,totalResults:n}=await b({directory:this.ldap.forRequest(t),base:s,filter:c,attributes:this.ldapAttributes(),startIndex:i,count:r,maxScanned:this.maxScanned});e.sortBy,e.sortOrder;const l=h.map(e=>d(e,this.mapping,this.ctx(t)));return{schemas:[o],totalResults:n,startIndex:i,itemsPerPage:l.length,Resources:l}}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:h}=p(o,this.mapping,this.ctx(r),this.objectClass);if(!c)throw A("userName is required");s(c,this.rdnAttribute),h[this.rdnAttribute]=c;const n=this.baseResolver.userBase(r),l=`${this.rdnAttribute}=${t(c)},${n}`;try{await this.ldap.forRequest(r).add(l,h)}catch(t){if(68===$(t))throw R(`User ${c} already exists`);if(void 0!==h[this.lockAttribute])throw this.lockSchemaError(t);throw t}const u=await this.get(r,c);return i(this.hooks.scimusercreatedone,u),u}async replace(t,s,r){const a=this.dnForId(s,t);let o;try{o=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},a)}catch(t){if(32===$(t))throw k(`User ${s} not found`);throw t}if(!o.searchEntries?.length)throw k(`User ${s} not found`);const c=o.searchEntries[0],h=(await e(this.hooks.scimuserupdate,[s,r,t]))[1],{attributes:n,active:l}=p(h,this.mapping,this.ctx(t),this.objectClass),u={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!=n[t]?u.replace[t]=n[t]:hasAttrValue(c[t])&&u.delete.push(t));if(!1===l?u.replace[this.lockAttribute]=this.lockValue:!0===l&&m(c,{lockAttribute:this.lockAttribute,lockValue:this.lockValue})&&u.delete.push(this.lockAttribute),0===Object.keys(u.replace||{}).length&&delete u.replace,0===u.delete.length&&delete u.delete,u.replace||u.delete)try{await this.ldap.forRequest(t).modify(a,u)}catch(t){if(void 0!==l)throw this.lockSchemaError(t);if(16!==$(t))throw t;throw new y(409,`${a} changed while this PUT was being applied; nothing was written. Retry the request.`)}const f=await this.get(t,s);return i(this.hooks.scimuserupdatedone,s,f),f}async patch(t,e,s){const r=this.dnForId(e,t);await g(r,async()=>{const i=await this.currentEntry(t,e),a=await w(s,{mapping:this.mapping,lockAttribute:this.lockAttribute,lockValue:this.lockValue,supportsActive:!0,current:i});try{await this.ldap.forRequest(t).modify(r,a,{permissive:!0})}catch(t){if(function(t,e){const s=e.toLowerCase(),i=t.delete;return[...Object.keys(t.replace||{}),...Object.keys(t.add||{}),...Array.isArray(i)?i:Object.keys(i||{})].some(t=>t.toLowerCase()===s)}(a,this.lockAttribute))throw this.lockSchemaError(t);throw t}});const a=await this.get(t,e);return i(this.hooks.scimuserupdatedone,e,a),a}lockSchemaError(t){const e=$(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===$(t))throw k(`User ${e} not found`);throw t}if(!i.searchEntries?.length)throw k(`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),o=s.includes("=")&&s.includes(",");if(o&&!r(s,i))return;const c=o?s:`${this.rdnAttribute}=${t(s)},${i}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},c);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==$(t))throw t}if(!o)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!==$(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 h,loadMappingFile as n,DEFAULT_USER_MAPPING as l,requiredLdapAttributes as u,ldapToScimUser as d,scimUserToLdap as p,isLocked as m}from"./mapping.js";import{scimFilterToLdap as b}from"./filter.js";import{pagedSearch as f}from"./list.js";import{writePatch as g,patchToModifyRequest as w}from"./patch.js";import{scimNotFound as k,ScimError as y,scimInvalidValue as A,scimUniqueness as R,kindOf as $}from"./errors.js";import{extractLdapCode as x}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=h(l,s?n(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 u(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===x(t))throw k(`User ${e} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw k(`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=b(e.filter,this.mapping,{lockAttribute:this.lockAttribute,lockValue:this.lockValue,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 y&&404===t.statusCode)return{schemas:[o],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:h,totalResults:n}=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 l=h.map(e=>d(e,this.mapping,this.ctx(t)));return{schemas:[o],totalResults:n,startIndex:i,itemsPerPage:l.length,Resources:l}}async create(r,a){if(!a.userName)throw A("userName is required");p(a,this.mapping,this.ctx(r),this.objectClass);const o=(await e(this.hooks.scimusercreate,[a,r]))[0],{rdn:c,attributes:h}=p(o,this.mapping,this.ctx(r),this.objectClass);if(!c)throw A("userName is required");s(c,this.rdnAttribute),h[this.rdnAttribute]=c;const n=this.baseResolver.userBase(r),l=`${this.rdnAttribute}=${t(c)},${n}`;try{await this.ldap.forRequest(r).add(l,h)}catch(t){if(68===x(t))throw R(`User ${c} already exists`);if(void 0!==h[this.lockAttribute])throw this.lockSchemaError(t);throw t}const u=await this.get(r,c);return i(this.hooks.scimusercreatedone,u),u}async replace(t,s,r){const a=this.dnForId(s,t);let o;try{o=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},a)}catch(t){if(32===x(t))throw k(`User ${s} not found`);throw t}if(!o.searchEntries?.length)throw k(`User ${s} not found`);const c=o.searchEntries[0],h=(await e(this.hooks.scimuserupdate,[s,r,t]))[1],{attributes:n,active:l}=p(h,this.mapping,this.ctx(t),this.objectClass),u={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!=n[t]?u.replace[t]=n[t]:hasAttrValue(c[t])&&u.delete.push(t));if(!1===l?u.replace[this.lockAttribute]=this.lockValue:!0===l&&m(c,{lockAttribute:this.lockAttribute,lockValue:this.lockValue})&&u.delete.push(this.lockAttribute),0===Object.keys(u.replace||{}).length&&delete u.replace,0===u.delete.length&&delete u.delete,u.replace||u.delete)try{await this.ldap.forRequest(t).modify(a,u)}catch(t){if(void 0!==l)throw this.lockSchemaError(t);if(16!==x(t))throw t;throw new y(409,`${a} changed while this PUT was being applied; nothing was written. Retry the request.`)}const b=await this.get(t,s);return i(this.hooks.scimuserupdatedone,s,b),b}async patch(t,e,s){const r=this.dnForId(e,t);await g(r,async()=>{const i=await this.currentEntry(t,e),a=await w(s,{mapping:this.mapping,lockAttribute:this.lockAttribute,lockValue:this.lockValue,supportsActive:!0,current:i});try{await this.ldap.forRequest(t).modify(r,a,{permissive:!0})}catch(t){if(function(t,e){const s=e.toLowerCase(),i=t.delete;return[...Object.keys(t.replace||{}),...Object.keys(t.add||{}),...Array.isArray(i)?i:Object.keys(i||{})].some(t=>t.toLowerCase()===s)}(a,this.lockAttribute))throw this.lockSchemaError(t);throw t}});const a=await this.get(t,e);return i(this.hooks.scimuserupdatedone,e,a),a}lockSchemaError(t){const e=x(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===x(t))throw k(`User ${e} not found`);throw t}if(!i.searchEntries?.length)throw k(`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("string"!=typeof s)throw A(`A member value must be a string, got ${$(s)}`);if(!s)return;const i=this.baseResolver.userBase(e),o=s.includes("=")&&s.includes(",");if(o&&!r(s,i))return;const c=o?s:`${this.rdnAttribute}=${t(s)},${i}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},c);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==x(t))throw t}if(!o)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!==x(t))throw t}}get userMapping(){return this.mapping}}export{ScimUsers};
@@ -1 +1 @@
1
- import t from"fs";import{resolve as e,join as s,relative as r,sep as n,isAbsolute as i}from"path";import{validateHeaderValue as o}from"http";import a from"express";import c from"../abstract/plugin.js";import{STATIC_CSP as h,STATIC_FRAME_OPTIONS as f}from"../config/args.js";import{notFound as m,serverError as p}from"../lib/expressFormatedResponses.js";import{staticName as u,configuredSchemaFiles as d,schemaUrl as g,loadSchemaFileAsync as l}from"../lib/schemaFile.js";const headerValue=(t,e,s,r)=>{if(!1===s)return;if(null!=s&&"string"!=typeof s)throw new Error(`${t}: a text is expected, or false for none`);const n=("string"==typeof s?s:"").replace(/\s+/g," ").trim();if(!n)return r;if("off"!==n.toLowerCase()){try{o(e,n)}catch{throw new Error(`${t}: "${n}" cannot be sent as ${e}`)}return n}};class Static extends c{constructor(){super(...arguments),this.name="static",this.roles=["api","configurable"],this.configured=new Map}api(e){const s=this.config.static_path;if(!s)throw new Error("--static-path is not defined");try{if(!t.statSync(s).isDirectory())throw new Error(`${s} isn't a directory`);t.accessSync(s,t.constants.R_OK)}catch(t){throw new Error(`Bad directory ${s}: ${t}`)}const r=this.config.static_name;if(null!=r&&"string"!=typeof r)throw new Error("--static-name: a text is expected, such as static");const n=u(this.config);if(n.split("/").some(t=>!t||/^\.+$/.test(t)||/[^\x21-\x7e]|[:*?+!()[\]{}\\#%"<>|^`]/.test(t)))throw new Error(`--static-name: "${n}" is not a path to serve under, such as static`);const i=`/${n}`,o=this.schemaServer(s),c=headerValue("--static-csp","Content-Security-Policy",this.config.static_csp,h),m=headerValue("--static-frame-options","X-Frame-Options",this.config.static_frame_options,f);e.use(i,(t,e,s)=>{c&&e.setHeader("Content-Security-Policy",c),m&&e.setHeader("X-Frame-Options",m),s()}),e.get(`${i}/schemas/:name`,(t,e,s)=>o([t.params.name],t,e,s)),e.get(`${i}/schemas/:dir/:name`,(t,e,s)=>o([t.params.dir,t.params.name],t,e,s)),e.get(`${i}/schemas/*path`,(t,e,s)=>o(t.params.path,t,e,s)),e.use(i,a.static(s))}schemaServer(t){const r=this.configured=this.configuredSchemas(t),n=e(s(t,"schemas"));return(t,i,o,a)=>{const c=r.get((t=>{try{return decodeURIComponent(t)}catch{return t}})(i.path));if(c)return this.sendSchema(c,o);if(!t[t.length-1].endsWith(".json"))return a();if(!t.every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t)))return void o.status(400).send("Invalid schema name");const h=e(s(n,...t));h.startsWith(n+"/")?this.sendSchema(h,o):o.status(403).send("Access denied")}}configuredSchemas(t){const r=new Map;for(const n of d(this.config)){const i=g(this.config,n);if(!i)continue;const o=s(t,i.substring(i.indexOf("/schemas/")));if(e(o)===e(n))continue;const a=r.get(i);a&&e(a)!==e(n)?this.logger.warn(`${n} is not served: ${a} already has its URL ${i}`):r.set(i,n)}return r}sendSchema(t,e){l(t,{config:this.config}).then(t=>e.type("json").send(JSON.stringify(t)),t=>{if(t.code)return m(e,"Schema not found");p(e,t)})}urlOf(s){const o=e(s);let a;try{if(!t.statSync(o).isFile())return;a=t.realpathSync(o)}catch{return}const same=s=>{if(e(s)===o)return!0;try{return t.realpathSync(s)===a}catch{return!1}},c=this.servedPath(),url=t=>`${c}/${t.map(encodeURIComponent).join("/")}`;for(const[t,e]of this.configured)if(same(e)){const e=t.slice(c.length+1).split("/");if(e.some(t=>["",".",".."].includes(t)))continue;return url(e)}const h=this.config.static_path;if(!h)return;let f;try{f=t.realpathSync(h)}catch{return}const m=[r(e(h),o),r(f,a)].find(t=>!!t&&".."!==t&&!t.startsWith(`..${n}`)&&!i(t));if(!m)return;const p=m.split(n);return p.some(t=>t.startsWith("."))||"schemas"===p[0].toLowerCase()&&"schemas"!==p[0]&&m.endsWith(".json")||"schemas"===p[0]&&m.endsWith(".json")&&!p.slice(1).every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t))||this.configured.has(`${c}/${p.join("/")}`)?void 0:url(p)}servedPath(){return`/${u(this.config)}`}getConfigApiData(){const t=this.servedPath();return{enabled:!0,staticPath:t,endpoints:{schema:`${t}/schemas/:name`,schemaInSubdir:`${t}/schemas/:dir/:name`,files:`${t}/*`}}}}export{Static as default};
1
+ import t from"fs";import{resolve as s,join as e,relative as n,sep as r,isAbsolute as i}from"path";import{validateHeaderValue as o}from"http";import c from"express";import a from"../abstract/plugin.js";import{STATIC_CSP as h,STATIC_FRAME_OPTIONS as f}from"../config/args.js";import{notFound as m,serverError as u}from"../lib/expressFormatedResponses.js";import{staticName as p,configuredSchemaFiles as d,schemaUrl as l,loadSchemaFileAsync as g}from"../lib/schemaFile.js";const headerValue=(t,s,e,n)=>{if(!1===e)return;if(null!=e&&"string"!=typeof e)throw new Error(`${t}: a text is expected, or false for none`);const r=("string"==typeof e?e:"").replace(/\s+/g," ").trim();if(!r)return n;if("off"!==r.toLowerCase()){try{o(s,r)}catch{throw new Error(`${t}: "${r}" cannot be sent as ${s}`)}return r}};class Static extends a{constructor(){super(...arguments),this.name="static",this.roles=["api","configurable"],this.configured=new Map}api(s){const e=this.config.static_path;if(!e)throw new Error("--static-path is not defined");try{if(!t.statSync(e).isDirectory())throw new Error(`${e} isn't a directory`);t.accessSync(e,t.constants.R_OK)}catch(t){throw new Error(`Bad directory ${e}: ${t}`)}const n=this.config.static_name;if(null!=n&&"string"!=typeof n)throw new Error("--static-name: a text is expected, such as static");const r=p(this.config);if(r.split("/").some(t=>!t||/^\.+$/.test(t)||/[^\x21-\x7e]|[:*?+!()[\]{}\\#%"<>|^`]/.test(t)))throw new Error(`--static-name: "${r}" is not a path to serve under, such as static`);const i=`/${r}`,o=this.schemaServer(e),a=headerValue("--static-csp","Content-Security-Policy",this.config.static_csp,h),m=headerValue("--static-frame-options","X-Frame-Options",this.config.static_frame_options,f);s.use(i,(t,s,e)=>{a&&s.setHeader("Content-Security-Policy",a),m&&s.setHeader("X-Frame-Options",m),e()}),s.get(`${i}/schemas/:name`,(t,s,e)=>o([t.params.name],t,s,e)),s.get(`${i}/schemas/:dir/:name`,(t,s,e)=>o([t.params.dir,t.params.name],t,s,e)),s.get(`${i}/schemas/*path`,(t,s,e)=>o(t.params.path,t,s,e)),s.use(i,c.static(e))}schemaServer(t){const n=this.configured=this.configuredSchemas(t),r=s(e(t,"schemas"));return(t,i,o,c)=>{const a=n.get((t=>{try{return decodeURIComponent(t)}catch{return t}})(i.path));if(a)return this.sendSchema(a,o);if(!t[t.length-1].endsWith(".json"))return c();if(!t.every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t)))return void o.status(400).send("Invalid schema name");const h=s(e(r,...t));h.startsWith(r+"/")?this.sendSchema(h,o):o.status(403).send("Access denied")}}configuredSchemas(t){const n=new Map;for(const r of d(this.config)){const i=l(this.config,r);if(!i)continue;const o=e(t,i.substring(i.indexOf("/schemas/")));if(s(o)===s(r))continue;const c=n.get(i);c&&s(c)!==s(r)?this.logger.warn(`${r} is not served: ${c} already has its URL ${i}`):n.set(i,r)}return n}sendSchema(t,s){g(t,{config:this.config}).then(t=>s.type("json").send(JSON.stringify(t)),t=>{if(t.code)return m(s,"Schema not found");u(s,t)})}urlOf(e){const o=s(e);let c;try{if(!t.statSync(o).isFile())return;c=t.realpathSync(o)}catch{return}const same=e=>{if(s(e)===o)return!0;try{return t.realpathSync(e)===c}catch{return!1}},a=this.servedPath(),url=t=>`${a}/${t.map(encodeURIComponent).join("/")}`;for(const[t,s]of this.configured)if(same(s)){const s=t.slice(a.length+1).split("/");if(s.some(t=>["",".",".."].includes(t)))continue;return url(s)}const h=this.config.static_path;if(!h)return;let f;try{f=t.realpathSync(h)}catch{return}const m=[n(s(h),o),n(f,c)].find(t=>!!t&&".."!==t&&!t.startsWith(`..${r}`)&&!i(t));if(!m)return;const u=m.split(r);return u.some(t=>t.startsWith("."))||"schemas"===u[0].toLowerCase()&&"schemas"!==u[0]&&m.endsWith(".json")||"schemas"===u[0]&&m.endsWith(".json")&&!u.slice(1).every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t))||this.configured.has(`${a}/${u.join("/")}`)?void 0:url(u)}schemaUrlOf(o){const realOf=s=>{try{return t.statSync(s).isFile()?t.realpathSync(s):void 0}catch{return}},c=realOf(o);if(!c)return;const a=this.servedPath(),url=t=>`${a}/${t.map(encodeURIComponent).join("/")}`;for(const[t,s]of this.configured)if(realOf(s)===c){const s=t.slice(a.length+1).split("/");if(s.some(t=>["",".",".."].includes(t)))continue;return url(s)}const h=this.config.static_path;if(!h)return;const f=e(h,"schemas"),m=[];for(const t of[s(o),c]){const s=t.split(r);s.forEach((t,e)=>{"schemas"===t&&m.push(s.slice(e+1))})}try{const s=n(t.realpathSync(f),c);!s||".."===s||s.startsWith(`..${r}`)||i(s)||m.push(s.split(r))}catch{}for(const t of m){if(!t.length||!t[t.length-1].endsWith(".json")||!t.every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t)))continue;const s=["schemas",...t];if(!this.configured.has(`${a}/${s.join("/")}`)&&realOf(e(f,...t))===c)return url(s)}}servedPath(){return`/${p(this.config)}`}getConfigApiData(){const t=this.servedPath();return{enabled:!0,staticPath:t,endpoints:{schema:`${t}/schemas/:name`,schemaInSubdir:`${t}/schemas/:dir/:name`,files:`${t}/*`}}}}export{Static as default};
@@ -1 +1 @@
1
- import{createHash as t}from"node:crypto";import{promises as e}from"node:fs";import{join as a}from"node:path";import r from"../../lib/storage/store.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t);class FileStore extends r{constructor(t,e,a){super(t),this.name="storage/file",this.dir=e,this.abandonedAfter=1e3*Math.max(a,60)}async ready(){await e.mkdir(this.dir,{recursive:!0,mode:448})}path(e){return a(this.dir,t("sha256").update(e).digest("hex"))}static encode(t,e){return`${e.deadline} ${FileStore.readable(t)}\n${e.value}\n`}static decode(t){const e=t.indexOf("\n");if(e<0)return null;const a=parseInt(t.slice(0,e).split(" ")[0],10);return Number.isFinite(a)?{deadline:a,value:t.slice(e+1).replace(/\n$/,"")}:null}async load(t){try{return FileStore.decode(await e.readFile(this.path(t),"utf8"))}catch(t){return"ENOENT"!==t?.code&&this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,a){await this.ready();const r=this.path(t),i=`${r}.${process.pid}.tmp`;try{await e.writeFile(i,FileStore.encode(t,a),{mode:384}),await e.rename(i,r)}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),await e.unlink(i).catch(()=>{}),t}}async drop(t){await e.unlink(this.path(t)).catch(t=>{"ENOENT"!==t?.code&&this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)})}async sweep(){const t=Date.now();let r,i=0;try{r=await e.readdir(this.dir)}catch{return 0}for(const n of r){const r=a(this.dir,n);if(n.endsWith(".tmp")){await e.stat(r).then(e=>t-e.mtimeMs).catch(()=>0)>this.abandonedAfter&&(await e.unlink(r).catch(()=>{}),i++);continue}let s=null;try{s=FileStore.decode(await e.readFile(r,"utf8"))}catch{continue}null!==s&&s.deadline>t||(await e.unlink(r).catch(()=>{}),i++)}return i}}export{FileStore as default};
1
+ import{createHash as t}from"node:crypto";import{promises as e,constants as a}from"node:fs";import{join as r}from"node:path";import i from"../../lib/storage/store.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t);class FileStore extends i{constructor(t,e,a){super(t),this.name="storage/file",this.dir=e,this.abandonedAfter=1e3*Math.max(a,60)}async ready(){await e.mkdir(this.dir,{recursive:!0,mode:448})}async open(){await this.ready()}async health(){if(!(await e.stat(this.dir)).isDirectory())throw new Error(`${this.dir} is not a directory`);return await e.access(this.dir,a.W_OK|a.X_OK),"ok"}path(e){return r(this.dir,t("sha256").update(e).digest("hex"))}static encode(t,e){return`${e.deadline} ${FileStore.readable(t)}\n${e.value}\n`}static decode(t){const e=t.indexOf("\n");if(e<0)return null;const a=parseInt(t.slice(0,e).split(" ")[0],10);return Number.isFinite(a)?{deadline:a,value:t.slice(e+1).replace(/\n$/,"")}:null}async load(t){try{return FileStore.decode(await e.readFile(this.path(t),"utf8"))}catch(t){return"ENOENT"!==t?.code&&this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,a){await this.ready();const r=this.path(t),i=`${r}.${process.pid}.tmp`;try{await e.writeFile(i,FileStore.encode(t,a),{mode:384}),await e.rename(i,r)}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),await e.unlink(i).catch(()=>{}),t}}async drop(t){await e.unlink(this.path(t)).catch(t=>{"ENOENT"!==t?.code&&this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)})}async sweep(){const t=Date.now();let a,i=0;try{a=await e.readdir(this.dir)}catch{return 0}for(const n of a){const a=r(this.dir,n);if(n.endsWith(".tmp")){await e.stat(a).then(e=>t-e.mtimeMs).catch(()=>0)>this.abandonedAfter&&(await e.unlink(a).catch(()=>{}),i++);continue}let s=null;try{s=FileStore.decode(await e.readFile(a,"utf8"))}catch{continue}null!==s&&s.deadline>t||(await e.unlink(a).catch(()=>{}),i++)}return i}}export{FileStore as default};
@@ -1 +1 @@
1
- import t from"../../lib/storage/store.js";import{outsideRequest as e}from"../../lib/requestContext.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t),quote=t=>`"${t.replace(/"/g,'""')}"`;class PostgresStore extends t{constructor(t,e,a){super(t),this.name="storage/postgres",this.url=e,this.table=a,this.quoted=a.split(".").map(quote).join(".")}async driver(){return import("pg")}async open(){await this.pool()}async close(){await super.close();const t=this.connection;this.connection=void 0,t&&await((await t.catch(()=>{}))?.end())}pool(){return this.connection??(this.connection=e(()=>this.connect()).catch(t=>{throw this.connection=void 0,t})),this.connection}async connect(){let t;try{t=await this.driver()}catch(t){throw new Error(`${this.name}: requires the optional dependency "pg", which is not installed. Install it to keep the records in PostgreSQL, or choose another --storage-backend. (${errorText(t)})`)}const e=new t.Pool({connectionString:this.url,connectionTimeoutMillis:1e4,allowExitOnIdle:!0});e.on("error",t=>this.logger.warn(`${this.name}: connection lost: ${errorText(t)}`));try{await this.ensureTable(e)}catch(t){throw await e.end().catch(()=>{}),new Error(`${this.name}: cannot use table ${this.table}: ${errorText(t)}`)}return e}async ensureTable(t){const e=await t.query("SELECT to_regclass($1) AS t",[this.quoted]);if(e.rows[0]?.t)return;const a=this.quoted,n=quote(`${this.table.split(".").pop()}_expires_at`);await t.query(`CREATE TABLE IF NOT EXISTS ${a} (\n namespace text NOT NULL,\n key text NOT NULL,\n value text NOT NULL,\n expires_at timestamptz NOT NULL,\n PRIMARY KEY (namespace, key)\n )`),await t.query(`CREATE INDEX IF NOT EXISTS ${n} ON ${a} (expires_at)`),this.logger.info(`${this.name}: created table ${this.table}`)}static parts(t){const e=t.indexOf("\0"),a=t.slice(e+1);return a.includes("\0")?null:[t.slice(0,e),a]}async load(t){const e=PostgresStore.parts(t);if(!e)return null;try{const t=await this.pool(),a=(await t.query(`SELECT value, expires_at FROM ${this.quoted}\n WHERE namespace = $1 AND key = $2`,e)).rows[0];return a?{value:a.value,deadline:a.expires_at.getTime()}:null}catch(t){return this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,e){const a=PostgresStore.parts(t);if(!a)throw new Error(`${this.name}: cannot keep a key holding a NUL`);try{const t=await this.pool();await t.query(`INSERT INTO ${this.quoted}\n (namespace, key, value, expires_at) VALUES ($1, $2, $3, $4)\n ON CONFLICT (namespace, key) DO UPDATE\n SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,[...a,e.value,new Date(e.deadline)])}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),t}}async drop(t){const e=PostgresStore.parts(t);if(e)try{const t=await this.pool();await t.query(`DELETE FROM ${this.quoted}\n WHERE namespace = $1 AND key = $2`,e)}catch(t){this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)}}async sweep(){const t=await this.pool();return(await t.query(`DELETE FROM ${this.quoted} WHERE expires_at <= $1`,[new Date])).rowCount??0}}export{PostgresStore as default};
1
+ import{AsyncResource as t}from"node:async_hooks";import e from"../../lib/storage/store.js";import{PROBE_TIMEOUT_MS as n}from"../../lib/health.js";import{outsideAnyRequest as a}from"../../lib/requestContext.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t),quote=t=>`"${t.replace(/"/g,'""')}"`;class PostgresStore extends e{constructor(t,e,n){super(t),this.name="storage/postgres",this.url=e,this.table=n,this.quoted=n.split(".").map(quote).join(".")}async driver(){return import("pg")}async open(){await this.pool()}async health(){const t=new((await this.driver()).Client)({connectionString:this.url,connectionTimeoutMillis:n,query_timeout:n});t.on("error",()=>{});try{return await t.connect(),await t.query("SELECT 1"),"ok"}finally{await t.end().catch(()=>{})}}async close(){await super.close();const t=this.connection;this.connection=void 0,t&&await((await t.catch(()=>{}))?.end())}pool(){return this.closed?Promise.reject(new Error(`${this.name}: closed`)):(this.connection??(this.connection=a(()=>this.connect()).catch(t=>{throw this.connection=void 0,t})),this.connection)}async connect(){let e;try{e=await this.driver()}catch(t){throw new Error(`${this.name}: requires the optional dependency "pg", which is not installed. Install it to keep the records in PostgreSQL, or choose another --storage-backend. (${errorText(t)})`)}const n=new e.Pool({connectionString:this.url,connectionTimeoutMillis:1e4,allowExitOnIdle:!0});n.on("error",t.bind(t=>this.logger.warn(`${this.name}: connection lost: ${errorText(t)}`)));try{await this.ensureTable(n)}catch(t){throw await n.end().catch(()=>{}),new Error(`${this.name}: cannot use table ${this.table}: ${errorText(t)}`)}return n}async ensureTable(t){const e=await t.query("SELECT to_regclass($1) AS t",[this.quoted]);if(e.rows[0]?.t)return;const n=this.quoted,a=quote(`${this.table.split(".").pop()}_expires_at`);await t.query(`CREATE TABLE IF NOT EXISTS ${n} (\n namespace text NOT NULL,\n key text NOT NULL,\n value text NOT NULL,\n expires_at timestamptz NOT NULL,\n PRIMARY KEY (namespace, key)\n )`),await t.query(`CREATE INDEX IF NOT EXISTS ${a} ON ${n} (expires_at)`),this.logger.info(`${this.name}: created table ${this.table}`)}static parts(t){const e=t.indexOf("\0"),n=t.slice(e+1);return n.includes("\0")?null:[t.slice(0,e),n]}async load(t){const e=PostgresStore.parts(t);if(!e)return null;try{const t=await this.pool(),n=(await t.query(`SELECT value, expires_at FROM ${this.quoted}\n WHERE namespace = $1 AND key = $2`,e)).rows[0];return n?{value:n.value,deadline:n.expires_at.getTime()}:null}catch(t){return this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,e){const n=PostgresStore.parts(t);if(!n)throw new Error(`${this.name}: cannot keep a key holding a NUL`);try{const t=await this.pool();await t.query(`INSERT INTO ${this.quoted}\n (namespace, key, value, expires_at) VALUES ($1, $2, $3, $4)\n ON CONFLICT (namespace, key) DO UPDATE\n SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,[...n,e.value,new Date(e.deadline)])}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),t}}async drop(t){const e=PostgresStore.parts(t);if(e)try{const t=await this.pool();await t.query(`DELETE FROM ${this.quoted}\n WHERE namespace = $1 AND key = $2`,e)}catch(t){this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)}}async sweep(){const t=await this.pool();return(await t.query(`DELETE FROM ${this.quoted} WHERE expires_at <= $1`,[new Date])).rowCount??0}}export{PostgresStore as default};
@@ -1 +1 @@
1
- import t from"../../lib/storage/store.js";import{outsideRequest as e}from"../../lib/requestContext.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t);class ValkeyStore extends t{constructor(t,e,n){super(t),this.name="storage/valkey",this.down=!1,this.url=e,this.prefix=n}async driver(){return import("iovalkey")}async open(){await this.client()}async close(){await super.close();const t=this.connection;this.connection=void 0;const e=await(t?.catch(()=>{}));e&&await e.quit().catch(()=>e.disconnect())}client(){return this.connection??(this.connection=e(()=>this.connect()).catch(t=>{throw this.connection=void 0,t})),this.connection}async connect(){let t;try{t=await this.driver()}catch(t){throw new Error(`${this.name}: requires the optional dependency "iovalkey", which is not installed. Install it to keep the records in Valkey, or choose another --storage-backend. (${errorText(t)})`)}const e=new t.Valkey(this.url,{lazyConnect:!0,enableOfflineQueue:!1,maxRetriesPerRequest:1,commandTimeout:5e3});let n,o=!1;e.on("error",t=>{n=t,o&&!this.down&&(this.down=!0,this.logger.warn(`${this.name}: connection lost: ${errorText(t)}`))}),e.on("ready",()=>{this.down&&this.logger.info(`${this.name}: connection back`),this.down=!1});try{await e.connect(),o=!0}catch(t){throw e.disconnect(),new Error(`${this.name}: cannot connect: ${errorText(n??t)}`)}return e}key(t){return`${this.prefix}${t}`}async load(t){try{const e=await(await this.client()).get(this.key(t));return null===e?null:ValkeyStore.decodeRecord(e)}catch(t){return this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,e){const n=Math.ceil(e.deadline-Date.now());try{const o=await this.client();if(n<=0)return void await o.del(this.key(t));await o.set(this.key(t),ValkeyStore.encodeRecord(t,e),"PX",n)}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),t}}async drop(t){try{await(await this.client()).del(this.key(t))}catch(t){this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)}}async sweep(){return 0}}export{ValkeyStore as default};
1
+ import{AsyncResource as t}from"node:async_hooks";import e from"../../lib/storage/store.js";import{outsideAnyRequest as n}from"../../lib/requestContext.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t);class ValkeyStore extends e{constructor(t,e,n){super(t),this.name="storage/valkey",this.down=!1,this.url=e,this.prefix=n}async driver(){return import("iovalkey")}async open(){await this.client()}async health(){return await(await this.client()).ping(),"ok"}async close(){await super.close();const t=this.connection;this.connection=void 0;const e=await(t?.catch(()=>{}));e&&await e.quit().catch(()=>e.disconnect())}client(){return this.closed?Promise.reject(new Error(`${this.name}: closed`)):(this.connection??(this.connection=n(()=>this.connect()).catch(t=>{throw this.connection=void 0,t})),this.connection)}async connect(){let e;try{e=await this.driver()}catch(t){throw new Error(`${this.name}: requires the optional dependency "iovalkey", which is not installed. Install it to keep the records in Valkey, or choose another --storage-backend. (${errorText(t)})`)}const n=new e.Valkey(this.url,{lazyConnect:!0,enableOfflineQueue:!1,maxRetriesPerRequest:1,commandTimeout:5e3});let o,i=!1;n.on("error",t.bind(t=>{o=t,i&&!this.down&&(this.down=!0,this.logger.warn(`${this.name}: connection lost: ${errorText(t)}`))})),n.on("ready",t.bind(()=>{this.down&&this.logger.info(`${this.name}: connection back`),this.down=!1}));try{await n.connect(),i=!0}catch(t){throw n.disconnect(),new Error(`${this.name}: cannot connect: ${errorText(o??t)}`)}return n}key(t){return`${this.prefix}${t}`}async load(t){try{const e=await(await this.client()).get(this.key(t));return null===e?null:ValkeyStore.decodeRecord(e)}catch(t){return this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,e){const n=Math.ceil(e.deadline-Date.now());try{const o=await this.client();if(n<=0)return void await o.del(this.key(t));await o.set(this.key(t),ValkeyStore.encodeRecord(t,e),"PX",n)}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),t}}async drop(t){try{await(await this.client()).del(this.key(t))}catch(t){this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)}}async sweep(){return 0}}export{ValkeyStore as default};
@@ -1 +1 @@
1
- import e from"../abstract/plugin.js";import r from"./storage/file.js";import s from"./storage/ldap.js";import t from"./storage/postgres.js";import o from"./storage/valkey.js";let a;const i="ldap, file, postgres, valkey";class Storage extends e{constructor(e){if(super(e),this.name="storage",a)throw new Error(`storage: an instance is already loaded, keeping its records in ${a.store.name}. A second one would put the same record in two places and leave registration order to decide which answers. Load one, and let its consumers share it.`);const n=e.config.storage_backend||"";switch(n){case"ldap":{const r=e.config.storage_ldap_base;if(!r)throw new Error("storage: the ldap backend needs --storage-ldap-base, the branch it writes its records to. It holds records and nothing else, so it belongs outside the branches the directory serves.");this.store=new s(e,r,e.config.storage_ldap_object_class||"applicationProcess");break}case"file":{const s=e.config.storage_file_directory;if(!s)throw new Error("storage: the file backend needs --storage-file-directory, the directory it writes its records to. It writes one file per record, so nothing else should be writing there.");this.store=new r(e.logger,s,e.config.storage_sweep_interval||600);break}case"postgres":{const r=e.config.storage_postgres_url;if(!r)throw new Error("storage: the postgres backend needs --storage-postgres-url, the database it writes its records to.");this.store=new t(e.logger,r,e.config.storage_postgres_table||"ldap_rest_storage");break}case"valkey":{const r=e.config.storage_valkey_url;if(!r)throw new Error("storage: the valkey backend needs --storage-valkey-url, the server it writes its records to.");this.store=new o(e.logger,r,e.config.storage_valkey_prefix??"");break}default:throw new Error(n?`storage: unknown backend "${n}". Known: ${i}.`:`storage: --storage-backend says which sub-plugin keeps the records. Known: ${i}.`)}a=this,this.logger.info(`storage: records kept by ${this.store.name}`),this.store.open().catch(e=>this.logger.error(e instanceof Error?e.message:String(e))),this.store.startSweeping(e.config.storage_sweep_interval||0)}static release(e){a===e&&(a=void 0)}}export{Storage as default};
1
+ import e from"../abstract/plugin.js";import r from"./storage/file.js";import s from"./storage/ldap.js";import t from"./storage/postgres.js";import o from"./storage/valkey.js";let a;const i="ldap, file, postgres, valkey";class Storage extends e{constructor(e){if(super(e),this.name="storage",this.degraded=!1,this.probes=0,a)throw new Error(`storage: an instance is already loaded, keeping its records in ${a.store.name}. A second one would put the same record in two places and leave registration order to decide which answers. Load one, and let its consumers share it.`);const n=e.config.storage_backend||"";switch(n){case"ldap":{const r=e.config.storage_ldap_base;if(!r)throw new Error("storage: the ldap backend needs --storage-ldap-base, the branch it writes its records to. It holds records and nothing else, so it belongs outside the branches the directory serves.");this.store=new s(e,r,e.config.storage_ldap_object_class||"applicationProcess");break}case"file":{const s=e.config.storage_file_directory;if(!s)throw new Error("storage: the file backend needs --storage-file-directory, the directory it writes its records to. It writes one file per record, so nothing else should be writing there.");this.store=new r(e.logger,s,e.config.storage_sweep_interval||600);break}case"postgres":{const r=e.config.storage_postgres_url;if(!r)throw new Error("storage: the postgres backend needs --storage-postgres-url, the database it writes its records to.");this.store=new t(e.logger,r,e.config.storage_postgres_table||"ldap_rest_storage");break}case"valkey":{const r=e.config.storage_valkey_url;if(!r)throw new Error("storage: the valkey backend needs --storage-valkey-url, the server it writes its records to.");this.store=new o(e.logger,r,e.config.storage_valkey_prefix??"");break}default:throw new Error(n?`storage: unknown backend "${n}". Known: ${i}.`:`storage: --storage-backend says which sub-plugin keeps the records. Known: ${i}.`)}a=this,this.logger.info(`storage: records kept by ${this.store.name}`),this.store.open().catch(e=>this.logger.error(e instanceof Error?e.message:String(e))),this.store.startSweeping(e.config.storage_sweep_interval||0)}async close(){await this.store.close()}async health(){const e=++this.probes;try{const r=await this.store.health();return e===this.probes&&(this.degraded&&this.logger.info(`${this.store.name}: answers again`),this.degraded=!1),r}catch(r){return e===this.probes&&(this.degraded||this.logger.warn(`${this.store.name}: does not answer: ${r instanceof Error?r.message:String(r)}`),this.degraded=!0),"degraded"}}static release(e){a===e&&(a=void 0)}}export{Storage as default};
@@ -1 +1 @@
1
- import e from"node-fetch";import{TwakePlugin as t}from"../../abstract/twakePlugin.js";import{isDnInBranch as r,rdnValue as s}from"../../lib/utils.js";function isRegisteredUser(e){return"object"==typeof e&&null!==e&&"string"==typeof e.id}class Calendar extends t{constructor(e){super(e,"calendar_webadmin_url","calendar_webadmin_token","calendar_concurrency"),this.name="calendar",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange"},this.hooks={ldapcalendarResourceadddone:async e=>{const[t,r]=e;if(!this.isResource(t,r))return;const s=this.buildResourceData(t,r);s?await this.callWebAdminApi("ldapcalendarResourceadddone",`${this.webadminUrl}/resources`,"POST",t,JSON.stringify(s),{resourceName:s.name}):this.logger.debug(`Skipping resource creation for ${t}: missing required fields`)},ldapcalendarResourcemodifydone:async e=>{const[t,r]=e;if(!this.isInResourceBranch(t))return;const s=this.getResourceId(t);if(!s)return;const i={};r.replace?.cn&&(i.name=Array.isArray(r.replace.cn)?String(r.replace.cn[0]):String(r.replace.cn)),r.replace?.description&&(i.description=Array.isArray(r.replace.description)?String(r.replace.description[0]):String(r.replace.description)),0!==Object.keys(i).length?await this.callWebAdminApi("ldapcalendarResourcemodifydone",this.resourceUrl(s),"PATCH",t,JSON.stringify(i),{resourceId:s,...i}):this.logger.debug(`No relevant changes for resource ${t}`)},ldapcalendarResourcedeletedone:async e=>{if(!this.isInResourceBranch(e))return;const t=this.getResourceId(e);t&&await this.callWebAdminApi("ldapcalendarResourcedeletedone",this.resourceUrl(t),"DELETE",e,null,{resourceId:t})},onLdapChange:async(e,t)=>{const r=t[this.mailAttr];if(r){const t=this.attributeToString(r[0]),s=this.attributeToString(r[1]);return void(t&&s&&t!==s&&await this.syncRegisteredUser("onLdapMailChange",e,t))}(t[this.firstnameAttr]||t[this.lastnameAttr])&&await this.syncRegisteredUser("onLdapDisplayNameChange",e)}},this.resourceBase=this.config.calendar_resource_base||"",this.resourceObjectClass=this.config.calendar_resource_objectclass||"",this.resourceCreator=this.config.calendar_resource_creator||"admin@example.com",this.resourceDomain=this.config.calendar_resource_domain||"";const t=this.config.ldap_base;this.resourceBase&&t&&!r(this.resourceBase,t)&&this.logger.warn(`Calendar plugin: calendar_resource_base (${this.resourceBase}) is not a DN under ${t}; no entry will be taken for a resource`),this.firstnameAttr=this.config.calendar_firstname_attribute||"givenName",this.lastnameAttr=this.config.calendar_lastname_attribute||"sn"}isResource(e,t){if(!this.isInResourceBranch(e))return!1;const r=t.objectClass;if(this.resourceObjectClass){return(Array.isArray(r)?r:[r]).some(e=>String(e).toLowerCase()===this.resourceObjectClass.toLowerCase())}return!0}isInResourceBranch(e){return!this.resourceBase||r(e,this.resourceBase)}resourceUrl(e){return`${this.webadminUrl}/resources/${encodeURIComponent(e)}`}buildResourceData(e,t){const r=t.cn,s=this.attributeToString(r);if(!s)return null;const i=this.attributeToString(t.description)||void 0,a=this.resourceCreator,n=this.resourceDomain||this.extractDomainFromDn(e),o=this.getResourceId(e);return o?{name:s,description:i,creator:a,domain:n,id:o}:null}getResourceId(e){return s(e)||null}async findRegisteredUser(t,r){const s={...r,step:"find_registered_user",searchEmail:t},i=new URL(`${this.webadminUrl}/registeredUsers`);i.searchParams.set("email",t);const a=await this.requestLimit(()=>e(i.toString(),{method:"GET",headers:this.createHeaders()}));if(404!==a.status){if(!a.ok)return this.logger.error({...s,http_status:a.status,http_status_text:a.statusText}),null;let e;try{e=await a.json()}catch(e){return this.logger.error({...s,http_status:a.status,error:`unreadable answer: ${e}`}),null}if(!Array.isArray(e))return isRegisteredUser(e)?e:(this.logger.error({...s,http_status:a.status,error:"answer is not a registered user"}),null);const r=e.find(e=>isRegisteredUser(e)&&e.email?.toLowerCase()===t.toLowerCase());if(r)return r}return this.logger.warn({...s,message:"user not registered in Calendar"}),null}async syncRegisteredUser(t,r,s){const i={plugin:this.name,event:t,result:"error",dn:r},a=await this.ldapGetAttributes(r,[this.mailAttr,this.firstnameAttr,this.lastnameAttr]);if(!a)return void this.logger.warn({...i,message:`Cannot sync registered user: entry not found for ${r}`});const n=this.attributeToString(a[this.mailAttr]);if(!n)return void this.logger.warn({...i,message:`Cannot sync registered user: no mail found for ${r}`});const o=this.attributeToString(a[this.firstnameAttr]),c=this.attributeToString(a[this.lastnameAttr]),u=s||n;try{const t=await this.findRegisteredUser(u,i);if(!t)return;const r=new URL(`${this.webadminUrl}/registeredUsers`);r.searchParams.set("id",t.id);const s={email:n};o&&(s.firstname=o),c&&(s.lastname=c);const a=await this.requestLimit(()=>e(r.toString(),{method:"PATCH",headers:this.createHeaders("application/json"),body:JSON.stringify(s)}));a.ok?this.logger.info({...i,result:"success",id:t.id,http_status:a.status,...s}):this.logger.error({...i,step:"patch_registered_user",id:t.id,http_status:a.status,http_status_text:a.statusText})}catch(e){this.logger.error({...i,error:`${e}`})}}async deleteUserData(t){const r={plugin:this.name,event:"deleteUserData",mail:t};try{const s=new URL(`${this.webadminUrl}/users/${t}`);s.searchParams.set("action","deleteData");const i=await this.requestLimit(()=>e(s.toString(),{method:"POST",headers:this.createHeaders()}));if(!i.ok)return this.logger.error({...r,http_status:i.status,http_status_text:i.statusText}),null;const a=await i.json();return this.logger.info({...r,http_status:i.status,taskId:a.taskId}),a}catch(e){return this.logger.error({...r,error:`${e}`}),null}}}export{Calendar as default};
1
+ import{TwakePlugin as e}from"../../abstract/twakePlugin.js";import{isDnInBranch as t,rdnValue as r}from"../../lib/utils.js";function isRegisteredUser(e){return"object"==typeof e&&null!==e&&"string"==typeof e.id}class Calendar extends e{constructor(e){super(e,"calendar_webadmin_url","calendar_webadmin_token","calendar_concurrency"),this.name="calendar",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange"},this.hooks={ldapcalendarResourceadddone:async e=>{const[t,r]=e;if(!this.isResource(t,r))return;const s=this.buildResourceData(t,r);s?await this.callWebAdminApi("ldapcalendarResourceadddone",`${this.webadminUrl}/resources`,"POST",t,JSON.stringify(s),{resourceName:s.name}):this.logger.debug(`Skipping resource creation for ${t}: missing required fields`)},ldapcalendarResourcemodifydone:async e=>{const[t,r]=e;if(!this.isInResourceBranch(t))return;const s=this.getResourceId(t);if(!s)return;const i={};r.replace?.cn&&(i.name=Array.isArray(r.replace.cn)?String(r.replace.cn[0]):String(r.replace.cn)),r.replace?.description&&(i.description=Array.isArray(r.replace.description)?String(r.replace.description[0]):String(r.replace.description)),0!==Object.keys(i).length?await this.callWebAdminApi("ldapcalendarResourcemodifydone",this.resourceUrl(s),"PATCH",t,JSON.stringify(i),{resourceId:s,...i}):this.logger.debug(`No relevant changes for resource ${t}`)},ldapcalendarResourcedeletedone:async e=>{if(!this.isInResourceBranch(e))return;const t=this.getResourceId(e);t&&await this.callWebAdminApi("ldapcalendarResourcedeletedone",this.resourceUrl(t),"DELETE",e,null,{resourceId:t})},onLdapChange:async(e,t)=>{const r=t[this.mailAttr];if(r){const t=this.attributeToString(r[0]),s=this.attributeToString(r[1]);return void(t&&s&&t!==s&&await this.syncRegisteredUser("onLdapMailChange",e,t))}(t[this.firstnameAttr]||t[this.lastnameAttr])&&await this.syncRegisteredUser("onLdapDisplayNameChange",e)}},this.resourceBase=this.config.calendar_resource_base||"",this.resourceObjectClass=this.config.calendar_resource_objectclass||"",this.resourceCreator=this.config.calendar_resource_creator||"admin@example.com",this.resourceDomain=this.config.calendar_resource_domain||"";const r=this.config.ldap_base;this.resourceBase&&r&&!t(this.resourceBase,r)&&this.logger.warn(`Calendar plugin: calendar_resource_base (${this.resourceBase}) is not a DN under ${r}; no entry will be taken for a resource`),this.firstnameAttr=this.config.calendar_firstname_attribute||"givenName",this.lastnameAttr=this.config.calendar_lastname_attribute||"sn"}isResource(e,t){if(!this.isInResourceBranch(e))return!1;const r=t.objectClass;if(this.resourceObjectClass){return(Array.isArray(r)?r:[r]).some(e=>String(e).toLowerCase()===this.resourceObjectClass.toLowerCase())}return!0}isInResourceBranch(e){return!this.resourceBase||t(e,this.resourceBase)}resourceUrl(e){return`${this.webadminUrl}/resources/${encodeURIComponent(e)}`}buildResourceData(e,t){const r=t.cn,s=this.attributeToString(r);if(!s)return null;const i=this.attributeToString(t.description)||void 0,a=this.resourceCreator,n=this.resourceDomain||this.extractDomainFromDn(e),o=this.getResourceId(e);return o?{name:s,description:i,creator:a,domain:n,id:o}:null}getResourceId(e){return r(e)||null}async findRegisteredUser(e,t){const r={...t,step:"find_registered_user",searchEmail:e},s=new URL(`${this.webadminUrl}/registeredUsers`);s.searchParams.set("email",e);const i=await this.requestLimit(()=>this.fetch(s.toString(),{method:"GET",headers:this.createHeaders()}));if(404!==i.status){if(!i.ok)return this.logger.error({...r,http_status:i.status,http_status_text:i.statusText}),null;let t;try{t=await i.json()}catch(e){return this.logger.error({...r,http_status:i.status,error:`unreadable answer: ${e}`}),null}if(!Array.isArray(t))return isRegisteredUser(t)?t:(this.logger.error({...r,http_status:i.status,error:"answer is not a registered user"}),null);const s=t.find(t=>isRegisteredUser(t)&&t.email?.toLowerCase()===e.toLowerCase());if(s)return s}return this.logger.warn({...r,message:"user not registered in Calendar"}),null}async syncRegisteredUser(e,t,r){const s={plugin:this.name,event:e,result:"error",dn:t},i=await this.ldapGetAttributes(t,[this.mailAttr,this.firstnameAttr,this.lastnameAttr]);if(!i)return void this.logger.warn({...s,message:`Cannot sync registered user: entry not found for ${t}`});const a=this.attributeToString(i[this.mailAttr]);if(!a)return void this.logger.warn({...s,message:`Cannot sync registered user: no mail found for ${t}`});const n=this.attributeToString(i[this.firstnameAttr]),o=this.attributeToString(i[this.lastnameAttr]),c=r||a;try{const e=await this.findRegisteredUser(c,s);if(!e)return;const t=new URL(`${this.webadminUrl}/registeredUsers`);t.searchParams.set("id",e.id);const r={email:a};n&&(r.firstname=n),o&&(r.lastname=o);const i=await this.requestLimit(()=>this.fetch(t.toString(),{method:"PATCH",headers:this.createHeaders("application/json"),body:JSON.stringify(r)}));i.ok?this.logger.info({...s,result:"success",id:e.id,http_status:i.status,...r}):this.logger.error({...s,step:"patch_registered_user",id:e.id,http_status:i.status,http_status_text:i.statusText})}catch(e){this.logger.error({...s,error:`${e}`})}}async deleteUserData(e){const t={plugin:this.name,event:"deleteUserData",mail:e};try{const r=new URL(`${this.webadminUrl}/users/${e}`);r.searchParams.set("action","deleteData");const s=await this.requestLimit(()=>this.fetch(r.toString(),{method:"POST",headers:this.createHeaders()}));if(!s.ok)return this.logger.error({...t,http_status:s.status,http_status_text:s.statusText}),null;const i=await s.json();return this.logger.info({...t,http_status:s.status,taskId:i.taskId}),i}catch(e){return this.logger.error({...t,error:`${e}`}),null}}}export{Calendar as default};
@@ -1 +1 @@
1
- import{setTimeout as e}from"node:timers/promises";import t from"node-fetch";import r from"../../abstract/plugin.js";import{instanceSlug as i}from"../../lib/instanceProviders.js";import{escapeDnValue as s,isChildOf as n}from"../../lib/utils.js";import{BaseResolver as a}from"../scim/baseResolver.js";const o=new Set(["admin","owner","member"]),l=1e3;class ClouderyProvision extends r{constructor(e){super(e),this.name="clouderyProvision",this.roles=["consistency"],this.dependencies={rabbitmq:"core/rabbitmq"},this.createStash=new Map,this.deleteStash=new Map,this.hooks={scimusercreate:e=>(this.normalizeUserName(e[0]),this.captureCreate(e[0],e[1]),e),scimusercreatedone:async e=>{await this.provision(e)},scimuserdelete:async e=>(e[0]=e[0].toLowerCase(),await this.captureDelete(e[0],e[1]),e),scimuserdeletedone:async e=>{await this.deprovision(e)}};const t=this.config;this.managerUrl=(t.cloudery_manager_url||"").replace(/\/$/,""),this.managerToken=t.cloudery_manager_token||"",this.offer=t.cloudery_offer||"b2b_twake_default",this.clouderyDomain=t.cloudery_domain||"",this.userBranch=t.cloudery_user_branch||"",this.orgIdHeader=(t.cloudery_org_id_header||"x-cloudery-org-id").toLowerCase(),this.orgRoleHeader=(t.cloudery_org_role_header||"x-cloudery-org-role").toLowerCase();const r=t.cloudery_default_org_role||"member",i=r.trim().toLowerCase();o.has(i)?this.defaultOrgRole=i:(this.defaultOrgRole="member",this.logger.warn(`${this.name}: cloudery_default_org_role "${r}" is not one of ${[...o].join(", ")} — using "member"`)),this.fqdnAttribute=t.cloudery_fqdn_attribute||"twakeWorkspaceUrl",this.orgIdAttribute=t.cloudery_org_id_attribute||"twakeOrganizationId",this.orgRoleAttribute=t.cloudery_org_role_attribute||"twakeOrganizationRole",this.phonesAttribute=t.cloudery_phones_attribute||"twakePhones",this.invitedAttribute=t.cloudery_invited_attribute||"twakeInvited",this.defaultLocale=t.cloudery_default_locale||"en",this.rdnAttribute=t.scim_user_rdn_attribute||"uid",this.authExchange=t.cozy_auth_exchange||"auth",this.b2bExchange=t.cozy_b2b_exchange||"b2b",this.userCreatedRoutingKey=t.cozy_user_created_routing_key||"user.created",this.userDeletedRoutingKey=t.cozy_user_deleted_routing_key??"domain.user.deleted",this.pollIntervalMs=Number(t.cloudery_workflow_poll_interval_ms)||2e3,this.maxPollAttempts=Number(t.cloudery_workflow_max_attempts)||60,this.baseResolver=new a(this.config),this.managerUrl||this.logger.warn(`${this.name}: cloudery_manager_url is empty — instance provisioning will be skipped`),this.userBranch||this.logger.warn(`${this.name}: cloudery_user_branch is empty — no users will be provisioned`),this.logger.info({plugin:this.name,event:"init",managerUrl:this.managerUrl||void 0,userBranch:this.userBranch||void 0,orgIdHeader:this.orgIdHeader,orgRoleHeader:this.orgRoleHeader,defaultOrgRole:this.defaultOrgRole,fqdnAttribute:this.fqdnAttribute,orgIdAttribute:this.orgIdAttribute,orgRoleAttribute:this.orgRoleAttribute,phonesAttribute:this.phonesAttribute,invitedAttribute:this.invitedAttribute,pollIntervalMs:this.pollIntervalMs,maxPollAttempts:this.maxPollAttempts})}captureCreate(e,t){this.pruneStash();const r=this.extractId(e),i=this.baseResolver.userBase(t);if(this.logger.info({plugin:this.name,event:"captureCreate",userName:r||void 0,base:i,hasManagerUrl:Boolean(this.managerUrl),hasUserBranch:Boolean(this.userBranch)}),!this.managerUrl||!this.userBranch)return void this.logger.info({plugin:this.name,event:"captureCreate",userName:r||void 0,base:i,result:"skipped",reason:"missing_configuration"});if(!r)return void this.logger.info({plugin:this.name,event:"captureCreate",base:i,result:"skipped",reason:"missing_user_name"});if(!this.isAtOrUnder(i,this.userBranch))return void this.logger.info({plugin:this.name,event:"captureCreate",userName:r,base:i,result:"skipped",reason:"outside_b2b_branch"});const s=this.readHeader(t,this.orgIdHeader);if(!s)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:`B2B branch create without ${this.orgIdHeader} — skipping provisioning`});const n=this.extractPrimaryEmail(e);if(!n)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:"B2B branch create without a primary email — skipping provisioning"});const a=n.trim(),o=this.resolveRole(t,r);this.createStash.set(a.toLowerCase(),{orgId:s.trim(),email:a,role:o,base:i,ts:Date.now()}),this.logger.info({plugin:this.name,event:"captureCreate",userName:r,base:i,orgId:s.trim(),email:a,role:o,result:"stashed"})}async provision(e){const t=this.extractId(e);if(this.logger.info({plugin:this.name,event:"provision",userName:t||void 0}),!t)return void this.logger.info({plugin:this.name,event:"provision",result:"skipped",reason:"missing_user_name"});const r=this.extractPrimaryEmail(e);if(!r)return void this.logger.info({plugin:this.name,event:"provision",userName:t,result:"skipped",reason:"missing_primary_email"});const n=r.trim().toLowerCase(),a=this.createStash.get(n);if(!a)return void this.logger.info({plugin:this.name,event:"provision",userName:t,email:r.trim(),result:"skipped",reason:"no_stashed_context"});this.createStash.delete(n),this.logger.info({plugin:this.name,event:"provision",userName:t,email:a.email,orgId:a.orgId,base:a.base,role:a.role,result:"context_loaded"});const o=a.email,l=emailDomain(o),u=i(t+a.orgId);if(!u)return void this.logger.error({plugin:this.name,event:"provision",userName:t,result:"no_slug"});const h=this.extractPrimaryPhone(e),g=this.extractPhones(e),d=this.extractPublicName(e)||t,c={plugin:this.name,event:"provision",userName:t,orgId:a.orgId,slug:u},m=`${u}.${this.clouderyDomain}`.toLowerCase();let p=null;try{p=await this.findInstanceByFqdn(m)}catch(e){this.logger.warn({...c,result:"existence_lookup_failed",fqdn:m,error:`${e}`})}if(p&&(p.internal_email&&p.internal_email.toLowerCase()!==o.toLowerCase()||p.oidc&&p.oidc.toLowerCase()!==u))return void this.logger.error({...c,result:"slug_taken",fqdn:m,uuid:p._id});const f=p?._id??null;let w;if(f)this.logger.info({...c,result:"instance_exists",fqdn:m,uuid:f}),w=m;else{let t;try{this.logger.info({...c,result:"creating_instance",orgDomain:l,email:o,mobile:h||void 0}),t=await this.createInstance({email:o,publicName:d,locale:this.extractLocale(e),oidc:u,phone:h||"",slug:u,public_name:d,offer:this.offer,domain:this.clouderyDomain,skip_email_validation:!0,internal_email:o,org_domain:l,org_id:a.orgId})}catch(e){return void this.logger.error({...c,result:"error",error:`${e}`})}if(!(!t.workflow||await this.waitForWorkflow(t.workflow)))return void this.logger.error({...c,result:"workflow_failed",workflow:t.workflow});w=t.fqdn}const v=`${this.rdnAttribute}=${s(t)},${a.base}`,b={[this.fqdnAttribute]:w,[this.orgIdAttribute]:a.orgId,[this.orgRoleAttribute]:a.role,[this.invitedAttribute]:"TRUE",cn:t};g.length>0&&(b[this.phonesAttribute]=JSON.stringify(g));try{this.logger.info({...c,result:"writing_fqdn",dn:v,hasPhones:g.length>0}),await this.server.ldap.system.modify(v,{replace:b},void 0,{declaredClasses:this.config.user_class}),this.logger.info({...c,result:"written_fqdn",dn:v})}catch(e){this.logger.error({...c,event:"writeFqdn",dn:v,error:`${e}`})}await this.publishUserCreated({twakeId:t,orgDomain:l,orgId:a.orgId,fqdn:w,email:o,mobile:h}),this.logger.info({...c,result:"success",fqdn:w})}async captureDelete(e,t){const r=this.baseResolver.userBase(t);if(this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,hasManagerUrl:Boolean(this.managerUrl),hasUserBranch:Boolean(this.userBranch)}),!this.managerUrl||!this.userBranch)return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,result:"skipped",reason:"missing_configuration"});if(!this.isAtOrUnder(r,this.userBranch))return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,result:"skipped",reason:"outside_b2b_branch"});const i=`${this.rdnAttribute}=${s(e)},${r}`;try{this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:i,result:"searching"});const t=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[this.fqdnAttribute,"mail"]},i),r=t.searchEntries?.[0],s=r?firstValue(r[this.fqdnAttribute]):void 0,n=s?s.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i,"").split("/")[0]:void 0;if(!n)return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:i,result:"skipped",reason:"missing_fqdn"});const a=r?firstValue(r.mail):void 0,o=a?emailDomain(a):"";this.deleteStash.set(e,{fqdn:n,domain:o}),this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:i,fqdn:n,domain:o,result:"stashed"})}catch(t){this.logger.warn({plugin:this.name,event:"captureDelete",id:e,error:`${t}`})}}async deprovision(e){this.logger.info({plugin:this.name,event:"deprovision",id:e});const t=this.deleteStash.get(e);if(!t)return void this.logger.info({plugin:this.name,event:"deprovision",id:e,result:"skipped",reason:"no_stashed_context"});this.deleteStash.delete(e);const r={plugin:this.name,event:"deprovision",id:e,fqdn:t.fqdn};let i=!1;try{this.logger.info({...r,result:"resolving_uuid"});const e=(await this.findInstanceByFqdn(t.fqdn,"_id"))?._id;e?(this.logger.info({...r,result:"deleting_instance",uuid:e}),await this.deleteInstance(e),i=!0,this.logger.info({...r,result:"deleted",uuid:e})):this.logger.warn({...r,result:"not_found"})}catch(e){this.logger.error({...r,result:"error",error:`${e}`})}i&&await this.publishUserDeleted(t.fqdn,t.domain)}async createInstance(e){this.logger.info({plugin:this.name,event:"createInstance",url:`${this.managerUrl}/api/v1/instances`});const r=await t(`${this.managerUrl}/api/v1/instances`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.managerToken}`},body:JSON.stringify(e)});if(!r.ok){const e=(await r.text().catch(()=>"")).slice(0,l);throw this.logger.error({plugin:this.name,event:"createInstance",result:"error",http_status:r.status,http_status_text:r.statusText,body:e||void 0}),new Error(`Cloudery createInstance HTTP ${r.status}`)}const i=await r.json();return this.logger.info({plugin:this.name,event:"createInstance",result:"success",id:i.id,fqdn:i.fqdn,workflow:i.workflow}),i}async waitForWorkflow(r){this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,maxPollAttempts:this.maxPollAttempts,pollIntervalMs:this.pollIntervalMs});for(let i=1;i<=this.maxPollAttempts;i++){try{const e=await t(`${this.managerUrl}/api/v1/workflows/${encodeURIComponent(r)}`,{headers:{Authorization:`Bearer ${this.managerToken}`}});if(e.ok){const t=await e.json();if(this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,status:t.status}),"succeeded"===t.status)return this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,result:"success"}),!0;if("failed"===t.status||"error"===t.status)return this.logger.warn({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,result:"failed",status:t.status}),!1}else{const t=(await e.text().catch(()=>"")).slice(0,l);this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,result:"http_error",http_status:e.status,http_status_text:e.statusText,body:t||void 0})}}catch(e){this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,error:`${e}`})}i<this.maxPollAttempts&&(this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,result:"retrying"}),await e(this.pollIntervalMs))}return this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,result:"timeout",maxPollAttempts:this.maxPollAttempts}),!1}async findInstanceByFqdn(e,r){this.logger.info({plugin:this.name,event:"findInstanceByFqdn",fqdn:e});const i=new URL(`${this.managerUrl}/api/v2/instances`);i.searchParams.set("fqdn",e),r&&i.searchParams.set("only",r),i.searchParams.set("limit","1");const s=await t(i.toString(),{headers:{Authorization:`Bearer ${this.managerToken}`}});if(!s.ok){const t=(await s.text().catch(()=>"")).slice(0,l);throw this.logger.error({plugin:this.name,event:"findInstanceByFqdn",fqdn:e,result:"error",http_status:s.status,http_status_text:s.statusText,body:t||void 0}),new Error(`Cloudery searchInstances HTTP ${s.status}`)}const n=await s.json(),a=n.items?.[0]??null,o=a?._id;return this.logger.info({plugin:this.name,event:"findInstanceByFqdn",fqdn:e,result:o?"found":"not_found",uuid:o||void 0}),a}async deleteInstance(e){const r=new URL(`${this.managerUrl}/api/v1/instances/${encodeURIComponent(e)}`);r.searchParams.set("user_request","true"),this.logger.info({plugin:this.name,event:"deleteInstance",uuid:e,url:r.toString()});const i=await t(r.toString(),{method:"DELETE",headers:{Authorization:`Bearer ${this.managerToken}`}});if(!i.ok){const t=(await i.text().catch(()=>"")).slice(0,l);throw this.logger.error({plugin:this.name,event:"deleteInstance",uuid:e,result:"error",http_status:i.status,http_status_text:i.statusText,body:t||void 0}),new Error(`Cloudery deleteInstance HTTP ${i.status}`)}this.logger.info({plugin:this.name,event:"deleteInstance",uuid:e,result:"success",http_status:i.status})}async publishUserCreated(e){const t=this.requirePlugin("rabbitmq");if(this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,orgId:e.orgId,orgDomain:e.orgDomain,hasRabbitmq:Boolean(t)}),!t)return void this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,result:"skipped",reason:"missing_rabbitmq"});const r={twakeId:e.twakeId,domain:e.orgDomain,organizationDomain:e.orgDomain,workplaceFqdn:e.fqdn,organizationId:e.orgId};e.email&&(r.internalEmail=e.email),e.mobile&&(r.mobile=e.mobile);try{await t.publish(this.authExchange,this.userCreatedRoutingKey,r),this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,exchange:this.authExchange,routingKey:this.userCreatedRoutingKey,result:"success"})}catch(t){this.logger.error({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,error:`${t}`})}}async publishUserDeleted(e,t){if(!this.userDeletedRoutingKey)return;const r=this.requirePlugin("rabbitmq");if(this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,domain:t,hasRabbitmq:Boolean(r)}),r)try{await r.publish(this.b2bExchange,this.userDeletedRoutingKey,{workplaceFqdn:e,domain:t}),this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,exchange:this.b2bExchange,routingKey:this.userDeletedRoutingKey,result:"success"})}catch(t){this.logger.error({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,error:`${t}`})}else this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,result:"skipped",reason:"missing_rabbitmq"})}readHeader(e,t){if(!e)return;const r=e.get?e.get(t):void 0;if("string"==typeof r&&r.length>0)return r;const i=e.query?.[t];return"string"==typeof i&&i.length>0?i:void 0}resolveRole(e,t){const r=this.readHeader(e,this.orgRoleHeader)?.trim().toLowerCase();return r?o.has(r)?(this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,input:r,result:"accepted",role:r}),r):(this.logger.warn({plugin:this.name,event:"captureCreate",userName:t,message:`Unknown ${this.orgRoleHeader} "${r}" — falling back to ${this.defaultOrgRole}`}),this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,input:r,result:"default",role:this.defaultOrgRole}),this.defaultOrgRole):(this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,result:"default",role:this.defaultOrgRole}),this.defaultOrgRole)}isAtOrUnder(e,t){return e.trim().toLowerCase()===t.trim().toLowerCase()||n(e,t)}pruneStash(){const e=Date.now()-3e5;for(const[t,r]of this.createStash)r.ts<e&&this.createStash.delete(t)}normalizeUserName(e){"string"==typeof e.userName&&(e.userName=e.userName.toLowerCase())}extractId(e){return"string"==typeof e.userName&&e.userName.length>0?e.userName:"string"==typeof e.id&&e.id.length>0?e.id:null}extractPrimaryEmail(e){const t=e.emails;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPrimaryPhone(e){const t=e.phoneNumbers;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPhones(e){const t=e.phoneNumbers;return t&&0!==t.length?t.filter(e=>"string"==typeof e.value&&e.value.trim().length>0).map(e=>({number:e.value.trim(),primary:Boolean(e.primary)})):[]}extractPublicName(e){if("string"==typeof e.displayName&&e.displayName.trim().length>0)return e.displayName.trim();const t=e.name?.formatted;if("string"==typeof t&&t.trim().length>0)return t.trim();const r=`${e.name?.givenName?.trim()??""} ${e.name?.familyName?.trim()??""}`.trim();return r.length>0?r:null}extractLocale(e){return"string"==typeof e.locale&&e.locale.length>0?e.locale:"string"==typeof e.preferredLanguage&&e.preferredLanguage.length>0?e.preferredLanguage:this.defaultLocale}}function emailDomain(e){const t=e.lastIndexOf("@");return t>=0?e.slice(t+1):""}function firstValue(e){if(Array.isArray(e)){const t=e[0];return"string"==typeof t?t:void 0}return"string"==typeof e?e:void 0}export{ClouderyProvision as default};
1
+ import{setTimeout as e}from"node:timers/promises";import t from"../../abstract/plugin.js";import{instanceSlug as r}from"../../lib/instanceProviders.js";import{escapeDnValue as i,isChildOf as s}from"../../lib/utils.js";import{BaseResolver as n}from"../scim/baseResolver.js";const a=new Set(["admin","owner","member"]),o=1e3;class ClouderyProvision extends t{constructor(e){super(e),this.name="clouderyProvision",this.roles=["consistency"],this.dependencies={rabbitmq:"core/rabbitmq"},this.createStash=new Map,this.deleteStash=new Map,this.hooks={scimusercreate:e=>(this.normalizeUserName(e[0]),this.captureCreate(e[0],e[1]),e),scimusercreatedone:async e=>{await this.provision(e)},scimuserdelete:async e=>(e[0]=e[0].toLowerCase(),await this.captureDelete(e[0],e[1]),e),scimuserdeletedone:async e=>{await this.deprovision(e)}};const t=this.config;this.managerUrl=(t.cloudery_manager_url||"").replace(/\/$/,""),this.managerToken=t.cloudery_manager_token||"",this.offer=t.cloudery_offer||"b2b_twake_default",this.clouderyDomain=t.cloudery_domain||"",this.userBranch=t.cloudery_user_branch||"",this.orgIdHeader=(t.cloudery_org_id_header||"x-cloudery-org-id").toLowerCase(),this.orgRoleHeader=(t.cloudery_org_role_header||"x-cloudery-org-role").toLowerCase();const r=t.cloudery_default_org_role||"member",i=r.trim().toLowerCase();a.has(i)?this.defaultOrgRole=i:(this.defaultOrgRole="member",this.logger.warn(`${this.name}: cloudery_default_org_role "${r}" is not one of ${[...a].join(", ")} — using "member"`)),this.fqdnAttribute=t.cloudery_fqdn_attribute||"twakeWorkspaceUrl",this.orgIdAttribute=t.cloudery_org_id_attribute||"twakeOrganizationId",this.orgRoleAttribute=t.cloudery_org_role_attribute||"twakeOrganizationRole",this.phonesAttribute=t.cloudery_phones_attribute||"twakePhones",this.invitedAttribute=t.cloudery_invited_attribute||"twakeInvited",this.defaultLocale=t.cloudery_default_locale||"en",this.rdnAttribute=t.scim_user_rdn_attribute||"uid",this.authExchange=t.cozy_auth_exchange||"auth",this.b2bExchange=t.cozy_b2b_exchange||"b2b",this.userCreatedRoutingKey=t.cozy_user_created_routing_key||"user.created",this.userDeletedRoutingKey=t.cozy_user_deleted_routing_key??"domain.user.deleted",this.pollIntervalMs=Number(t.cloudery_workflow_poll_interval_ms)||2e3,this.maxPollAttempts=Number(t.cloudery_workflow_max_attempts)||60,this.baseResolver=new n(this.config),this.managerUrl||this.logger.warn(`${this.name}: cloudery_manager_url is empty — instance provisioning will be skipped`),this.userBranch||this.logger.warn(`${this.name}: cloudery_user_branch is empty — no users will be provisioned`),this.logger.info({plugin:this.name,event:"init",managerUrl:this.managerUrl||void 0,userBranch:this.userBranch||void 0,orgIdHeader:this.orgIdHeader,orgRoleHeader:this.orgRoleHeader,defaultOrgRole:this.defaultOrgRole,fqdnAttribute:this.fqdnAttribute,orgIdAttribute:this.orgIdAttribute,orgRoleAttribute:this.orgRoleAttribute,phonesAttribute:this.phonesAttribute,invitedAttribute:this.invitedAttribute,pollIntervalMs:this.pollIntervalMs,maxPollAttempts:this.maxPollAttempts})}captureCreate(e,t){this.pruneStash();const r=this.extractId(e),i=this.baseResolver.userBase(t);if(this.logger.info({plugin:this.name,event:"captureCreate",userName:r||void 0,base:i,hasManagerUrl:Boolean(this.managerUrl),hasUserBranch:Boolean(this.userBranch)}),!this.managerUrl||!this.userBranch)return void this.logger.info({plugin:this.name,event:"captureCreate",userName:r||void 0,base:i,result:"skipped",reason:"missing_configuration"});if(!r)return void this.logger.info({plugin:this.name,event:"captureCreate",base:i,result:"skipped",reason:"missing_user_name"});if(!this.isAtOrUnder(i,this.userBranch))return void this.logger.info({plugin:this.name,event:"captureCreate",userName:r,base:i,result:"skipped",reason:"outside_b2b_branch"});const s=this.readHeader(t,this.orgIdHeader);if(!s)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:`B2B branch create without ${this.orgIdHeader} — skipping provisioning`});const n=this.extractPrimaryEmail(e);if(!n)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:"B2B branch create without a primary email — skipping provisioning"});const a=n.trim(),o=this.resolveRole(t,r);this.createStash.set(a.toLowerCase(),{orgId:s.trim(),email:a,role:o,base:i,ts:Date.now()}),this.logger.info({plugin:this.name,event:"captureCreate",userName:r,base:i,orgId:s.trim(),email:a,role:o,result:"stashed"})}async provision(e){const t=this.extractId(e);if(this.logger.info({plugin:this.name,event:"provision",userName:t||void 0}),!t)return void this.logger.info({plugin:this.name,event:"provision",result:"skipped",reason:"missing_user_name"});const s=this.extractPrimaryEmail(e);if(!s)return void this.logger.info({plugin:this.name,event:"provision",userName:t,result:"skipped",reason:"missing_primary_email"});const n=s.trim().toLowerCase(),a=this.createStash.get(n);if(!a)return void this.logger.info({plugin:this.name,event:"provision",userName:t,email:s.trim(),result:"skipped",reason:"no_stashed_context"});this.createStash.delete(n),this.logger.info({plugin:this.name,event:"provision",userName:t,email:a.email,orgId:a.orgId,base:a.base,role:a.role,result:"context_loaded"});const o=a.email,l=emailDomain(o),u=r(t+a.orgId);if(!u)return void this.logger.error({plugin:this.name,event:"provision",userName:t,result:"no_slug"});const h=this.extractPrimaryPhone(e),g=this.extractPhones(e),d=this.extractPublicName(e)||t,c={plugin:this.name,event:"provision",userName:t,orgId:a.orgId,slug:u},m=`${u}.${this.clouderyDomain}`.toLowerCase();let p=null;try{p=await this.findInstanceByFqdn(m)}catch(e){this.logger.warn({...c,result:"existence_lookup_failed",fqdn:m,error:`${e}`})}if(p&&(p.internal_email&&p.internal_email.toLowerCase()!==o.toLowerCase()||p.oidc&&p.oidc.toLowerCase()!==u))return void this.logger.error({...c,result:"slug_taken",fqdn:m,uuid:p._id});const f=p?._id??null;let w;if(f)this.logger.info({...c,result:"instance_exists",fqdn:m,uuid:f}),w=m;else{let t;try{this.logger.info({...c,result:"creating_instance",orgDomain:l,email:o,mobile:h||void 0}),t=await this.createInstance({email:o,publicName:d,locale:this.extractLocale(e),oidc:u,phone:h||"",slug:u,public_name:d,offer:this.offer,domain:this.clouderyDomain,skip_email_validation:!0,internal_email:o,org_domain:l,org_id:a.orgId})}catch(e){return void this.logger.error({...c,result:"error",error:`${e}`})}if(!(!t.workflow||await this.waitForWorkflow(t.workflow)))return void this.logger.error({...c,result:"workflow_failed",workflow:t.workflow});w=t.fqdn}const v=`${this.rdnAttribute}=${i(t)},${a.base}`,b={[this.fqdnAttribute]:w,[this.orgIdAttribute]:a.orgId,[this.orgRoleAttribute]:a.role,[this.invitedAttribute]:"TRUE",cn:t};g.length>0&&(b[this.phonesAttribute]=JSON.stringify(g));try{this.logger.info({...c,result:"writing_fqdn",dn:v,hasPhones:g.length>0}),await this.server.ldap.system.modify(v,{replace:b},void 0,{declaredClasses:this.config.user_class}),this.logger.info({...c,result:"written_fqdn",dn:v})}catch(e){this.logger.error({...c,event:"writeFqdn",dn:v,error:`${e}`})}await this.publishUserCreated({twakeId:t,orgDomain:l,orgId:a.orgId,fqdn:w,email:o,mobile:h}),this.logger.info({...c,result:"success",fqdn:w})}async captureDelete(e,t){const r=this.baseResolver.userBase(t);if(this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,hasManagerUrl:Boolean(this.managerUrl),hasUserBranch:Boolean(this.userBranch)}),!this.managerUrl||!this.userBranch)return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,result:"skipped",reason:"missing_configuration"});if(!this.isAtOrUnder(r,this.userBranch))return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,result:"skipped",reason:"outside_b2b_branch"});const s=`${this.rdnAttribute}=${i(e)},${r}`;try{this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:s,result:"searching"});const t=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[this.fqdnAttribute,"mail"]},s),r=t.searchEntries?.[0],i=r?firstValue(r[this.fqdnAttribute]):void 0,n=i?i.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i,"").split("/")[0]:void 0;if(!n)return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:s,result:"skipped",reason:"missing_fqdn"});const a=r?firstValue(r.mail):void 0,o=a?emailDomain(a):"";this.deleteStash.set(e,{fqdn:n,domain:o}),this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:s,fqdn:n,domain:o,result:"stashed"})}catch(t){this.logger.warn({plugin:this.name,event:"captureDelete",id:e,error:`${t}`})}}async deprovision(e){this.logger.info({plugin:this.name,event:"deprovision",id:e});const t=this.deleteStash.get(e);if(!t)return void this.logger.info({plugin:this.name,event:"deprovision",id:e,result:"skipped",reason:"no_stashed_context"});this.deleteStash.delete(e);const r={plugin:this.name,event:"deprovision",id:e,fqdn:t.fqdn};let i=!1;try{this.logger.info({...r,result:"resolving_uuid"});const e=(await this.findInstanceByFqdn(t.fqdn,"_id"))?._id;e?(this.logger.info({...r,result:"deleting_instance",uuid:e}),await this.deleteInstance(e),i=!0,this.logger.info({...r,result:"deleted",uuid:e})):this.logger.warn({...r,result:"not_found"})}catch(e){this.logger.error({...r,result:"error",error:`${e}`})}i&&await this.publishUserDeleted(t.fqdn,t.domain)}async createInstance(e){this.logger.info({plugin:this.name,event:"createInstance",url:`${this.managerUrl}/api/v1/instances`});const t=await this.fetch(`${this.managerUrl}/api/v1/instances`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.managerToken}`},body:JSON.stringify(e)});if(!t.ok){const e=(await t.text().catch(()=>"")).slice(0,o);throw this.logger.error({plugin:this.name,event:"createInstance",result:"error",http_status:t.status,http_status_text:t.statusText,body:e||void 0}),new Error(`Cloudery createInstance HTTP ${t.status}`)}const r=await t.json();return this.logger.info({plugin:this.name,event:"createInstance",result:"success",id:r.id,fqdn:r.fqdn,workflow:r.workflow}),r}async waitForWorkflow(t){this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,maxPollAttempts:this.maxPollAttempts,pollIntervalMs:this.pollIntervalMs});for(let r=1;r<=this.maxPollAttempts;r++){try{const e=await this.fetch(`${this.managerUrl}/api/v1/workflows/${encodeURIComponent(t)}`,{headers:{Authorization:`Bearer ${this.managerToken}`}});if(e.ok){const i=await e.json();if(this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,status:i.status}),"succeeded"===i.status)return this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,result:"success"}),!0;if("failed"===i.status||"error"===i.status)return this.logger.warn({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,result:"failed",status:i.status}),!1}else{const i=(await e.text().catch(()=>"")).slice(0,o);this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,result:"http_error",http_status:e.status,http_status_text:e.statusText,body:i||void 0})}}catch(e){this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,error:`${e}`})}r<this.maxPollAttempts&&(this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,result:"retrying"}),await e(this.pollIntervalMs))}return this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,result:"timeout",maxPollAttempts:this.maxPollAttempts}),!1}async findInstanceByFqdn(e,t){this.logger.info({plugin:this.name,event:"findInstanceByFqdn",fqdn:e});const r=new URL(`${this.managerUrl}/api/v2/instances`);r.searchParams.set("fqdn",e),t&&r.searchParams.set("only",t),r.searchParams.set("limit","1");const i=await this.fetch(r.toString(),{headers:{Authorization:`Bearer ${this.managerToken}`}});if(!i.ok){const t=(await i.text().catch(()=>"")).slice(0,o);throw this.logger.error({plugin:this.name,event:"findInstanceByFqdn",fqdn:e,result:"error",http_status:i.status,http_status_text:i.statusText,body:t||void 0}),new Error(`Cloudery searchInstances HTTP ${i.status}`)}const s=await i.json(),n=s.items?.[0]??null,a=n?._id;return this.logger.info({plugin:this.name,event:"findInstanceByFqdn",fqdn:e,result:a?"found":"not_found",uuid:a||void 0}),n}async deleteInstance(e){const t=new URL(`${this.managerUrl}/api/v1/instances/${encodeURIComponent(e)}`);t.searchParams.set("user_request","true"),this.logger.info({plugin:this.name,event:"deleteInstance",uuid:e,url:t.toString()});const r=await this.fetch(t.toString(),{method:"DELETE",headers:{Authorization:`Bearer ${this.managerToken}`}});if(!r.ok){const t=(await r.text().catch(()=>"")).slice(0,o);throw this.logger.error({plugin:this.name,event:"deleteInstance",uuid:e,result:"error",http_status:r.status,http_status_text:r.statusText,body:t||void 0}),new Error(`Cloudery deleteInstance HTTP ${r.status}`)}this.logger.info({plugin:this.name,event:"deleteInstance",uuid:e,result:"success",http_status:r.status})}async publishUserCreated(e){const t=this.requirePlugin("rabbitmq");if(this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,orgId:e.orgId,orgDomain:e.orgDomain,hasRabbitmq:Boolean(t)}),!t)return void this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,result:"skipped",reason:"missing_rabbitmq"});const r={twakeId:e.twakeId,domain:e.orgDomain,organizationDomain:e.orgDomain,workplaceFqdn:e.fqdn,organizationId:e.orgId};e.email&&(r.internalEmail=e.email),e.mobile&&(r.mobile=e.mobile);try{await t.publish(this.authExchange,this.userCreatedRoutingKey,r),this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,exchange:this.authExchange,routingKey:this.userCreatedRoutingKey,result:"success"})}catch(t){this.logger.error({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,error:`${t}`})}}async publishUserDeleted(e,t){if(!this.userDeletedRoutingKey)return;const r=this.requirePlugin("rabbitmq");if(this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,domain:t,hasRabbitmq:Boolean(r)}),r)try{await r.publish(this.b2bExchange,this.userDeletedRoutingKey,{workplaceFqdn:e,domain:t}),this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,exchange:this.b2bExchange,routingKey:this.userDeletedRoutingKey,result:"success"})}catch(t){this.logger.error({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,error:`${t}`})}else this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,result:"skipped",reason:"missing_rabbitmq"})}readHeader(e,t){if(!e)return;const r=e.get?e.get(t):void 0;if("string"==typeof r&&r.length>0)return r;const i=e.query?.[t];return"string"==typeof i&&i.length>0?i:void 0}resolveRole(e,t){const r=this.readHeader(e,this.orgRoleHeader)?.trim().toLowerCase();return r?a.has(r)?(this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,input:r,result:"accepted",role:r}),r):(this.logger.warn({plugin:this.name,event:"captureCreate",userName:t,message:`Unknown ${this.orgRoleHeader} "${r}" — falling back to ${this.defaultOrgRole}`}),this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,input:r,result:"default",role:this.defaultOrgRole}),this.defaultOrgRole):(this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,result:"default",role:this.defaultOrgRole}),this.defaultOrgRole)}isAtOrUnder(e,t){return e.trim().toLowerCase()===t.trim().toLowerCase()||s(e,t)}pruneStash(){const e=Date.now()-3e5;for(const[t,r]of this.createStash)r.ts<e&&this.createStash.delete(t)}normalizeUserName(e){"string"==typeof e.userName&&(e.userName=e.userName.toLowerCase())}extractId(e){return"string"==typeof e.userName&&e.userName.length>0?e.userName:"string"==typeof e.id&&e.id.length>0?e.id:null}extractPrimaryEmail(e){const t=e.emails;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPrimaryPhone(e){const t=e.phoneNumbers;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPhones(e){const t=e.phoneNumbers;return t&&0!==t.length?t.filter(e=>"string"==typeof e.value&&e.value.trim().length>0).map(e=>({number:e.value.trim(),primary:Boolean(e.primary)})):[]}extractPublicName(e){if("string"==typeof e.displayName&&e.displayName.trim().length>0)return e.displayName.trim();const t=e.name?.formatted;if("string"==typeof t&&t.trim().length>0)return t.trim();const r=`${e.name?.givenName?.trim()??""} ${e.name?.familyName?.trim()??""}`.trim();return r.length>0?r:null}extractLocale(e){return"string"==typeof e.locale&&e.locale.length>0?e.locale:"string"==typeof e.preferredLanguage&&e.preferredLanguage.length>0?e.preferredLanguage:this.defaultLocale}}function emailDomain(e){const t=e.lastIndexOf("@");return t>=0?e.slice(t+1):""}function firstValue(e){if(Array.isArray(e)){const t=e[0];return"string"==typeof t?t:void 0}return"string"==typeof e?e:void 0}export{ClouderyProvision as default};
@@ -1 +1 @@
1
- import{Buffer as t}from"buffer";import e from"node-fetch";import s from"../../abstract/plugin.js";class CozyProvision extends s{constructor(e){super(e),this.name="cozyProvision",this.roles=["consistency"],this.dependencies={rabbitmq:"core/rabbitmq"},this.hooks={scimusercreatedone:async t=>{await this.createCozyInstance(t)&&await this.publishUserCreated(t)},scimuserdeletedone:async t=>{await this.destroyCozyInstance(t)&&await this.publishUserDeleted(t)}},this.cozyAdminUrl=(this.config.cozy_admin_url||"").replace(/\/$/,""),this.cozyAdminUser=this.config.cozy_admin_user||"admin",this.cozyAdminPassphrase=this.config.cozy_admin_passphrase||"",this.cozyOrgId=this.config.cozy_org_id||"",this.cozyOrgDomain=this.config.cozy_org_domain||"",this.cozyDefaultLocale=this.config.cozy_default_locale||"fr",this.cozyContextName=this.config.cozy_context_name||"default",this.cozyApps=this.config.cozy_apps??"",this.authExchange=this.config.cozy_auth_exchange||"auth",this.b2bExchange=this.config.cozy_b2b_exchange||"b2b",this.userCreatedRoutingKey=this.config.cozy_user_created_routing_key||"user.created",this.userDeletedRoutingKey=this.config.cozy_user_deleted_routing_key??"domain.user.deleted",this.cozyAdminAuthHeader=`Basic ${t.from(`${this.cozyAdminUser}:${this.cozyAdminPassphrase}`).toString("base64")}`,this.cozyAdminUrl||this.logger.warn(`${this.name}: cozy_admin_url is empty — Cozy instance creation will be skipped`),this.cozyOrgDomain||this.logger.warn(`${this.name}: cozy_org_domain is empty — workplaceFqdn cannot be composed`)}async createCozyInstance(t){if(!this.cozyAdminUrl)return!1;const s=this.extractId(t);if(!s)return this.logger.warn({plugin:this.name,event:"createCozyInstance",message:"user has no userName/id — skipping"}),!1;if(!this.cozyOrgDomain)return this.logger.error({plugin:this.name,event:"createCozyInstance",id:s,message:"cozy_org_domain not configured — cannot compose Domain"}),!1;const r=`${s}.${this.cozyOrgDomain}`,i=new URLSearchParams;i.set("Domain",r),i.set("Locale",this.extractLocale(t));const n=this.extractPrimaryEmail(t);n&&i.set("Email",n);const o=this.extractPublicName(t);o&&i.set("PublicName",o);const a=this.extractPrimaryPhone(t);a&&i.set("Phone",a),this.cozyOrgId&&i.set("OrgID",this.cozyOrgId),this.cozyOrgDomain&&i.set("OrgDomain",this.cozyOrgDomain),this.cozyContextName&&i.set("ContextName",this.cozyContextName),this.cozyApps&&i.set("Apps",this.cozyApps),i.set("OIDCID",s);const c=`${this.cozyAdminUrl}/instances?${i.toString()}`,h={plugin:this.name,event:"createCozyInstance",id:s,domain:r,email:n||void 0};try{const t=await e(c,{method:"POST",headers:{Authorization:this.cozyAdminAuthHeader,Accept:"application/json"}});return t.ok?(this.logger.info({...h,result:"success",http_status:t.status}),await this.markInstanceOnboarded(r),!0):409===t.status?(this.logger.info({...h,result:"already_exists",http_status:t.status}),!0):(this.logger.error({...h,result:"error",http_status:t.status,http_status_text:t.statusText}),!1)}catch(t){return this.logger.error({...h,result:"error",error:`${t}`}),!1}}async markInstanceOnboarded(t){const s=`${this.cozyAdminUrl}/instances/${encodeURIComponent(t)}?OnboardingFinished=true`,r={plugin:this.name,event:"markInstanceOnboarded",domain:t};try{const t=await e(s,{method:"PATCH",headers:{Authorization:this.cozyAdminAuthHeader,Accept:"application/json"}});if(!t.ok)return void this.logger.error({...r,result:"error",http_status:t.status,http_status_text:t.statusText});this.logger.info({...r,result:"success",http_status:t.status})}catch(t){this.logger.error({...r,result:"error",error:`${t}`})}}async destroyCozyInstance(t){if(!this.cozyAdminUrl)return!1;if(!this.cozyOrgDomain)return this.logger.error({plugin:this.name,event:"destroyCozyInstance",id:t,message:"cozy_org_domain not configured — cannot compose Domain"}),!1;const s=`${t}.${this.cozyOrgDomain}`,r=`${this.cozyAdminUrl}/instances/${encodeURIComponent(s)}`,i={plugin:this.name,event:"destroyCozyInstance",id:t,domain:s};try{const t=await e(r,{method:"DELETE",headers:{Authorization:this.cozyAdminAuthHeader,Accept:"application/json"}});return t.ok||404===t.status?(this.logger.info({...i,result:t.ok?"success":"not_found",http_status:t.status}),!0):(this.logger.error({...i,result:"error",http_status:t.status,http_status_text:t.statusText}),!1)}catch(t){return this.logger.error({...i,result:"error",error:`${t}`}),!1}}async publishUserCreated(t){const e=this.rabbitmq();if(!e)return;const s=this.extractId(t);if(!s)return;const r=this.extractPrimaryEmail(t),i=this.extractPrimaryPhone(t),n={twakeId:s,domain:this.cozyOrgDomain,organizationDomain:this.cozyOrgDomain,workplaceFqdn:`${s}.${this.cozyOrgDomain}`};r&&(n.internalEmail=r),i&&(n.mobile=i);try{await e.publish(this.authExchange,this.userCreatedRoutingKey,n),this.logger.info({plugin:this.name,event:"publishUserCreated",result:"success",exchange:this.authExchange,routingKey:this.userCreatedRoutingKey,twakeId:s})}catch(t){this.logger.error({plugin:this.name,event:"publishUserCreated",twakeId:s,error:`${t}`})}}async publishUserDeleted(t){if(!this.userDeletedRoutingKey)return;const e=this.rabbitmq();if(!e)return;if(!this.cozyOrgDomain)return void this.logger.error({plugin:this.name,event:"publishUserDeleted",id:t,message:"cozy_org_domain not configured — cannot compose workplaceFqdn"});const s={workplaceFqdn:`${t}.${this.cozyOrgDomain}`,domain:this.cozyOrgDomain};try{await e.publish(this.b2bExchange,this.userDeletedRoutingKey,s),this.logger.info({plugin:this.name,event:"publishUserDeleted",result:"success",exchange:this.b2bExchange,routingKey:this.userDeletedRoutingKey,workplaceFqdn:s.workplaceFqdn})}catch(e){this.logger.error({plugin:this.name,event:"publishUserDeleted",id:t,error:`${e}`})}}rabbitmq(){return this.requirePlugin("rabbitmq")}extractId(t){return"string"==typeof t.userName&&t.userName.length>0?t.userName:"string"==typeof t.id&&t.id.length>0?t.id:null}extractPrimaryEmail(t){const e=t.emails;if(!e||0===e.length)return null;const s=(e.find(t=>t.primary)||e[0]).value;return"string"==typeof s&&s.length>0?s:null}extractPrimaryPhone(t){const e=t.phoneNumbers;if(!e||0===e.length)return null;const s=(e.find(t=>t.primary)||e[0]).value;return"string"==typeof s&&s.length>0?s:null}extractPublicName(t){if("string"==typeof t.displayName&&t.displayName.trim().length>0)return t.displayName.trim();const e=t.name?.formatted;if("string"==typeof e&&e.trim().length>0)return e.trim();const s=`${t.name?.givenName?.trim()??""} ${t.name?.familyName?.trim()??""}`.trim();return s.length>0?s:this.extractId(t)}extractLocale(t){return"string"==typeof t.locale&&t.locale.length>0?t.locale:"string"==typeof t.preferredLanguage&&t.preferredLanguage.length>0?t.preferredLanguage:this.cozyDefaultLocale}}export{CozyProvision as default};
1
+ import{Buffer as t}from"buffer";import e from"../../abstract/plugin.js";class CozyProvision extends e{constructor(e){super(e),this.name="cozyProvision",this.roles=["consistency"],this.dependencies={rabbitmq:"core/rabbitmq"},this.hooks={scimusercreatedone:async t=>{await this.createCozyInstance(t)&&await this.publishUserCreated(t)},scimuserdeletedone:async t=>{await this.destroyCozyInstance(t)&&await this.publishUserDeleted(t)}},this.cozyAdminUrl=(this.config.cozy_admin_url||"").replace(/\/$/,""),this.cozyAdminUser=this.config.cozy_admin_user||"admin",this.cozyAdminPassphrase=this.config.cozy_admin_passphrase||"",this.cozyOrgId=this.config.cozy_org_id||"",this.cozyOrgDomain=this.config.cozy_org_domain||"",this.cozyDefaultLocale=this.config.cozy_default_locale||"fr",this.cozyContextName=this.config.cozy_context_name||"default",this.cozyApps=this.config.cozy_apps??"",this.authExchange=this.config.cozy_auth_exchange||"auth",this.b2bExchange=this.config.cozy_b2b_exchange||"b2b",this.userCreatedRoutingKey=this.config.cozy_user_created_routing_key||"user.created",this.userDeletedRoutingKey=this.config.cozy_user_deleted_routing_key??"domain.user.deleted",this.cozyAdminAuthHeader=`Basic ${t.from(`${this.cozyAdminUser}:${this.cozyAdminPassphrase}`).toString("base64")}`,this.cozyAdminUrl||this.logger.warn(`${this.name}: cozy_admin_url is empty — Cozy instance creation will be skipped`),this.cozyOrgDomain||this.logger.warn(`${this.name}: cozy_org_domain is empty — workplaceFqdn cannot be composed`)}async createCozyInstance(t){if(!this.cozyAdminUrl)return!1;const e=this.extractId(t);if(!e)return this.logger.warn({plugin:this.name,event:"createCozyInstance",message:"user has no userName/id — skipping"}),!1;if(!this.cozyOrgDomain)return this.logger.error({plugin:this.name,event:"createCozyInstance",id:e,message:"cozy_org_domain not configured — cannot compose Domain"}),!1;const s=`${e}.${this.cozyOrgDomain}`,r=new URLSearchParams;r.set("Domain",s),r.set("Locale",this.extractLocale(t));const i=this.extractPrimaryEmail(t);i&&r.set("Email",i);const n=this.extractPublicName(t);n&&r.set("PublicName",n);const o=this.extractPrimaryPhone(t);o&&r.set("Phone",o),this.cozyOrgId&&r.set("OrgID",this.cozyOrgId),this.cozyOrgDomain&&r.set("OrgDomain",this.cozyOrgDomain),this.cozyContextName&&r.set("ContextName",this.cozyContextName),this.cozyApps&&r.set("Apps",this.cozyApps),r.set("OIDCID",e);const a=`${this.cozyAdminUrl}/instances?${r.toString()}`,c={plugin:this.name,event:"createCozyInstance",id:e,domain:s,email:i||void 0};try{const t=await this.fetch(a,{method:"POST",headers:{Authorization:this.cozyAdminAuthHeader,Accept:"application/json"}});return t.ok?(this.logger.info({...c,result:"success",http_status:t.status}),await this.markInstanceOnboarded(s),!0):409===t.status?(this.logger.info({...c,result:"already_exists",http_status:t.status}),!0):(this.logger.error({...c,result:"error",http_status:t.status,http_status_text:t.statusText}),!1)}catch(t){return this.logger.error({...c,result:"error",error:`${t}`}),!1}}async markInstanceOnboarded(t){const e=`${this.cozyAdminUrl}/instances/${encodeURIComponent(t)}?OnboardingFinished=true`,s={plugin:this.name,event:"markInstanceOnboarded",domain:t};try{const t=await this.fetch(e,{method:"PATCH",headers:{Authorization:this.cozyAdminAuthHeader,Accept:"application/json"}});if(!t.ok)return void this.logger.error({...s,result:"error",http_status:t.status,http_status_text:t.statusText});this.logger.info({...s,result:"success",http_status:t.status})}catch(t){this.logger.error({...s,result:"error",error:`${t}`})}}async destroyCozyInstance(t){if(!this.cozyAdminUrl)return!1;if(!this.cozyOrgDomain)return this.logger.error({plugin:this.name,event:"destroyCozyInstance",id:t,message:"cozy_org_domain not configured — cannot compose Domain"}),!1;const e=`${t}.${this.cozyOrgDomain}`,s=`${this.cozyAdminUrl}/instances/${encodeURIComponent(e)}`,r={plugin:this.name,event:"destroyCozyInstance",id:t,domain:e};try{const t=await this.fetch(s,{method:"DELETE",headers:{Authorization:this.cozyAdminAuthHeader,Accept:"application/json"}});return t.ok||404===t.status?(this.logger.info({...r,result:t.ok?"success":"not_found",http_status:t.status}),!0):(this.logger.error({...r,result:"error",http_status:t.status,http_status_text:t.statusText}),!1)}catch(t){return this.logger.error({...r,result:"error",error:`${t}`}),!1}}async publishUserCreated(t){const e=this.rabbitmq();if(!e)return;const s=this.extractId(t);if(!s)return;const r=this.extractPrimaryEmail(t),i=this.extractPrimaryPhone(t),n={twakeId:s,domain:this.cozyOrgDomain,organizationDomain:this.cozyOrgDomain,workplaceFqdn:`${s}.${this.cozyOrgDomain}`};r&&(n.internalEmail=r),i&&(n.mobile=i);try{await e.publish(this.authExchange,this.userCreatedRoutingKey,n),this.logger.info({plugin:this.name,event:"publishUserCreated",result:"success",exchange:this.authExchange,routingKey:this.userCreatedRoutingKey,twakeId:s})}catch(t){this.logger.error({plugin:this.name,event:"publishUserCreated",twakeId:s,error:`${t}`})}}async publishUserDeleted(t){if(!this.userDeletedRoutingKey)return;const e=this.rabbitmq();if(!e)return;if(!this.cozyOrgDomain)return void this.logger.error({plugin:this.name,event:"publishUserDeleted",id:t,message:"cozy_org_domain not configured — cannot compose workplaceFqdn"});const s={workplaceFqdn:`${t}.${this.cozyOrgDomain}`,domain:this.cozyOrgDomain};try{await e.publish(this.b2bExchange,this.userDeletedRoutingKey,s),this.logger.info({plugin:this.name,event:"publishUserDeleted",result:"success",exchange:this.b2bExchange,routingKey:this.userDeletedRoutingKey,workplaceFqdn:s.workplaceFqdn})}catch(e){this.logger.error({plugin:this.name,event:"publishUserDeleted",id:t,error:`${e}`})}}rabbitmq(){return this.requirePlugin("rabbitmq")}extractId(t){return"string"==typeof t.userName&&t.userName.length>0?t.userName:"string"==typeof t.id&&t.id.length>0?t.id:null}extractPrimaryEmail(t){const e=t.emails;if(!e||0===e.length)return null;const s=(e.find(t=>t.primary)||e[0]).value;return"string"==typeof s&&s.length>0?s:null}extractPrimaryPhone(t){const e=t.phoneNumbers;if(!e||0===e.length)return null;const s=(e.find(t=>t.primary)||e[0]).value;return"string"==typeof s&&s.length>0?s:null}extractPublicName(t){if("string"==typeof t.displayName&&t.displayName.trim().length>0)return t.displayName.trim();const e=t.name?.formatted;if("string"==typeof e&&e.trim().length>0)return e.trim();const s=`${t.name?.givenName?.trim()??""} ${t.name?.familyName?.trim()??""}`.trim();return s.length>0?s:this.extractId(t)}extractLocale(t){return"string"==typeof t.locale&&t.locale.length>0?t.locale:"string"==typeof t.preferredLanguage&&t.preferredLanguage.length>0?t.preferredLanguage:this.cozyDefaultLocale}}export{CozyProvision as default};