ldap-rest 0.20.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/abstract/ldapFlat.js +1 -1
  2. package/dist/config/args.js +1 -1
  3. package/dist/config/schema.js +1 -1
  4. package/dist/lib/expressFormatedResponses.js +1 -1
  5. package/dist/lib/instanceProviders.js +1 -1
  6. package/dist/lib/ldapActions.js +1 -1
  7. package/dist/plugins/ldap/accountLifecycle.js +1 -1
  8. package/dist/plugins/ldap/groups.js +1 -1
  9. package/dist/plugins/ldap/organizations.js +1 -1
  10. package/dist/plugins/ldap/passwordPolicy.js +1 -1
  11. package/dist/plugins/ldap/raw.js +1 -1
  12. package/dist/plugins/twake/clouderyProvision.js +1 -1
  13. package/dist/plugins/twake/instances.js +1 -1
  14. package/dist/src/browser/ldap-group-editor/api/GroupApiClient.d.ts +10 -5
  15. package/dist/src/browser/ldap-group-editor/types.d.ts +8 -0
  16. package/dist/src/browser/ldap-resource-editor/api/ResourceApiClient.d.ts +7 -1
  17. package/dist/src/browser/ldap-unit-editor/api/UnitApiClient.d.ts +7 -5
  18. package/dist/src/browser/ldap-user-editor/LdapUserEditor.d.ts +2 -0
  19. package/dist/src/browser/ldap-user-editor/api/UserApiClient.d.ts +7 -1
  20. package/dist/src/browser/shared/utils/form.d.ts +30 -0
  21. package/dist/src/config/args.d.ts +1 -0
  22. package/dist/src/config/schema.d.ts +30 -1
  23. package/dist/src/lib/instanceProviders.d.ts +11 -0
  24. package/dist/src/lib/ldapActions.d.ts +21 -3
  25. package/dist/src/plugins/ldap/accountLifecycle.d.ts +2 -0
  26. package/dist/src/plugins/ldap/passwordPolicy.d.ts +95 -18
  27. package/dist/src/plugins/twake/clouderyProvision.d.ts +7 -1
  28. package/docs/client-development/api/rest-api.md +19 -0
  29. package/docs/usage/configuration.md +16 -0
  30. package/docs/usage/plugins/integrations/instances.md +18 -6
  31. package/docs/usage/plugins/ldap/README.md +7 -1
  32. package/docs/usage/plugins/ldap/account-lifecycle.md +9 -1
  33. package/docs/usage/plugins/ldap/flat-generic.md +14 -12
  34. package/docs/usage/plugins/ldap/password-policy.md +88 -4
  35. package/docs/usage/upgrading.md +43 -13
  36. package/examples/web/README.md +5 -1
  37. package/examples/web/ldap-user-editor.html +9 -15
  38. package/examples/web/totp-client.html +51 -14
  39. package/package.json +1 -1
  40. package/static/browser/ldap-group-editor/api/GroupApiClient.js +27 -13
  41. package/static/browser/ldap-group-editor/api/GroupApiClient.js.map +1 -1
  42. package/static/browser/ldap-group-editor/components/CreateGroupModal.js +5 -17
  43. package/static/browser/ldap-group-editor/components/CreateGroupModal.js.map +1 -1
  44. package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js +28 -12
  45. package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js.map +1 -1
  46. package/static/browser/ldap-group-editor/components/MoveGroupModal.js.map +1 -1
  47. package/static/browser/ldap-group-editor.esm.js +114 -42
  48. package/static/browser/ldap-group-editor.esm.js.map +1 -1
  49. package/static/browser/ldap-group-editor.js +114 -42
  50. package/static/browser/ldap-group-editor.js.map +1 -1
  51. package/static/browser/ldap-group-editor.min.js +1 -1
  52. package/static/browser/ldap-group-editor.min.js.map +1 -1
  53. package/static/browser/ldap-resource-editor/api/ResourceApiClient.js +8 -2
  54. package/static/browser/ldap-resource-editor/api/ResourceApiClient.js.map +1 -1
  55. package/static/browser/ldap-unit-editor/api/UnitApiClient.js +8 -13
  56. package/static/browser/ldap-unit-editor/api/UnitApiClient.js.map +1 -1
  57. package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js +5 -2
  58. package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js.map +1 -1
  59. package/static/browser/ldap-unit-editor.esm.js +52 -15
  60. package/static/browser/ldap-unit-editor.esm.js.map +1 -1
  61. package/static/browser/ldap-unit-editor.js +52 -15
  62. package/static/browser/ldap-unit-editor.js.map +1 -1
  63. package/static/browser/ldap-unit-editor.min.js +1 -1
  64. package/static/browser/ldap-unit-editor.min.js.map +1 -1
  65. package/static/browser/ldap-user-editor/LdapUserEditor.js +22 -21
  66. package/static/browser/ldap-user-editor/LdapUserEditor.js.map +1 -1
  67. package/static/browser/ldap-user-editor/api/UserApiClient.js +8 -2
  68. package/static/browser/ldap-user-editor/api/UserApiClient.js.map +1 -1
  69. package/static/browser/ldap-user-editor/components/UserEditor.js +19 -9
  70. package/static/browser/ldap-user-editor/components/UserEditor.js.map +1 -1
  71. package/static/browser/ldap-user-editor.esm.js +88 -32
  72. package/static/browser/ldap-user-editor.esm.js.map +1 -1
  73. package/static/browser/ldap-user-editor.js +88 -32
  74. package/static/browser/ldap-user-editor.js.map +1 -1
  75. package/static/browser/ldap-user-editor.min.js +1 -1
  76. package/static/browser/ldap-user-editor.min.js.map +1 -1
  77. package/static/browser/shared/index.js +1 -1
  78. package/static/browser/shared/utils/form.js +51 -1
  79. package/static/browser/shared/utils/form.js.map +1 -1
  80. package/static/browser/shared.esm.js +51 -1
  81. package/static/browser/shared.esm.js.map +1 -1
  82. package/static/browser/shared.js +53 -0
  83. package/static/browser/shared.js.map +1 -1
  84. package/static/browser/shared.min.js +1 -1
  85. package/static/browser/shared.min.js.map +1 -1
@@ -1 +1 @@
1
- import e from"../../abstract/plugin.js";import{asyncHandler as i,escapeDnValue as s}from"../../lib/utils.js";import{NotFoundError as t,BadRequestError as a}from"../../lib/errors.js";const r=["pwdChangedTime","pwdAccountLockedTime","pwdFailureTime","pwdGraceUseTime","pwdReset","pwdPolicySubentry"],o=["pwdMaxAge","pwdMinAge","pwdInHistory","pwdCheckQuality","pwdMinLength","pwdMaxFailure","pwdLockout","pwdLockoutDuration","pwdGraceAuthNLimit","pwdExpireWarning","pwdMustChange","pwdAllowUserChange"],n=/[!@#$%^&*()_+\-=[\]{}|;:'",.<>?/]/;class PasswordPolicy extends e{constructor(){super(...arguments),this.name="ldapPasswordPolicy",this.roles=["api","configurable"],this.policyCache=null,this.policyCacheTime=0,this.POLICY_CACHE_TTL=6e4}api(e){const s=`${this.config.api_prefix}/v1`;e.get(`${s}/password-policy`,i(async(e,i)=>{const s=await this.readPolicyConfig();i.json(s)})),e.get(`${s}/users/:id/password-status`,i(async(e,i)=>{const s=Array.isArray(e.params.id)?e.params.id[0]:e.params.id,t=await this.getPasswordStatus(s,e);i.json(t)})),e.post(`${s}/users/:id/unlock`,i(async(e,i)=>{const s=Array.isArray(e.params.id)?e.params.id[0]:e.params.id;await this.unlockAccount(s,e),i.json({success:!0,message:"Account unlocked"})})),e.get(`${s}/password-policy/expiring-soon`,i(async(e,i)=>{const s=parseInt(e.query.days)||this.config.ppolicy_warn_days||14,t=await this.findExpiringPasswords(s,e);i.json({warningDays:s,users:t})})),e.get(`${s}/password-policy/locked-accounts`,i(async(e,i)=>{const s=await this.findLockedAccounts(e);i.json({accounts:s})})),this.config.ppolicy_validate_complexity&&e.post(`${s}/password/validate`,i(async(e,i)=>{const{password:s}=e.body;if(!s)throw new a("password required");const t=this.validateComplexity(s);i.json(t)}))}async getPasswordStatus(e,i){const s=this.resolveUserDn(e);try{const a=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:["*",...r],paged:!1},s,i);if(!a.searchEntries||0===a.searchEntries.length)throw new t(`User not found: ${e}`);const o=a.searchEntries[0],n=await this.readPolicyConfig();return this.calculateStatus(s,o,n)}catch(i){if(i instanceof Error&&("NoSuchObjectError"===i.name||i.message.includes("0x20")||i.message.includes("Code: 32")))throw new t(`User not found: ${e}`);throw i}}calculateStatus(e,i,s){const t=Date.now(),a=this.parseGeneralizedTime(i.pwdChangedTime);let r=null,o=!1,n=null;s.pwdMaxAge&&a&&(r=new Date(a.getTime()+1e3*s.pwdMaxAge),o=t>r.getTime(),n=Math.ceil((r.getTime()-t)/864e5));let c=!1;const p=this.config.ppolicy_warn_days||14;null!==n&&n>0&&(c=n<=p);const d=i.pwdAccountLockedTime;let l=!!d;if(l&&!("000001010000Z"===d)&&s.pwdLockoutDuration){const e=this.parseGeneralizedTime(d);if(e){t>new Date(e.getTime()+1e3*s.pwdLockoutDuration).getTime()&&(l=!1)}}const u=i.pwdFailureTime,h=Array.isArray(u)?u.length:0,w=i.pwdGraceUseTime,g=Array.isArray(w)?w.length:0,y="TRUE"===i.pwdReset;return{dn:e,passwordSet:!!i.userPassword,lastChanged:a?.toISOString()||null,expiresAt:r?.toISOString()||null,daysUntilExpiration:n,isExpired:o,isExpiringSoon:c,mustChange:y,isLocked:l,lockedAt:d&&this.parseGeneralizedTime(d)?.toISOString()||null,failureCount:h,graceLoginsUsed:g}}async unlockAccount(e,i){const s=this.resolveUserDn(e),a=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:["pwdAccountLockedTime","pwdFailureTime"],paged:!1},s,i);if(!a.searchEntries||0===a.searchEntries.length)throw new t(`User not found: ${e}`);const r=a.searchEntries[0],o={};r.pwdAccountLockedTime&&(o.pwdAccountLockedTime=[]),r.pwdFailureTime&&(o.pwdFailureTime=[]),Object.keys(o).length>0?(await this.server.ldap.modify(s,{delete:o},i),this.logger.info(`Account unlocked: ${s}`)):this.logger.info(`Account was not locked: ${s}`)}async findExpiringPasswords(e,i){const s=await this.readPolicyConfig();if(!s.pwdMaxAge)return[];const t=this.config.ldap_users_base||this.config.ldap_base,a=await this.server.ldap.search({filter:"(pwdChangedTime=*)",scope:"sub",attributes:["uid","cn","displayName","mail","pwdChangedTime"],paged:!1},t,i),r=Date.now(),o=24*e*60*60*1e3,n=[];for(const e of a.searchEntries){const i=e,t=this.parseGeneralizedTime(i.pwdChangedTime);if(!t)continue;const a=new Date(t.getTime()+1e3*s.pwdMaxAge),c=a.getTime()-r;c>0&&c<o&&n.push({dn:i.dn,uid:i.uid,displayName:i.displayName||i.cn,mail:i.mail,expiresAt:a.toISOString(),daysUntilExpiration:Math.ceil(c/864e5)})}return n.sort((e,i)=>e.daysUntilExpiration-i.daysUntilExpiration)}async findLockedAccounts(e){const i=this.config.ldap_users_base||this.config.ldap_base;return(await this.server.ldap.search({filter:"(pwdAccountLockedTime=*)",scope:"sub",attributes:["uid","cn","displayName","mail","pwdAccountLockedTime","pwdFailureTime"],paged:!1},i,e)).searchEntries.map(e=>{const i=e;return{dn:i.dn,uid:i.uid,displayName:i.displayName||i.cn,mail:i.mail,lockedAt:this.parseGeneralizedTime(i.pwdAccountLockedTime)?.toISOString(),failureCount:Array.isArray(i.pwdFailureTime)?i.pwdFailureTime.length:0}})}async readPolicyConfig(){if(this.policyCache&&Date.now()-this.policyCacheTime<this.POLICY_CACHE_TTL)return this.policyCache;const e=this.config.ppolicy_default_dn;if(e)try{const i=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:o,paged:!1},e);if(i.searchEntries.length>0)return this.policyCache=this.parsePolicyEntry(i.searchEntries[0],e),this.policyCacheTime=Date.now(),this.policyCache}catch(i){this.logger.warn(`Failed to read ppolicy from ${e}: ${i}`)}try{const e=await this.server.ldap.search({filter:"(objectClass=pwdPolicy)",scope:"sub",attributes:o,paged:!1},this.config.ldap_base);if(e.searchEntries.length>0){const i=e.searchEntries[0];return this.policyCache=this.parsePolicyEntry(i,i.dn),this.policyCacheTime=Date.now(),this.policyCache}}catch(e){this.logger.debug(`No ppolicy found: ${e}`)}return this.policyCache={},this.policyCacheTime=Date.now(),this.policyCache}parsePolicyEntry(e,i){return{dn:i,pwdMaxAge:this.parseNumber(e.pwdMaxAge),pwdMinAge:this.parseNumber(e.pwdMinAge),pwdInHistory:this.parseNumber(e.pwdInHistory),pwdCheckQuality:this.parseNumber(e.pwdCheckQuality),pwdMinLength:this.parseNumber(e.pwdMinLength),pwdMaxFailure:this.parseNumber(e.pwdMaxFailure),pwdLockout:"TRUE"===e.pwdLockout,pwdLockoutDuration:this.parseNumber(e.pwdLockoutDuration),pwdGraceAuthNLimit:this.parseNumber(e.pwdGraceAuthNLimit),pwdExpireWarning:this.parseNumber(e.pwdExpireWarning),pwdMustChange:"TRUE"===e.pwdMustChange,pwdAllowUserChange:void 0===e.pwdAllowUserChange||"TRUE"===e.pwdAllowUserChange}}parseNumber(e){if(void 0===e)return;const i=parseInt(e,10);return isNaN(i)?void 0:i}parseGeneralizedTime(e){if(!e)return null;const i=e.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);if(!i)return null;const[,s,t,a,r,o,n]=i;return new Date(Date.UTC(+s,+t-1,+a,+r,+o,+n))}validateComplexity(e){const i=[],s=this.config,t=s.ppolicy_min_length||12;return e.length<t&&i.push(`Minimum ${t} characters required`),s.ppolicy_require_uppercase&&!/[A-Z]/.test(e)&&i.push("At least one uppercase letter required"),s.ppolicy_require_lowercase&&!/[a-z]/.test(e)&&i.push("At least one lowercase letter required"),s.ppolicy_require_digit&&!/\d/.test(e)&&i.push("At least one digit required"),s.ppolicy_require_special&&!n.test(e)&&i.push("At least one special character required"),{valid:0===i.length,errors:i}}resolveUserDn(e){if(e.includes("="))return e;const i=this.config.ldap_users_base||this.config.ldap_base;return`${this.config.ldap_user_main_attribute||"uid"}=${s(e)},${i}`}getConfigApiData(){const e=this.config.api_prefix||"/api",i=!!this.config.ppolicy_validate_complexity;return{name:this.name,enabled:!0,endpoints:{getPolicy:`${e}/v1/password-policy`,getUserStatus:`${e}/v1/users/:id/password-status`,unlockUser:`${e}/v1/users/:id/unlock`,getExpiringSoon:`${e}/v1/password-policy/expiring-soon`,getLockedAccounts:`${e}/v1/password-policy/locked-accounts`,validatePassword:i?`${e}/v1/password/validate`:void 0},config:{warnDays:this.config.ppolicy_warn_days||14,validateComplexity:i,...i&&{complexityRules:{minLength:this.config.ppolicy_min_length||12,requireUppercase:this.config.ppolicy_require_uppercase??!0,requireLowercase:this.config.ppolicy_require_lowercase??!0,requireDigit:this.config.ppolicy_require_digit??!0,requireSpecial:this.config.ppolicy_require_special??!0}}},ldapPolicy:this.policyCache||void 0}}}export{PasswordPolicy as default};
1
+ import{isDeepStrictEqual as e}from"node:util";import i from"../../abstract/plugin.js";import{asyncHandler as t,normalizeDn as s,escapeLdapFilter as a,validateDnValue as o,escapeDnValue as r}from"../../lib/utils.js";import{jsonBody as n}from"../../lib/expressFormatedResponses.js";import{extractLdapCode as c}from"../../lib/ldapCodes.js";import{NotFoundError as l,ConflictError as d,BadRequestError as p}from"../../lib/errors.js";const u=["pwdChangedTime","pwdAccountLockedTime","pwdFailureTime","pwdGraceUseTime","pwdReset","pwdPolicySubentry"],w={pwdMinAge:"integer",pwdMaxAge:"integer",pwdInHistory:"integer",pwdCheckQuality:"integer",pwdMinLength:"integer",pwdMaxLength:"integer",pwdExpireWarning:"integer",pwdGraceAuthNLimit:"integer",pwdGraceExpiry:"integer",pwdLockout:"boolean",pwdLockoutDuration:"integer",pwdMaxFailure:"integer",pwdFailureCountInterval:"integer",pwdMustChange:"boolean",pwdAllowUserChange:"boolean",pwdSafeModify:"boolean",pwdMinDelay:"integer",pwdMaxDelay:"integer",pwdMaxIdle:"integer",pwdMaxRecordedFailure:"integer",pwdCheckModuleArg:"text",pwdUseCheckModule:"boolean"},h=["pwdCheckModuleArg","pwdUseCheckModule"],y=[...Object.keys(w),"pwdAttribute","pwdCheckModule"],isWritablePolicyAttribute=e=>Object.prototype.hasOwnProperty.call(w,e),values=e=>(Array.isArray(e)?e:void 0===e?[]:[e]).map(e=>Buffer.isBuffer(e)?e.toString():String(e)).filter(e=>""!==e),first=e=>values(e)[0],userEntry=e=>({dn:e.dn,userPassword:first(e.userPassword),uid:first(e.uid),cn:first(e.cn),displayName:first(e.displayName),mail:first(e.mail),pwdChangedTime:first(e.pwdChangedTime),pwdAccountLockedTime:first(e.pwdAccountLockedTime),pwdFailureTime:values(e.pwdFailureTime),pwdGraceUseTime:values(e.pwdGraceUseTime),pwdReset:first(e.pwdReset),pwdPolicySubentry:first(e.pwdPolicySubentry)}),typedPolicy=e=>{const i={};for(const t of y){const s=values(e[t]);if(0!==s.length)switch(w[t]){case"integer":{const e=parseInt(s[0],10);isNaN(e)||(i[t]=e);break}case"boolean":i[t]="TRUE"===s[0];break;default:i[t]=1===s.length?s[0]:s}}return i},g=/[!@#$%^&*()_+\-=[\]{}|;:'",.<>?/]/;class PasswordPolicy extends i{constructor(){super(...arguments),this.name="ldapPasswordPolicy",this.roles=["api","configurable"],this.policyCache=new Map,this.discoveredDefault=null,this.warnedAmbiguousDefault=!1,this.warnedMissingBase=!1,this.POLICY_CACHE_TTL=6e4}api(e){const i=`${this.config.api_prefix}/v1`;e.get(`${i}/password-policy`,t(async(e,i)=>{const t=await this.readPolicyConfig();i.json(t)})),e.get(`${i}/users/:id/password-status`,t(async(e,i)=>{const t=this.param(e,"id"),s=await this.getPasswordStatus(t,e);i.json(s)})),e.post(`${i}/users/:id/unlock`,t(async(e,i)=>{const t=this.param(e,"id");await this.unlockAccount(t,e),i.json({success:!0,message:"Account unlocked"})})),e.get(`${i}/password-policy/expiring-soon`,t(async(e,i)=>{const t=parseInt(e.query.days)||this.config.ppolicy_warn_days||14,s=await this.findExpiringPasswords(t,e);i.json({warningDays:t,users:s})})),e.get(`${i}/password-policy/locked-accounts`,t(async(e,i)=>{const t=await this.findLockedAccounts(e);i.json({accounts:t})})),e.get(`${i}/password-policies`,t(async(e,i)=>{i.json({policies:await this.listPolicies(e)})})),e.post(`${i}/password-policies`,t(async(e,i)=>{const t=n(e,i);if(!t)return;const s=await this.createPolicy(t,e);i.status(201).json(s)})),e.get(`${i}/password-policies/:cn`,t(async(e,i)=>{const t=await this.findPolicy(this.param(e,"cn"),e);i.json(await this.toPolicyRecord(t))})),e.patch(`${i}/password-policies/:cn`,t(async(e,i)=>{const t=n(e,i);if(!t)return;const s=await this.updatePolicy(this.param(e,"cn"),t,e);i.json(s)})),e.delete(`${i}/password-policies/:cn`,t(async(e,i)=>{await this.deletePolicy(this.param(e,"cn"),e),i.json({success:!0})})),e.put(`${i}/users/:id/password-policy`,t(async(e,i)=>{const t=n(e,i);if(!t)return;const{policy:s}=t;if(null!==s&&("string"!=typeof s||!s))throw new p("policy must be a policy name or null");i.json(await this.assignPolicy(this.param(e,"id"),s,e))})),this.config.ppolicy_validate_complexity&&e.post(`${i}/password/validate`,t(async(e,i)=>{const t=n(e,i);if(!t)return;const{password:s}=t;if(!s)throw new p("password required");const a=this.validateComplexity(s);i.json(a)}))}async getPasswordStatus(e,i){const t=this.resolveUserDn(e),s=await this.readUser(t,e,["*",...u],i),a=await this.policyFor(s);return this.calculateStatus(t,s,a)}async readUser(e,i,t,s){try{const a=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:t,paged:!1},e,s);if(!a.searchEntries||0===a.searchEntries.length)throw new l(`User not found: ${i}`);return userEntry(a.searchEntries[0])}catch(e){if(e instanceof Error&&("NoSuchObjectError"===e.name||e.message.includes("0x20")||e.message.includes("Code: 32")))throw new l(`User not found: ${i}`);throw e}}calculateStatus(e,i,t){const s=Date.now(),a=this.parseGeneralizedTime(i.pwdChangedTime);let o=null,r=!1,n=null;t.pwdMaxAge&&a&&(o=new Date(a.getTime()+1e3*t.pwdMaxAge),r=s>o.getTime(),n=Math.ceil((o.getTime()-s)/864e5));let c=!1;const l=this.config.ppolicy_warn_days||14;null!==n&&n>0&&(c=n<=l);const d=i.pwdAccountLockedTime;let p=!!d;if(p&&!("000001010000Z"===d)&&t.pwdLockoutDuration){const e=this.parseGeneralizedTime(d);if(e){s>new Date(e.getTime()+1e3*t.pwdLockoutDuration).getTime()&&(p=!1)}}const u=i.pwdFailureTime,w=Array.isArray(u)?u.length:0,h=i.pwdGraceUseTime,y=Array.isArray(h)?h.length:0,g="TRUE"===i.pwdReset;return{dn:e,passwordSet:!!i.userPassword,lastChanged:a?.toISOString()||null,expiresAt:o?.toISOString()||null,daysUntilExpiration:n,isExpired:r,isExpiringSoon:c,mustChange:g,isLocked:p,lockedAt:d&&this.parseGeneralizedTime(d)?.toISOString()||null,failureCount:w,graceLoginsUsed:y,policy:t.dn??null}}async unlockAccount(e,i){const t=this.resolveUserDn(e),s=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:["pwdAccountLockedTime","pwdFailureTime"],paged:!1},t,i);if(!s.searchEntries||0===s.searchEntries.length)throw new l(`User not found: ${e}`);const a=userEntry(s.searchEntries[0]),o={};a.pwdAccountLockedTime&&(o.pwdAccountLockedTime=[]),a.pwdFailureTime?.length&&(o.pwdFailureTime=[]),Object.keys(o).length>0?(await this.server.ldap.modify(t,{delete:o},i),this.logger.info(`Account unlocked: ${t}`)):this.logger.info(`Account was not locked: ${t}`)}async findExpiringPasswords(e,i){const t=this.config.ldap_users_base||this.config.ldap_base,s=await this.server.ldap.search({filter:"(pwdChangedTime=*)",scope:"sub",attributes:["uid","cn","displayName","mail","pwdChangedTime","pwdPolicySubentry"],paged:!1},t,i),a=Date.now(),o=24*e*60*60*1e3,r=[];for(const e of s.searchEntries){const i=userEntry(e),t=this.parseGeneralizedTime(i.pwdChangedTime);if(!t)continue;const s=await this.policyFor(i);if(!s.pwdMaxAge)continue;const n=new Date(t.getTime()+1e3*s.pwdMaxAge),c=n.getTime()-a;c>0&&c<o&&r.push({dn:i.dn,uid:i.uid,displayName:i.displayName||i.cn,mail:i.mail,expiresAt:n.toISOString(),daysUntilExpiration:Math.ceil(c/864e5)})}return r.sort((e,i)=>e.daysUntilExpiration-i.daysUntilExpiration)}async findLockedAccounts(e){const i=this.config.ldap_users_base||this.config.ldap_base;return(await this.server.ldap.search({filter:"(pwdAccountLockedTime=*)",scope:"sub",attributes:["uid","cn","displayName","mail","pwdAccountLockedTime","pwdFailureTime"],paged:!1},i,e)).searchEntries.map(e=>{const i=userEntry(e);return{dn:i.dn,uid:i.uid,displayName:i.displayName||i.cn,mail:i.mail,lockedAt:this.parseGeneralizedTime(i.pwdAccountLockedTime)?.toISOString(),failureCount:Array.isArray(i.pwdFailureTime)?i.pwdFailureTime.length:0}})}async policyFor(e){if(e.pwdPolicySubentry){const i=await this.readPolicyAt(e.pwdPolicySubentry);if(i)return i}return this.readPolicyConfig()}async readPolicyConfig(){const e=this.config.ppolicy_default_dn||await this.discoverDefaultDn(),i=e?await this.readPolicyAt(e):null;return this.lastDefaultPolicy=i??{},this.lastDefaultPolicy}async discoverDefaultDn(){if(this.discoveredDefault&&Date.now()-this.discoveredDefault.time<this.POLICY_CACHE_TTL)return this.discoveredDefault.dn;let e=null;try{const i=(await this.server.ldap.search({filter:"(objectClass=pwdPolicy)",scope:"sub",attributes:["1.1"],paged:!1},this.policyBase())).searchEntries.map(e=>e.dn);1===i.length?e=i[0]:i.length>1&&!this.warnedAmbiguousDefault&&(this.warnedAmbiguousDefault=!0,this.logger.warn(`${i.length} password policies found and no --ppolicy-default-dn: accounts without pwdPolicySubentry are reported without a policy`))}catch(e){this.logger.debug(`No ppolicy found: ${e}`)}return this.discoveredDefault={dn:e,time:Date.now()},e}async readPolicyAt(e){const i=s(e),t=this.policyCache.get(i);if(t&&Date.now()-t.time<this.POLICY_CACHE_TTL)return t.policy;let a,o=null;try{const i=await this.server.ldap.search({filter:"(objectClass=pwdPolicy)",scope:"base",attributes:y,paged:!1},e);i.searchEntries.length>0&&(o=((e,i)=>{const t=typedPolicy(e);return{dn:i,pwdMaxAge:t.pwdMaxAge,pwdMinAge:t.pwdMinAge,pwdInHistory:t.pwdInHistory,pwdCheckQuality:t.pwdCheckQuality,pwdMinLength:t.pwdMinLength,pwdMaxFailure:t.pwdMaxFailure,pwdLockout:!0===t.pwdLockout,pwdLockoutDuration:t.pwdLockoutDuration,pwdGraceAuthNLimit:t.pwdGraceAuthNLimit,pwdExpireWarning:t.pwdExpireWarning,pwdMustChange:!0===t.pwdMustChange,pwdAllowUserChange:!1!==t.pwdAllowUserChange}})(i.searchEntries[0],e))}catch(e){a=String(e)}const r=this.config.ppolicy_default_dn;return!o&&r&&s(r)===i?this.logger.warn(`--ppolicy-default-dn ${e} holds no password policy${a?`: ${a}`:""}`):a&&this.logger.debug(`Failed to read ppolicy from ${e}: ${a}`),this.policyCache.set(i,{policy:o,time:Date.now()}),o}forgetPolicies(){this.policyCache.clear(),this.discoveredDefault=null}policyBase(){return this.config.ppolicy_base||this.config.ldap_base}async searchPolicies(e,i){try{return(await this.server.ldap.search({filter:`(&(objectClass=pwdPolicy)${e})`,scope:"sub",attributes:["objectClass","cn",...y],paged:!1},this.policyBase(),i)).searchEntries}catch(e){if(32!==c(e))throw e;return this.warnedMissingBase||(this.warnedMissingBase=!0,this.logger.warn(`${this.policyBase()} does not exist: no password policy is found there, check --ppolicy-base`)),[]}}async findPolicy(e,i){const t=await this.searchPolicies(`(cn=${a(e)})`,i);if(0===t.length)throw new l(`No password policy named ${e}`);if(t.length>1)throw new d(`Several password policies are named ${e}`);return t[0]}async listPolicies(e){const i=[];for(const t of await this.searchPolicies("",e))i.push(await this.toPolicyRecord(t));return i.sort((e,i)=>e.cn.localeCompare(i.cn))}async toPolicyRecord(e){const i=(await this.readPolicyConfig()).dn;return{...{dn:e.dn,cn:values(e.cn)[0]??"",isDefault:!!i&&s(i)===s(e.dn)},...typedPolicy(e)}}policyValue(e,i){switch(w[e]){case"integer":if("number"!=typeof i||!Number.isInteger(i)||i<0||"pwdCheckQuality"===e&&i>2)throw new p("pwdCheckQuality"===e?"pwdCheckQuality must be 0, 1 or 2":`${e} must be a non-negative integer`);return String(i);case"boolean":if("boolean"!=typeof i)throw new p(`${e} must be a boolean`);return i?"TRUE":"FALSE";default:if("string"!=typeof i||""===i)throw new p(`${e} must be a non-empty string`);return i}}async createPolicy(e,i){const{cn:t,...s}=e;if("string"!=typeof t)throw new p("cn required");try{o(t,"cn")}catch(e){throw new p(e.message)}const a=await this.findPolicy(t,i).catch(e=>{if(e instanceof l)return null;throw e});if(a)throw new d(`A password policy is named ${t}: ${a.dn}`);const n=["top","person","pwdPolicy"],u={objectClass:n,cn:t,sn:t,pwdAttribute:"userPassword"};for(const[e,i]of Object.entries(s)){if(!isWritablePolicyAttribute(e))throw new p(`${e} cannot be written`);null!==i&&(u[e]=this.policyValue(e,i))}h.some(e=>e in u)&&n.push("pwdPolicyChecker");try{await this.server.ldap.add(`cn=${r(t)},${this.policyBase()}`,u,i)}catch(e){if(32!==c(e))throw e;throw new l(`${this.policyBase()} does not exist: create it or change --ppolicy-base`)}return this.forgetPolicies(),this.toPolicyRecord(await this.findPolicy(t,i))}async updatePolicy(i,t,a){const o=await this.findPolicy(i,a),r=await this.toPolicyRecord(o),n={},c=[];for(const[i,a]of Object.entries(t)){if(!isWritablePolicyAttribute(i)){if("dn"===i?"string"==typeof a&&s(a)===s(r.dn):Object.prototype.hasOwnProperty.call(r,i)&&e(r[i],a))continue;throw new p(`${i} cannot be written`)}null!==a?n[i]=this.policyValue(i,a):void 0!==r[i]&&c.push(i)}const l={};if(Object.keys(n).length>0&&(l.replace=n),c.length>0&&(l.delete=c),!l.replace&&!l.delete)return r;const d=values(o.objectClass).map(e=>e.toLowerCase());return h.some(e=>e in n)&&!d.includes("pwdpolicychecker")&&(l.add={objectClass:"pwdPolicyChecker"}),await this.server.ldap.modify(o.dn,l,a),this.forgetPolicies(),this.toPolicyRecord(await this.findPolicy(i,a))}async deletePolicy(e,i){const t=await this.findPolicy(e,i),o=(await this.readPolicyConfig()).dn;if(o&&s(o)===s(t.dn))throw new d(`${t.dn} is the default password policy`);if((await this.server.ldap.search({filter:`(pwdPolicySubentry=${a(t.dn)})`,scope:"sub",attributes:["1.1"],sizeLimit:1,paged:!1},this.config.ldap_base)).searchEntries.length>0)throw new d(`${t.dn} is still applied to at least one entry`);await this.server.ldap.delete(t.dn,i),this.forgetPolicies()}async assignPolicy(e,i,t){const s=this.resolveUserDn(e);if(null===i){try{await this.server.ldap.modify(s,{delete:["pwdPolicySubentry"]},t)}catch(e){if(16!==c(e))throw e}return{dn:s,policy:null}}const a=await this.findPolicy(i,t);return await this.server.ldap.modify(s,{replace:{pwdPolicySubentry:a.dn}},t),{dn:s,policy:a.dn}}param(e,i){const t=e.params[i];return Array.isArray(t)?t[0]:t}parseGeneralizedTime(e){if(!e)return null;const i=e.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);if(!i)return null;const[,t,s,a,o,r,n]=i;return new Date(Date.UTC(+t,+s-1,+a,+o,+r,+n))}validateComplexity(e){const i=[],t=this.config,s=t.ppolicy_min_length||12;return e.length<s&&i.push(`Minimum ${s} characters required`),t.ppolicy_require_uppercase&&!/[A-Z]/.test(e)&&i.push("At least one uppercase letter required"),t.ppolicy_require_lowercase&&!/[a-z]/.test(e)&&i.push("At least one lowercase letter required"),t.ppolicy_require_digit&&!/\d/.test(e)&&i.push("At least one digit required"),t.ppolicy_require_special&&!g.test(e)&&i.push("At least one special character required"),{valid:0===i.length,errors:i}}resolveUserDn(e){if(e.includes("="))return e;const i=this.config.ldap_users_base||this.config.ldap_base;return`${this.config.ldap_user_main_attribute||"uid"}=${r(e)},${i}`}getConfigApiData(){const e=this.config.api_prefix||"/api",i=!!this.config.ppolicy_validate_complexity;return{name:this.name,enabled:!0,endpoints:{getPolicy:`${e}/v1/password-policy`,getUserStatus:`${e}/v1/users/:id/password-status`,unlockUser:`${e}/v1/users/:id/unlock`,getExpiringSoon:`${e}/v1/password-policy/expiring-soon`,getLockedAccounts:`${e}/v1/password-policy/locked-accounts`,listPolicies:`${e}/v1/password-policies`,policy:`${e}/v1/password-policies/:cn`,assignPolicy:`${e}/v1/users/:id/password-policy`,validatePassword:i?`${e}/v1/password/validate`:void 0},config:{warnDays:this.config.ppolicy_warn_days||14,validateComplexity:i,...i&&{complexityRules:{minLength:this.config.ppolicy_min_length||12,requireUppercase:this.config.ppolicy_require_uppercase??!0,requireLowercase:this.config.ppolicy_require_lowercase??!0,requireDigit:this.config.ppolicy_require_digit??!0,requireSpecial:this.config.ppolicy_require_special??!0}}},ldapPolicy:this.lastDefaultPolicy}}}export{PasswordPolicy as default};
@@ -1 +1 @@
1
- import{FilterParser as t}from"ldapts";import e from"../../abstract/plugin.js";import{tryMethodData as s}from"../../lib/expressFormatedResponses.js";import{isDnInBranch as r,getRdn as i,getParentDn as a,asyncHandler as n}from"../../lib/utils.js";import{BadRequestError as o,ForbiddenError as h,NotFoundError as c}from"../../lib/errors.js";const d=["base","one","sub"],l=["userpassword","pwdhistory","sambantpassword","sambalmpassword","sambapasswordhistory","krbprincipalkey","krbextradata","krbpwdhistory","unicodepwd","dbcspwd","lmpwdhistory","ntpwdhistory","supplementalcredentials","userpkcs12","authpassword"];class LdapRaw extends e{constructor(t){super(t),this.name="ldapRaw",this.roles=["api"];const e=(this.config.ldap_raw_base||[]).filter(t=>t&&t.length>0);if(0===e.length){if(!this.config.ldap_base)throw new Error("Missing --ldap-base or --ldap-raw-base for plugin core/ldap/raw");e.push(this.config.ldap_base)}this.bases=e,this.showSecrets=!0===this.config.ldap_raw_show_secrets,this.hiddenAttributes=new Set([...this.showSecrets?[]:l,...(this.config.ldap_raw_hidden_attribute||[]).filter(t=>t&&t.length>0).map(t=>t.toLowerCase())]),this.maxResults=this.config.ldap_raw_max_results||200,this.logger.info(`LDAP raw API enabled on ${this.bases.join(", ")} (read-only)`),this.showSecrets&&this.logger.warn("LDAP raw API: --ldap-raw-show-secrets is set, password hashes and other credential attributes are served over the API")}checkDn(t){if(!t||!t.trim())throw new o("DN is required");if(!this.bases.some(e=>r(t,e)||t.toLowerCase()===e.toLowerCase()))throw new h(`DN ${t} is outside the exposed bases`)}checkFilter(e){try{t.parseString(e)}catch(t){const s=t instanceof Error?t.message:String(t);throw new o(`Invalid LDAP filter ${JSON.stringify(e)}: ${s}. A filter must be parenthesised, e.g. (cn=foo) or (|(cn=*foo*)(ou=*foo*))`)}}formatEntry(t){const e={};for(const[s,r]of Object.entries(t)){if("dn"===s)continue;if("*"===s||"+"===s||"1.1"===s)continue;if(this.hiddenAttributes.has(s.split(";")[0].toLowerCase()))continue;const t=Array.isArray(r)?r:[r],i=t.some(t=>Buffer.isBuffer(t));e[s]={values:t.map(t=>Buffer.isBuffer(t)?t.toString("base64"):i?Buffer.from(String(t),"utf8").toString("base64"):String(t)),binary:i}}return{dn:t.dn,attributes:e}}async searchDirectory(t,e,s){try{return await this.server.ldap.search({paged:!1,...e},t,s)}catch(e){if(32===e?.code)throw new c(`Entry ${t||"(root DSE)"} not found`);throw e}}async getRootDse(){const t=(await this.searchDirectory("",{scope:"base",filter:"(objectClass=*)",attributes:["*","+"]})).searchEntries[0];if(!t)throw new c("Root DSE is not readable");return this.formatEntry({...t,dn:""})}async getSubschemaDn(){return this.server.ldap.subschemaDn()}async getSchemaIndex(){return this.server.ldap.schemaIndex()}async getSchema(){return(await this.getSchemaIndex()).schema}async getEntry(t,e){this.checkDn(t);const read=async s=>this.searchDirectory(t,{scope:"base",filter:"(objectClass=*)",attributes:s},e);let s;try{s=await read(["*","+"])}catch(e){if(e instanceof Error&&("statusCode"in e||/\[authz-forbidden\]/.test(e.message)))throw e;this.logger.debug(`Reading ${t} with operational attributes failed (${e}), retrying without`),s=await read(["*"])}const r=s.searchEntries[0];if(!r)throw new c(`Entry ${t} not found`);return this.formatEntry(r)}async getChildren(t,e,s){this.checkDn(t);const r=await this.searchDirectory(t,{scope:"one",filter:"(objectClass=*)",attributes:["objectClass"],sizeLimit:this.maxResults+1},s),a=r.searchEntries.length>this.maxResults,n=[];for(const t of r.searchEntries.slice(0,this.maxResults)){const e=t.objectClass;n.push({dn:t.dn,rdn:i(t.dn),objectClass:Array.isArray(e)?e.map(String):e?[String(e)]:[],hasChildren:!1})}return n.sort((t,e)=>t.rdn.localeCompare(e.rdn)),e&&await Promise.all(n.map(async t=>{t.hasChildren=await this.hasChildren(t.dn,s)})),a&&this.logger.info(`Children of ${t} truncated to ${this.maxResults} entries`),{children:n,truncated:a}}async hasChildren(t,e){try{return(await this.searchDirectory(t,{scope:"one",filter:"(objectClass=*)",attributes:["1.1"],sizeLimit:1},e)).searchEntries.length>0}catch{return!1}}async search(t,e){this.checkDn(t.base),this.checkFilter(t.filter);const s=Math.min(t.limit||this.maxResults,this.maxResults),r=await this.searchDirectory(t.base,{scope:t.scope,filter:t.filter,attributes:t.attributes?.length?t.attributes:["*"],sizeLimit:s+1},e),i=r.searchEntries.length>s;return{entries:r.searchEntries.slice(0,s).map(t=>this.formatEntry(t)),truncated:i}}parentOf(t){if(this.bases.some(e=>t.toLowerCase()===e.toLowerCase()))return null;const e=a(t);return e&&e!==t?e:null}api(t){const e=`${this.config.api_prefix}/v1/ldap/raw`;t.get(`${e}/bases`,n(async(t,e)=>{await s(e,()=>({bases:this.bases}))})),t.get(`${e}/rootdse`,n(async(t,e)=>{await s(e,this.getRootDse.bind(this))})),t.get(`${e}/schema`,n(async(t,e)=>{await s(e,this.getSchema.bind(this))})),t.get(`${e}/search`,n(async(t,e)=>{const r=t.query.scope||"sub";if(!d.includes(r))throw new o(`Invalid scope "${r}", expected one of ${d.join(", ")}`);const i="string"==typeof t.query.attributes?t.query.attributes.split(",").map(t=>t.trim()).filter(Boolean):void 0,a="string"==typeof t.query.limit?parseInt(t.query.limit,10):void 0;if(void 0!==a&&(isNaN(a)||a<1))throw new o("limit must be a positive integer");await s(e,this.search.bind(this),{base:t.query.base||this.bases[0],scope:r,filter:t.query.filter||"(objectClass=*)",attributes:i,limit:a},t)})),t.get(`${e}/children/:dn`,n(async(t,e)=>{const r=decodeURIComponent(t.params.dn),i=void 0!==t.query.children&&"false"!==t.query.children;await s(e,this.getChildren.bind(this),r,i,t)})),t.get(`${e}/entry/:dn`,n(async(t,e)=>{const r=decodeURIComponent(t.params.dn);await s(e,this.getEntry.bind(this),r,t)}))}getConfigApiData(){return{bases:this.bases,readOnly:!0,maxResults:this.maxResults,hiddenAttributes:[...this.hiddenAttributes],showSecrets:this.showSecrets}}}export{LdapRaw as default};
1
+ import{FilterParser as t}from"ldapts";import e from"../../abstract/plugin.js";import{tryMethodData as s}from"../../lib/expressFormatedResponses.js";import{isDnInBranch as r,getRdn as i,getParentDn as a,asyncHandler as n}from"../../lib/utils.js";import{BadRequestError as o,ForbiddenError as h,NotFoundError as c}from"../../lib/errors.js";const d=["base","one","sub"],l=["userpassword","pwdhistory","sambantpassword","sambalmpassword","sambapasswordhistory","krbprincipalkey","krbextradata","krbpwdhistory","unicodepwd","dbcspwd","lmpwdhistory","ntpwdhistory","supplementalcredentials","userpkcs12","authpassword"];class LdapRaw extends e{constructor(t){super(t),this.name="ldapRaw",this.roles=["api"];const e=(this.config.ldap_raw_base||[]).filter(t=>t&&t.length>0);if(0===e.length){if(!this.config.ldap_base)throw new Error("Missing --ldap-base or --ldap-raw-base for plugin core/ldap/raw");e.push(this.config.ldap_base)}this.bases=e,this.showSecrets=!0===this.config.ldap_raw_show_secrets,this.hiddenAttributes=new Set([...this.showSecrets?[]:l,...(this.config.ldap_raw_hidden_attribute||[]).filter(t=>t&&t.length>0).map(t=>t.toLowerCase())]),this.maxResults=this.config.ldap_raw_max_results||200,this.logger.info(`LDAP raw API enabled on ${this.bases.join(", ")} (read-only)`),this.showSecrets&&this.logger.warn("LDAP raw API: --ldap-raw-show-secrets is set, password hashes and other credential attributes are served over the API")}checkDn(t){if(!t||!t.trim())throw new o("DN is required");if(!this.bases.some(e=>r(t,e)||t.toLowerCase()===e.toLowerCase()))throw new h(`DN ${t} is outside the exposed bases`)}checkFilter(e){try{t.parseString(e)}catch(t){const s=t instanceof Error?t.message:String(t);throw new o(`Invalid LDAP filter ${JSON.stringify(e)}: ${s}. A filter must be parenthesised, e.g. (cn=foo) or (|(cn=*foo*)(ou=*foo*))`)}}formatEntry(t){const e={};for(const[s,r]of Object.entries(t)){if("dn"===s)continue;if(this.hiddenAttributes.has(s.split(";")[0].toLowerCase()))continue;const t=Array.isArray(r)?r:[r],i=t.some(t=>Buffer.isBuffer(t));e[s]={values:t.map(t=>Buffer.isBuffer(t)?t.toString("base64"):i?Buffer.from(String(t),"utf8").toString("base64"):String(t)),binary:i}}return{dn:t.dn,attributes:e}}async searchDirectory(t,e,s){try{return await this.server.ldap.search({paged:!1,...e},t,s)}catch(e){if(32===e?.code)throw new c(`Entry ${t||"(root DSE)"} not found`);throw e}}async getRootDse(){const t=(await this.searchDirectory("",{scope:"base",filter:"(objectClass=*)",attributes:["*","+"]})).searchEntries[0];if(!t)throw new c("Root DSE is not readable");return this.formatEntry({...t,dn:""})}async getSubschemaDn(){return this.server.ldap.subschemaDn()}async getSchemaIndex(){return this.server.ldap.schemaIndex()}async getSchema(){return(await this.getSchemaIndex()).schema}async getEntry(t,e){this.checkDn(t);const read=async s=>this.searchDirectory(t,{scope:"base",filter:"(objectClass=*)",attributes:s},e);let s;try{s=await read(["*","+"])}catch(e){if(e instanceof Error&&("statusCode"in e||/\[authz-forbidden\]/.test(e.message)))throw e;this.logger.debug(`Reading ${t} with operational attributes failed (${e}), retrying without`),s=await read(["*"])}const r=s.searchEntries[0];if(!r)throw new c(`Entry ${t} not found`);return this.formatEntry(r)}async getChildren(t,e,s){this.checkDn(t);const r=await this.searchDirectory(t,{scope:"one",filter:"(objectClass=*)",attributes:["objectClass"],sizeLimit:this.maxResults+1},s),a=r.searchEntries.length>this.maxResults,n=[];for(const t of r.searchEntries.slice(0,this.maxResults)){const e=t.objectClass;n.push({dn:t.dn,rdn:i(t.dn),objectClass:Array.isArray(e)?e.map(String):e?[String(e)]:[],hasChildren:!1})}return n.sort((t,e)=>t.rdn.localeCompare(e.rdn)),e&&await Promise.all(n.map(async t=>{t.hasChildren=await this.hasChildren(t.dn,s)})),a&&this.logger.info(`Children of ${t} truncated to ${this.maxResults} entries`),{children:n,truncated:a}}async hasChildren(t,e){try{return(await this.searchDirectory(t,{scope:"one",filter:"(objectClass=*)",attributes:["1.1"],sizeLimit:1},e)).searchEntries.length>0}catch{return!1}}async search(t,e){this.checkDn(t.base),this.checkFilter(t.filter);const s=Math.min(t.limit||this.maxResults,this.maxResults),r=await this.searchDirectory(t.base,{scope:t.scope,filter:t.filter,attributes:t.attributes?.length?t.attributes:["*"],sizeLimit:s+1},e),i=r.searchEntries.length>s;return{entries:r.searchEntries.slice(0,s).map(t=>this.formatEntry(t)),truncated:i}}parentOf(t){if(this.bases.some(e=>t.toLowerCase()===e.toLowerCase()))return null;const e=a(t);return e&&e!==t?e:null}api(t){const e=`${this.config.api_prefix}/v1/ldap/raw`;t.get(`${e}/bases`,n(async(t,e)=>{await s(e,()=>({bases:this.bases}))})),t.get(`${e}/rootdse`,n(async(t,e)=>{await s(e,this.getRootDse.bind(this))})),t.get(`${e}/schema`,n(async(t,e)=>{await s(e,this.getSchema.bind(this))})),t.get(`${e}/search`,n(async(t,e)=>{const r=t.query.scope||"sub";if(!d.includes(r))throw new o(`Invalid scope "${r}", expected one of ${d.join(", ")}`);const i="string"==typeof t.query.attributes?t.query.attributes.split(",").map(t=>t.trim()).filter(Boolean):void 0,a="string"==typeof t.query.limit?parseInt(t.query.limit,10):void 0;if(void 0!==a&&(isNaN(a)||a<1))throw new o("limit must be a positive integer");await s(e,this.search.bind(this),{base:t.query.base||this.bases[0],scope:r,filter:t.query.filter||"(objectClass=*)",attributes:i,limit:a},t)})),t.get(`${e}/children/:dn`,n(async(t,e)=>{const r=decodeURIComponent(t.params.dn),i=void 0!==t.query.children&&"false"!==t.query.children;await s(e,this.getChildren.bind(this),r,i,t)})),t.get(`${e}/entry/:dn`,n(async(t,e)=>{const r=decodeURIComponent(t.params.dn);await s(e,this.getEntry.bind(this),r,t)}))}getConfigApiData(){return{bases:this.bases,readOnly:!0,maxResults:this.maxResults,hiddenAttributes:[...this.hiddenAttributes],showSecrets:this.showSecrets}}}export{LdapRaw 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{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 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();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 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 s=r.trim().toLowerCase(),n=this.createStash.get(s);if(!n)return void this.logger.info({plugin:this.name,event:"provision",userName:t,email:r.trim(),result:"skipped",reason:"no_stashed_context"});this.createStash.delete(s),this.logger.info({plugin:this.name,event:"provision",userName:t,email:n.email,orgId:n.orgId,base:n.base,role:n.role,result:"context_loaded"});const a=n.email,o=emailDomain(a),l=t.replace(/\./g,"")+n.orgId;const u=this.extractPrimaryPhone(e),h=this.extractPhones(e),g=this.extractPublicName(e)||t,d={plugin:this.name,event:"provision",userName:t,orgId:n.orgId,slug:l},c=`${l}.${this.clouderyDomain}`.toLowerCase();let m,p=null;try{p=await this.findInstanceUuidByFqdn(c)}catch(e){this.logger.warn({...d,result:"existence_lookup_failed",fqdn:c,error:`${e}`})}if(p)this.logger.info({...d,result:"instance_exists",fqdn:c,uuid:p}),m=c;else{let t;try{this.logger.info({...d,result:"creating_instance",orgDomain:o,email:a,mobile:u||void 0}),t=await this.createInstance({email:a,publicName:g,locale:this.extractLocale(e),oidc:l,phone:u||"",slug:l,public_name:g,offer:this.offer,domain:this.clouderyDomain,skip_email_validation:!0,internal_email:a,org_domain:o,org_id:n.orgId})}catch(e){return void this.logger.error({...d,result:"error",error:`${e}`})}if(!(!t.workflow||await this.waitForWorkflow(t.workflow)))return void this.logger.error({...d,result:"workflow_failed",workflow:t.workflow});m=t.fqdn}const f=`${this.rdnAttribute}=${i(t)},${n.base}`,w={[this.fqdnAttribute]:m,[this.orgIdAttribute]:n.orgId,[this.orgRoleAttribute]:n.role,[this.invitedAttribute]:"TRUE",cn:t};h.length>0&&(w[this.phonesAttribute]=JSON.stringify(h));try{this.logger.info({...d,result:"writing_fqdn",dn:f,hasPhones:h.length>0}),await this.server.ldap.modify(f,{replace:w}),this.logger.info({...d,result:"written_fqdn",dn:f})}catch(e){this.logger.error({...d,event:"writeFqdn",dn:f,error:`${e}`})}await this.publishUserCreated({twakeId:t,orgDomain:o,orgId:n.orgId,fqdn:m,email:a,mobile:u}),this.logger.info({...d,result:"success",fqdn:m})}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.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.findInstanceUuidByFqdn(t.fqdn);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,o);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,o);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 findInstanceUuidByFqdn(e){this.logger.info({plugin:this.name,event:"findInstanceUuidByFqdn",fqdn:e});const r=new URL(`${this.managerUrl}/api/v2/instances`);r.searchParams.set("fqdn",e),r.searchParams.append("only","_id"),r.searchParams.set("limit","1");const i=await t(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:"findInstanceUuidByFqdn",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&&s.items.length>0?s.items[0]._id:null;return this.logger.info({plugin:this.name,event:"findInstanceUuidByFqdn",fqdn:e,result:n?"found":"not_found",uuid:n||void 0}),n}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,o);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?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
+ 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.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.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 +1 @@
1
- import t from"../../abstract/plugin.js";import{NotFoundError as i,BadRequestError as e}from"../../lib/errors.js";import{jsonBody as a}from"../../lib/expressFormatedResponses.js";import{CozyStackProvider as n,ClouderyProvider as r}from"../../lib/instanceProviders.js";import{extractLdapCode as s}from"../../lib/ldapCodes.js";import{rdnValue as o,asyncHandler as c,escapeLdapFilter as h}from"../../lib/utils.js";import{lifecycleAttributes as d,isTombstone as l}from"./lifecycleAttributes.js";const first=t=>{const i=Array.isArray(t)?t[0]:t;return(null==i?"":i.toString())||void 0},u=/(^|\/)twake\/(cozy|cloudery)Provision(\.js)?$/;class TwakeInstances extends t{constructor(t){super(t),this.name="twakeInstances",this.roles=["consistency","api"],this.dependencies={rabbitmq:"core/rabbitmq"},this.offers=[],this.queues=new Map,this.announcing=new Map,this.hooks={ldapaddrequest:([t,i,e])=>{if(this.patterns.some(i=>i.test(t))||this.isOrganization(t)){const t=[this.fqdnAttribute,this.sentAttribute,this.organizationFqdn].map(t=>t.toLowerCase());for(const e of Object.keys(i))t.includes(e.toLowerCase())&&delete i[e]}return[t,i,e]},ldapaddafter:async([t,i])=>{const e=this.account(t,i);e&&(await this.serialized(t,async()=>{try{await this.provide(t,i,e)}catch(i){this.logger.error({plugin:this.name,event:"create",dn:t,error:String(i)})}}),e.orgId&&await this.announcing.get(e.orgId.toLowerCase()))},ldapdeletedone:async t=>{const i=this.provider;if(i instanceof n)for(const e of Array.isArray(t)?t:[t]){const t=this.match(e),a=/^uid=/i.test(e)?o(e):"";t&&a&&await this.serialized(e,async()=>{try{await i.destroy(this.id(a,t),a)}catch(t){this.logger.error({plugin:this.name,event:"destroy",dn:e,error:String(t)})}})}}};const i=this.config,e=(i.plugin||[]).find(t=>u.test(t));if(e)throw new Error(`${this.name}: ${e} gives accounts their instance too; load one of them`);this.patterns=(i.twake_instance_dn||[]).map(t=>new RegExp(t,"i"));const a=new Set(["uid"]);for(const t of this.patterns)for(const[,i]of t.source.matchAll(/\(\?<(\w+)>/g))a.add(i);for(const[,t]of(i.twake_instance_id||"{uid}").matchAll(/\{(\w+)\}/g))if(!a.has(t))throw new Error(`${this.name}: --twake-instance-id names {${t}}, which no --twake-instance-dn group captures`);this.lifecycle=d(i),this.fqdnAttribute=i.twake_instance_fqdn_attribute||"twakeWorkspaceUrl",this.sentAttribute=i.twake_instance_sent_attribute||"twakeCreatedEventAt";const s=i.twake_instance_timeout||3e4;if("cozy-stack"===i.twake_instance_provider){if(!i.twake_instance_cozy_url||!i.twake_instance_cozy_domain)throw new Error(`${this.name}: --twake-instance-cozy-url and --twake-instance-cozy-domain are required`);this.provider=new n(i.twake_instance_cozy_url.replace(/\/$/,""),i.twake_instance_cozy_user||"admin",i.twake_instance_cozy_passphrase||"",i.twake_instance_cozy_domain,i.twake_instance_cozy_context||"",i.twake_instance_cozy_apps||"",s),this.locale=i.twake_instance_locale||"fr"}else{if("cloudery"!==i.twake_instance_provider)throw new Error(`${this.name}: --twake-instance-provider must be cloudery or cozy-stack`);{if(!i.twake_instance_cloudery_url||!i.twake_instance_cloudery_domain)throw new Error(`${this.name}: --twake-instance-cloudery-url and --twake-instance-cloudery-domain are required`);const t=(i.twake_instance_cloudery_offer||"").split(";").map(t=>t.trim());if(t.length>1&&""===t[t.length-1]&&t.pop(),t.length>1&&t.length>this.patterns.length)throw new Error(`${this.name}: --twake-instance-cloudery-offer has more offers than --twake-instance-dn has expressions`);t.length>1&&(this.offers=t),this.provider=new r(i.twake_instance_cloudery_url.replace(/\/$/,""),i.twake_instance_cloudery_token||"",i.twake_instance_cloudery_domain,t.length>1?"":t[0],s),this.locale=i.twake_instance_locale||"en"}}}async api(t){const n=this.requirePlugin("rabbitmq");if(!n||!await n.getRawClient())throw new Error(`${this.name}: RabbitMQ is required and unreachable`);const s=await this.server.ldap.schemaIndex(),o=[this.fqdnAttribute,this.sentAttribute,...this.config.twake_instance_organization_base?[this.organizationDomain,this.organizationFqdn,this.organizationName]:[]].filter(t=>t&&!s.getAttributeType(t));if(o.length)throw new Error(`${this.name}: the directory schema defines no ${o.join(", ")}; load a schema that does, or name other attributes`);if(this.organizationFqdn&&this.organizationFqdn.toLowerCase()===this.organizationName.toLowerCase())throw new Error(`${this.name}: an organization's name and address cannot share the attribute ${this.organizationName}`);t.post(`${this.config.api_prefix}/v1/twake/instances/ensure`,c(async(t,n)=>{const r=a(t,n);if(!1===r)return;let s=r?.dn;if(void 0===s&&"string"==typeof r?.mail&&r.mail){const e=await this.findByMail(r.mail,t);if(!e)throw new i(`No account for ${r.mail}`);s=e.dn}if("string"!=typeof s||!s)throw new e("Give the account as dn or mail");n.json({state:await this.ensureInstance(s,t)})})),this.provider instanceof r&&await n.subscribe(this.config.twake_instance_auth_exchange||"auth",this.config.twake_instance_workplace_created_key||"workplace.created",this.config.twake_instance_queue||"workplace.created.ldap-rest",t=>this.onWorkplaceCreated(t))}async ensureInstance(t,a){let n;const r=await this.serialized(t,async()=>{const r=await this.read(t,a);if(!r)throw new i(`No entry at ${t}`);const s=this.account(t,r);if(!s)throw new e(`${t} gets no instance`);if(n=s.orgId,first(this.value(r,this.sentAttribute)))return"ready";a&&await this.server.ldap.modify(t,{replace:{[this.sentAttribute]:[]}},a);const o=first(this.value(r,this.fqdnAttribute));if(o){const i=o.toLowerCase()===this.provider.address(s)?{fqdn:o,ready:!0}:await this.provider.find(s,o);if(i?.ready)return await this.ready(t,r,s,i.fqdn,a),"ready"}return this.provide(t,r,s,a)});return n&&await this.announcing.get(n.toLowerCase()),r}async ensureOrganization(t){return this.serialized(`organization:${t.id}`,async()=>{const e=await this.findOrganization(t.id);if(!e)throw new i(`No organization ${t.id}`);t.name&&!first(this.value(e,this.organizationName))&&(await this.server.ldap.modify(e.dn,{replace:{[this.organizationName]:t.name}}),e[this.organizationName]=t.name);const a=this.organizationFqdn&&first(this.value(e,this.organizationFqdn)),n=this.organizationRequest(t),s=a?{fqdn:a,ready:!0}:await this.provider.find(n);if(first(this.value(e,this.sentAttribute)))return s?.ready?await this.organizationReady(e,s.fqdn):await this.releaseMembers(e.dn),"ready";let o=s?.ready&&s.fqdn;return s||(this.provider instanceof r&&await this.provider.createOrganization(t),o=await this.provider.create(n)),o?(await this.organizationReady(e,o),"ready"):"pending"})}organizationRequest(t){return{id:t.id,email:this.organizationMail(t.id,t.domain),publicName:t.name,locale:this.locale,orgId:t.id,orgDomain:t.domain,offer:this.config.twake_instance_cloudery_organization_offer,organization:!0}}async requestOrganization(t,i){if(!(this.provider instanceof r))return;const e=await this.findOrganization(t);if(!e||first(this.value(e,this.sentAttribute)))return;const a=first(this.value(e,this.organizationName)),n=first(this.value(e,this.organizationDomain));if(!a||!n)return void this.logger.warn(`${this.name}: organization ${t} has no ${this.organizationName} or ${this.organizationDomain}, so its instance cannot be asked for`);const s=this.organizationRequest({id:t,name:a,domain:n}),o=await this.provider.find(s);if(o){const r=t.toLowerCase();if(!o.ready||this.announcing.has(r))return;return i&&await this.server.ldap.modify(e.dn,{},i),void(this.announcing.has(r)||this.announcing.set(r,this.ensureOrganization({id:t,name:a,domain:n}).catch(i=>{this.logger.error({plugin:this.name,event:"organization",id:t,error:String(i)})}).finally(()=>this.announcing.delete(r))))}await this.provider.createOrganization({id:t,name:a,domain:n});try{await this.provider.create(s)}catch(t){if(!await this.provider.find(s))throw t}}async onWorkplaceCreated(t){const{internalEmail:i,workplaceFqdn:e}=t;if(!i||!e)return;const a=t.twakeId&&await this.findOrganization(t.twakeId);if(a){const n=t.twakeId.toLowerCase(),r=first(this.value(a,this.organizationDomain)),s=i.toLowerCase();if(r?this.organizationMail(n,r)===s:s.startsWith(`${n}@`))return this.serialized(`organization:${t.twakeId}`,async()=>{const t=await this.read(a.dn);t&&await this.organizationReady(t,e)})}const n=await this.findByMail(i);n?await this.settle(n.dn,e):this.logger.debug(`${this.name}: no account here for ${i}`)}async settle(t,i){await this.serialized(t,async()=>{const e=await this.read(t),a=e&&this.account(t,e);e&&a&&(first(this.value(e,this.sentAttribute))||await this.ready(t,e,a,i))})}async provide(t,i,e,a){e.orgId&&await this.requestOrganization(e.orgId,a);const n=await this.provider.find(e),r=n?n.ready&&n.fqdn:await this.provider.create(e);return r?(await this.ready(t,i,e,r,a),"ready"):"pending"}async ready(t,i,e,a,n){if(first(this.value(i,this.fqdnAttribute))!==a&&await this.server.ldap.modify(t,{replace:{[this.fqdnAttribute]:a}},n),first(this.value(i,this.sentAttribute)))return;const s=this.provider instanceof r&&e.orgId&&await this.findOrganization(e.orgId);if(s&&!first(this.value(s,this.sentAttribute))){if(t.toLowerCase().endsWith(`,${s.dn.toLowerCase()}`))return void this.logger.info(`${this.name}: ${t} waits for organization ${e.orgId}`);this.logger.warn(`${this.name}: ${t} is not below its organization entry, so it is announced before organization ${e.orgId} is linked; clear its ${this.sentAttribute} and send it again once the organization is announced`)}await this.announce(this.config.twake_instance_auth_exchange||"auth",this.config.twake_instance_user_created_key||"user.created",{twakeId:e.twakeId,internalEmail:e.email,workplaceFqdn:a,...e.orgId?{organizationId:e.orgId}:{},...e.orgDomain?{domain:e.orgDomain,organizationDomain:e.orgDomain}:{},...e.phone?{mobile:e.phone}:{}}),await this.server.ldap.modify(t,{replace:{[this.sentAttribute]:(new Date).toISOString()}},n)}async organizationReady(t,i){const e=t.dn,a=first(this.value(t,this.organizationId));if(a&&this.provider instanceof r&&await this.provider.linkOrganization(a,i),first(this.value(t,this.sentAttribute)))return this.releaseMembers(e);this.organizationFqdn&&!first(this.value(t,this.organizationFqdn))&&await this.server.ldap.modify(e,{replace:{[this.organizationFqdn]:i}});const n=first(this.value(t,this.organizationDomain)),s=first(this.value(t,this.organizationName));await this.announce(this.config.twake_instance_b2b_exchange||"b2b",this.config.twake_instance_organization_created_key||"organization.created",{organizationId:a,workplaceFqdn:i,...s?{organization:s}:{},...n?{domain:n}:{}}),await this.server.ldap.modify(e,{replace:{[this.sentAttribute]:(new Date).toISOString()}}),await this.releaseMembers(e)}async releaseMembers(t){const i=await this.server.ldap.search({paged:!1,scope:"sub",filter:`(&(${this.fqdnAttribute}=*)(!(${this.sentAttribute}=*)))`},t);let e=0;for(const t of i.searchEntries)try{await this.release(t)}catch(i){e++,this.logger.error(`${this.name}: ${t.dn} not released: ${String(i)}`)}if(e)throw new Error(`${this.name}: ${e} member(s) of ${t} not released`)}async release(t){const i=t.dn,e=this.account(i,t),a=first(this.value(t,this.fqdnAttribute));if(!e||!a)return;const n=a.toLowerCase()===this.provider.address(e)?{fqdn:a,ready:!0}:await this.provider.find(e,a);if(n?.ready)return this.settle(i,n.fqdn);this.logger.warn(`${this.name}: ${i} stays held, its address ${a} is not confirmed; ensureInstance sends it once it is`)}async serialized(t,i){const e=t.toLowerCase(),a=(this.queues.get(e)??Promise.resolve()).catch(()=>{}).then(i),n=a.catch(()=>{});return this.queues.set(e,n),n.then(()=>{this.queues.get(e)===n&&this.queues.delete(e)}),a}get organizationId(){return this.config.twake_instance_organization_id_attribute||"ou"}get organizationDomain(){return this.config.twake_instance_organization_domain_attribute||"twakeDomain"}get organizationName(){return this.config.twake_instance_organization_name_attribute||"description"}get organizationFqdn(){return this.config.twake_instance_organization_fqdn_attribute||""}organizationMail(t,i){return`${t}@${i}`.toLowerCase()}isOrganization(t){const i=this.config.twake_instance_organization_base;if(!i)return!1;return t.slice(t.indexOf(",")+1).toLowerCase()===i.toLowerCase()}async findOrganization(t){const i=this.config.twake_instance_organization_base;if(!i)return;return(await this.server.ldap.search({paged:!1,scope:"one",filter:`(${this.organizationId}=${h(t)})`},i)).searchEntries[0]}async read(t,i){try{return(await this.server.ldap.search({paged:!1,scope:"base"},t,i)).searchEntries[0]}catch(i){const a=s(i);if(32===a)return;if(34===a)throw new e(`Invalid DN: ${t}`);throw i}}async announce(t,i,e){const a=this.requirePlugin("rabbitmq");if(!a||!await a.getRawClient())throw new Error(`${this.name}: RabbitMQ unreachable, ${i} not sent`);await a.publish(t,i,e),this.logger.info({plugin:this.name,event:i,exchange:t})}account(t,i){const e=this.match(t);if(!e)return;if(l(i,this.lifecycle))return;const a=this.config.twake_instance_skip_attribute;if(a&&first(this.value(i,a))?.toLowerCase()===(this.config.twake_instance_skip_value||"").toLowerCase())return;const r=first(this.value(i,"mail")),s=first(this.value(i,"uid"));if(!r||!s)return void this.logger.warn(`${this.name}: ${t} has no mail or uid, no instance`);const o=this.provider instanceof n,c=e.groups?.org||(o?this.config.twake_instance_cozy_org_id:"")||void 0,h=e.groups?.org?r.split("@")[1]:o&&this.config.twake_instance_cozy_org_domain||void 0,d=this.offers[this.patterns.findIndex(i=>i.test(t))];return{id:this.id(s,e),...o?{oidc:s}:{},twakeId:s,email:r,publicName:first(this.value(i,"displayName"))||first(this.value(i,"cn"))||s,locale:first(this.value(i,"preferredLanguage"))||this.locale,phone:first(this.value(i,"mobile")),...c?{orgId:c}:{},...h?{orgDomain:h}:{},...d?{offer:d}:{}}}match(t){return this.patterns.map(i=>i.exec(t)).find(t=>null!==t)}id(t,i){const e={uid:t,...i.groups};return(this.config.twake_instance_id||"{uid}").replace(/\{(\w+)\}/g,(t,i)=>e[i]??"").replace(/\./g,"").toLowerCase()}value(t,i){const e=i.toLowerCase(),a=Object.keys(t).find(t=>t.toLowerCase()===e);return a?t[a]:void 0}async findByMail(t,i){return(await this.server.ldap.search({paged:!1,scope:"sub",filter:`(mail=${h(t)})`},this.config.ldap_base,i)).searchEntries.find(t=>this.patterns.some(i=>i.test(t.dn)))}}export{TwakeInstances as default};
1
+ import t from"../../abstract/plugin.js";import{NotFoundError as i,BadRequestError as e}from"../../lib/errors.js";import{jsonBody as a}from"../../lib/expressFormatedResponses.js";import{CozyStackProvider as n,ClouderyProvider as r,instanceSlug as s}from"../../lib/instanceProviders.js";import{extractLdapCode as o}from"../../lib/ldapCodes.js";import{rdnValue as c,asyncHandler as h,escapeLdapFilter as d}from"../../lib/utils.js";import{lifecycleAttributes as l,isTombstone as u}from"./lifecycleAttributes.js";const first=t=>{const i=Array.isArray(t)?t[0]:t;return(null==i?"":i.toString())||void 0},g=/(^|\/)twake\/(cozy|cloudery)Provision(\.js)?$/;class TwakeInstances extends t{constructor(t){super(t),this.name="twakeInstances",this.roles=["consistency","api"],this.dependencies={rabbitmq:"core/rabbitmq"},this.offers=[],this.queues=new Map,this.announcing=new Map,this.hooks={ldapaddrequest:([t,i,e])=>{if(this.patterns.some(i=>i.test(t))||this.isOrganization(t)){const t=[this.fqdnAttribute,this.sentAttribute,this.organizationFqdn].map(t=>t.toLowerCase());for(const e of Object.keys(i))t.includes(e.toLowerCase())&&delete i[e]}return[t,i,e]},ldapaddafter:async([t,i])=>{const e=this.account(t,i);e&&(await this.serialized(t,async()=>{try{await this.provide(t,i,e)}catch(i){this.logger.error({plugin:this.name,event:"create",dn:t,error:String(i)})}}),e.orgId&&await this.announcing.get(e.orgId.toLowerCase()))},ldapdeletedone:async t=>{const i=this.provider;if(i instanceof n)for(const e of Array.isArray(t)?t:[t]){const t=this.match(e),a=/^uid=/i.test(e)?c(e):"";t&&a&&await this.serialized(e,async()=>{try{await i.destroy(this.id(a,t),a)}catch(t){this.logger.error({plugin:this.name,event:"destroy",dn:e,error:String(t)})}})}}};const i=this.config,e=(i.plugin||[]).find(t=>g.test(t));if(e)throw new Error(`${this.name}: ${e} gives accounts their instance too; load one of them`);this.patterns=(i.twake_instance_dn||[]).map(t=>new RegExp(t,"i"));const a=new Set(["uid"]);for(const t of this.patterns)for(const[,i]of t.source.matchAll(/\(\?<(\w+)>/g))a.add(i);for(const[,t]of(i.twake_instance_id||"{uid}").matchAll(/\{(\w+)\}/g))if(!a.has(t))throw new Error(`${this.name}: --twake-instance-id names {${t}}, which no --twake-instance-dn group captures`);this.lifecycle=l(i),this.fqdnAttribute=i.twake_instance_fqdn_attribute||"twakeWorkspaceUrl",this.sentAttribute=i.twake_instance_sent_attribute||"twakeCreatedEventAt";const s=i.twake_instance_timeout||3e4;if("cozy-stack"===i.twake_instance_provider){if(!i.twake_instance_cozy_url||!i.twake_instance_cozy_domain)throw new Error(`${this.name}: --twake-instance-cozy-url and --twake-instance-cozy-domain are required`);this.provider=new n(i.twake_instance_cozy_url.replace(/\/$/,""),i.twake_instance_cozy_user||"admin",i.twake_instance_cozy_passphrase||"",i.twake_instance_cozy_domain,i.twake_instance_cozy_context||"",i.twake_instance_cozy_apps||"",s),this.locale=i.twake_instance_locale||"fr"}else{if("cloudery"!==i.twake_instance_provider)throw new Error(`${this.name}: --twake-instance-provider must be cloudery or cozy-stack`);{if(!i.twake_instance_cloudery_url||!i.twake_instance_cloudery_domain)throw new Error(`${this.name}: --twake-instance-cloudery-url and --twake-instance-cloudery-domain are required`);const t=(i.twake_instance_cloudery_offer||"").split(";").map(t=>t.trim());if(t.length>1&&""===t[t.length-1]&&t.pop(),t.length>1&&t.length>this.patterns.length)throw new Error(`${this.name}: --twake-instance-cloudery-offer has more offers than --twake-instance-dn has expressions`);t.length>1&&(this.offers=t),this.provider=new r(i.twake_instance_cloudery_url.replace(/\/$/,""),i.twake_instance_cloudery_token||"",i.twake_instance_cloudery_domain,t.length>1?"":t[0],s),this.locale=i.twake_instance_locale||"en"}}}async api(t){const n=this.requirePlugin("rabbitmq");if(!n||!await n.getRawClient())throw new Error(`${this.name}: RabbitMQ is required and unreachable`);const s=await this.server.ldap.schemaIndex(),o=[this.fqdnAttribute,this.sentAttribute,...this.config.twake_instance_organization_base?[this.organizationDomain,this.organizationFqdn,this.organizationName]:[]].filter(t=>t&&!s.getAttributeType(t));if(o.length)throw new Error(`${this.name}: the directory schema defines no ${o.join(", ")}; load a schema that does, or name other attributes`);if(this.organizationFqdn&&this.organizationFqdn.toLowerCase()===this.organizationName.toLowerCase())throw new Error(`${this.name}: an organization's name and address cannot share the attribute ${this.organizationName}`);t.post(`${this.config.api_prefix}/v1/twake/instances/ensure`,h(async(t,n)=>{const r=a(t,n);if(!r)return;let s=r.dn;if(void 0===s&&"string"==typeof r.mail&&r.mail){const e=await this.findByMail(r.mail,t);if(!e)throw new i(`No account for ${r.mail}`);s=e.dn}if("string"!=typeof s||!s)throw new e("Give the account as dn or mail");n.json({state:await this.ensureInstance(s,t)})})),this.provider instanceof r&&await n.subscribe(this.config.twake_instance_auth_exchange||"auth",this.config.twake_instance_workplace_created_key||"workplace.created",this.config.twake_instance_queue||"workplace.created.ldap-rest",t=>this.onWorkplaceCreated(t))}async ensureInstance(t,a){let n;const r=await this.serialized(t,async()=>{const r=await this.read(t,a);if(!r)throw new i(`No entry at ${t}`);const s=this.account(t,r);if(!s)throw new e(`${t} gets no instance`);if(n=s.orgId,first(this.value(r,this.sentAttribute)))return"ready";a&&await this.server.ldap.modify(t,{replace:{[this.sentAttribute]:[]}},a);const o=first(this.value(r,this.fqdnAttribute));if(o){const i=o.toLowerCase()===this.provider.address(s)?{fqdn:o,ready:!0}:await this.provider.find(s,o);if(i?.ready)return await this.ready(t,r,s,i.fqdn,a),"ready"}return this.provide(t,r,s,a)});return n&&await this.announcing.get(n.toLowerCase()),r}async ensureOrganization(t){return this.serialized(`organization:${t.id}`,async()=>{const e=await this.findOrganization(t.id);if(!e)throw new i(`No organization ${t.id}`);t.name&&!first(this.value(e,this.organizationName))&&(await this.server.ldap.modify(e.dn,{replace:{[this.organizationName]:t.name}}),e[this.organizationName]=t.name);const a=this.organizationFqdn&&first(this.value(e,this.organizationFqdn)),n=this.organizationRequest(t),s=a?{fqdn:a,ready:!0}:await this.provider.find(n);if(first(this.value(e,this.sentAttribute)))return s?.ready?await this.organizationReady(e,s.fqdn):await this.releaseMembers(e.dn),"ready";let o=s?.ready&&s.fqdn;return s||(this.provider instanceof r&&await this.provider.createOrganization(t),o=await this.provider.create(n)),o?(await this.organizationReady(e,o),"ready"):"pending"})}organizationRequest(t){return{id:t.id,email:this.organizationMail(t.id,t.domain),publicName:t.name,locale:this.locale,orgId:t.id,orgDomain:t.domain,offer:this.config.twake_instance_cloudery_organization_offer,organization:!0}}async requestOrganization(t,i){if(!(this.provider instanceof r))return;const e=await this.findOrganization(t);if(!e||first(this.value(e,this.sentAttribute)))return;const a=first(this.value(e,this.organizationName)),n=first(this.value(e,this.organizationDomain));if(!a||!n)return void this.logger.warn(`${this.name}: organization ${t} has no ${this.organizationName} or ${this.organizationDomain}, so its instance cannot be asked for`);const s=this.organizationRequest({id:t,name:a,domain:n}),o=await this.provider.find(s);if(o){const r=t.toLowerCase();if(!o.ready||this.announcing.has(r))return;return i&&await this.server.ldap.modify(e.dn,{},i),void(this.announcing.has(r)||this.announcing.set(r,this.ensureOrganization({id:t,name:a,domain:n}).catch(i=>{this.logger.error({plugin:this.name,event:"organization",id:t,error:String(i)})}).finally(()=>this.announcing.delete(r))))}await this.provider.createOrganization({id:t,name:a,domain:n});try{await this.provider.create(s)}catch(t){if(!await this.provider.find(s))throw t}}async onWorkplaceCreated(t){const{internalEmail:i,workplaceFqdn:e}=t;if(!i||!e)return;const a=t.twakeId&&await this.findOrganization(t.twakeId);if(a){const n=t.twakeId.toLowerCase(),r=first(this.value(a,this.organizationDomain)),s=i.toLowerCase();if(r?this.organizationMail(n,r)===s:s.startsWith(`${n}@`))return this.serialized(`organization:${t.twakeId}`,async()=>{const t=await this.read(a.dn);t&&await this.organizationReady(t,e)})}const n=await this.findByMail(i);n?await this.settle(n.dn,e):this.logger.debug(`${this.name}: no account here for ${i}`)}async settle(t,i){await this.serialized(t,async()=>{const e=await this.read(t),a=e&&this.account(t,e);e&&a&&(first(this.value(e,this.sentAttribute))||await this.ready(t,e,a,i))})}async provide(t,i,e,a){e.orgId&&await this.requestOrganization(e.orgId,a);const n=await this.provider.find(e),r=n?n.ready&&n.fqdn:await this.provider.create(e);return r?(await this.ready(t,i,e,r,a),"ready"):"pending"}async ready(t,i,e,a,n){const s={declaredClasses:this.config.user_class};if(first(this.value(i,this.fqdnAttribute))!==a&&await this.server.ldap.modify(t,{replace:{[this.fqdnAttribute]:a}},n,s),first(this.value(i,this.sentAttribute)))return;const o=this.provider instanceof r&&e.orgId&&await this.findOrganization(e.orgId);if(o&&!first(this.value(o,this.sentAttribute))){if(t.toLowerCase().endsWith(`,${o.dn.toLowerCase()}`))return void this.logger.info(`${this.name}: ${t} waits for organization ${e.orgId}`);this.logger.warn(`${this.name}: ${t} is not below its organization entry, so it is announced before organization ${e.orgId} is linked; clear its ${this.sentAttribute} and send it again once the organization is announced`)}await this.announce(this.config.twake_instance_auth_exchange||"auth",this.config.twake_instance_user_created_key||"user.created",{twakeId:e.twakeId,internalEmail:e.email,workplaceFqdn:a,...e.orgId?{organizationId:e.orgId}:{},...e.orgDomain?{domain:e.orgDomain,organizationDomain:e.orgDomain}:{},...e.phone?{mobile:e.phone}:{}}),await this.server.ldap.modify(t,{replace:{[this.sentAttribute]:(new Date).toISOString()}},n,s)}async organizationReady(t,i){const e=t.dn,a=first(this.value(t,this.organizationId));if(a&&this.provider instanceof r&&await this.provider.linkOrganization(a,i),first(this.value(t,this.sentAttribute)))return this.releaseMembers(e);this.organizationFqdn&&!first(this.value(t,this.organizationFqdn))&&await this.server.ldap.modify(e,{replace:{[this.organizationFqdn]:i}});const n=first(this.value(t,this.organizationDomain)),s=first(this.value(t,this.organizationName));await this.announce(this.config.twake_instance_b2b_exchange||"b2b",this.config.twake_instance_organization_created_key||"organization.created",{organizationId:a,workplaceFqdn:i,...s?{organization:s}:{},...n?{domain:n}:{}}),await this.server.ldap.modify(e,{replace:{[this.sentAttribute]:(new Date).toISOString()}}),await this.releaseMembers(e)}async releaseMembers(t){const i=await this.server.ldap.search({paged:!1,scope:"sub",filter:`(&(${this.fqdnAttribute}=*)(!(${this.sentAttribute}=*)))`},t);let e=0;for(const t of i.searchEntries)try{await this.release(t)}catch(i){e++,this.logger.error(`${this.name}: ${t.dn} not released: ${String(i)}`)}if(e)throw new Error(`${this.name}: ${e} member(s) of ${t} not released`)}async release(t){const i=t.dn,e=this.account(i,t),a=first(this.value(t,this.fqdnAttribute));if(!e||!a)return;const n=a.toLowerCase()===this.provider.address(e)?{fqdn:a,ready:!0}:await this.provider.find(e,a);if(n?.ready)return this.settle(i,n.fqdn);this.logger.warn(`${this.name}: ${i} stays held, its address ${a} is not confirmed; ensureInstance sends it once it is`)}async serialized(t,i){const e=t.toLowerCase(),a=(this.queues.get(e)??Promise.resolve()).catch(()=>{}).then(i),n=a.catch(()=>{});return this.queues.set(e,n),n.then(()=>{this.queues.get(e)===n&&this.queues.delete(e)}),a}get organizationId(){return this.config.twake_instance_organization_id_attribute||"ou"}get organizationDomain(){return this.config.twake_instance_organization_domain_attribute||"twakeDomain"}get organizationName(){return this.config.twake_instance_organization_name_attribute||"description"}get organizationFqdn(){return this.config.twake_instance_organization_fqdn_attribute||""}organizationMail(t,i){return`${t}@${i}`.toLowerCase()}isOrganization(t){const i=this.config.twake_instance_organization_base;if(!i)return!1;return t.slice(t.indexOf(",")+1).toLowerCase()===i.toLowerCase()}async findOrganization(t){const i=this.config.twake_instance_organization_base;if(!i)return;return(await this.server.ldap.search({paged:!1,scope:"one",filter:`(${this.organizationId}=${d(t)})`},i)).searchEntries[0]}async read(t,i){try{return(await this.server.ldap.search({paged:!1,scope:"base"},t,i)).searchEntries[0]}catch(i){const a=o(i);if(32===a)return;if(34===a)throw new e(`Invalid DN: ${t}`);throw i}}async announce(t,i,e){const a=this.requirePlugin("rabbitmq");if(!a||!await a.getRawClient())throw new Error(`${this.name}: RabbitMQ unreachable, ${i} not sent`);await a.publish(t,i,e),this.logger.info({plugin:this.name,event:i,exchange:t})}account(t,i){const e=this.match(t);if(!e)return;if(u(i,this.lifecycle))return;const a=this.config.twake_instance_skip_attribute;if(a&&first(this.value(i,a))?.toLowerCase()===(this.config.twake_instance_skip_value||"").toLowerCase())return;const r=first(this.value(i,"mail")),s=first(this.value(i,"uid"));if(!r||!s)return void this.logger.warn(`${this.name}: ${t} has no mail or uid, no instance`);const o=this.provider instanceof n,c=e.groups?.org||(o?this.config.twake_instance_cozy_org_id:"")||void 0,h=e.groups?.org?r.split("@")[1]:o&&this.config.twake_instance_cozy_org_domain||void 0,d=this.offers[this.patterns.findIndex(i=>i.test(t))],l=this.id(s,e);if(l)return{id:l,...o?{oidc:s}:{},twakeId:s,email:r,publicName:first(this.value(i,"displayName"))||first(this.value(i,"cn"))||s,locale:first(this.value(i,"preferredLanguage"))||this.locale,phone:first(this.value(i,"mobile")),...c?{orgId:c}:{},...h?{orgDomain:h}:{},...d?{offer:d}:{}};this.logger.warn(`${this.name}: ${t} gives no instance slug, no instance`)}match(t){return this.patterns.map(i=>i.exec(t)).find(t=>null!==t)}id(t,i){const e={uid:t,...i.groups};return s((this.config.twake_instance_id||"{uid}").replace(/\{(\w+)\}/g,(t,i)=>e[i]??""))}value(t,i){const e=i.toLowerCase(),a=Object.keys(t).find(t=>t.toLowerCase()===e);return a?t[a]:void 0}async findByMail(t,i){return(await this.server.ldap.search({paged:!1,scope:"sub",filter:`(mail=${d(t)})`},this.config.ldap_base,i)).searchEntries.find(t=>this.patterns.some(i=>i.test(t.dn)))}}export{TwakeInstances as default};
@@ -11,13 +11,18 @@ export declare class GroupApiClient {
11
11
  }>;
12
12
  getGroups(orgDn: string): Promise<LdapGroup[]>;
13
13
  getGroup(dn: string): Promise<LdapGroup>;
14
- updateGroup(dn: string, data: Partial<LdapGroup>): Promise<void>;
14
+ /**
15
+ * Replace attributes of an entry.
16
+ *
17
+ * @param dn entry to change
18
+ * @param replace attributes to replace; see `changedAttributes`
19
+ */
20
+ updateGroup(dn: string, replace: Record<string, unknown>): Promise<void>;
15
21
  createEntry(dn: string, data: Record<string, unknown>): Promise<void>;
16
22
  deleteEntry(dn: string): Promise<void>;
23
+ /** Add members through the route that checks them */
24
+ addMembers(dn: string, member: string[]): Promise<void>;
25
+ removeMember(dn: string, member: string): Promise<void>;
17
26
  moveGroup(cn: string, targetOrgDn: string): Promise<void>;
18
27
  renameGroup(cn: string, newCn: string): Promise<void>;
19
- getPointerOptions(branch: string): Promise<Array<{
20
- dn: string;
21
- label: string;
22
- }>>;
23
28
  }
@@ -19,9 +19,17 @@ export interface Config {
19
19
  schemaUrl?: string;
20
20
  schema?: SchemaDefinition;
21
21
  };
22
+ ldapOrganizations?: {
23
+ linkAttribute?: string;
24
+ pathAttribute?: string;
25
+ };
22
26
  };
23
27
  [key: string]: unknown;
24
28
  }
29
+ /** An entry as the API returns it */
30
+ export type Entry = {
31
+ dn: string;
32
+ } & Record<string, unknown>;
25
33
  export interface ResourceConfig {
26
34
  name: string;
27
35
  pluralName?: string;
@@ -22,7 +22,13 @@ export declare class ResourceApiClient {
22
22
  getSchema(schemaUrl: string): Promise<Schema>;
23
23
  getResources(search?: string): Promise<LdapResource[]>;
24
24
  getResource(dn: string): Promise<LdapResource>;
25
- updateResource(dn: string, data: Partial<LdapResource>): Promise<LdapResource>;
25
+ /**
26
+ * Replace attributes of an entry.
27
+ *
28
+ * @param dn entry to change
29
+ * @param replace attributes to replace; see `changedAttributes`
30
+ */
31
+ updateResource(dn: string, replace: Record<string, unknown>): Promise<LdapResource>;
26
32
  createResource(data: Partial<LdapResource>): Promise<LdapResource>;
27
33
  deleteResource(dn: string): Promise<void>;
28
34
  /**
@@ -10,13 +10,15 @@ export declare class UnitApiClient {
10
10
  dn: string;
11
11
  }>;
12
12
  getUnit(dn: string): Promise<LdapUnit>;
13
- updateUnit(dn: string, data: Partial<LdapUnit>): Promise<void>;
13
+ /**
14
+ * Replace attributes of an entry.
15
+ *
16
+ * @param dn entry to change
17
+ * @param replace attributes to replace; see `changedAttributes`
18
+ */
19
+ updateUnit(dn: string, replace: Record<string, unknown>): Promise<void>;
14
20
  createEntry(dn: string, data: Record<string, unknown>): Promise<void>;
15
21
  deleteEntry(dn: string): Promise<void>;
16
- getPointerOptions(branch: string): Promise<Array<{
17
- dn: string;
18
- label: string;
19
- }>>;
20
22
  moveUnit(dn: string, targetOrgDn: string): Promise<{
21
23
  success: boolean;
22
24
  newDn?: string;
@@ -25,6 +25,8 @@ export declare class LdapUserEditor {
25
25
  destroy(): void;
26
26
  createUser(userData: Partial<LdapUser>): Promise<LdapUser>;
27
27
  deleteUser(dn: string): Promise<void>;
28
+ /** Clear the selection and refresh the list once `dn` is deleted */
29
+ private userDeleted;
28
30
  getConfig(): Config | null;
29
31
  getApi(): UserApiClient;
30
32
  getCurrentOrgDn(): string | null;
@@ -20,7 +20,13 @@ export declare class UserApiClient {
20
20
  getSchema(schemaUrl: string): Promise<Schema>;
21
21
  getUsers(search?: string): Promise<LdapUser[]>;
22
22
  getUser(dn: string): Promise<LdapUser>;
23
- updateUser(dn: string, data: Partial<LdapUser>): Promise<LdapUser>;
23
+ /**
24
+ * Replace attributes of an entry.
25
+ *
26
+ * @param dn entry to change
27
+ * @param replace attributes to replace; see `changedAttributes`
28
+ */
29
+ updateUser(dn: string, replace: Record<string, unknown>): Promise<LdapUser>;
24
30
  createUser(data: Partial<LdapUser>): Promise<LdapUser>;
25
31
  deleteUser(dn: string): Promise<void>;
26
32
  moveUser(dn: string, targetOrgDn: string): Promise<{
@@ -26,3 +26,33 @@ export declare function collectPointerArrayValues(container: HTMLElement): Recor
26
26
  * Group schema fields by category
27
27
  */
28
28
  export declare function groupSchemaFields(schema: Record<string, SchemaAttribute>, categorizer?: (fieldName: string, attribute: SchemaAttribute) => string | null): Map<string, [string, SchemaAttribute][]>;
29
+ /**
30
+ * A value as the list it stands for: the API gives an attribute holding one
31
+ * value as that value, not as a one-element list.
32
+ *
33
+ * @param value attribute value as the API gives it
34
+ * @returns its values as text
35
+ */
36
+ export declare const listValues: (value: unknown) => string[];
37
+ /**
38
+ * The values to add and remove to go from `before` to `after`.
39
+ *
40
+ * @param before values as loaded
41
+ * @param after values as edited
42
+ * @returns what was added, and what was removed
43
+ */
44
+ export declare function listChanges(before: unknown, after: unknown): {
45
+ added: string[];
46
+ removed: string[];
47
+ };
48
+ /**
49
+ * The attributes of `edited` whose value differs from `original`, as the
50
+ * `replace` of a modify request: the routes take only `add`, `replace` and
51
+ * `delete`, and refuse a computed attribute even sent unchanged. One emptied
52
+ * in the form is replaced with no value, which clears it.
53
+ *
54
+ * @param original entry as loaded
55
+ * @param edited attributes as the form holds them
56
+ * @returns the attributes to replace, empty when nothing changed
57
+ */
58
+ export declare function changedAttributes(original: Record<string, unknown> | null | undefined, edited: Record<string, unknown>): Record<string, unknown>;
@@ -294,6 +294,7 @@ export interface Config {
294
294
  trash_add_metadata?: string;
295
295
  trash_auto_create?: string;
296
296
  ppolicy_default_dn?: string;
297
+ ppolicy_base?: string;
297
298
  ppolicy_warn_days?: number;
298
299
  ppolicy_validate_complexity?: boolean;
299
300
  ppolicy_min_length?: number;
@@ -1,4 +1,4 @@
1
- import type { AttributeValue } from '../lib/ldapActions';
1
+ import type { AttributesList, AttributeValue, ModifyRequest } from '../lib/ldapActions';
2
2
  /**
3
3
  * Scalar and container types an entity schema may declare.
4
4
  *
@@ -317,6 +317,35 @@ export declare function modifiedAttributeNames(body: {
317
317
  replace?: object;
318
318
  delete?: string[] | object;
319
319
  }): string[];
320
+ /**
321
+ * The text the directory stores for a JSON value. ldapts throws on anything
322
+ * but a string or a Buffer, which reached the client as a 500: a number is
323
+ * written as text where the schema declares one, a boolean as LDAP's
324
+ * `TRUE` / `FALSE`, and anything else is refused.
325
+ *
326
+ * @param schema schema of the entry, if any
327
+ * @param field attribute name
328
+ * @param value value as the client sent it
329
+ * @returns the value to write
330
+ * @throws BadRequestError when the value cannot be written as text
331
+ */
332
+ export declare function toDirectoryValue(schema: Schema | undefined, field: string, value: AttributeValue): AttributeValue;
333
+ /**
334
+ * {@link toDirectoryValue} on every value of an entry about to be created,
335
+ * in place.
336
+ *
337
+ * @param schema schema of the entry, if any
338
+ * @param entry attributes sent by the client
339
+ */
340
+ export declare function toDirectoryEntry(schema: Schema | undefined, entry: AttributesList): void;
341
+ /**
342
+ * {@link toDirectoryValue} on every value a modify request carries, in place:
343
+ * added, replaced, and deleted when `delete` names values.
344
+ *
345
+ * @param schema schema of the entry, if any
346
+ * @param changes modify request
347
+ */
348
+ export declare function toDirectoryChanges(schema: Schema | undefined, changes: ModifyRequest): void;
320
349
  /**
321
350
  * Lowercased names of the attributes marked `neverReturn` in any of the given
322
351
  * schemas.
@@ -6,6 +6,17 @@
6
6
  * admin API, which builds it before answering.
7
7
  */
8
8
  import { type RequestInit, type Response } from 'node-fetch';
9
+ /**
10
+ * Reduce a name to what the Cloudery takes in a slug; it refused `_` or `@`
11
+ * with "Slug is invalid". `-` is kept, except at either end where no host
12
+ * name takes it: an instance already created with one would otherwise change
13
+ * address. Distinct uids can reduce to one slug, so a creation first checks
14
+ * that an instance already there is the account's own.
15
+ *
16
+ * @param value uid, or the slug template filled in
17
+ * @returns the slug, empty when nothing of `value` is kept
18
+ */
19
+ export declare const instanceSlug: (value: string) => string;
9
20
  export interface InstanceRequest {
10
21
  /** Slug of the instance, and its OIDC id unless `oidc` says otherwise */
11
22
  id: string;
@@ -36,6 +36,15 @@ export interface ModifyRequest {
36
36
  replace?: AttributesList;
37
37
  delete?: string[] | AttributesList;
38
38
  }
39
+ /**
40
+ * Refuse a modify body a client got wrong: any other key was ignored, and a
41
+ * body naming no attribute in `add`, `replace` or `delete` answered success
42
+ * without changing anything.
43
+ *
44
+ * @param body parsed request body
45
+ * @throws BadRequestError naming what is wrong
46
+ */
47
+ export declare function assertModifyRequest(body: object): asserts body is ModifyRequest;
39
48
  export interface ModifyOptions {
40
49
  /**
41
50
  * Send the permissive modify control, not critical: a directory that
@@ -48,6 +57,13 @@ export interface ModifyOptions {
48
57
  * behalf of a request but not authorized as it.
49
58
  */
50
59
  context?: ChangeContext;
60
+ /**
61
+ * Object classes the entry may be given besides those declared for its
62
+ * branch, for a writer that knows what kind of entry it writes: an account
63
+ * another tool created may lack the classes the write needs, see
64
+ * ldapActions.classRepair.
65
+ */
66
+ declaredClasses?: string[];
51
67
  }
52
68
  declare class ldapActions {
53
69
  config: Config;
@@ -196,8 +212,8 @@ declare class ldapActions {
196
212
  /**
197
213
  * The auxiliary classes a modify needs added: for each attribute it writes
198
214
  * that the entry's classes do not allow, a class declared for its branch
199
- * that allows it, provided the entry holds or receives that class's
200
- * mandatory attributes once the change is applied.
215
+ * or by the writer that allows it, provided the entry holds or receives
216
+ * that class's mandatory attributes once the change is applied.
201
217
  *
202
218
  * Only what the write needs is added. Entities sharing a branch declare
203
219
  * classes that are not all meant for every entry: `shadowAccount` is not
@@ -209,7 +225,9 @@ declare class ldapActions {
209
225
  *
210
226
  * @param dn entry being modified
211
227
  * @param changes the modification, after the hooks
212
- * @returns names of the classes to add
228
+ * @param extra classes the writer declares, see {@link ModifyOptions}
229
+ * @returns names of the classes to add, or `allowed` when the entry's
230
+ * classes take the change as it is
213
231
  */
214
232
  private classRepair;
215
233
  /**
@@ -51,6 +51,8 @@ export default class LdapAccountLifecycle extends DmPlugin {
51
51
  * @param entity flat entity holding the account
52
52
  * @param id identifier or DN of the entry
53
53
  * @param changes modification to apply
54
+ * @param req request this comes from: without it, every authorization
55
+ * plugin built on `AuthzBase` skips its branch check
54
56
  */
55
57
  private modify;
56
58
  }