ldap-rest 0.26.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/abstract/twakePlugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/authz/base.js +1 -1
- package/dist/lib/authz/composition.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/mailDomains.js +1 -0
- package/dist/lib/requestContext.js +1 -0
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/auth/authzDynamic.js +1 -1
- package/dist/plugins/auth/authzLinid1.js +1 -1
- package/dist/plugins/auth/authzPerBranch.js +1 -1
- package/dist/plugins/auth/authzScope.js +1 -1
- package/dist/plugins/auth/openidconnect.js +1 -1
- package/dist/plugins/bcl.js +1 -1
- package/dist/plugins/configApi.js +1 -1
- package/dist/plugins/ldap/bulkImport.js +1 -1
- package/dist/plugins/ldap/departmentSync.js +1 -1
- package/dist/plugins/ldap/enterpriseRules.js +1 -1
- package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/onChange.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/ldap/passwordPolicy.js +1 -1
- package/dist/plugins/ldap/raw.js +1 -1
- package/dist/plugins/ldap/trash.js +1 -1
- package/dist/plugins/rabbitmq.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/scim/users.js +1 -1
- package/dist/plugins/storage/ldap.js +1 -1
- package/dist/plugins/storage/postgres.js +1 -1
- package/dist/plugins/storage/valkey.js +1 -1
- package/dist/plugins/twake/appAccountsApi.js +1 -1
- package/dist/plugins/twake/appAccountsConsistency.js +1 -1
- package/dist/plugins/twake/clouderyProvision.js +1 -1
- package/dist/plugins/twake/groups.js +1 -1
- package/dist/plugins/twake/instances.js +1 -1
- package/dist/plugins/twake/james.js +1 -1
- package/dist/plugins/twake/lifecycleEvents.js +1 -1
- package/dist/plugins/twake/spaces.js +1 -1
- package/dist/plugins/twake/tombstone.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +25 -6
- package/dist/src/abstract/plugin.d.ts +1 -1
- package/dist/src/bin/index.d.ts +12 -0
- package/dist/src/hooks.d.ts +12 -3
- package/dist/src/lib/authz/base.d.ts +87 -4
- package/dist/src/lib/authz/composition.d.ts +21 -0
- package/dist/src/lib/ldapActions.d.ts +139 -18
- package/dist/src/lib/mailDomains.d.ts +41 -0
- package/dist/src/lib/requestContext.d.ts +23 -0
- package/dist/src/lib/utils.d.ts +41 -2
- package/dist/src/plugins/auth/authzDynamic.d.ts +5 -12
- package/dist/src/plugins/auth/authzLinid1.d.ts +3 -1
- package/dist/src/plugins/auth/authzPerBranch.d.ts +5 -9
- package/dist/src/plugins/auth/openidconnect.d.ts +47 -0
- package/dist/src/plugins/bcl.d.ts +2 -1
- package/dist/src/plugins/ldap/accountLifecycle.d.ts +2 -2
- package/dist/src/plugins/ldap/bulkImport.d.ts +30 -5
- package/dist/src/plugins/ldap/enterpriseRules.d.ts +11 -1
- package/dist/src/plugins/ldap/externalUsersInGroups.d.ts +20 -3
- package/dist/src/plugins/ldap/groups.d.ts +48 -1
- package/dist/src/plugins/ldap/organizations.d.ts +4 -4
- package/dist/src/plugins/twake/appAccountsApi.d.ts +3 -67
- package/dist/src/plugins/twake/groups.d.ts +46 -2
- package/dist/src/plugins/twake/james.d.ts +0 -11
- package/docs/README.md +25 -0
- package/docs/plugin-development/README.md +61 -15
- package/docs/plugin-development/dependencies.md +8 -2
- package/docs/plugin-development/hooks.md +4 -5
- package/docs/usage/configuration.md +11 -8
- package/docs/usage/plugins/auth/README.md +22 -3
- package/docs/usage/plugins/auth/authz-linid1.md +14 -5
- package/docs/usage/plugins/auth/authz-per-branch.md +60 -9
- package/docs/usage/plugins/auth/back-channel-logout.md +28 -0
- package/docs/usage/plugins/auth/oidc.md +8 -0
- package/docs/usage/plugins/integrations/app-accounts.md +5 -0
- package/docs/usage/plugins/integrations/groups.md +36 -3
- package/docs/usage/plugins/integrations/scim.md +7 -0
- package/docs/usage/plugins/integrations/spaces.md +3 -1
- package/docs/usage/plugins/ldap/bulk-import.md +20 -0
- package/docs/usage/plugins/ldap/enterprise-rules.md +12 -2
- package/docs/usage/plugins/ldap/external-users.md +8 -3
- package/docs/usage/plugins/ldap/flat-generic.md +3 -0
- package/docs/usage/plugins/ldap/groups.md +6 -0
- package/docs/usage/plugins/ldap/organizations.md +17 -0
- package/docs/usage/plugins/ldap/raw.md +1 -1
- package/docs/usage/upgrading.md +228 -0
- package/package.json +1 -1
- package/static/schemas/example/groups.json +1 -0
- package/static/schemas/example/organizations.json +1 -0
- package/static/schemas/example/users.json +3 -0
- package/static/schemas/twake/groups.json +2 -0
- package/static/schemas/twake/organizations.json +1 -0
- package/static/schemas/twake/teamMailboxes.json +3 -1
- package/static/schemas/twake/users.json +3 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isDeepStrictEqual as e}from"node:util";import t from"../../abstract/plugin.js";import{asyncHandler as i,normalizeDn as s,escapeLdapFilter as a,validateDnValue as r,escapeDnValue as o}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"],h={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"},w=["pwdCheckModuleArg","pwdUseCheckModule"],y=[...Object.keys(h),"pwdAttribute","pwdCheckModule"],isWritablePolicyAttribute=e=>Object.prototype.hasOwnProperty.call(h,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 t={};for(const i of y){const s=values(e[i]);if(0!==s.length)switch(h[i]){case"integer":{const e=parseInt(s[0],10);isNaN(e)||(t[i]=e);break}case"boolean":t[i]="TRUE"===s[0];break;default:t[i]=1===s.length?s[0]:s}}return t},g=/[!@#$%^&*()_+\-=[\]{}|;:'",.<>?/]/;class PasswordPolicy extends t{constructor(){super(...arguments),this.name="ldapPasswordPolicy",this.roles=["api","configurable"],this.policyCache=new Map,this.discoveredDefault=null,this.warnedAmbiguousDefault=!1,this.warnedMissingBase=!1,this.warnedUnreadableSchema=!1,this.POLICY_CACHE_TTL=6e4}api(e){const t=`${this.config.api_prefix}/v1`;e.get(`${t}/password-policy`,i(async(e,t)=>{const i=await this.readPolicyConfig();if(i.dn)try{await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:["1.1"],paged:!1},i.dn,e)}catch(e){if(32!==c(e))throw e;return this.forgetPolicies(),void t.json({})}t.json(i)})),e.get(`${t}/users/:id/password-status`,i(async(e,t)=>{const i=this.param(e,"id"),s=await this.getPasswordStatus(i,e);t.json(s)})),e.post(`${t}/users/:id/unlock`,i(async(e,t)=>{const i=this.param(e,"id");await this.unlockAccount(i,e),t.json({success:!0,message:"Account unlocked"})})),e.get(`${t}/password-policy/expiring-soon`,i(async(e,t)=>{const i=parseInt(e.query.days)||this.config.ppolicy_warn_days||14,s=await this.findExpiringPasswords(i,e);t.json({warningDays:i,users:s})})),e.get(`${t}/password-policy/locked-accounts`,i(async(e,t)=>{const i=await this.findLockedAccounts(e);t.json({accounts:i})})),e.get(`${t}/password-policies`,i(async(e,t)=>{t.json({policies:await this.listPolicies(e),attributes:await this.definedAttributes()})})),e.post(`${t}/password-policies`,i(async(e,t)=>{const i=n(e,t);if(!i)return;const s=await this.createPolicy(i,e);t.status(201).json(s)})),e.get(`${t}/password-policies/:cn`,i(async(e,t)=>{const i=await this.findPolicy(this.param(e,"cn"),e);t.json(await this.toPolicyRecord(i))})),e.patch(`${t}/password-policies/:cn`,i(async(e,t)=>{const i=n(e,t);if(!i)return;const s=await this.updatePolicy(this.param(e,"cn"),i,e);t.json(s)})),e.delete(`${t}/password-policies/:cn`,i(async(e,t)=>{await this.deletePolicy(this.param(e,"cn"),e),t.json({success:!0})})),e.put(`${t}/users/:id/password-policy`,i(async(e,t)=>{const i=n(e,t);if(!i)return;const{policy:s}=i;if(null!==s&&("string"!=typeof s||!s))throw new p("policy must be a policy name or null");t.json(await this.assignPolicy(this.param(e,"id"),s,e))})),this.config.ppolicy_validate_complexity&&e.post(`${t}/password/validate`,i(async(e,t)=>{const i=n(e,t);if(!i)return;const{password:s}=i;if(!s)throw new p("password required");const a=this.validateComplexity(s);t.json(a)}))}async getPasswordStatus(e,t){const i=this.resolveUserDn(e),s=await this.readUser(i,e,["*",...u],t),a=await this.policyFor(s);return this.calculateStatus(i,s,a)}async readUser(e,t,i,s){try{const a=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:i,paged:!1},e,s);if(!a.searchEntries||0===a.searchEntries.length)throw new l(`User not found: ${t}`);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: ${t}`);throw e}}calculateStatus(e,t,i){const s=Date.now(),a=this.parseGeneralizedTime(t.pwdChangedTime);let r=null,o=!1,n=null;i.pwdMaxAge&&a&&(r=new Date(a.getTime()+1e3*i.pwdMaxAge),o=s>r.getTime(),n=Math.ceil((r.getTime()-s)/864e5));let c=!1;const l=this.config.ppolicy_warn_days||14;null!==n&&n>0&&(c=n<=l);const d=t.pwdAccountLockedTime;let p=!!d;if(p&&!("000001010000Z"===d)&&i.pwdLockoutDuration){const e=this.parseGeneralizedTime(d);if(e){s>new Date(e.getTime()+1e3*i.pwdLockoutDuration).getTime()&&(p=!1)}}const u=t.pwdFailureTime,h=Array.isArray(u)?u.length:0,w=t.pwdGraceUseTime,y=Array.isArray(w)?w.length:0,g="TRUE"===t.pwdReset;return{dn:e,passwordSet:!!t.userPassword,lastChanged:a?.toISOString()||null,expiresAt:r?.toISOString()||null,daysUntilExpiration:n,isExpired:o,isExpiringSoon:c,mustChange:g,isLocked:p,lockedAt:d&&this.parseGeneralizedTime(d)?.toISOString()||null,failureCount:h,graceLoginsUsed:y,policy:i.dn??null}}async unlockAccount(e,t){const i=this.resolveUserDn(e),s=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:["pwdAccountLockedTime","pwdFailureTime"],paged:!1},i,t);if(!s.searchEntries||0===s.searchEntries.length)throw new l(`User not found: ${e}`);const a=userEntry(s.searchEntries[0]),r={};a.pwdAccountLockedTime&&(r.pwdAccountLockedTime=[]),a.pwdFailureTime?.length&&(r.pwdFailureTime=[]),Object.keys(r).length>0?(await this.server.ldap.modify(i,{delete:r},t),this.logger.info(`Account unlocked: ${i}`)):this.logger.info(`Account was not locked: ${i}`)}async findExpiringPasswords(e,t){const i=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},i,t),a=Date.now(),r=24*e*60*60*1e3,o=[];for(const e of s.searchEntries){const t=userEntry(e),i=this.parseGeneralizedTime(t.pwdChangedTime);if(!i)continue;const s=await this.policyFor(t);if(!s.pwdMaxAge)continue;const n=new Date(i.getTime()+1e3*s.pwdMaxAge),c=n.getTime()-a;c>0&&c<r&&o.push({dn:t.dn,uid:t.uid,displayName:t.displayName||t.cn,mail:t.mail,expiresAt:n.toISOString(),daysUntilExpiration:Math.ceil(c/864e5)})}return o.sort((e,t)=>e.daysUntilExpiration-t.daysUntilExpiration)}async findLockedAccounts(e){const t=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},t,e)).searchEntries.map(e=>{const t=userEntry(e);return{dn:t.dn,uid:t.uid,displayName:t.displayName||t.cn,mail:t.mail,lockedAt:this.parseGeneralizedTime(t.pwdAccountLockedTime)?.toISOString(),failureCount:Array.isArray(t.pwdFailureTime)?t.pwdFailureTime.length:0}})}async policyFor(e){if(e.pwdPolicySubentry){const t=await this.readPolicyAt(e.pwdPolicySubentry);if(t)return t}return this.readPolicyConfig()}async readPolicyConfig(){const e=this.config.ppolicy_default_dn||await this.discoverDefaultDn();return(e?await this.readPolicyAt(e):null)??{}}async discoverDefaultDn(){if(this.discoveredDefault&&Date.now()-this.discoveredDefault.time<this.POLICY_CACHE_TTL)return this.discoveredDefault.dn;let e=null;try{const t=(await this.server.ldap.search({filter:"(objectClass=pwdPolicy)",scope:"sub",attributes:["1.1"],paged:!1},this.policyBase())).searchEntries.map(e=>e.dn);1===t.length?e=t[0]:t.length>1&&!this.warnedAmbiguousDefault&&(this.warnedAmbiguousDefault=!0,this.logger.warn(`${t.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 t=s(e),i=this.policyCache.get(t);if(i&&Date.now()-i.time<this.POLICY_CACHE_TTL)return i.policy;let a,r=null;try{const t=await this.server.ldap.search({filter:"(objectClass=pwdPolicy)",scope:"base",attributes:y,paged:!1},e);t.searchEntries.length>0&&(r=((e,t)=>{const i=typedPolicy(e);return{dn:t,pwdMaxAge:i.pwdMaxAge,pwdMinAge:i.pwdMinAge,pwdInHistory:i.pwdInHistory,pwdCheckQuality:i.pwdCheckQuality,pwdMinLength:i.pwdMinLength,pwdMaxFailure:i.pwdMaxFailure,pwdLockout:!0===i.pwdLockout,pwdLockoutDuration:i.pwdLockoutDuration,pwdGraceAuthNLimit:i.pwdGraceAuthNLimit,pwdExpireWarning:i.pwdExpireWarning,pwdMustChange:!0===i.pwdMustChange,pwdAllowUserChange:!1!==i.pwdAllowUserChange}})(t.searchEntries[0],e))}catch(e){a=String(e)}const o=this.config.ppolicy_default_dn;return!r&&o&&s(o)===t?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(t,{policy:r,time:Date.now()}),r}forgetPolicies(){this.policyCache.clear(),this.discoveredDefault=null}policyBase(){return this.config.ppolicy_base||this.config.ldap_base}async searchPolicies(e,t){try{return(await this.server.ldap.search({filter:`(&(objectClass=pwdPolicy)${e})`,scope:"sub",attributes:["objectClass","cn",...y],paged:!1},this.policyBase(),t)).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,t){const i=await this.searchPolicies(`(cn=${a(e)})`,t);if(0===i.length)throw new l(`No password policy named ${e}`);if(i.length>1)throw new d(`Several password policies are named ${e}`);return i[0]}async listPolicies(e){const t=[];for(const i of await this.searchPolicies("",e))t.push(await this.toPolicyRecord(i));return t.sort((e,t)=>e.cn.localeCompare(t.cn))}async definedAttributes(){const e=Object.keys(h);try{const t=await this.server.ldap.schemaIndex();return e.filter(e=>t.getAttributeType(e))}catch(t){return this.warnedUnreadableSchema||(this.warnedUnreadableSchema=!0,this.logger.warn(`Unable to read the schema, every policy attribute is listed: ${String(t)}`)),e}}async toPolicyRecord(e){const t=(await this.readPolicyConfig()).dn;return{...{dn:e.dn,cn:values(e.cn)[0]??"",isDefault:!!t&&s(t)===s(e.dn)},...typedPolicy(e)}}policyValue(e,t){switch(h[e]){case"integer":if("number"!=typeof t||!Number.isInteger(t)||t<0||"pwdCheckQuality"===e&&t>2)throw new p("pwdCheckQuality"===e?"pwdCheckQuality must be 0, 1 or 2":`${e} must be a non-negative integer`);return String(t);case"boolean":if("boolean"!=typeof t)throw new p(`${e} must be a boolean`);return t?"TRUE":"FALSE";default:if("string"!=typeof t||""===t)throw new p(`${e} must be a non-empty string`);return t}}async createPolicy(e,t){const{cn:i,...s}=e;if("string"!=typeof i)throw new p("cn required");try{r(i,"cn")}catch(e){throw new p(e.message)}const a=await this.findPolicy(i,t).catch(e=>{if(e instanceof l)return null;throw e});if(a)throw new d(`A password policy is named ${i}: ${a.dn}`);const n=["top","person","pwdPolicy"],u={objectClass:n,cn:i,sn:i,pwdAttribute:"userPassword"};for(const[e,t]of Object.entries(s)){if(!isWritablePolicyAttribute(e))throw new p(`${e} cannot be written`);null!==t&&(u[e]=this.policyValue(e,t))}w.some(e=>e in u)&&n.push("pwdPolicyChecker");try{await this.server.ldap.add(`cn=${o(i)},${this.policyBase()}`,u,t)}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(i,t))}async updatePolicy(t,i,a){const r=await this.findPolicy(t,a),o=await this.toPolicyRecord(r),n={},c=[];for(const[t,a]of Object.entries(i)){if(!isWritablePolicyAttribute(t)){if("dn"===t?"string"==typeof a&&s(a)===s(o.dn):Object.prototype.hasOwnProperty.call(o,t)&&e(o[t],a))continue;throw new p(`${t} cannot be written`)}null!==a?n[t]=this.policyValue(t,a):void 0!==o[t]&&c.push(t)}const l={};if(Object.keys(n).length>0&&(l.replace=n),c.length>0&&(l.delete=c),!l.replace&&!l.delete)return o;const d=values(r.objectClass).map(e=>e.toLowerCase());return w.some(e=>e in n)&&!d.includes("pwdpolicychecker")&&(l.add={objectClass:"pwdPolicyChecker"}),await this.server.ldap.modify(r.dn,l,a),this.forgetPolicies(),this.toPolicyRecord(await this.findPolicy(t,a))}async deletePolicy(e,t){const i=await this.findPolicy(e,t),r=(await this.readPolicyConfig()).dn;if(r&&s(r)===s(i.dn))throw new d(`${i.dn} is the default password policy`);if((await this.server.ldap.search({filter:`(pwdPolicySubentry=${a(i.dn)})`,scope:"sub",attributes:["1.1"],sizeLimit:1,paged:!1},this.config.ldap_base)).searchEntries.length>0)throw new d(`${i.dn} is still applied to at least one entry`);await this.server.ldap.delete(i.dn,t),this.forgetPolicies()}async assignPolicy(e,t,i){const s=this.resolveUserDn(e);if(null===t){try{await this.server.ldap.modify(s,{delete:["pwdPolicySubentry"]},i)}catch(e){if(16!==c(e))throw e}return{dn:s,policy:null}}const a=await this.findPolicy(t,i);return await this.server.ldap.modify(s,{replace:{pwdPolicySubentry:a.dn}},i),{dn:s,policy:a.dn}}param(e,t){const i=e.params[t];return Array.isArray(i)?i[0]:i}parseGeneralizedTime(e){if(!e)return null;const t=e.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);if(!t)return null;const[,i,s,a,r,o,n]=t;return new Date(Date.UTC(+i,+s-1,+a,+r,+o,+n))}validateComplexity(e){const t=[],i=this.config,s=i.ppolicy_min_length||12;return e.length<s&&t.push(`Minimum ${s} characters required`),i.ppolicy_require_uppercase&&!/[A-Z]/.test(e)&&t.push("At least one uppercase letter required"),i.ppolicy_require_lowercase&&!/[a-z]/.test(e)&&t.push("At least one lowercase letter required"),i.ppolicy_require_digit&&!/\d/.test(e)&&t.push("At least one digit required"),i.ppolicy_require_special&&!g.test(e)&&t.push("At least one special character required"),{valid:0===t.length,errors:t}}resolveUserDn(e){if(e.includes("="))return e;const t=this.config.ldap_users_base||this.config.ldap_base;return`${this.config.ldap_user_main_attribute||"uid"}=${o(e)},${t}`}getConfigApiData(){const e=this.config.api_prefix||"/api",t=!!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:t?`${e}/v1/password/validate`:void 0},config:{warnDays:this.config.ppolicy_warn_days||14,validateComplexity:t,...t&&{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}}}}}}export{PasswordPolicy as default};
|
|
1
|
+
import{isDeepStrictEqual as e}from"node:util";import t from"../../abstract/plugin.js";import{asyncHandler as i,normalizeDn as s,escapeLdapFilter as a,validateDnValue as r,escapeDnValue as o}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"],h={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"},w=["pwdCheckModuleArg","pwdUseCheckModule"],y=[...Object.keys(h),"pwdAttribute","pwdCheckModule"],isWritablePolicyAttribute=e=>Object.prototype.hasOwnProperty.call(h,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 t={};for(const i of y){const s=values(e[i]);if(0!==s.length)switch(h[i]){case"integer":{const e=parseInt(s[0],10);isNaN(e)||(t[i]=e);break}case"boolean":t[i]="TRUE"===s[0];break;default:t[i]=1===s.length?s[0]:s}}return t},g=/[!@#$%^&*()_+\-=[\]{}|;:'",.<>?/]/;class PasswordPolicy extends t{constructor(){super(...arguments),this.name="ldapPasswordPolicy",this.roles=["api","configurable"],this.policyCache=new Map,this.discoveredDefault=null,this.warnedAmbiguousDefault=!1,this.warnedMissingBase=!1,this.warnedUnreadableSchema=!1,this.POLICY_CACHE_TTL=6e4}api(e){const t=`${this.config.api_prefix}/v1`;e.get(`${t}/password-policy`,i(async(e,t)=>{const i=await this.readPolicyConfig();if(i.dn)try{await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:["1.1"],paged:!1},i.dn,e)}catch(e){if(32!==c(e))throw e;return this.forgetPolicies(),void t.json({})}t.json(i)})),e.get(`${t}/users/:id/password-status`,i(async(e,t)=>{const i=this.param(e,"id"),s=await this.getPasswordStatus(i,e);t.json(s)})),e.post(`${t}/users/:id/unlock`,i(async(e,t)=>{const i=this.param(e,"id");await this.unlockAccount(i,e),t.json({success:!0,message:"Account unlocked"})})),e.get(`${t}/password-policy/expiring-soon`,i(async(e,t)=>{const i=parseInt(e.query.days)||this.config.ppolicy_warn_days||14,s=await this.findExpiringPasswords(i,e);t.json({warningDays:i,users:s})})),e.get(`${t}/password-policy/locked-accounts`,i(async(e,t)=>{const i=await this.findLockedAccounts(e);t.json({accounts:i})})),e.get(`${t}/password-policies`,i(async(e,t)=>{t.json({policies:await this.listPolicies(e),attributes:await this.definedAttributes()})})),e.post(`${t}/password-policies`,i(async(e,t)=>{const i=n(e,t);if(!i)return;const s=await this.createPolicy(i,e);t.status(201).json(s)})),e.get(`${t}/password-policies/:cn`,i(async(e,t)=>{const i=await this.findPolicy(this.param(e,"cn"),e);t.json(await this.toPolicyRecord(i))})),e.patch(`${t}/password-policies/:cn`,i(async(e,t)=>{const i=n(e,t);if(!i)return;const s=await this.updatePolicy(this.param(e,"cn"),i,e);t.json(s)})),e.delete(`${t}/password-policies/:cn`,i(async(e,t)=>{await this.deletePolicy(this.param(e,"cn"),e),t.json({success:!0})})),e.put(`${t}/users/:id/password-policy`,i(async(e,t)=>{const i=n(e,t);if(!i)return;const{policy:s}=i;if(null!==s&&("string"!=typeof s||!s))throw new p("policy must be a policy name or null");t.json(await this.assignPolicy(this.param(e,"id"),s,e))})),this.config.ppolicy_validate_complexity&&e.post(`${t}/password/validate`,i(async(e,t)=>{const i=n(e,t);if(!i)return;const{password:s}=i;if(!s)throw new p("password required");const a=this.validateComplexity(s);t.json(a)}))}async getPasswordStatus(e,t){const i=this.resolveUserDn(e),s=await this.readUser(i,e,["*",...u],t),a=await this.policyFor(s);return this.calculateStatus(i,s,a)}async readUser(e,t,i,s){try{const a=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:i,paged:!1},e,s);if(!a.searchEntries||0===a.searchEntries.length)throw new l(`User not found: ${t}`);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: ${t}`);throw e}}calculateStatus(e,t,i){const s=Date.now(),a=this.parseGeneralizedTime(t.pwdChangedTime);let r=null,o=!1,n=null;i.pwdMaxAge&&a&&(r=new Date(a.getTime()+1e3*i.pwdMaxAge),o=s>r.getTime(),n=Math.ceil((r.getTime()-s)/864e5));let c=!1;const l=this.config.ppolicy_warn_days||14;null!==n&&n>0&&(c=n<=l);const d=t.pwdAccountLockedTime;let p=!!d;if(p&&!("000001010000Z"===d)&&i.pwdLockoutDuration){const e=this.parseGeneralizedTime(d);if(e){s>new Date(e.getTime()+1e3*i.pwdLockoutDuration).getTime()&&(p=!1)}}const u=t.pwdFailureTime,h=Array.isArray(u)?u.length:0,w=t.pwdGraceUseTime,y=Array.isArray(w)?w.length:0,g="TRUE"===t.pwdReset;return{dn:e,passwordSet:!!t.userPassword,lastChanged:a?.toISOString()||null,expiresAt:r?.toISOString()||null,daysUntilExpiration:n,isExpired:o,isExpiringSoon:c,mustChange:g,isLocked:p,lockedAt:d&&this.parseGeneralizedTime(d)?.toISOString()||null,failureCount:h,graceLoginsUsed:y,policy:i.dn??null}}async unlockAccount(e,t){const i=this.resolveUserDn(e),s=await this.server.ldap.search({filter:"(objectClass=*)",scope:"base",attributes:["pwdAccountLockedTime","pwdFailureTime"],paged:!1},i,t).catch(t=>{if(32===c(t))throw new l(`User not found: ${e}`);throw t});if(!s.searchEntries||0===s.searchEntries.length)throw new l(`User not found: ${e}`);const a=userEntry(s.searchEntries[0]),r={};a.pwdAccountLockedTime&&(r.pwdAccountLockedTime=[]),a.pwdFailureTime?.length&&(r.pwdFailureTime=[]),Object.keys(r).length>0?(await this.server.ldap.modify(i,{delete:r},t),this.logger.info(`Account unlocked: ${i}`)):this.logger.info(`Account was not locked: ${i}`)}async findExpiringPasswords(e,t){const i=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},i,t),a=Date.now(),r=24*e*60*60*1e3,o=[];for(const e of s.searchEntries){const t=userEntry(e),i=this.parseGeneralizedTime(t.pwdChangedTime);if(!i)continue;const s=await this.policyFor(t);if(!s.pwdMaxAge)continue;const n=new Date(i.getTime()+1e3*s.pwdMaxAge),c=n.getTime()-a;c>0&&c<r&&o.push({dn:t.dn,uid:t.uid,displayName:t.displayName||t.cn,mail:t.mail,expiresAt:n.toISOString(),daysUntilExpiration:Math.ceil(c/864e5)})}return o.sort((e,t)=>e.daysUntilExpiration-t.daysUntilExpiration)}async findLockedAccounts(e){const t=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},t,e)).searchEntries.map(e=>{const t=userEntry(e);return{dn:t.dn,uid:t.uid,displayName:t.displayName||t.cn,mail:t.mail,lockedAt:this.parseGeneralizedTime(t.pwdAccountLockedTime)?.toISOString(),failureCount:Array.isArray(t.pwdFailureTime)?t.pwdFailureTime.length:0}})}async policyFor(e){if(e.pwdPolicySubentry){const t=await this.readPolicyAt(e.pwdPolicySubentry);if(t)return t}return this.readPolicyConfig()}async readPolicyConfig(){const e=this.config.ppolicy_default_dn||await this.discoverDefaultDn();return(e?await this.readPolicyAt(e):null)??{}}async discoverDefaultDn(){if(this.discoveredDefault&&Date.now()-this.discoveredDefault.time<this.POLICY_CACHE_TTL)return this.discoveredDefault.dn;let e=null;try{const t=(await this.server.ldap.system.search({filter:"(objectClass=pwdPolicy)",scope:"sub",attributes:["1.1"],paged:!1},this.policyBase())).searchEntries.map(e=>e.dn);1===t.length?e=t[0]:t.length>1&&!this.warnedAmbiguousDefault&&(this.warnedAmbiguousDefault=!0,this.logger.warn(`${t.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 t=s(e),i=this.policyCache.get(t);if(i&&Date.now()-i.time<this.POLICY_CACHE_TTL)return i.policy;let a,r=null;try{const t=await this.server.ldap.system.search({filter:"(objectClass=pwdPolicy)",scope:"base",attributes:y,paged:!1},e);t.searchEntries.length>0&&(r=((e,t)=>{const i=typedPolicy(e);return{dn:t,pwdMaxAge:i.pwdMaxAge,pwdMinAge:i.pwdMinAge,pwdInHistory:i.pwdInHistory,pwdCheckQuality:i.pwdCheckQuality,pwdMinLength:i.pwdMinLength,pwdMaxFailure:i.pwdMaxFailure,pwdLockout:!0===i.pwdLockout,pwdLockoutDuration:i.pwdLockoutDuration,pwdGraceAuthNLimit:i.pwdGraceAuthNLimit,pwdExpireWarning:i.pwdExpireWarning,pwdMustChange:!0===i.pwdMustChange,pwdAllowUserChange:!1!==i.pwdAllowUserChange}})(t.searchEntries[0],e))}catch(e){a=String(e)}const o=this.config.ppolicy_default_dn;return!r&&o&&s(o)===t?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(t,{policy:r,time:Date.now()}),r}forgetPolicies(){this.policyCache.clear(),this.discoveredDefault=null}policyBase(){return this.config.ppolicy_base||this.config.ldap_base}async searchPolicies(e,t){try{return(await this.server.ldap.search({filter:`(&(objectClass=pwdPolicy)${e})`,scope:"sub",attributes:["objectClass","cn",...y],paged:!1},this.policyBase(),t)).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,t){const i=await this.searchPolicies(`(cn=${a(e)})`,t);if(0===i.length)throw new l(`No password policy named ${e}`);if(i.length>1)throw new d(`Several password policies are named ${e}`);return i[0]}async listPolicies(e){const t=[];for(const i of await this.searchPolicies("",e))t.push(await this.toPolicyRecord(i));return t.sort((e,t)=>e.cn.localeCompare(t.cn))}async definedAttributes(){const e=Object.keys(h);try{const t=await this.server.ldap.schemaIndex();return e.filter(e=>t.getAttributeType(e))}catch(t){return this.warnedUnreadableSchema||(this.warnedUnreadableSchema=!0,this.logger.warn(`Unable to read the schema, every policy attribute is listed: ${String(t)}`)),e}}async toPolicyRecord(e){const t=(await this.readPolicyConfig()).dn;return{...{dn:e.dn,cn:values(e.cn)[0]??"",isDefault:!!t&&s(t)===s(e.dn)},...typedPolicy(e)}}policyValue(e,t){switch(h[e]){case"integer":if("number"!=typeof t||!Number.isInteger(t)||t<0||"pwdCheckQuality"===e&&t>2)throw new p("pwdCheckQuality"===e?"pwdCheckQuality must be 0, 1 or 2":`${e} must be a non-negative integer`);return String(t);case"boolean":if("boolean"!=typeof t)throw new p(`${e} must be a boolean`);return t?"TRUE":"FALSE";default:if("string"!=typeof t||""===t)throw new p(`${e} must be a non-empty string`);return t}}async createPolicy(e,t){const{cn:i,...s}=e;if("string"!=typeof i)throw new p("cn required");try{r(i,"cn")}catch(e){throw new p(e.message)}const a=await this.findPolicy(i,t).catch(e=>{if(e instanceof l)return null;throw e});if(a)throw new d(`A password policy is named ${i}: ${a.dn}`);const n=["top","person","pwdPolicy"],u={objectClass:n,cn:i,sn:i,pwdAttribute:"userPassword"};for(const[e,t]of Object.entries(s)){if(!isWritablePolicyAttribute(e))throw new p(`${e} cannot be written`);null!==t&&(u[e]=this.policyValue(e,t))}w.some(e=>e in u)&&n.push("pwdPolicyChecker");try{await this.server.ldap.add(`cn=${o(i)},${this.policyBase()}`,u,t)}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(i,t))}async updatePolicy(t,i,a){const r=await this.findPolicy(t,a),o=await this.toPolicyRecord(r),n={},c=[];for(const[t,a]of Object.entries(i)){if(!isWritablePolicyAttribute(t)){if("dn"===t?"string"==typeof a&&s(a)===s(o.dn):Object.prototype.hasOwnProperty.call(o,t)&&e(o[t],a))continue;throw new p(`${t} cannot be written`)}null!==a?n[t]=this.policyValue(t,a):void 0!==o[t]&&c.push(t)}const l={};if(Object.keys(n).length>0&&(l.replace=n),c.length>0&&(l.delete=c),!l.replace&&!l.delete)return o;const d=values(r.objectClass).map(e=>e.toLowerCase());return w.some(e=>e in n)&&!d.includes("pwdpolicychecker")&&(l.add={objectClass:"pwdPolicyChecker"}),await this.server.ldap.modify(r.dn,l,a),this.forgetPolicies(),this.toPolicyRecord(await this.findPolicy(t,a))}async deletePolicy(e,t){const i=await this.findPolicy(e,t);await this.server.ldap.authorize("ldapdeleterequest",[i.dn,t]);const r=(await this.readPolicyConfig()).dn;if(r&&s(r)===s(i.dn))throw new d(`${i.dn} is the default password policy`);if((await this.server.ldap.system.search({filter:`(pwdPolicySubentry=${a(i.dn)})`,scope:"sub",attributes:["1.1"],sizeLimit:1,paged:!1},this.config.ldap_base)).searchEntries.length>0)throw new d(`${i.dn} is still applied to at least one entry`);await this.server.ldap.delete(i.dn,t),this.forgetPolicies()}async assignPolicy(e,t,i){const s=this.resolveUserDn(e);if(null===t){try{await this.server.ldap.modify(s,{delete:["pwdPolicySubentry"]},i)}catch(e){if(16!==c(e))throw e}return{dn:s,policy:null}}const a=await this.findPolicy(t,i);return await this.server.ldap.modify(s,{replace:{pwdPolicySubentry:a.dn}},i),{dn:s,policy:a.dn}}param(e,t){const i=e.params[t];return Array.isArray(i)?i[0]:i}parseGeneralizedTime(e){if(!e)return null;const t=e.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);if(!t)return null;const[,i,s,a,r,o,n]=t;return new Date(Date.UTC(+i,+s-1,+a,+r,+o,+n))}validateComplexity(e){const t=[],i=this.config,s=i.ppolicy_min_length||12;return e.length<s&&t.push(`Minimum ${s} characters required`),i.ppolicy_require_uppercase&&!/[A-Z]/.test(e)&&t.push("At least one uppercase letter required"),i.ppolicy_require_lowercase&&!/[a-z]/.test(e)&&t.push("At least one lowercase letter required"),i.ppolicy_require_digit&&!/\d/.test(e)&&t.push("At least one digit required"),i.ppolicy_require_special&&!g.test(e)&&t.push("At least one special character required"),{valid:0===t.length,errors:t}}resolveUserDn(e){if(e.includes("="))return e;const t=this.config.ldap_users_base||this.config.ldap_base;return`${this.config.ldap_user_main_attribute||"uid"}=${o(e)},${t}`}getConfigApiData(){const e=this.config.api_prefix||"/api",t=!!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:t?`${e}/v1/password/validate`:void 0},config:{warnDays:this.config.ppolicy_warn_days||14,validateComplexity:t,...t&&{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}}}}}}export{PasswordPolicy as default};
|
package/dist/plugins/ldap/raw.js
CHANGED
|
@@ -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
|
|
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";import{outsideRequest as d}from"../../lib/requestContext.js";const l=["base","one","sub"],u=["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?[]:u,...(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 d(()=>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){if(this.checkDn(t),!await this.server.ldap.readableBy(t,s))throw new c(`Entry ${t} not found`);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){if(this.checkDn(t.base),this.checkFilter(t.filter),!await this.server.ldap.readableBy(t.base,e))throw new c(`Entry ${t.base} not found`);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(!l.includes(r))throw new o(`Invalid scope "${r}", expected one of ${l.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 t from"../../abstract/plugin.js";import{parseDn as s,normalizeDn as
|
|
1
|
+
import t from"../../abstract/plugin.js";import{parseDn as s,normalizeDn as e,isDnInBranch as a}from"../../lib/utils.js";class TrashPlugin extends t{constructor(t){super(t),this.name="trash",this.roles=["consistency"],this.watchedBases=[],this.trashBase="",this.addMetadata=!0,this.autoCreateTrash=!0,this.trashInitialized=!1,this.hooks={ldapdeleterequest:async([t,s])=>{const e=Array.isArray(t)?t:[t],a=[];for(const t of e){if(!this.isWatched(t)){this.logger.debug(`Trash plugin: ${t} not in watched branches, allowing native delete`),a.push(t);continue}this.logger.info(`Trash plugin: Intercepting delete of ${t}`),await this.ensureTrashExists();const e=this.buildTrashDN(t);await this.removeOldTrashEntry(e);try{this.logger.debug(`Moving ${t} to ${e} using LDAP move (atomic)`),await this.server.ldap.move(t,e,s),this.logger.info(`Entry ${t} moved to trash at ${e}`),await this.addMetadataToTrash(e,t)}catch(s){if(this.logger.error(`Trash plugin: Failed to move ${t} to trash: ${String(s)}`),s instanceof Error&&(s.message.includes("Insufficient access")||s.message.includes("permissions")||s.message.includes("access denied")))throw new Error(`Trash plugin: Insufficient LDAP permissions to move ${t} to trash. Please grant modifyDN permissions or disable trash plugin. Original error: ${s.message}`);throw new Error(`Trash plugin: Failed to move ${t} to trash: ${String(s)}`)}}return[Array.isArray(t)?a:a[0]??void 0,s]}};const r=this.config.trash_base;this.trashBase="string"==typeof r?r:"ou=trash,dc=example,dc=com";const i=this.config.trash_watched_bases;"string"==typeof i&&i&&(this.watchedBases=i.split(";").map(t=>t.trim()).filter(t=>t.length>0));const h=this.config.ldap_base;if(h){const t=s(e(h)),holdsBase=s=>s.some((e,a)=>t.every((t,e)=>s[a+e]===t));for(const r of this.watchedBases)a(r,h)&&!holdsBase(s(e(r)).slice(0,-t.length))||this.logger.warn(`Trash watched base "${r}" is not one DN under ${h}: --trash-watched-bases takes full DNs separated by ";", and as written the trash may catch nothing under it`)}this.addMetadata="false"!==this.config.trash_add_metadata,this.autoCreateTrash="false"!==this.config.trash_auto_create,this.logger.info(`Trash plugin initialized: base=${this.trashBase}, watched=${this.watchedBases.join(";")}, metadata=${this.addMetadata}`)}isWatched(t){return!a(t,this.trashBase)&&(0===this.watchedBases.length||this.watchedBases.some(s=>a(t,s)))}extractRDN(t){return t.split(",")[0]}buildTrashDN(t){return`${this.extractRDN(t)},${this.trashBase}`}async ensureTrashExists(){if(!this.trashInitialized)if(this.autoCreateTrash){try{if((await this.server.ldap.system.search({paged:!1},this.trashBase)).searchEntries.length>0)return this.logger.debug(`Trash branch ${this.trashBase} already exists`),void(this.trashInitialized=!0)}catch{this.logger.info(`Creating trash branch ${this.trashBase}`)}try{const t=this.extractRDN(this.trashBase).split("=")[1];await this.server.ldap.system.add(this.trashBase,{objectClass:["top","organizationalUnit"],ou:t,description:"LDAP Trash - Deleted entries are moved here"}),this.logger.info(`Trash branch ${this.trashBase} created successfully`),this.trashInitialized=!0}catch(t){throw this.logger.error(`Failed to create trash branch ${this.trashBase}: ${String(t)}`),new Error(`Trash plugin: Unable to create trash branch ${this.trashBase}. Please create it manually or set DM_TRASH_AUTO_CREATE=false`)}}else this.trashInitialized=!0}async removeOldTrashEntry(t){try{(await this.server.ldap.system.search({paged:!1},t)).searchEntries.length>0&&(this.logger.info(`Removing old trash entry ${t} before overwrite`),await this.server.ldap.system.delete(t),this.logger.debug(`Old trash entry ${t} deleted`))}catch(s){this.logger.debug(`No old trash entry to remove at ${t}: ${String(s)}`)}}async addMetadataToTrash(t,s){if(this.addMetadata)try{const e=`Deleted on ${(new Date).toISOString()} from ${s}`;await this.server.ldap.system.modify(t,{replace:{description:e}}),this.logger.debug(`Metadata added to ${t}`)}catch(s){this.logger.warn(`Failed to add metadata to ${t}: ${String(s)}. Entry moved to trash but without metadata.`)}}}export{TrashPlugin as default};
|
package/dist/plugins/rabbitmq.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../abstract/plugin.js";class RabbitMq extends t{constructor(t){super(t),this.name="rabbitmq",this.client=null,this.clientInit=null,this.rabbitmqUrl=this.config.rabbitmq_url||"",this.rabbitmqUrl||this.logger.warn(`${this.name}: rabbitmq_url is empty — AMQP publishes/subscribes will be skipped`),this.registerShutdown()}isAvailable(){return this.rabbitmqUrl.length>0}async publish(t,i,n,e){const s=await this.getClient();s&&await s.publish(t,i,n,e)}async subscribe(t,i,n,e,s){const r=await this.getClient();r&&await r.subscribe(t,i,n,e,s)}async unsubscribe(t){const i=await this.getClient();i&&await i.unsubscribe(t)}async getRawClient(){return this.getClient()}async getClient(){return this.client?this.client:this.rabbitmqUrl?(this.clientInit||(this.clientInit=(async()=>{try{const t=await this.connect();return this.client=t,this.logger.info(`${this.name}: connected to RabbitMQ at ${function(t){try{const i=new URL(t);return i.username="",i.password="",i.toString()}catch{return"amqp://[broker]"}}(this.rabbitmqUrl)}`),t}catch(t){return this.logger.warn({plugin:this.name,event:"rabbitmq_init",message:"@linagora/rabbitmq-client unavailable or broker unreachable — AMQP disabled",error:`${t}`}),this.clientInit=null,null}})()),this.clientInit):null}async connect(){const t=new((await import("@linagora/rabbitmq-client")).RabbitMQClient)({url:this.rabbitmqUrl});return await t.init(),t}registerShutdown(){!function(t){if(
|
|
1
|
+
import t from"../abstract/plugin.js";import{outsideRequest as i}from"../lib/requestContext.js";class RabbitMq extends t{constructor(t){super(t),this.name="rabbitmq",this.client=null,this.clientInit=null,this.rabbitmqUrl=this.config.rabbitmq_url||"",this.rabbitmqUrl||this.logger.warn(`${this.name}: rabbitmq_url is empty — AMQP publishes/subscribes will be skipped`),this.registerShutdown()}isAvailable(){return this.rabbitmqUrl.length>0}async publish(t,i,n,e){const s=await this.getClient();s&&await s.publish(t,i,n,e)}async subscribe(t,i,n,e,s){const r=await this.getClient();r&&await r.subscribe(t,i,n,e,s)}async unsubscribe(t){const i=await this.getClient();i&&await i.unsubscribe(t)}async getRawClient(){return this.getClient()}async getClient(){return this.client?this.client:this.rabbitmqUrl?(this.clientInit||(this.clientInit=(async()=>{try{const t=await i(()=>this.connect());return this.client=t,this.logger.info(`${this.name}: connected to RabbitMQ at ${function(t){try{const i=new URL(t);return i.username="",i.password="",i.toString()}catch{return"amqp://[broker]"}}(this.rabbitmqUrl)}`),t}catch(t){return this.logger.warn({plugin:this.name,event:"rabbitmq_init",message:"@linagora/rabbitmq-client unavailable or broker unreachable — AMQP disabled",error:`${t}`}),this.clientInit=null,null}})()),this.clientInit):null}async connect(){const t=new((await import("@linagora/rabbitmq-client")).RabbitMQClient)({url:this.rabbitmqUrl});return await t.init(),t}registerShutdown(){!function(t){if(n.push(t),e)return;e=!0;const fanOut=()=>{for(const t of n.splice(0))try{t()}catch{}};process.once("SIGTERM",fanOut),process.once("SIGINT",fanOut)}(()=>{const t=this.client;this.client=null,this.clientInit=null,t&&t.close().catch(t=>{this.logger.warn({plugin:this.name,event:"shutdown",error:`${t}`})})})}}const n=[];let e=!1;export{RabbitMq as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{escapeDnValue as t,isDummyMemberDn as e,launchHooksChained as s,validateDnValue as r,launchHooks as i,isChildOf as o,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as n}from"./types.js";import{withExternalId as c,mergeMapping as h,loadMappingFile as u,DEFAULT_GROUP_MAPPING as d,requiredLdapAttributes as l,ldapToScimGroup as m,scimGroupToLdap as p}from"./mapping.js";import{scimFilterToLdap as f}from"./filter.js";import{pagedSearch as g}from"./list.js";import{writePatch as b,patchToModifyRequest as w}from"./patch.js";import{scimNotFound as y,ScimError as R,scimInvalidValue as _,scimUniqueness as $}from"./errors.js";import{extractLdapCode as v}from"../../lib/ldapCodes.js";class ScimGroups{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_group_rdn_attribute||"cn",this.objectClass=this.config.scim_group_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=this.config.scim_group_mapping||"";this.mapping=c(h(d,e?u(e):void 0),this.config.scim_group_external_id_attribute||""),this.attributes=l(this.mapping,[this.rdnAttribute,"member"])}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"Group",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:"",lockValue:""}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const s=this.baseResolver.userBase(t),r=this.baseResolver.groupBase(t),i=this.scimPrefix,o=this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),a=this.config.scim_user_rdn_attribute||"uid",n=this.rdnAttribute,c=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(e(t,c))return;const h=/^([^=]+)=((?:\\.|[^,])+)/.exec(t);if(!h)return;const u=h[2].replace(/\\(.)/g,"$1"),d=t.toLowerCase();let l;s&&d.endsWith(s.toLowerCase())?l="User":r&&d.endsWith(r.toLowerCase())?l="Group":h[1]===a?l="User":h[1]===n&&(l="Group");const m={value:u};if(l&&(m.type=l,o)){const t="User"===l?"Users":"Groups";m.$ref=`${o.replace(/\/$/,"")}${i}/${t}/${encodeURIComponent(u)}`}return m}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.attributes},s)}catch(t){if(32===v(t))throw y(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw y(`Group ${e} not found`);return m(r.searchEntries[0],this.mapping,this.ctx(t),this.buildMemberResolver(t))}async list(t,e){const s=this.baseResolver.groupBase(t),r=Math.max(1,e.startIndex||1),i=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let o,a=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=f(e.filter,this.mapping);t.idEquals?o=t.idEquals:a=`(&${a}${t.ldapFilter})`}if(o)try{const e=await this.get(t,o);return{schemas:[n],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof R&&404===t.statusCode)return{schemas:[n],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:c,totalResults:h}=await g({directory:this.ldap.forRequest(t),base:s,filter:a,attributes:this.attributes,startIndex:r,count:i,maxScanned:this.maxScanned}),u=this.buildMemberResolver(t),d=c.map(e=>m(e,this.mapping,this.ctx(t),u));return{schemas:[n],totalResults:h,startIndex:r,itemsPerPage:d.length,Resources:d}}async create(e,o){if(!o.displayName)throw _("displayName is required");const a=this.baseResolver.groupBase(e),n=(await s(this.hooks.scimgroupcreate,[{...o,id:void 0},e,a]))[0],{rdn:c,attributes:h}=p(n,this.mapping,this.objectClass),u=!h[this.rdnAttribute]&&n.id||c;if(!u)throw _("displayName is required");r(u,this.rdnAttribute),h[this.rdnAttribute]=u;const d=[];if(n.members&&Array.isArray(n.members))for(const t of n.members){if(!t||!t.value)continue;const s=await this.users.resolveRef(e,t.value);s&&d.push(s)}if(0===d.length){const t=this.config.group_dummy_user||"cn=fakeuser";d.push(t)}h.member=d;const l=`${this.rdnAttribute}=${t(u)},${a}`;try{await this.ldap.forRequest(e).add(l,h)}catch(t){if(68===v(t))throw $(`Group ${u} already exists`);throw t}const m=await this.get(e,u);return i(this.hooks.scimgroupcreatedone,m),m}async replace(t,e,r){await this.get(t,e);const o=(await s(this.hooks.scimgroupupdate,[e,r,t]))[1],{attributes:a}=p(o,this.mapping,this.objectClass),n=[];if(o.members&&Array.isArray(o.members))for(const e of o.members){if(!e||!e.value)continue;const s=await this.users.resolveRef(t,e.value);s&&n.push(s)}if(0===n.length){const t=this.config.group_dummy_user||"cn=fakeuser";n.push(t)}const c=this.dnForId(e,t),h={replace:{}};for(const[t,e]of Object.entries(a))"objectClass"!==t&&t!==this.rdnAttribute&&(h.replace[t]=e);h.replace.member=n,await this.ldap.forRequest(t).modify(c,h);const u=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,u),u}async patch(t,e,s){const r=this.dnForId(e,t);await b(r,async()=>{const i=await this.currentEntry(t,e),o=await w(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e),current:i}),a=o.delete&&!Array.isArray(o.delete)?o.delete.member:void 0;if(void 0!==a){const t=this.config.group_dummy_user||"cn=fakeuser";if(""===a)delete o.delete.member,0===Object.keys(o.delete).length&&delete o.delete,o.replace||(o.replace={}),o.replace.member=[t];else{const e=[].concat(a);0===[].concat(i.member??[],o.add?.member??[]).map(String).filter(t=>!e.includes(t)).length&&(o.add||(o.add={}),o.add.member=t)}}await this.ldap.forRequest(t).modify(r,o,{permissive:!0})});const o=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,o),o}async currentEntry(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.attributes},s)}catch(t){if(32===v(t))throw y(`Group ${e} not found`);throw t}if(!r.searchEntries?.length)throw y(`Group ${e} not found`);return r.searchEntries[0]}async delete(t,e){await this.get(t,e);const r=(await s(this.hooks.scimgroupdelete,[e,t]))[0],o=this.dnForId(r,t);await this.ldap.forRequest(t).delete(o),i(this.hooks.scimgroupdeletedone,r)}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e)
|
|
1
|
+
import{escapeDnValue as t,isDummyMemberDn as e,launchHooksChained as s,validateDnValue as r,launchHooks as i,isChildOf as o,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as n}from"./types.js";import{withExternalId as c,mergeMapping as h,loadMappingFile as u,DEFAULT_GROUP_MAPPING as d,requiredLdapAttributes as l,ldapToScimGroup as m,scimGroupToLdap as p}from"./mapping.js";import{scimFilterToLdap as f}from"./filter.js";import{pagedSearch as g}from"./list.js";import{writePatch as b,patchToModifyRequest as w}from"./patch.js";import{scimNotFound as y,ScimError as R,scimInvalidValue as _,scimUniqueness as $}from"./errors.js";import{extractLdapCode as v}from"../../lib/ldapCodes.js";class ScimGroups{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_group_rdn_attribute||"cn",this.objectClass=this.config.scim_group_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=this.config.scim_group_mapping||"";this.mapping=c(h(d,e?u(e):void 0),this.config.scim_group_external_id_attribute||""),this.attributes=l(this.mapping,[this.rdnAttribute,"member"])}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"Group",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:"",lockValue:""}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const s=this.baseResolver.userBase(t),r=this.baseResolver.groupBase(t),i=this.scimPrefix,o=this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),a=this.config.scim_user_rdn_attribute||"uid",n=this.rdnAttribute,c=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(e(t,c))return;const h=/^([^=]+)=((?:\\.|[^,])+)/.exec(t);if(!h)return;const u=h[2].replace(/\\(.)/g,"$1"),d=t.toLowerCase();let l;s&&d.endsWith(s.toLowerCase())?l="User":r&&d.endsWith(r.toLowerCase())?l="Group":h[1]===a?l="User":h[1]===n&&(l="Group");const m={value:u};if(l&&(m.type=l,o)){const t="User"===l?"Users":"Groups";m.$ref=`${o.replace(/\/$/,"")}${i}/${t}/${encodeURIComponent(u)}`}return m}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.attributes},s)}catch(t){if(32===v(t))throw y(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw y(`Group ${e} not found`);return m(r.searchEntries[0],this.mapping,this.ctx(t),this.buildMemberResolver(t))}async list(t,e){const s=this.baseResolver.groupBase(t),r=Math.max(1,e.startIndex||1),i=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let o,a=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=f(e.filter,this.mapping);t.idEquals?o=t.idEquals:a=`(&${a}${t.ldapFilter})`}if(o)try{const e=await this.get(t,o);return{schemas:[n],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof R&&404===t.statusCode)return{schemas:[n],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:c,totalResults:h}=await g({directory:this.ldap.forRequest(t),base:s,filter:a,attributes:this.attributes,startIndex:r,count:i,maxScanned:this.maxScanned}),u=this.buildMemberResolver(t),d=c.map(e=>m(e,this.mapping,this.ctx(t),u));return{schemas:[n],totalResults:h,startIndex:r,itemsPerPage:d.length,Resources:d}}async create(e,o){if(!o.displayName)throw _("displayName is required");const a=this.baseResolver.groupBase(e),n=(await s(this.hooks.scimgroupcreate,[{...o,id:void 0},e,a]))[0],{rdn:c,attributes:h}=p(n,this.mapping,this.objectClass),u=!h[this.rdnAttribute]&&n.id||c;if(!u)throw _("displayName is required");r(u,this.rdnAttribute),h[this.rdnAttribute]=u;const d=[];if(n.members&&Array.isArray(n.members))for(const t of n.members){if(!t||!t.value)continue;const s=await this.users.resolveRef(e,t.value);s&&d.push(s)}if(0===d.length){const t=this.config.group_dummy_user||"cn=fakeuser";d.push(t)}h.member=d;const l=`${this.rdnAttribute}=${t(u)},${a}`;try{await this.ldap.forRequest(e).add(l,h)}catch(t){if(68===v(t))throw $(`Group ${u} already exists`);throw t}const m=await this.get(e,u);return i(this.hooks.scimgroupcreatedone,m),m}async replace(t,e,r){await this.get(t,e);const o=(await s(this.hooks.scimgroupupdate,[e,r,t]))[1],{attributes:a}=p(o,this.mapping,this.objectClass),n=[];if(o.members&&Array.isArray(o.members))for(const e of o.members){if(!e||!e.value)continue;const s=await this.users.resolveRef(t,e.value);s&&n.push(s)}if(0===n.length){const t=this.config.group_dummy_user||"cn=fakeuser";n.push(t)}const c=this.dnForId(e,t),h={replace:{}};for(const[t,e]of Object.entries(a))"objectClass"!==t&&t!==this.rdnAttribute&&(h.replace[t]=e);h.replace.member=n,await this.ldap.forRequest(t).modify(c,h);const u=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,u),u}async patch(t,e,s){const r=this.dnForId(e,t);await b(r,async()=>{const i=await this.currentEntry(t,e),o=await w(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e),current:i}),a=o.delete&&!Array.isArray(o.delete)?o.delete.member:void 0;if(void 0!==a){const t=this.config.group_dummy_user||"cn=fakeuser";if(""===a)delete o.delete.member,0===Object.keys(o.delete).length&&delete o.delete,o.replace||(o.replace={}),o.replace.member=[t];else{const e=[].concat(a);0===[].concat(i.member??[],o.add?.member??[]).map(String).filter(t=>!e.includes(t)).length&&(o.add||(o.add={}),o.add.member=t)}}await this.ldap.forRequest(t).modify(r,o,{permissive:!0})});const o=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,o),o}async currentEntry(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.attributes},s)}catch(t){if(32===v(t))throw y(`Group ${e} not found`);throw t}if(!r.searchEntries?.length)throw y(`Group ${e} not found`);return r.searchEntries[0]}async delete(t,e){await this.get(t,e);const r=(await s(this.hooks.scimgroupdelete,[e,t]))[0],o=this.dnForId(r,t);await this.ldap.forRequest(t).delete(o),i(this.hooks.scimgroupdeletedone,r)}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e),i=s.includes("=")&&s.includes(",");if(i&&!o(s,r))return;const n=i?s:`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},n);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==v(t))throw t}if(!i)try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},r);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==v(t))throw t}}get groupMapping(){return this.mapping}}export{ScimGroups};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{escapeDnValue as t,launchHooksChained as e,validateDnValue as s,launchHooks as i,isChildOf as r,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as o}from"./types.js";import{resolveLockConfig as c,mergeMapping as h,loadMappingFile as n,DEFAULT_USER_MAPPING as l,requiredLdapAttributes as u,ldapToScimUser as d,scimUserToLdap as p,isLocked as m}from"./mapping.js";import{scimFilterToLdap as
|
|
1
|
+
import{escapeDnValue as t,launchHooksChained as e,validateDnValue as s,launchHooks as i,isChildOf as r,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as o}from"./types.js";import{resolveLockConfig as c,mergeMapping as h,loadMappingFile as n,DEFAULT_USER_MAPPING as l,requiredLdapAttributes as u,ldapToScimUser as d,scimUserToLdap as p,isLocked as m}from"./mapping.js";import{scimFilterToLdap as f}from"./filter.js";import{pagedSearch as b}from"./list.js";import{writePatch as g,patchToModifyRequest as w}from"./patch.js";import{scimNotFound as k,ScimError as y,scimInvalidValue as A,scimUniqueness as R}from"./errors.js";import{extractLdapCode as $}from"../../lib/ldapCodes.js";class ScimUsers{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_user_rdn_attribute||"uid",this.objectClass=this.config.scim_user_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=c(this.config.scim_user_lock_attribute||"",this.config.scim_user_lock_value||"");this.lockAttribute=e.attribute,this.lockValue=e.value;const s=this.config.scim_user_mapping||"";this.mapping=h(l,s?n(s):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"User",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:this.lockAttribute,lockValue:this.lockValue}}ldapAttributes(){return u(this.mapping,[this.lockAttribute])}dnForId(e,s){const i=this.baseResolver.userBase(s);return`${this.rdnAttribute}=${t(e)},${i}`}async get(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===$(t))throw k(`User ${e} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw k(`User ${e} not found`);return d(i.searchEntries[0],this.mapping,this.ctx(t))}async list(t,e){const s=this.baseResolver.userBase(t),i=Math.max(1,e.startIndex||1),r=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let a,c=`(${this.objectClass.includes("inetOrgPerson")?"objectClass=inetOrgPerson":`objectClass=${this.objectClass[0]}`})`;if(e.filter){const t=f(e.filter,this.mapping,{lockAttribute:this.lockAttribute,lockValue:this.lockValue,supportsActive:!0});t.idEquals?a=t.idEquals:c=`(&${c}${t.ldapFilter})`}if(a)try{const e=await this.get(t,a);return{schemas:[o],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof y&&404===t.statusCode)return{schemas:[o],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:h,totalResults:n}=await b({directory:this.ldap.forRequest(t),base:s,filter:c,attributes:this.ldapAttributes(),startIndex:i,count:r,maxScanned:this.maxScanned});e.sortBy,e.sortOrder;const l=h.map(e=>d(e,this.mapping,this.ctx(t)));return{schemas:[o],totalResults:n,startIndex:i,itemsPerPage:l.length,Resources:l}}async create(r,a){if(!a.userName)throw A("userName is required");const o=(await e(this.hooks.scimusercreate,[a,r]))[0],{rdn:c,attributes:h}=p(o,this.mapping,this.ctx(r),this.objectClass);if(!c)throw A("userName is required");s(c,this.rdnAttribute),h[this.rdnAttribute]=c;const n=this.baseResolver.userBase(r),l=`${this.rdnAttribute}=${t(c)},${n}`;try{await this.ldap.forRequest(r).add(l,h)}catch(t){if(68===$(t))throw R(`User ${c} already exists`);if(void 0!==h[this.lockAttribute])throw this.lockSchemaError(t);throw t}const u=await this.get(r,c);return i(this.hooks.scimusercreatedone,u),u}async replace(t,s,r){const a=this.dnForId(s,t);let o;try{o=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},a)}catch(t){if(32===$(t))throw k(`User ${s} not found`);throw t}if(!o.searchEntries?.length)throw k(`User ${s} not found`);const c=o.searchEntries[0],h=(await e(this.hooks.scimuserupdate,[s,r,t]))[1],{attributes:n,active:l}=p(h,this.mapping,this.ctx(t),this.objectClass),u={replace:{},delete:[]},d=new Set(["objectClass","entryUUID","createTimestamp","modifyTimestamp",this.rdnAttribute,this.lockAttribute]),hasAttrValue=t=>null!=t&&("string"==typeof t?t.length>0:!Array.isArray(t)||t.length>0&&t.some(t=>null!=t));for(const t of this.ldapAttributes())d.has(t)||(null!=n[t]?u.replace[t]=n[t]:hasAttrValue(c[t])&&u.delete.push(t));if(!1===l?u.replace[this.lockAttribute]=this.lockValue:!0===l&&m(c,{lockAttribute:this.lockAttribute,lockValue:this.lockValue})&&u.delete.push(this.lockAttribute),0===Object.keys(u.replace||{}).length&&delete u.replace,0===u.delete.length&&delete u.delete,u.replace||u.delete)try{await this.ldap.forRequest(t).modify(a,u)}catch(t){if(void 0!==l)throw this.lockSchemaError(t);if(16!==$(t))throw t;throw new y(409,`${a} changed while this PUT was being applied; nothing was written. Retry the request.`)}const f=await this.get(t,s);return i(this.hooks.scimuserupdatedone,s,f),f}async patch(t,e,s){const r=this.dnForId(e,t);await g(r,async()=>{const i=await this.currentEntry(t,e),a=await w(s,{mapping:this.mapping,lockAttribute:this.lockAttribute,lockValue:this.lockValue,supportsActive:!0,current:i});try{await this.ldap.forRequest(t).modify(r,a,{permissive:!0})}catch(t){if(function(t,e){const s=e.toLowerCase(),i=t.delete;return[...Object.keys(t.replace||{}),...Object.keys(t.add||{}),...Array.isArray(i)?i:Object.keys(i||{})].some(t=>t.toLowerCase()===s)}(a,this.lockAttribute))throw this.lockSchemaError(t);throw t}});const a=await this.get(t,e);return i(this.hooks.scimuserupdatedone,e,a),a}lockSchemaError(t){const e=$(t);if(17!==e&&65!==e)return t;const s=t instanceof Error?t.message:String(t);return A(`The directory rejected '${this.lockAttribute}', the attribute backing 'active': ${s}. Point --scim-user-lock-attribute and --scim-user-lock-value at an attribute this directory defines (nsAccountLock / TRUE on 389-ds); the default needs the ppolicy overlay.`)}async currentEntry(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===$(t))throw k(`User ${e} not found`);throw t}if(!i.searchEntries?.length)throw k(`User ${e} not found`);return i.searchEntries[0]}async delete(t,s){await this.get(t,s);const r=(await e(this.hooks.scimuserdelete,[s,t]))[0],a=this.dnForId(r,t);await this.ldap.forRequest(t).delete(a),i(this.hooks.scimuserdeletedone,r)}async resolveRef(e,s){if(!s)return;const i=this.baseResolver.userBase(e),o=s.includes("=")&&s.includes(",");if(o&&!r(s,i))return;const c=o?s:`${this.rdnAttribute}=${t(s)},${i}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},c);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==$(t))throw t}if(!o)try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==$(t))throw t}}get userMapping(){return this.mapping}}export{ScimUsers};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash as e}from"node:crypto";import
|
|
1
|
+
import{createHash as e}from"node:crypto";import s from"../../lib/storage/store.js";const errorText=e=>e instanceof Error?e.message:JSON.stringify(e);class LdapStore extends s{constructor(e,s,t){super(e.logger),this.name="storage/ldap",this.server=e,this.base=s,this.objectClass=t}dn(s){return`cn=${e("sha256").update(s).digest("hex")},${this.base}`}async load(e){try{const s=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:["description"]},this.dn(e)),t=s.searchEntries?.[0];return t?LdapStore.decodeRecord(t.description):null}catch(e){return 32!==e?.code&&this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(e)}`),null}}async save(e,s){const t=this.dn(e),r=LdapStore.encodeRecord(e,s);try{await this.server.ldap.system.add(t,{objectClass:["top",this.objectClass],cn:t.slice(3,t.indexOf(",")),description:r})}catch(e){if(68!==e?.code)throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(e)}`),e;await this.server.ldap.system.modify(t,{replace:{description:r}})}}async drop(e){await this.server.ldap.system.delete(this.dn(e)).catch(e=>{32!==e?.code&&this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(e)}`)})}async sweep(){const e=Date.now();let s=0;const t=await this.server.ldap.system.search({paged:!1,scope:"one",filter:`(objectClass=${this.objectClass})`,attributes:["description"]},this.base);for(const r of t.searchEntries||[]){const t=LdapStore.decodeRecord(r.description);null!==t&&t.deadline>e||(await this.server.ldap.system.delete(String(r.dn)).catch(()=>{}),s++)}return s}}export{LdapStore as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../../lib/storage/store.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t),quote=t=>`"${t.replace(/"/g,'""')}"`;class PostgresStore extends t{constructor(t,e,a){super(t),this.name="storage/postgres",this.url=e,this.table=a,this.quoted=a.split(".").map(quote).join(".")}async driver(){return import("pg")}async open(){await this.pool()}async close(){await super.close();const t=this.connection;this.connection=void 0,t&&await((await t.catch(()=>{}))?.end())}pool(){return this.connection??(this.connection=this.connect().catch(t=>{throw this.connection=void 0,t})),this.connection}async connect(){let t;try{t=await this.driver()}catch(t){throw new Error(`${this.name}: requires the optional dependency "pg", which is not installed. Install it to keep the records in PostgreSQL, or choose another --storage-backend. (${errorText(t)})`)}const e=new t.Pool({connectionString:this.url,connectionTimeoutMillis:1e4,allowExitOnIdle:!0});e.on("error",t=>this.logger.warn(`${this.name}: connection lost: ${errorText(t)}`));try{await this.ensureTable(e)}catch(t){throw await e.end().catch(()=>{}),new Error(`${this.name}: cannot use table ${this.table}: ${errorText(t)}`)}return e}async ensureTable(t){const e=await t.query("SELECT to_regclass($1) AS t",[this.quoted]);if(e.rows[0]?.t)return;const a=this.quoted,n=quote(`${this.table.split(".").pop()}_expires_at`);await t.query(`CREATE TABLE IF NOT EXISTS ${a} (\n namespace text NOT NULL,\n key text NOT NULL,\n value text NOT NULL,\n expires_at timestamptz NOT NULL,\n PRIMARY KEY (namespace, key)\n )`),await t.query(`CREATE INDEX IF NOT EXISTS ${n} ON ${a} (expires_at)`),this.logger.info(`${this.name}: created table ${this.table}`)}static parts(t){const e=t.indexOf("\0"),a=t.slice(e+1);return a.includes("\0")?null:[t.slice(0,e),a]}async load(t){const e=PostgresStore.parts(t);if(!e)return null;try{const t=await this.pool(),a=(await t.query(`SELECT value, expires_at FROM ${this.quoted}\n WHERE namespace = $1 AND key = $2`,e)).rows[0];return a?{value:a.value,deadline:a.expires_at.getTime()}:null}catch(t){return this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,e){const a=PostgresStore.parts(t);if(!a)throw new Error(`${this.name}: cannot keep a key holding a NUL`);try{const t=await this.pool();await t.query(`INSERT INTO ${this.quoted}\n (namespace, key, value, expires_at) VALUES ($1, $2, $3, $4)\n ON CONFLICT (namespace, key) DO UPDATE\n SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,[...a,e.value,new Date(e.deadline)])}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),t}}async drop(t){const e=PostgresStore.parts(t);if(e)try{const t=await this.pool();await t.query(`DELETE FROM ${this.quoted}\n WHERE namespace = $1 AND key = $2`,e)}catch(t){this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)}}async sweep(){const t=await this.pool();return(await t.query(`DELETE FROM ${this.quoted} WHERE expires_at <= $1`,[new Date])).rowCount??0}}export{PostgresStore as default};
|
|
1
|
+
import t from"../../lib/storage/store.js";import{outsideRequest as e}from"../../lib/requestContext.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t),quote=t=>`"${t.replace(/"/g,'""')}"`;class PostgresStore extends t{constructor(t,e,a){super(t),this.name="storage/postgres",this.url=e,this.table=a,this.quoted=a.split(".").map(quote).join(".")}async driver(){return import("pg")}async open(){await this.pool()}async close(){await super.close();const t=this.connection;this.connection=void 0,t&&await((await t.catch(()=>{}))?.end())}pool(){return this.connection??(this.connection=e(()=>this.connect()).catch(t=>{throw this.connection=void 0,t})),this.connection}async connect(){let t;try{t=await this.driver()}catch(t){throw new Error(`${this.name}: requires the optional dependency "pg", which is not installed. Install it to keep the records in PostgreSQL, or choose another --storage-backend. (${errorText(t)})`)}const e=new t.Pool({connectionString:this.url,connectionTimeoutMillis:1e4,allowExitOnIdle:!0});e.on("error",t=>this.logger.warn(`${this.name}: connection lost: ${errorText(t)}`));try{await this.ensureTable(e)}catch(t){throw await e.end().catch(()=>{}),new Error(`${this.name}: cannot use table ${this.table}: ${errorText(t)}`)}return e}async ensureTable(t){const e=await t.query("SELECT to_regclass($1) AS t",[this.quoted]);if(e.rows[0]?.t)return;const a=this.quoted,n=quote(`${this.table.split(".").pop()}_expires_at`);await t.query(`CREATE TABLE IF NOT EXISTS ${a} (\n namespace text NOT NULL,\n key text NOT NULL,\n value text NOT NULL,\n expires_at timestamptz NOT NULL,\n PRIMARY KEY (namespace, key)\n )`),await t.query(`CREATE INDEX IF NOT EXISTS ${n} ON ${a} (expires_at)`),this.logger.info(`${this.name}: created table ${this.table}`)}static parts(t){const e=t.indexOf("\0"),a=t.slice(e+1);return a.includes("\0")?null:[t.slice(0,e),a]}async load(t){const e=PostgresStore.parts(t);if(!e)return null;try{const t=await this.pool(),a=(await t.query(`SELECT value, expires_at FROM ${this.quoted}\n WHERE namespace = $1 AND key = $2`,e)).rows[0];return a?{value:a.value,deadline:a.expires_at.getTime()}:null}catch(t){return this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,e){const a=PostgresStore.parts(t);if(!a)throw new Error(`${this.name}: cannot keep a key holding a NUL`);try{const t=await this.pool();await t.query(`INSERT INTO ${this.quoted}\n (namespace, key, value, expires_at) VALUES ($1, $2, $3, $4)\n ON CONFLICT (namespace, key) DO UPDATE\n SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,[...a,e.value,new Date(e.deadline)])}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),t}}async drop(t){const e=PostgresStore.parts(t);if(e)try{const t=await this.pool();await t.query(`DELETE FROM ${this.quoted}\n WHERE namespace = $1 AND key = $2`,e)}catch(t){this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)}}async sweep(){const t=await this.pool();return(await t.query(`DELETE FROM ${this.quoted} WHERE expires_at <= $1`,[new Date])).rowCount??0}}export{PostgresStore as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../../lib/storage/store.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t);class ValkeyStore extends t{constructor(t,e,n){super(t),this.name="storage/valkey",this.down=!1,this.url=e,this.prefix=n}async driver(){return import("iovalkey")}async open(){await this.client()}async close(){await super.close();const t=this.connection;this.connection=void 0;const e=await(t?.catch(()=>{}));e&&await e.quit().catch(()=>e.disconnect())}client(){return this.connection??(this.connection=this.connect().catch(t=>{throw this.connection=void 0,t})),this.connection}async connect(){let t;try{t=await this.driver()}catch(t){throw new Error(`${this.name}: requires the optional dependency "iovalkey", which is not installed. Install it to keep the records in Valkey, or choose another --storage-backend. (${errorText(t)})`)}const e=new t.Valkey(this.url,{lazyConnect:!0,enableOfflineQueue:!1,maxRetriesPerRequest:1,commandTimeout:5e3});let n,o=!1;e.on("error",t=>{n=t,o&&!this.down&&(this.down=!0,this.logger.warn(`${this.name}: connection lost: ${errorText(t)}`))}),e.on("ready",()=>{this.down&&this.logger.info(`${this.name}: connection back`),this.down=!1});try{await e.connect(),o=!0}catch(t){throw e.disconnect(),new Error(`${this.name}: cannot connect: ${errorText(n??t)}`)}return e}key(t){return`${this.prefix}${t}`}async load(t){try{const e=await(await this.client()).get(this.key(t));return null===e?null:ValkeyStore.decodeRecord(e)}catch(t){return this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,e){const n=Math.ceil(e.deadline-Date.now());try{const o=await this.client();if(n<=0)return void await o.del(this.key(t));await o.set(this.key(t),ValkeyStore.encodeRecord(t,e),"PX",n)}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),t}}async drop(t){try{await(await this.client()).del(this.key(t))}catch(t){this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)}}async sweep(){return 0}}export{ValkeyStore as default};
|
|
1
|
+
import t from"../../lib/storage/store.js";import{outsideRequest as e}from"../../lib/requestContext.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t);class ValkeyStore extends t{constructor(t,e,n){super(t),this.name="storage/valkey",this.down=!1,this.url=e,this.prefix=n}async driver(){return import("iovalkey")}async open(){await this.client()}async close(){await super.close();const t=this.connection;this.connection=void 0;const e=await(t?.catch(()=>{}));e&&await e.quit().catch(()=>e.disconnect())}client(){return this.connection??(this.connection=e(()=>this.connect()).catch(t=>{throw this.connection=void 0,t})),this.connection}async connect(){let t;try{t=await this.driver()}catch(t){throw new Error(`${this.name}: requires the optional dependency "iovalkey", which is not installed. Install it to keep the records in Valkey, or choose another --storage-backend. (${errorText(t)})`)}const e=new t.Valkey(this.url,{lazyConnect:!0,enableOfflineQueue:!1,maxRetriesPerRequest:1,commandTimeout:5e3});let n,o=!1;e.on("error",t=>{n=t,o&&!this.down&&(this.down=!0,this.logger.warn(`${this.name}: connection lost: ${errorText(t)}`))}),e.on("ready",()=>{this.down&&this.logger.info(`${this.name}: connection back`),this.down=!1});try{await e.connect(),o=!0}catch(t){throw e.disconnect(),new Error(`${this.name}: cannot connect: ${errorText(n??t)}`)}return e}key(t){return`${this.prefix}${t}`}async load(t){try{const e=await(await this.client()).get(this.key(t));return null===e?null:ValkeyStore.decodeRecord(e)}catch(t){return this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,e){const n=Math.ceil(e.deadline-Date.now());try{const o=await this.client();if(n<=0)return void await o.del(this.key(t));await o.set(this.key(t),ValkeyStore.encodeRecord(t,e),"PX",n)}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),t}}async drop(t){try{await(await this.client()).del(this.key(t))}catch(t){this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)}}async sweep(){return 0}}export{ValkeyStore as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomInt as t}from"crypto";import r from"../../abstract/plugin.js";import{wantJson as s}from"../../lib/expressFormatedResponses.js";import{
|
|
1
|
+
import{randomInt as t}from"crypto";import r from"../../abstract/plugin.js";import{wantJson as s}from"../../lib/expressFormatedResponses.js";import{HttpError as e,errorBody as i,AUTHZ_REFUSED as a}from"../../lib/errors.js";import{escapeLdapFilter as o,isDnInBranch as n,escapeDnValue as c,isAuthzRefusal as u}from"../../lib/utils.js";const answeredRefusal=(t,r)=>u(t)?(r.status(403).json(i(t,a)),!0):t instanceof e&&t.statusCode<500&&(r.status(t.statusCode).json(i(t,t.message)),!0);class AppAccountsApi extends r{constructor(t){if(super(t),this.name="appAccountsApi",this.roles=["api","configurable"],this.dependencies={appAccountsConsistency:"core/twake/appAccountsConsistency"},this.applicativeAccountBase=this.config.applicative_account_base,this.maxAppAccounts=this.config.max_app_accounts||5,this.mailAttr=this.config.mail_attribute||"mail",this.userAttr=this.config.app_accounts_user_attribute||this.mailAttr,!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}, max_app_accounts=${this.maxAppAccounts}, user_attribute=${this.userAttr}`)}api(t){const r=this.config.api_prefix||"/api";t.get(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.listAccounts(t,r)),t.post(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.createAccount(t,r)),t.delete(`${r}/v1/users/:user/app-accounts/:uid`,(t,r)=>this.deleteAccount(t,r)),this.logger.info(`AppAccounts API registered at ${r}/v1/users/:user/app-accounts`)}async resolvePrincipal(t,r,s,e){let i=((await this.server.ldap.system.search({scope:"sub",filter:`(${this.userAttr}=${o(t)})`,paged:!1,...e?{attributes:e}:{}},this.config.ldap_base||"")).searchEntries||[]).filter(t=>!n(t.dn,this.applicativeAccountBase));const a=[];for(const t of i)await this.server.ldap.readableBy(String(t.dn),r)&&a.push(t);return i=a,0===i.length?(s.status(404).json({error:`User ${t} not found`}),null):i.length>1?(this.logger.error(`${this.name}: Ambiguous principal lookup for ${t}: ${i.length} entries share ${this.userAttr}`),s.status(409).json({error:`Multiple users share ${this.userAttr} ${t}`}),null):i[0]}async listAccounts(t,r){if(!s(t,r))return;const e=t.params.user;try{const s=await this.resolvePrincipal(e,t,r,[this.mailAttr]);if(!s)return;const i=s[this.mailAttr];if(!i)return void r.status(400).json({error:`User ${e} has no mail attribute`});const a=Array.isArray(i)?String(i[0]):String(i),n=await this.server.ldap.system.search({scope:"sub",filter:`(${this.mailAttr}=${o(a)})`,paged:!1},this.applicativeAccountBase),c=(n.searchEntries||[]).filter(t=>{const r=t.uid;return(Array.isArray(r)?String(r[0]):String(r)).toLowerCase()!==a.toLowerCase()}).map(t=>{const r=t.uid,s=Array.isArray(r)?String(r[0]):String(r),e=t.description,i=e?Array.isArray(e)?String(e[0]):String(e):void 0;return{uid:s,...i?{name:i}:{}}}).sort((t,r)=>t.uid.localeCompare(r.uid));r.json(c)}catch(t){this.logger.error(`${this.name}: Failed to list accounts for ${e}:`,t),r.status(500).json({error:"Internal server error"})}}async createAccount(t,r){if(!s(t,r))return;const e=t.params.user,{name:i}=t.body||{};try{const s=await this.resolvePrincipal(e,t,r);if(!s)return;const a=s[this.mailAttr];if(!a)return void r.status(400).json({error:`User ${e} has no mail attribute`});const n=Array.isArray(a)?String(a[0]):String(a),u=this.appUidPrefix(e),p=(await this.server.ldap.system.search({scope:"sub",filter:`(${this.mailAttr}=${o(n)})`,paged:!1},this.applicativeAccountBase)).searchEntries||[];if(p.filter(t=>{const r=t.uid;return(Array.isArray(r)?String(r[0]):String(r)).toLowerCase()!==n.toLowerCase()}).length>=this.maxAppAccounts)return void r.status(400).json({error:`Maximum number of accounts (${this.maxAppAccounts}) reached`});let l,h=0;do{if(l=`${u}_${this.generateAccountId()}`,h++,h>100)throw new Error("Failed to generate unique account ID after 100 attempts")}while(await this.uidExists(l));const d=this.generatePassword(),A={objectClass:["inetOrgPerson"],uid:l,userPassword:d},g=[...new Set([...this.config.applicative_account_attribute||[],this.mailAttr])].filter(t=>"objectClass"!==t&&"description"!==t);for(const t of g)if(s[t]){const r=s[t];Array.isArray(r)?A[t]=r.map(t=>Buffer.isBuffer(t)?t.toString():String(t)):A[t]=Buffer.isBuffer(r)?r.toString():String(r)}i&&(A.description=i);const f=`uid=${c(l)},${this.applicativeAccountBase}`;await this.server.ldap.add(f,A,t),this.logger.info(`${this.name}: Created applicative account ${f} for user ${e}`);const m=`uid=${c(n)},${this.applicativeAccountBase}`;try{await this.server.ldap.modify(m,{add:{userPassword:d}},t)}catch(s){this.logger.error(`${this.name}: Failed to add password to principal account ${m}:`,s);try{await this.server.ldap.delete(f,t)}catch(t){this.logger.error(`${this.name}: Failed to roll back applicative account ${f}:`,t)}return void r.status(500).json({error:"Failed to register the password on the principal account"})}r.json({uid:l,pwd:d,mail:n})}catch(t){if(answeredRefusal(t,r))return;this.logger.error(`${this.name}: Failed to create account for ${e}:`,t),r.status(500).json({error:"Internal server error"})}}appUidPrefix(t){return t.replace(/[^A-Za-z0-9_-]/g,"_")}async uidExists(t){return((await this.server.ldap.system.search({scope:"sub",filter:`(uid=${o(t)})`,paged:!1,attributes:["uid"]},this.applicativeAccountBase)).searchEntries||[]).length>0}async deleteAccount(t,r){if(!s(t,r))return;const i=t.params.user,a=t.params.uid;try{await this.server.ldap.authorize("ldapdeleterequest",[`uid=${c(a)},${this.applicativeAccountBase}`,t]);const s=await this.server.ldap.system.search({scope:"sub",filter:`(uid=${o(a)})`,paged:!1,attributes:["uid",this.mailAttr,"userPassword"]},this.applicativeAccountBase),n=s.searchEntries?.[0];if(!n)return this.logger.warn(`${this.name}: Account ${a} already deleted or not found`),void r.json({uid:a});const p=n[this.mailAttr],l=p?Array.isArray(p)?String(p[0]):String(p):null;let h=i;if(this.userAttr!==this.mailAttr){const s=await this.resolvePrincipal(i,t,r,[this.mailAttr]);if(!s)return;const e=s[this.mailAttr],a=e?Array.isArray(e)?String(e[0]):String(e):null;if(!a)return void r.status(400).json({error:`User ${i} has no mail attribute`});h=a}if(!l||l.toLowerCase()!==h.toLowerCase())return void r.status(403).json({error:`Account ${a} does not belong to user ${i}`});const d=n.userPassword;if(!d)return this.logger.error(`${this.name}: Account ${a} has no readable userPassword, refusing to delete: the bind DN needs read access on userPassword in the applicative branch`),void r.status(500).json({error:`Cannot revoke account ${a}: its password is not readable`});const A=`uid=${c(l)},${this.applicativeAccountBase}`;try{const r=Array.isArray(d)?d[0]:d;await this.server.ldap.modify(A,{delete:{userPassword:r}},t)}catch(t){const s=String(t.message??"");if(16!==t.code&&!/no ?such ?attribute/i.test(s))return u(t)||t instanceof e&&403===t.statusCode?void answeredRefusal(t,r):(this.logger.error(`${this.name}: Failed to delete password from principal account ${A}:`,t),void r.status(500).json({error:`Failed to revoke the password of account ${a}`}));this.logger.warn(`${this.name}: Password of ${a} was already absent from principal account ${A}, resuming deletion`)}const g=n.dn;await this.server.ldap.delete(g,t),this.logger.info(`${this.name}: Deleted applicative account ${g}`),r.json({uid:a})}catch(t){if(answeredRefusal(t,r))return;this.logger.error(`${this.name}: Failed to delete account ${a} for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}generateAccountId(){return`c${t(0,1e8).toString().padStart(8,"0")}`}generatePassword(){const r="ABCDEFGHJKMNPQRSTUVWXYZ",s="abcdefghjkmnpqrstuvwxyz",e="23456789",i="@!#$%",a=[];for(let o=0;o<6;o++){const o=t(0,23),n=t(0,23),c=t(0,8),u=t(0,5),p=[r[o],s[n],e[c],i[u]];for(let r=p.length-1;r>0;r--){const s=t(0,r+1);[p[r],p[s]]=[p[s],p[r]]}a.push(p.join(""))}return a.join("-")}getConfigApiData(){const t=this.config.api_prefix||"/api";return{enabled:!0,base:this.applicativeAccountBase,maxAccounts:this.maxAppAccounts,mailAttribute:this.mailAttr,endpoints:{list:`${t}/v1/users/:user/app-accounts`,create:`${t}/v1/users/:user/app-accounts`,delete:`${t}/v1/users/:user/app-accounts/:uid`}}}}export{AppAccountsApi as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../../abstract/plugin.js";import{isDnInBranch as e,escapeDnValue as i,escapeLdapFilter as a}from"../../lib/utils.js";function isNoSuchObject(t){if("object"!=typeof t||null===t)return!1;const{code:e,name:i,message:a}=t;return 32===e||"NoSuchObjectError"===i||"string"==typeof a&&a.includes("Code: 0x20")}class AppAccountsConsistency extends t{constructor(t){super(t),this.name="appAccountsConsistency",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange"},this.hooks={onLdapMailChange:async(t,e,i)=>{if(this.isInApplicativeBranch(t))this.logger.debug(`${this.name}: Ignoring mail change event originating in the applicative branch`);else try{const a=null!=e?Array.isArray(e)?String(e[0]):String(e):null,
|
|
1
|
+
import t from"../../abstract/plugin.js";import{isDnInBranch as e,escapeDnValue as i,escapeLdapFilter as a}from"../../lib/utils.js";function isNoSuchObject(t){if("object"!=typeof t||null===t)return!1;const{code:e,name:i,message:a}=t;return 32===e||"NoSuchObjectError"===i||"string"==typeof a&&a.includes("Code: 0x20")}class AppAccountsConsistency extends t{constructor(t){super(t),this.name="appAccountsConsistency",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange"},this.hooks={onLdapMailChange:async(t,e,i)=>{if(this.isInApplicativeBranch(t))this.logger.debug(`${this.name}: Ignoring mail change event originating in the applicative branch`);else try{const a=null!=e?Array.isArray(e)?String(e[0]):String(e):null,s=null!=i?Array.isArray(i)?String(i[0]):String(i):null;if(!a&&s)return void await this.createApplicativeAccount(t,s);if(a&&!s)return void await this.deleteApplicativeAccount(a);if(a&&s&&a!==s)return void await this.updateApplicativeAccount(t,a,s)}catch(e){this.logger.error(`${this.name}: Failed to handle mail change for ${t}:`,e)}}},this.mailAttr=this.config.mail_attribute||"mail",this.applicativeAccountBase=this.config.applicative_account_base,this.operationalAttributes=this.config.ldap_operational_attribute||[];const e=this.config.applicative_account_attribute||[],i=["objectClass",this.mailAttr],a=i.filter(t=>!e.includes(t));if(e.length>0&&a.length>0&&this.logger.warn(`${this.name}: applicative_account_attribute omits ${a.join(", ")} — copied anyway, an applicative entry cannot be created without them`),this.copiedAttributes=new Set([...e,...i]),!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}`)}shouldExcludeAttribute(t){return"dn"===t||this.operationalAttributes.includes(t)}async deleteApplicativeEntry(t){try{await this.server.ldap.system.delete(t),this.logger.info(`${this.name}: Deleted applicative account ${t}`)}catch(e){if(isNoSuchObject(e))return void this.logger.debug(`${this.name}: Applicative account ${t} already deleted`);throw e}}pickCopiedAttributes(t){const e={};for(const[i,a]of Object.entries(t))this.copiedAttributes.has(i)&&null!=a&&(Array.isArray(a)&&0===a.length||(e[i]=a));return e}isInApplicativeBranch(t){return e(t,this.applicativeAccountBase)}async createApplicativeAccount(t,e){const a=`uid=${i(e)},${this.applicativeAccountBase}`;try{const i=await this.server.ldap.system.search({scope:"base",paged:!1},t),s=i.searchEntries?.[0];if(!s)return void this.logger.warn(`${this.name}: Could not find user ${t} to create applicative account`);const c=this.pickCopiedAttributes(s);c.uid=e,await this.server.ldap.system.add(a,c),this.logger.info(`${this.name}: Created applicative account ${a} for user ${t}`)}catch(e){if(68===e.code||e.message?.includes("AlreadyExists"))return void this.logger.debug(`${this.name}: Applicative account ${a} already exists`);this.logger.error(`${this.name}: Failed to create applicative account for ${t}:`,e)}}async deleteApplicativeAccount(t){try{const e=`(${this.mailAttr}=${a(t)})`,i=(await this.server.ldap.system.search({filter:e,scope:"sub",paged:!1},this.applicativeAccountBase)).searchEntries||[];if(0===i.length)return void this.logger.debug(`${this.name}: No applicative accounts found for mail ${t}`);for(const t of i){const e=t.dn;try{await this.deleteApplicativeEntry(e)}catch(t){this.logger.error(`${this.name}: Failed to delete applicative account ${e}:`,t)}}}catch(e){if(isNoSuchObject(e))return void this.logger.debug(`${this.name}: Applicative account base does not exist or no accounts found for mail ${t}`);this.logger.error(`${this.name}: Failed to delete applicative account for mail ${t}:`,e)}}async updateApplicativeAccount(t,e,s){try{const c=`(${this.mailAttr}=${a(e)})`,r=(await this.server.ldap.system.search({filter:c,scope:"sub",paged:!1},this.applicativeAccountBase)).searchEntries||[];if(0===r.length)return void this.logger.debug(`${this.name}: No applicative account found for old mail ${e}`);for(const a of r){const c=a.dn,r=Array.isArray(a.uid)?String(a.uid[0]):String(a.uid);if(!(r===e)){await this.deleteApplicativeEntry(c);continue}const n=`uid=${i(s)},${this.applicativeAccountBase}`;try{const i={};for(const[t,e]of Object.entries(a))this.shouldExcludeAttribute(t)||null!=e&&(Array.isArray(e)&&0===e.length||(i[t]=e));await this.deleteApplicativeEntry(c);const r={...i},o=await this.server.ldap.system.search({scope:"base",paged:!1},t),l=o.searchEntries?.[0];if(!l)return void this.logger.warn(`${this.name}: Could not find user ${t} to update applicative account`);Object.assign(r,this.pickCopiedAttributes(l)),r.uid=s,await this.server.ldap.system.add(n,r),this.logger.info(`${this.name}: Created new applicative account ${n} (mail changed from ${e} to ${s})`)}catch(t){if(68===t.code||t.message?.includes("AlreadyExists"))return void this.logger.debug(`${this.name}: Applicative account ${n} already exists after mail change`);this.logger.error(`${this.name}: Failed to update applicative account for mail change:`,t)}}}catch(e){this.logger.error(`${this.name}: Failed to update applicative account for ${t}:`,e)}}}export{AppAccountsConsistency as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{setTimeout as e}from"node:timers/promises";import t from"node-fetch";import r from"../../abstract/plugin.js";import{instanceSlug as i}from"../../lib/instanceProviders.js";import{escapeDnValue as s,isChildOf as n}from"../../lib/utils.js";import{BaseResolver as a}from"../scim/baseResolver.js";const o=new Set(["admin","owner","member"]),l=1e3;class ClouderyProvision extends r{constructor(e){super(e),this.name="clouderyProvision",this.roles=["consistency"],this.dependencies={rabbitmq:"core/rabbitmq"},this.createStash=new Map,this.deleteStash=new Map,this.hooks={scimusercreate:e=>(this.normalizeUserName(e[0]),this.captureCreate(e[0],e[1]),e),scimusercreatedone:async e=>{await this.provision(e)},scimuserdelete:async e=>(e[0]=e[0].toLowerCase(),await this.captureDelete(e[0],e[1]),e),scimuserdeletedone:async e=>{await this.deprovision(e)}};const t=this.config;this.managerUrl=(t.cloudery_manager_url||"").replace(/\/$/,""),this.managerToken=t.cloudery_manager_token||"",this.offer=t.cloudery_offer||"b2b_twake_default",this.clouderyDomain=t.cloudery_domain||"",this.userBranch=t.cloudery_user_branch||"",this.orgIdHeader=(t.cloudery_org_id_header||"x-cloudery-org-id").toLowerCase(),this.orgRoleHeader=(t.cloudery_org_role_header||"x-cloudery-org-role").toLowerCase();const r=t.cloudery_default_org_role||"member",i=r.trim().toLowerCase();o.has(i)?this.defaultOrgRole=i:(this.defaultOrgRole="member",this.logger.warn(`${this.name}: cloudery_default_org_role "${r}" is not one of ${[...o].join(", ")} — using "member"`)),this.fqdnAttribute=t.cloudery_fqdn_attribute||"twakeWorkspaceUrl",this.orgIdAttribute=t.cloudery_org_id_attribute||"twakeOrganizationId",this.orgRoleAttribute=t.cloudery_org_role_attribute||"twakeOrganizationRole",this.phonesAttribute=t.cloudery_phones_attribute||"twakePhones",this.invitedAttribute=t.cloudery_invited_attribute||"twakeInvited",this.defaultLocale=t.cloudery_default_locale||"en",this.rdnAttribute=t.scim_user_rdn_attribute||"uid",this.authExchange=t.cozy_auth_exchange||"auth",this.b2bExchange=t.cozy_b2b_exchange||"b2b",this.userCreatedRoutingKey=t.cozy_user_created_routing_key||"user.created",this.userDeletedRoutingKey=t.cozy_user_deleted_routing_key??"domain.user.deleted",this.pollIntervalMs=Number(t.cloudery_workflow_poll_interval_ms)||2e3,this.maxPollAttempts=Number(t.cloudery_workflow_max_attempts)||60,this.baseResolver=new a(this.config),this.managerUrl||this.logger.warn(`${this.name}: cloudery_manager_url is empty — instance provisioning will be skipped`),this.userBranch||this.logger.warn(`${this.name}: cloudery_user_branch is empty — no users will be provisioned`),this.logger.info({plugin:this.name,event:"init",managerUrl:this.managerUrl||void 0,userBranch:this.userBranch||void 0,orgIdHeader:this.orgIdHeader,orgRoleHeader:this.orgRoleHeader,defaultOrgRole:this.defaultOrgRole,fqdnAttribute:this.fqdnAttribute,orgIdAttribute:this.orgIdAttribute,orgRoleAttribute:this.orgRoleAttribute,phonesAttribute:this.phonesAttribute,invitedAttribute:this.invitedAttribute,pollIntervalMs:this.pollIntervalMs,maxPollAttempts:this.maxPollAttempts})}captureCreate(e,t){this.pruneStash();const r=this.extractId(e),i=this.baseResolver.userBase(t);if(this.logger.info({plugin:this.name,event:"captureCreate",userName:r||void 0,base:i,hasManagerUrl:Boolean(this.managerUrl),hasUserBranch:Boolean(this.userBranch)}),!this.managerUrl||!this.userBranch)return void this.logger.info({plugin:this.name,event:"captureCreate",userName:r||void 0,base:i,result:"skipped",reason:"missing_configuration"});if(!r)return void this.logger.info({plugin:this.name,event:"captureCreate",base:i,result:"skipped",reason:"missing_user_name"});if(!this.isAtOrUnder(i,this.userBranch))return void this.logger.info({plugin:this.name,event:"captureCreate",userName:r,base:i,result:"skipped",reason:"outside_b2b_branch"});const s=this.readHeader(t,this.orgIdHeader);if(!s)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:`B2B branch create without ${this.orgIdHeader} — skipping provisioning`});const n=this.extractPrimaryEmail(e);if(!n)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:"B2B branch create without a primary email — skipping provisioning"});const a=n.trim(),o=this.resolveRole(t,r);this.createStash.set(a.toLowerCase(),{orgId:s.trim(),email:a,role:o,base:i,ts:Date.now()}),this.logger.info({plugin:this.name,event:"captureCreate",userName:r,base:i,orgId:s.trim(),email:a,role:o,result:"stashed"})}async provision(e){const t=this.extractId(e);if(this.logger.info({plugin:this.name,event:"provision",userName:t||void 0}),!t)return void this.logger.info({plugin:this.name,event:"provision",result:"skipped",reason:"missing_user_name"});const r=this.extractPrimaryEmail(e);if(!r)return void this.logger.info({plugin:this.name,event:"provision",userName:t,result:"skipped",reason:"missing_primary_email"});const n=r.trim().toLowerCase(),a=this.createStash.get(n);if(!a)return void this.logger.info({plugin:this.name,event:"provision",userName:t,email:r.trim(),result:"skipped",reason:"no_stashed_context"});this.createStash.delete(n),this.logger.info({plugin:this.name,event:"provision",userName:t,email:a.email,orgId:a.orgId,base:a.base,role:a.role,result:"context_loaded"});const o=a.email,l=emailDomain(o),u=i(t+a.orgId);if(!u)return void this.logger.error({plugin:this.name,event:"provision",userName:t,result:"no_slug"});const h=this.extractPrimaryPhone(e),g=this.extractPhones(e),d=this.extractPublicName(e)||t,c={plugin:this.name,event:"provision",userName:t,orgId:a.orgId,slug:u},m=`${u}.${this.clouderyDomain}`.toLowerCase();let p=null;try{p=await this.findInstanceByFqdn(m)}catch(e){this.logger.warn({...c,result:"existence_lookup_failed",fqdn:m,error:`${e}`})}if(p&&(p.internal_email&&p.internal_email.toLowerCase()!==o.toLowerCase()||p.oidc&&p.oidc.toLowerCase()!==u))return void this.logger.error({...c,result:"slug_taken",fqdn:m,uuid:p._id});const f=p?._id??null;let w;if(f)this.logger.info({...c,result:"instance_exists",fqdn:m,uuid:f}),w=m;else{let t;try{this.logger.info({...c,result:"creating_instance",orgDomain:l,email:o,mobile:h||void 0}),t=await this.createInstance({email:o,publicName:d,locale:this.extractLocale(e),oidc:u,phone:h||"",slug:u,public_name:d,offer:this.offer,domain:this.clouderyDomain,skip_email_validation:!0,internal_email:o,org_domain:l,org_id:a.orgId})}catch(e){return void this.logger.error({...c,result:"error",error:`${e}`})}if(!(!t.workflow||await this.waitForWorkflow(t.workflow)))return void this.logger.error({...c,result:"workflow_failed",workflow:t.workflow});w=t.fqdn}const v=`${this.rdnAttribute}=${s(t)},${a.base}`,b={[this.fqdnAttribute]:w,[this.orgIdAttribute]:a.orgId,[this.orgRoleAttribute]:a.role,[this.invitedAttribute]:"TRUE",cn:t};g.length>0&&(b[this.phonesAttribute]=JSON.stringify(g));try{this.logger.info({...c,result:"writing_fqdn",dn:v,hasPhones:g.length>0}),await this.server.ldap.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
|
+
import{setTimeout as e}from"node:timers/promises";import t from"node-fetch";import r from"../../abstract/plugin.js";import{instanceSlug as i}from"../../lib/instanceProviders.js";import{escapeDnValue as s,isChildOf as n}from"../../lib/utils.js";import{BaseResolver as a}from"../scim/baseResolver.js";const o=new Set(["admin","owner","member"]),l=1e3;class ClouderyProvision extends r{constructor(e){super(e),this.name="clouderyProvision",this.roles=["consistency"],this.dependencies={rabbitmq:"core/rabbitmq"},this.createStash=new Map,this.deleteStash=new Map,this.hooks={scimusercreate:e=>(this.normalizeUserName(e[0]),this.captureCreate(e[0],e[1]),e),scimusercreatedone:async e=>{await this.provision(e)},scimuserdelete:async e=>(e[0]=e[0].toLowerCase(),await this.captureDelete(e[0],e[1]),e),scimuserdeletedone:async e=>{await this.deprovision(e)}};const t=this.config;this.managerUrl=(t.cloudery_manager_url||"").replace(/\/$/,""),this.managerToken=t.cloudery_manager_token||"",this.offer=t.cloudery_offer||"b2b_twake_default",this.clouderyDomain=t.cloudery_domain||"",this.userBranch=t.cloudery_user_branch||"",this.orgIdHeader=(t.cloudery_org_id_header||"x-cloudery-org-id").toLowerCase(),this.orgRoleHeader=(t.cloudery_org_role_header||"x-cloudery-org-role").toLowerCase();const r=t.cloudery_default_org_role||"member",i=r.trim().toLowerCase();o.has(i)?this.defaultOrgRole=i:(this.defaultOrgRole="member",this.logger.warn(`${this.name}: cloudery_default_org_role "${r}" is not one of ${[...o].join(", ")} — using "member"`)),this.fqdnAttribute=t.cloudery_fqdn_attribute||"twakeWorkspaceUrl",this.orgIdAttribute=t.cloudery_org_id_attribute||"twakeOrganizationId",this.orgRoleAttribute=t.cloudery_org_role_attribute||"twakeOrganizationRole",this.phonesAttribute=t.cloudery_phones_attribute||"twakePhones",this.invitedAttribute=t.cloudery_invited_attribute||"twakeInvited",this.defaultLocale=t.cloudery_default_locale||"en",this.rdnAttribute=t.scim_user_rdn_attribute||"uid",this.authExchange=t.cozy_auth_exchange||"auth",this.b2bExchange=t.cozy_b2b_exchange||"b2b",this.userCreatedRoutingKey=t.cozy_user_created_routing_key||"user.created",this.userDeletedRoutingKey=t.cozy_user_deleted_routing_key??"domain.user.deleted",this.pollIntervalMs=Number(t.cloudery_workflow_poll_interval_ms)||2e3,this.maxPollAttempts=Number(t.cloudery_workflow_max_attempts)||60,this.baseResolver=new a(this.config),this.managerUrl||this.logger.warn(`${this.name}: cloudery_manager_url is empty — instance provisioning will be skipped`),this.userBranch||this.logger.warn(`${this.name}: cloudery_user_branch is empty — no users will be provisioned`),this.logger.info({plugin:this.name,event:"init",managerUrl:this.managerUrl||void 0,userBranch:this.userBranch||void 0,orgIdHeader:this.orgIdHeader,orgRoleHeader:this.orgRoleHeader,defaultOrgRole:this.defaultOrgRole,fqdnAttribute:this.fqdnAttribute,orgIdAttribute:this.orgIdAttribute,orgRoleAttribute:this.orgRoleAttribute,phonesAttribute:this.phonesAttribute,invitedAttribute:this.invitedAttribute,pollIntervalMs:this.pollIntervalMs,maxPollAttempts:this.maxPollAttempts})}captureCreate(e,t){this.pruneStash();const r=this.extractId(e),i=this.baseResolver.userBase(t);if(this.logger.info({plugin:this.name,event:"captureCreate",userName:r||void 0,base:i,hasManagerUrl:Boolean(this.managerUrl),hasUserBranch:Boolean(this.userBranch)}),!this.managerUrl||!this.userBranch)return void this.logger.info({plugin:this.name,event:"captureCreate",userName:r||void 0,base:i,result:"skipped",reason:"missing_configuration"});if(!r)return void this.logger.info({plugin:this.name,event:"captureCreate",base:i,result:"skipped",reason:"missing_user_name"});if(!this.isAtOrUnder(i,this.userBranch))return void this.logger.info({plugin:this.name,event:"captureCreate",userName:r,base:i,result:"skipped",reason:"outside_b2b_branch"});const s=this.readHeader(t,this.orgIdHeader);if(!s)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:`B2B branch create without ${this.orgIdHeader} — skipping provisioning`});const n=this.extractPrimaryEmail(e);if(!n)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:"B2B branch create without a primary email — skipping provisioning"});const a=n.trim(),o=this.resolveRole(t,r);this.createStash.set(a.toLowerCase(),{orgId:s.trim(),email:a,role:o,base:i,ts:Date.now()}),this.logger.info({plugin:this.name,event:"captureCreate",userName:r,base:i,orgId:s.trim(),email:a,role:o,result:"stashed"})}async provision(e){const t=this.extractId(e);if(this.logger.info({plugin:this.name,event:"provision",userName:t||void 0}),!t)return void this.logger.info({plugin:this.name,event:"provision",result:"skipped",reason:"missing_user_name"});const r=this.extractPrimaryEmail(e);if(!r)return void this.logger.info({plugin:this.name,event:"provision",userName:t,result:"skipped",reason:"missing_primary_email"});const n=r.trim().toLowerCase(),a=this.createStash.get(n);if(!a)return void this.logger.info({plugin:this.name,event:"provision",userName:t,email:r.trim(),result:"skipped",reason:"no_stashed_context"});this.createStash.delete(n),this.logger.info({plugin:this.name,event:"provision",userName:t,email:a.email,orgId:a.orgId,base:a.base,role:a.role,result:"context_loaded"});const o=a.email,l=emailDomain(o),u=i(t+a.orgId);if(!u)return void this.logger.error({plugin:this.name,event:"provision",userName:t,result:"no_slug"});const h=this.extractPrimaryPhone(e),g=this.extractPhones(e),d=this.extractPublicName(e)||t,c={plugin:this.name,event:"provision",userName:t,orgId:a.orgId,slug:u},m=`${u}.${this.clouderyDomain}`.toLowerCase();let p=null;try{p=await this.findInstanceByFqdn(m)}catch(e){this.logger.warn({...c,result:"existence_lookup_failed",fqdn:m,error:`${e}`})}if(p&&(p.internal_email&&p.internal_email.toLowerCase()!==o.toLowerCase()||p.oidc&&p.oidc.toLowerCase()!==u))return void this.logger.error({...c,result:"slug_taken",fqdn:m,uuid:p._id});const f=p?._id??null;let w;if(f)this.logger.info({...c,result:"instance_exists",fqdn:m,uuid:f}),w=m;else{let t;try{this.logger.info({...c,result:"creating_instance",orgDomain:l,email:o,mobile:h||void 0}),t=await this.createInstance({email:o,publicName:d,locale:this.extractLocale(e),oidc:u,phone:h||"",slug:u,public_name:d,offer:this.offer,domain:this.clouderyDomain,skip_email_validation:!0,internal_email:o,org_domain:l,org_id:a.orgId})}catch(e){return void this.logger.error({...c,result:"error",error:`${e}`})}if(!(!t.workflow||await this.waitForWorkflow(t.workflow)))return void this.logger.error({...c,result:"workflow_failed",workflow:t.workflow});w=t.fqdn}const v=`${this.rdnAttribute}=${s(t)},${a.base}`,b={[this.fqdnAttribute]:w,[this.orgIdAttribute]:a.orgId,[this.orgRoleAttribute]:a.role,[this.invitedAttribute]:"TRUE",cn:t};g.length>0&&(b[this.phonesAttribute]=JSON.stringify(g));try{this.logger.info({...c,result:"writing_fqdn",dn:v,hasPhones:g.length>0}),await this.server.ldap.system.modify(v,{replace:b},void 0,{declaredClasses:this.config.user_class}),this.logger.info({...c,result:"written_fqdn",dn:v})}catch(e){this.logger.error({...c,event:"writeFqdn",dn:v,error:`${e}`})}await this.publishUserCreated({twakeId:t,orgDomain:l,orgId:a.orgId,fqdn:w,email:o,mobile:h}),this.logger.info({...c,result:"success",fqdn:w})}async captureDelete(e,t){const r=this.baseResolver.userBase(t);if(this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,hasManagerUrl:Boolean(this.managerUrl),hasUserBranch:Boolean(this.userBranch)}),!this.managerUrl||!this.userBranch)return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,result:"skipped",reason:"missing_configuration"});if(!this.isAtOrUnder(r,this.userBranch))return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,result:"skipped",reason:"outside_b2b_branch"});const i=`${this.rdnAttribute}=${s(e)},${r}`;try{this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:i,result:"searching"});const t=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[this.fqdnAttribute,"mail"]},i),r=t.searchEntries?.[0],s=r?firstValue(r[this.fqdnAttribute]):void 0,n=s?s.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i,"").split("/")[0]:void 0;if(!n)return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:i,result:"skipped",reason:"missing_fqdn"});const a=r?firstValue(r.mail):void 0,o=a?emailDomain(a):"";this.deleteStash.set(e,{fqdn:n,domain:o}),this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:i,fqdn:n,domain:o,result:"stashed"})}catch(t){this.logger.warn({plugin:this.name,event:"captureDelete",id:e,error:`${t}`})}}async deprovision(e){this.logger.info({plugin:this.name,event:"deprovision",id:e});const t=this.deleteStash.get(e);if(!t)return void this.logger.info({plugin:this.name,event:"deprovision",id:e,result:"skipped",reason:"no_stashed_context"});this.deleteStash.delete(e);const r={plugin:this.name,event:"deprovision",id:e,fqdn:t.fqdn};let i=!1;try{this.logger.info({...r,result:"resolving_uuid"});const e=(await this.findInstanceByFqdn(t.fqdn,"_id"))?._id;e?(this.logger.info({...r,result:"deleting_instance",uuid:e}),await this.deleteInstance(e),i=!0,this.logger.info({...r,result:"deleted",uuid:e})):this.logger.warn({...r,result:"not_found"})}catch(e){this.logger.error({...r,result:"error",error:`${e}`})}i&&await this.publishUserDeleted(t.fqdn,t.domain)}async createInstance(e){this.logger.info({plugin:this.name,event:"createInstance",url:`${this.managerUrl}/api/v1/instances`});const r=await t(`${this.managerUrl}/api/v1/instances`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.managerToken}`},body:JSON.stringify(e)});if(!r.ok){const e=(await r.text().catch(()=>"")).slice(0,l);throw this.logger.error({plugin:this.name,event:"createInstance",result:"error",http_status:r.status,http_status_text:r.statusText,body:e||void 0}),new Error(`Cloudery createInstance HTTP ${r.status}`)}const i=await r.json();return this.logger.info({plugin:this.name,event:"createInstance",result:"success",id:i.id,fqdn:i.fqdn,workflow:i.workflow}),i}async waitForWorkflow(r){this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,maxPollAttempts:this.maxPollAttempts,pollIntervalMs:this.pollIntervalMs});for(let i=1;i<=this.maxPollAttempts;i++){try{const e=await t(`${this.managerUrl}/api/v1/workflows/${encodeURIComponent(r)}`,{headers:{Authorization:`Bearer ${this.managerToken}`}});if(e.ok){const t=await e.json();if(this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,status:t.status}),"succeeded"===t.status)return this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,result:"success"}),!0;if("failed"===t.status||"error"===t.status)return this.logger.warn({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,result:"failed",status:t.status}),!1}else{const t=(await e.text().catch(()=>"")).slice(0,l);this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,result:"http_error",http_status:e.status,http_status_text:e.statusText,body:t||void 0})}}catch(e){this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,error:`${e}`})}i<this.maxPollAttempts&&(this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,attempt:i,result:"retrying"}),await e(this.pollIntervalMs))}return this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:r,result:"timeout",maxPollAttempts:this.maxPollAttempts}),!1}async findInstanceByFqdn(e,r){this.logger.info({plugin:this.name,event:"findInstanceByFqdn",fqdn:e});const i=new URL(`${this.managerUrl}/api/v2/instances`);i.searchParams.set("fqdn",e),r&&i.searchParams.set("only",r),i.searchParams.set("limit","1");const s=await t(i.toString(),{headers:{Authorization:`Bearer ${this.managerToken}`}});if(!s.ok){const t=(await s.text().catch(()=>"")).slice(0,l);throw this.logger.error({plugin:this.name,event:"findInstanceByFqdn",fqdn:e,result:"error",http_status:s.status,http_status_text:s.statusText,body:t||void 0}),new Error(`Cloudery searchInstances HTTP ${s.status}`)}const n=await s.json(),a=n.items?.[0]??null,o=a?._id;return this.logger.info({plugin:this.name,event:"findInstanceByFqdn",fqdn:e,result:o?"found":"not_found",uuid:o||void 0}),a}async deleteInstance(e){const r=new URL(`${this.managerUrl}/api/v1/instances/${encodeURIComponent(e)}`);r.searchParams.set("user_request","true"),this.logger.info({plugin:this.name,event:"deleteInstance",uuid:e,url:r.toString()});const i=await t(r.toString(),{method:"DELETE",headers:{Authorization:`Bearer ${this.managerToken}`}});if(!i.ok){const t=(await i.text().catch(()=>"")).slice(0,l);throw this.logger.error({plugin:this.name,event:"deleteInstance",uuid:e,result:"error",http_status:i.status,http_status_text:i.statusText,body:t||void 0}),new Error(`Cloudery deleteInstance HTTP ${i.status}`)}this.logger.info({plugin:this.name,event:"deleteInstance",uuid:e,result:"success",http_status:i.status})}async publishUserCreated(e){const t=this.requirePlugin("rabbitmq");if(this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,orgId:e.orgId,orgDomain:e.orgDomain,hasRabbitmq:Boolean(t)}),!t)return void this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,result:"skipped",reason:"missing_rabbitmq"});const r={twakeId:e.twakeId,domain:e.orgDomain,organizationDomain:e.orgDomain,workplaceFqdn:e.fqdn,organizationId:e.orgId};e.email&&(r.internalEmail=e.email),e.mobile&&(r.mobile=e.mobile);try{await t.publish(this.authExchange,this.userCreatedRoutingKey,r),this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,exchange:this.authExchange,routingKey:this.userCreatedRoutingKey,result:"success"})}catch(t){this.logger.error({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,error:`${t}`})}}async publishUserDeleted(e,t){if(!this.userDeletedRoutingKey)return;const r=this.requirePlugin("rabbitmq");if(this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,domain:t,hasRabbitmq:Boolean(r)}),r)try{await r.publish(this.b2bExchange,this.userDeletedRoutingKey,{workplaceFqdn:e,domain:t}),this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,exchange:this.b2bExchange,routingKey:this.userDeletedRoutingKey,result:"success"})}catch(t){this.logger.error({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,error:`${t}`})}else this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,result:"skipped",reason:"missing_rabbitmq"})}readHeader(e,t){if(!e)return;const r=e.get?e.get(t):void 0;if("string"==typeof r&&r.length>0)return r;const i=e.query?.[t];return"string"==typeof i&&i.length>0?i:void 0}resolveRole(e,t){const r=this.readHeader(e,this.orgRoleHeader)?.trim().toLowerCase();return r?o.has(r)?(this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,input:r,result:"accepted",role:r}),r):(this.logger.warn({plugin:this.name,event:"captureCreate",userName:t,message:`Unknown ${this.orgRoleHeader} "${r}" — falling back to ${this.defaultOrgRole}`}),this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,input:r,result:"default",role:this.defaultOrgRole}),this.defaultOrgRole):(this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,result:"default",role:this.defaultOrgRole}),this.defaultOrgRole)}isAtOrUnder(e,t){return e.trim().toLowerCase()===t.trim().toLowerCase()||n(e,t)}pruneStash(){const e=Date.now()-3e5;for(const[t,r]of this.createStash)r.ts<e&&this.createStash.delete(t)}normalizeUserName(e){"string"==typeof e.userName&&(e.userName=e.userName.toLowerCase())}extractId(e){return"string"==typeof e.userName&&e.userName.length>0?e.userName:"string"==typeof e.id&&e.id.length>0?e.id:null}extractPrimaryEmail(e){const t=e.emails;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPrimaryPhone(e){const t=e.phoneNumbers;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPhones(e){const t=e.phoneNumbers;return t&&0!==t.length?t.filter(e=>"string"==typeof e.value&&e.value.trim().length>0).map(e=>({number:e.value.trim(),primary:Boolean(e.primary)})):[]}extractPublicName(e){if("string"==typeof e.displayName&&e.displayName.trim().length>0)return e.displayName.trim();const t=e.name?.formatted;if("string"==typeof t&&t.trim().length>0)return t.trim();const r=`${e.name?.givenName?.trim()??""} ${e.name?.familyName?.trim()??""}`.trim();return r.length>0?r:null}extractLocale(e){return"string"==typeof e.locale&&e.locale.length>0?e.locale:"string"==typeof e.preferredLanguage&&e.preferredLanguage.length>0?e.preferredLanguage:this.defaultLocale}}function emailDomain(e){const t=e.lastIndexOf("@");return t>=0?e.slice(t+1):""}function firstValue(e){if(Array.isArray(e)){const t=e[0];return"string"==typeof t?t:void 0}return"string"==typeof e?e:void 0}export{ClouderyProvision as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID as t}from"node:crypto";import{BadRequestError as e,AUTHZ_REFUSED as r,HttpError as s}from"../../lib/errors.js";import{extractLdapCode as o}from"../../lib/ldapCodes.js";import{parseDn as i,escapeDnValue as a,isDummyMemberDn as n,escapeLdapFilter as c,isAuthzRefusal as u,unescapeDnValue as p,normalizeDn as h}from"../../lib/utils.js";import d from"../ldap/groups.js";import{BaseResolver as m}from"../scim/baseResolver.js";import{mergeMapping as g,loadMappingFile as l,DEFAULT_GROUP_MAPPING as f}from"../scim/mapping.js";import{valueOf as w,lifecycleAttributes as y,first as b}from"./lifecycleAttributes.js";const read=(t,e)=>b(w(t,e)),_="{org}",O=["displayName","description","createdAt"],v=["uid","displayName","mail","jobTitle"],$={uid:"uid",_id:"entryUUID",cn:"cn",sn:"sn",givenName:"givenName",displayName:"displayName",mail:"mail",mobile:"mobile",jobTitle:"twakeJobTitle",company:"twakeCompany",organizationRole:"twakeOrganizationRole",organizationId:"twakeOrganizationId"},I={familyName:"sn",givenName:"givenName",additionalName:"twakeAdditionalName",namePrefix:"twakeNamePrefix"},R="twakeIsTechnical";class RouteError extends Error{constructor(t,e,r){super(e),this.status=t,this.code=r}}const notFound=t=>new RouteError(404,`${t[0].toUpperCase()}${t.slice(1)} not found`,`${t.toUpperCase()}_NOT_FOUND`),invalid=(t,e="INVALID_INPUT")=>new RouteError(400,t,e),N="name must be a non-blank string of at most 256 characters with no control characters";function isDisplayName(t){return"string"==typeof t&&t.trim().length>0&&t.length<=256&&!/[\x00-\x1f\x7f]/.test(t)}const j=/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/,isColor=t=>"string"==typeof t&&j.test(t),text=t=>"string"==typeof t||"number"==typeof t?String(t):"";function bodyOf(t){const e=t.body;if(!e||"object"!=typeof e||Array.isArray(e))throw invalid("Request body must be a JSON object");return e}function values(t){return null==t?[]:(Array.isArray(t)?t:[t]).map(t=>t.toString())}function dnKey(t){try{return h(t)}catch{return t.toLowerCase()}}function parentOf(t){return i(t).slice(1).join(",")}function rdnValue(t){const[e]=i(t),r=e.indexOf("=");return r>0?p(e.slice(r+1).trim()):t}function branchPattern(t){const[e,r]=i(t).join(",").split(_).map(t=>t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"));return new RegExp(`^${e}(?<org>(?:\\\\.|[^,])+)${r}$`,"i")}function movedInto(t,e,r){const s=organizationIn(t,r);return void 0!==s&&dnKey(parentOf(r))!==dnKey(parentOf(e))?s:void 0}const k=Symbol("whole member lists");function organizationIn(t,e){const r=t.exec(parentOf(i(e).join(",")));return r?.groups?p(r.groups.org):void 0}class TwakeGroups extends d{constructor(r){super(r),this.name="twakeGroups",this.hooks={ldapaddrequest:([t,e,r])=>(this.checkMembers(t,e.member),void 0!==this.organizationOf(t)&&void 0!==w(e,this.displayName)&&void 0===w(e,this.createdAt)&&(e={...e,[this.createdAt]:(new Date).toISOString()}),[t,e,r]),ldapmodifyrequest:([t,e,r,s])=>(this.checkMembers(t,e.add?.member),this.checkMembers(t,e.replace?.member),[t,e,r,s]),ldaprenamerequest:async([t,r,s])=>{const i=movedInto(this.groupPattern,t,r);if(void 0===i)return[t,r,s];let a;try{const{searchEntries:e}=await this.ldap.search({paged:!1,scope:"base",attributes:["member"],[k]:!0},t);a=e[0]?.member}catch(t){if(32!==o(t))throw t}if(void 0!==this.foreign(i,a))throw new e(`The group holds members of another organization than ${i}`);return[t,r,s]},ldapsearchfilter:async([t,e,r])=>[r?.[k]?t:await this.hideTombstones(t),e,r],scimgroupcreate:([e,r,s])=>(s??(s=new m(this.config).groupBase(r)),[this.groupPattern.test(i(s).join(","))?{...e,id:t()}:e,r,s])},this.groupBase=this.config.twake_group_base||"",this.userBase=this.config.twake_group_user_base||"";for(const[t,e]of[["--twake-group-base",this.groupBase],["--twake-group-user-base",this.userBase]])if(!e.includes(_))throw new Error(`${this.name}: ${t} must hold ${_}`);this.groupPattern=branchPattern(this.groupBase),this.attrs=y(this.config),this.orgPattern=this.config.twake_group_organization_dn||"",this.displayName=this.config.twake_group_display_name_attribute||"twakeDisplayName",this.color=this.config.twake_group_color_attribute||"twakeGroupColor",this.createdAt=this.config.twake_group_created_at_attribute||"twakeCreatedAt",this.maxPage=this.config.twake_group_max_page_limit||1e3,this.memberFields={...$,...this.config.twake_group_member_fields}}adaptSchema(t){const e={[this.displayName]:{type:"string",required:!0},[this.color]:{type:"string",test:j.source},[this.createdAt]:{type:"string"}};for(const[r,s]of Object.entries(e)){const e=Object.keys(t.attributes).find(t=>t.toLowerCase()===r.toLowerCase()),o=e?t.attributes[e]:s;e&&delete t.attributes[e],t.attributes[r]=o}return t}afterLoad(){if(!this.server.loadedPlugins.scim)return;const t=this.config.scim_group_rdn_attribute||"cn",e=this.config.scim_group_mapping||"",r=g(f,e?l(e):void 0).entries.find(e=>e.ldap?.toLowerCase()===t.toLowerCase());r&&this.logger.warn(`${this.name}: the SCIM group mapping writes ${r.scim} to ${t}, so SCIM groups keep it as their RDN instead of a generated cn: map ${r.scim} to another attribute with --scim-group-mapping`)}organizationOf(t){return organizationIn(this.groupPattern,t)}groupBaseOf(t){return this.groupBase.replace(_,a(t))}userBaseOf(t){return this.userBase.replace(_,a(t))}checkMembers(t,r){const s=this.organizationOf(t);if(!s)return;const o=this.foreign(s,r);if(void 0!==o)throw new e(`${o} is not a user of organization ${s}`)}foreign(t,e){const r=dnKey(this.userBaseOf(t));return values(e).find(t=>!n(t,this.config.group_dummy_user)&&dnKey(parentOf(t))!==r)}async hideTombstones(t){if(!this.attrs.deleted)return t;const e=new Set;for(const r of t.searchEntries){const t=this.organizationOf(r.dn);t&&void 0!==r.member&&e.add(t)}if(0===e.size)return t;const r=new Set;for(const t of e)for(const e of await this.tombstonesOf(t))r.add(dnKey(e));return 0===r.size?t:{...t,searchEntries:t.searchEntries.map(t=>void 0!==t.member&&this.organizationOf(t.dn)?{...t,member:values(t.member).filter(t=>!r.has(dnKey(t)))}:t)}}async tombstonesOf(t){const{deleted:e,deletedValue:r}=this.attrs;if(!e)return[];try{const{searchEntries:s}=await this.ldap.search({paged:!1,scope:"one",filter:`(${e}=${c(r)})`,attributes:["dn"]},this.userBaseOf(t));return s.map(t=>t.dn)}catch(t){if(32===o(t))return[];throw t}}api(t){const e=`${this.config.api_prefix}/v1/organizations/:id/groups`,r=this.organizationRoute.bind(this);t.get(e,r("listing groups",(t,e,r)=>this.listRoute(t,e,r))),t.post(e,r("creating group",(t,e,r)=>this.createRoute(t,e,r))),t.get(`${e}/:groupId`,r("getting group",async(t,e,r)=>{const s=await this.readGroup(r,t.params.groupId);if(!s)throw notFound("group");e.json(s)})),t.get(`${e}/:groupId/members`,r("listing group members",(t,e,r)=>this.membersRoute(t,e,r))),t.patch(`${e}/:groupId`,r("updating group",(t,e,r)=>this.updateRoute(t,e,r))),t.delete(`${e}/:groupId`,r("deleting group",async(t,e,r)=>{await this.onGroup(()=>this.deleteGroup(this.groupDn(r,t.params.groupId),t)),e.json({success:!0})})),t.post(`${e}/:groupId/members`,r("adding group members",(t,e,r)=>this.addMembersRoute(t,e,r))),t.delete(`${e}/:groupId/members/:userId`,r("removing group member",(t,e,r)=>this.removeMemberRoute(t,e,r)))}organizationRoute(t,e,o=this.name){return async(i,a)=>{try{const t=i.params.id;await this.checkOrganization(t),await e(i,a,t)}catch(e){if(e instanceof RouteError)return void a.status(e.status).json({error:e.message,code:e.code});if(u(e))return void a.status(403).json({error:r,code:"REFUSED"});if(e instanceof s&&e.statusCode<500)return void a.status(e.statusCode).json({error:e.message,code:400===e.statusCode?"INVALID_INPUT":"REFUSED"});this.logger.error({plugin:o,context:t,error:String(e)}),a.status(500).json({error:`Error ${t}`,code:"INTERNAL_ERROR"})}}}groupDn(t,e){return`${this.cn}=${a(e)},${this.groupBaseOf(t)}`}get userAttribute(){return this.config.ldap_user_main_attribute||"uid"}userDn(t,e){return`${this.userAttribute}=${a(e)},${this.userBaseOf(t)}`}async checkOrganization(t){if(!this.orgPattern)return;const e=this.config.twake_group_organization_status_attribute||"twakeOrgStatus";let r;try{r=(await this.ldap.search({paged:!1,scope:"base",attributes:[e]},this.orgPattern.replace(_,a(t)))).searchEntries[0]}catch(t){if(32!==o(t))throw t}if(!r)throw notFound("organization");const s=this.config.twake_group_organization_deleted_value||"deleted";if(read(r,e)===s)throw new RouteError(410,"Organization has been deleted","ORGANIZATION_DELETED")}page(t,e){const r=Math.max(1,parseInt(t.query.page)||1),s=Math.max(1,Math.min(parseInt(t.query.limit)||20,this.maxPage)),o=t.query.search||void 0;if(void 0!==o&&("string"!=typeof o||o.length<2))throw invalid("Search query must be at least 2 characters","INVALID_SEARCH_QUERY");const i=t.query.sortBy;if(void 0!==i&&!e.includes(i))throw invalid(`sortBy must be one of: ${e.join(", ")}`,"INVALID_SORT_FIELD");return{page:r,limit:s,offset:(r-1)*s,search:o,sortBy:i,desc:"desc"===t.query.sortOrder}}sorted(t,e,r){if(!e)return t;const s=r?-1:1;return t.sort((t,r)=>text(t[e]).localeCompare(text(r[e]))*s)}groupOf(t,e){const r=read(e,this.cn)||"",s=dnKey(this.userBaseOf(t)),o=values(e.member).filter(t=>!n(t,this.config.group_dummy_user)).map(t=>dnKey(parentOf(t))===s?rdnValue(t):t);return{id:r,cn:r,displayName:read(e,this.displayName)??r,description:read(e,"description"),color:read(e,this.color),organizationId:t,baseDN:this.groupDn(t,r),members:o,createdAt:read(e,this.createdAt)}}async groups(t,e,r,s){try{const{searchEntries:o}=await this.ldap.search({paged:!1,scope:"one",filter:e,...r&&{attributes:r}},this.groupBaseOf(t),s);return o}catch(t){if(32===o(t))return[];throw t}}async readGroup(t,e){try{const{searchEntries:r}=await this.ldap.search({paged:!1,scope:"base"},this.groupDn(t,e));return r[0]&&this.groupOf(t,r[0])}catch(t){if(32===o(t))return;throw t}}async named(t,e){const[r]=await this.groups(t,`(${this.displayName}=${c(e)})`);return r&&read(r,this.cn)}async onGroup(t){try{return await t()}catch(t){if(32===o(t))throw notFound("group");throw t}}async listRoute(t,e,r){const s=this.page(t,O);let o=`(${this.cn}=*)`;if(s.search){const t=c(s.search);o=`(&${o}(|(${this.displayName}=*${t}*)(description=*${t}*)(&(!(${this.displayName}=*))(${this.cn}=*${t}*))))`}const i=this.sorted((await this.groups(r,o)).map(t=>this.groupOf(r,t)),s.sortBy,s.desc);e.json({organizationId:r,groups:i.slice(s.offset,s.offset+s.limit),pagination:{page:s.page,limit:s.limit,total:i.length,totalPages:Math.ceil(i.length/s.limit)}})}async createRoute(e,r,s){const o=bodyOf(e);if(!isDisplayName(o.name))throw invalid(N);if(void 0!==o.description&&"string"!=typeof o.description)throw invalid("description must be a string");if(void 0!==o.color&&!isColor(o.color))throw invalid("color must be a hex code (e.g. #RRGGBB or #RGB)");if(await this.named(s,o.name))throw new RouteError(409,"Group already exists","GROUP_EXISTS");const i=t(),a={objectClass:this.config.group_class||["top","groupOfNames"],[this.displayName]:o.name,[this.createdAt]:(new Date).toISOString()};o.description&&(a.description=o.description),o.color&&(a[this.color]=o.color),await this.addGroup(this.groupDn(s,i),[],a,e);const n=await this.readGroup(s,i);r.status(201).json(n)}async updateRoute(t,e,r){const s=bodyOf(t),o=t.params.groupId,i=Object.keys(s).filter(t=>!["name","description","color"].includes(t));if(i.length)throw invalid(`Only "name", "description" and "color" can be updated; unsupported field(s): ${i.join(", ")}`);const{name:a,description:n,color:c}=s;if(void 0===a&&void 0===n&&void 0===c)throw invalid('at least one of "name", "description" or "color" must be provided');if(void 0!==a&&!isDisplayName(a))throw invalid(N);if(void 0!==n&&"string"!=typeof n)throw invalid("description must be a string");if(void 0!==c&&""!==c&&!isColor(c))throw invalid("color must be a hex code (e.g. #RRGGBB or #RGB) or empty to clear it");const u={};if(void 0!==a){const t=await this.named(r,a);if(t&&t!==o)throw new RouteError(409,"Group already exists","GROUP_EXISTS");u[this.displayName]=a}void 0!==n&&(u.description=n||[]),void 0!==c&&(u[this.color]=c||[]),await this.onGroup(()=>this.modifyGroup(this.groupDn(r,o),{replace:u},t)),e.json({success:!0})}async membersRoute(t,e,r){const s=this.page(t,v),o=t.params.groupId,i=await this.readGroup(r,o);if(!i)throw notFound("group");e.json({organizationId:r,id:o,...await this.memberPage(s,r,i.members)})}async memberPage(t,e,r,s=()=>({}),o){const i=[...new Map(r.map(t=>[t.toLowerCase(),t])).values()],a=await this.profiles(e,i,o);let n=i.map(t=>({...a.get(t.toLowerCase())??{uid:t},...s(t)}));if(t.search){const e=t.search.toLowerCase();n=n.filter(t=>[t.uid,t.displayName||t.cn,t.mail].some(t=>text(t).toLowerCase().includes(e)))}n=this.sorted(n,t.sortBy??"uid",t.desc);const c=Math.ceil(n.length/t.limit);return{members:n.slice(t.offset,t.offset+t.limit),pagination:{page:t.page,limit:t.limit,total:n.length,totalPages:c,hasNextPage:t.page<c,hasPreviousPage:t.page>1}}}async users(t,e,r,s){const{deleted:i,deletedValue:a}=this.attrs,n=i?`(!(${i}=${c(a)}))`:"",u=[];for(let i=0;i<e.length;i+=this.maxPage){const a=`(&(|${e.slice(i,i+this.maxPage).map(t=>`(${this.userAttribute}=${c(t)})`).join("")})${n})`;try{const{searchEntries:e}=await this.ldap.search({paged:!1,scope:"one",filter:a,attributes:r},this.userBaseOf(t),s);u.push(...e)}catch(t){if(32!==o(t))throw t}}return u}async profiles(t,e,r){const s=[...new Set([...Object.values(this.memberFields),...Object.values(I),R])],o=new Map;for(const i of await this.users(t,e,s,r)){const t=read(i,this.userAttribute);if(!t)continue;const e={};for(const[t,r]of Object.entries(this.memberFields)){const s=read(i,r);void 0!==s&&(e[t]=s)}const r={};for(const[t,e]of Object.entries(I)){const s=read(i,e);void 0!==s&&(r[t]=s)}Object.keys(r).length&&(e.name=r),e.isTechnical="TRUE"===read(i,R)?.toUpperCase(),o.set(t.toLowerCase(),e)}return o}async addMembersRoute(t,e,r){const{usernames:s}=bodyOf(t);if(!Array.isArray(s)||0===s.length)throw invalid("usernames must be a non-empty array");if(!s.every(t=>"string"==typeof t&&t.length>0))throw invalid("usernames must be an array of non-empty strings");if(s.length>this.maxPage)throw invalid(`usernames cannot exceed ${this.maxPage} entries per request`);const o=t.params.groupId,i=await this.readGroup(r,o);if(!i)throw notFound("group");const a=[...new Map(s.map(t=>[t.toLowerCase(),t])).keys()],n=new Map((await this.users(r,a,[this.userAttribute])).map(t=>{const e=read(t,this.userAttribute)||"";return[e.toLowerCase(),e]}));if(a.some(t=>!n.has(t)))throw notFound("user");const c=new Set(i.members.map(t=>t.toLowerCase())),u=a.filter(t=>!c.has(t)).map(t=>this.userDn(r,n.get(t))),p=this.groupDn(r,o);u.length&&(await this.onGroup(()=>this.ldap.modify(p,{add:{member:u}},t)),await this.dropPlaceholder(p,t)),e.json({success:!0})}async dropPlaceholder(t,e){const r=this.config.group_dummy_user;if(r)try{await this.ldap.modify(t,{delete:{member:r}},e)}catch(t){if(![16,65].includes(o(t)??0))throw t}}async removeMemberRoute(t,e,r){const s=this.groupDn(r,t.params.groupId),i=this.userDn(r,t.params.userId);try{await this.ldap.modify(s,{delete:{member:i}},t)}catch(e){const r=o(e);if(32===r)throw notFound("group");if(16===r)throw notFound("member");if(65!==r||!this.config.group_dummy_user)throw e;await this.ldap.modify(s,{replace:{member:[this.config.group_dummy_user]}},t)}e.json({success:!0})}}export{N as NAME_RULE,_ as ORG,RouteError,bodyOf,branchPattern,TwakeGroups as default,dnKey,invalid,isDisplayName,movedInto,notFound,organizationIn,parentOf,rdnValue,read,values};
|
|
1
|
+
import{randomUUID as t}from"node:crypto";import{BadRequestError as e,AUTHZ_REFUSED as r,HttpError as s}from"../../lib/errors.js";import{extractLdapCode as i}from"../../lib/ldapCodes.js";import{parseDn as o,escapeDnValue as a,isDummyMemberDn as n,escapeLdapFilter as c,isAuthzRefusal as u,normalizeDn as d,unescapeDnValue as h}from"../../lib/utils.js";import p from"../ldap/groups.js";import{BaseResolver as m}from"../scim/baseResolver.js";import{mergeMapping as g,loadMappingFile as l,DEFAULT_GROUP_MAPPING as f}from"../scim/mapping.js";import{outsideRequest as y}from"../../lib/requestContext.js";import{valueOf as w,lifecycleAttributes as b,first as _}from"./lifecycleAttributes.js";const read=(t,e)=>_(w(t,e)),v="{org}",O=["displayName","description","createdAt"],$=["uid","displayName","mail","jobTitle"],k={uid:"uid",_id:"entryUUID",cn:"cn",sn:"sn",givenName:"givenName",displayName:"displayName",mail:"mail",mobile:"mobile",jobTitle:"twakeJobTitle",company:"twakeCompany",organizationRole:"twakeOrganizationRole",organizationId:"twakeOrganizationId"},I={familyName:"sn",givenName:"givenName",additionalName:"twakeAdditionalName",namePrefix:"twakeNamePrefix"},j="twakeIsTechnical";class RouteError extends Error{constructor(t,e,r){super(e),this.status=t,this.code=r}}const notFound=t=>new RouteError(404,`${t[0].toUpperCase()}${t.slice(1)} not found`,`${t.toUpperCase()}_NOT_FOUND`),invalid=(t,e="INVALID_INPUT")=>new RouteError(400,t,e),N="name must be a non-blank string of at most 256 characters with no control characters";function isDisplayName(t){return"string"==typeof t&&t.trim().length>0&&t.length<=256&&!/[\x00-\x1f\x7f]/.test(t)}const R=/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/,isColor=t=>"string"==typeof t&&R.test(t),text=t=>"string"==typeof t||"number"==typeof t?String(t):"";function bodyOf(t){const e=t.body;if(!e||"object"!=typeof e||Array.isArray(e))throw invalid("Request body must be a JSON object");return e}function values(t){return null==t?[]:(Array.isArray(t)?t:[t]).map(t=>t.toString())}function dnKey(t){try{return d(t)}catch{return t.toLowerCase()}}function parentOf(t){return o(t).slice(1).join(",")}function rdnValue(t){const[e]=o(t),r=e.indexOf("=");return r>0?h(e.slice(r+1).trim()):t}function branchPattern(t){const[e,r]=o(t).join(",").split(v).map(t=>t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"));return new RegExp(`^${e}(?<org>(?:\\\\.|[^,])+)${r}$`,"i")}function movedInto(t,e,r){const s=organizationIn(t,r);return void 0!==s&&dnKey(parentOf(r))!==dnKey(parentOf(e))?s:void 0}const P=["twakegroup","twakestaticgroup"],z=Symbol("whole member lists");function organizationIn(t,e){const r=t.exec(parentOf(o(e).join(",")));return r?.groups?h(r.groups.org):void 0}class TwakeGroups extends p{constructor(r){super(r),this.name="twakeGroups",this.hooks={ldapaddrequest:([t,e,r])=>{this.checkMembers(t,e.member);const s=this.organizationOf(t);return void 0!==s&&void 0!==w(e,this.displayName)&&void 0===w(e,this.createdAt)&&(e={...e,[this.createdAt]:(new Date).toISOString()}),void 0!==s&&(e=this.linked(t,s,e)),[t,e,r]},ldapmodifyrequest:([t,e,r,s])=>(this.checkMembers(t,e.add?.member),this.checkMembers(t,e.replace?.member),this.checkLinkChange(t,e),[t,e,r,s]),ldaprenamerequest:async([t,r,s])=>{const o=movedInto(this.groupPattern,t,r);if(void 0===o)return[t,r,s];let a;try{const{searchEntries:e}=await this.ldap.system.search({paged:!1,scope:"base",attributes:["member"],[z]:!0},t);a=e[0]?.member}catch(t){if(32!==i(t))throw t}if(void 0!==this.foreign(o,a))throw new e(`The group holds members of another organization than ${o}`);return[t,r,s]},ldaprenamedone:async([t,e])=>{const r=movedInto(this.groupPattern,t,e),s=this.config.ldap_organization_link_attribute;if(void 0===r||!s||!this.orgPattern||!this.judgedByAttachment())return;const{searchEntries:i}=await this.ldap.system.search({paged:!1,scope:"base",attributes:[s]},e);void 0!==w(i[0]??{},s)&&await this.ldap.system.modify(e,{replace:{[s]:this.orgPattern.replace(v,a(r))}})},ldapsearchfilter:async([t,e,r])=>[r?.[z]?t:await this.hideTombstones(t),e,r],scimgroupcreate:([e,r,s])=>(s??(s=new m(this.config).groupBase(r)),[this.groupPattern.test(o(s).join(","))?{...e,id:t()}:e,r,s])},this.groupBase=this.config.twake_group_base||"",this.userBase=this.config.twake_group_user_base||"";for(const[t,e]of[["--twake-group-base",this.groupBase],["--twake-group-user-base",this.userBase]])if(!e.includes(v))throw new Error(`${this.name}: ${t} must hold ${v}`);this.groupPattern=branchPattern(this.groupBase),this.attrs=b(this.config),this.orgPattern=this.config.twake_group_organization_dn||"",this.displayName=this.config.twake_group_display_name_attribute||"twakeDisplayName",this.color=this.config.twake_group_color_attribute||"twakeGroupColor",this.createdAt=this.config.twake_group_created_at_attribute||"twakeCreatedAt",this.maxPage=this.config.twake_group_max_page_limit||1e3,this.memberFields={...k,...this.config.twake_group_member_fields}}adaptSchema(t){const e={[this.displayName]:{type:"string",required:!0},[this.color]:{type:"string",test:R.source},[this.createdAt]:{type:"string"}},r=this.config.ldap_organization_link_attribute;r&&this.orgPattern&&(e[r]={type:"string",generated:!0});for(const[r,s]of Object.entries(e)){const e=Object.keys(t.attributes).find(t=>t.toLowerCase()===r.toLowerCase()),i=e?t.attributes[e]:s;e&&delete t.attributes[e],t.attributes[r]=i}return t}afterLoad(){if(!this.server.loadedPlugins.scim)return;const t=this.config.scim_group_rdn_attribute||"cn",e=this.config.scim_group_mapping||"",r=g(f,e?l(e):void 0).entries.find(e=>e.ldap?.toLowerCase()===t.toLowerCase());r&&this.logger.warn(`${this.name}: the SCIM group mapping writes ${r.scim} to ${t}, so SCIM groups keep it as their RDN instead of a generated cn: map ${r.scim} to another attribute with --scim-group-mapping`)}linked(t,r,s){const i=this.config.ldap_organization_link_attribute;if(!i||!this.orgPattern||!this.judgedByAttachment())return s;const o=this.orgPattern.replace(v,a(r)),n=read(s,i);if(void 0!==n){if(dnKey(n)!==dnKey(o))throw new e(`${t} lies in the branch of ${r}, and must be linked to it`);return s}const c=values(s.objectClass);return c.some(t=>P.includes(t.toLowerCase()))||c.push("twakeGroup"),{...s,objectClass:c,[i]:o}}checkLinkChange(t,r){const s=this.config.ldap_organization_link_attribute,i=this.organizationOf(t);if(void 0===i||!s||!this.orgPattern||!this.judgedByAttachment())return;const o=dnKey(this.orgPattern.replace(v,a(i))),named=t=>Object.keys(t??{}).filter(t=>t.toLowerCase()===s.toLowerCase()),n=[...named(r.add).flatMap(t=>values(r.add[t])),...named(r.replace).flatMap(t=>values(r.replace[t]))],c=Array.isArray(r.delete)?r.delete.some(t=>t.toLowerCase()===s.toLowerCase()):named(r.delete).length>0,u=named(r.replace).length>0&&0===n.length;if(c||u||n.some(t=>dnKey(t)!==o))throw new e(`${t} lies in the branch of ${i}, and must stay linked to it`)}judgedByAttachment(){return Object.values(this.server.loadedPlugins).some(t=>t.roles?.includes("authz")&&!0===t.judgesByAttachment?.())}organizationOf(t){return organizationIn(this.groupPattern,t)}groupBaseOf(t){return this.groupBase.replace(v,a(t))}userBaseOf(t){return this.userBase.replace(v,a(t))}checkMembers(t,r){const s=this.organizationOf(t);if(!s)return;const i=this.foreign(s,r);if(void 0!==i)throw new e(`${i} is not a user of organization ${s}`)}foreign(t,e){const r=dnKey(this.userBaseOf(t));return values(e).find(t=>!n(t,this.config.group_dummy_user)&&dnKey(parentOf(t))!==r)}async hideTombstones(t){if(!this.attrs.deleted)return t;const e=new Set;for(const r of t.searchEntries){const t=this.organizationOf(r.dn);t&&void 0!==r.member&&e.add(t)}if(0===e.size)return t;const r=new Set;for(const t of e)for(const e of await this.tombstonesOf(t))r.add(dnKey(e));return 0===r.size?t:{...t,searchEntries:t.searchEntries.map(t=>void 0!==t.member&&this.organizationOf(t.dn)?{...t,member:values(t.member).filter(t=>!r.has(dnKey(t)))}:t)}}async tombstonesOf(t){const{deleted:e,deletedValue:r}=this.attrs;if(!e)return[];try{const{searchEntries:s}=await this.ldap.system.search({paged:!1,scope:"one",filter:`(${e}=${c(r)})`,attributes:["dn"]},this.userBaseOf(t));return s.map(t=>t.dn)}catch(t){if(32===i(t))return[];throw t}}api(t){const e=`${this.config.api_prefix}/v1/organizations/:id/groups`,r=this.organizationRoute.bind(this);t.get(e,r("listing groups",(t,e,r)=>this.listRoute(t,e,r))),t.post(e,r("creating group",(t,e,r)=>this.createRoute(t,e,r))),t.get(`${e}/:groupId`,r("getting group",async(t,e,r)=>{const s=await this.readGroup(r,t.params.groupId,t);if(!s)throw notFound("group");e.json(s)})),t.get(`${e}/:groupId/members`,r("listing group members",(t,e,r)=>this.membersRoute(t,e,r))),t.patch(`${e}/:groupId`,r("updating group",(t,e,r)=>this.updateRoute(t,e,r))),t.delete(`${e}/:groupId`,r("deleting group",async(t,e,r)=>{await this.onGroup(()=>this.deleteGroup(this.groupDn(r,t.params.groupId),t)),e.json({success:!0})})),t.post(`${e}/:groupId/members`,r("adding group members",(t,e,r)=>this.addMembersRoute(t,e,r))),t.delete(`${e}/:groupId/members/:userId`,r("removing group member",(t,e,r)=>this.removeMemberRoute(t,e,r)))}organizationRoute(t,e,i=this.name){return async(o,a)=>{try{const t=o.params.id;await this.checkOrganization(t,o),await e(o,a,t)}catch(e){if(e instanceof RouteError)return void a.status(e.status).json({error:e.message,code:e.code});if(u(e))return void a.status(403).json({error:r,code:"REFUSED"});if(e instanceof s&&e.statusCode<500)return void a.status(e.statusCode).json({error:e.message,code:400===e.statusCode?"INVALID_INPUT":"REFUSED"});this.logger.error({plugin:i,context:t,error:String(e)}),a.status(500).json({error:`Error ${t}`,code:"INTERNAL_ERROR"})}}}groupDn(t,e){return`${this.cn}=${a(e)},${this.groupBaseOf(t)}`}get userAttribute(){return this.config.ldap_user_main_attribute||"uid"}userDn(t,e){return`${this.userAttribute}=${a(e)},${this.userBaseOf(t)}`}async checkOrganization(t,e){if(!this.orgPattern)return;const r=this.config.twake_group_organization_status_attribute||"twakeOrgStatus",s=this.orgPattern.replace(v,a(t));if(!await this.ldap.readableBy(s,e))throw notFound("organization");let o;try{o=(await this.ldap.system.search({paged:!1,scope:"base",attributes:[r]},s)).searchEntries[0]}catch(t){if(32!==i(t))throw t}if(!o)throw notFound("organization");const n=this.config.twake_group_organization_deleted_value||"deleted";if(read(o,r)===n)throw new RouteError(410,"Organization has been deleted","ORGANIZATION_DELETED")}page(t,e){const r=Math.max(1,parseInt(t.query.page)||1),s=Math.max(1,Math.min(parseInt(t.query.limit)||20,this.maxPage)),i=t.query.search||void 0;if(void 0!==i&&("string"!=typeof i||i.length<2))throw invalid("Search query must be at least 2 characters","INVALID_SEARCH_QUERY");const o=t.query.sortBy;if(void 0!==o&&!e.includes(o))throw invalid(`sortBy must be one of: ${e.join(", ")}`,"INVALID_SORT_FIELD");return{page:r,limit:s,offset:(r-1)*s,search:i,sortBy:o,desc:"desc"===t.query.sortOrder}}sorted(t,e,r){if(!e)return t;const s=r?-1:1;return t.sort((t,r)=>text(t[e]).localeCompare(text(r[e]))*s)}groupOf(t,e){const r=read(e,this.cn)||"",s=dnKey(this.userBaseOf(t)),i=values(e.member).filter(t=>!n(t,this.config.group_dummy_user)).map(t=>dnKey(parentOf(t))===s?rdnValue(t):t);return{id:r,cn:r,displayName:read(e,this.displayName)??r,description:read(e,"description"),color:read(e,this.color),organizationId:t,baseDN:this.groupDn(t,r),members:i,createdAt:read(e,this.createdAt)}}async groups(t,e,r,s){try{const{searchEntries:i}=await this.ldap.search({paged:!1,scope:"one",filter:e,...r&&{attributes:r}},this.groupBaseOf(t),s);return i}catch(t){if(32===i(t))return[];throw t}}async readGroup(t,e,r){try{const{searchEntries:s}=await this.ldap.search({paged:!1,scope:"base"},this.groupDn(t,e),r);return s[0]&&this.groupOf(t,s[0])}catch(t){if(32===i(t)||u(t))return;throw t}}async named(t,e){const[r]=await this.groups(t,`(${this.displayName}=${c(e)})`);return r&&read(r,this.cn)}async onGroup(t){try{return await t()}catch(t){if(32===i(t))throw notFound("group");throw t}}async listRoute(t,e,r){const s=this.page(t,O);let i=`(${this.cn}=*)`;if(s.search){const t=c(s.search);i=`(&${i}(|(${this.displayName}=*${t}*)(description=*${t}*)(&(!(${this.displayName}=*))(${this.cn}=*${t}*))))`}const o=this.sorted((await this.groups(r,i,void 0,t)).map(t=>this.groupOf(r,t)),s.sortBy,s.desc);e.json({organizationId:r,groups:o.slice(s.offset,s.offset+s.limit),pagination:{page:s.page,limit:s.limit,total:o.length,totalPages:Math.ceil(o.length/s.limit)}})}async createRoute(e,r,s){const i=bodyOf(e);if(!isDisplayName(i.name))throw invalid(N);if(void 0!==i.description&&"string"!=typeof i.description)throw invalid("description must be a string");if(void 0!==i.color&&!isColor(i.color))throw invalid("color must be a hex code (e.g. #RRGGBB or #RGB)");const o=t(),a={objectClass:this.config.group_class||["top","groupOfNames"],[this.displayName]:i.name,[this.createdAt]:(new Date).toISOString()};i.description&&(a.description=i.description),i.color&&(a[this.color]=i.color);const n=this.linked(this.groupDn(s,o),s,a);await this.ldap.authorize("ldapaddrequest",[this.groupDn(s,o),this.candidateGroup(o,[],n),e]);const c=i.name;if(await y(()=>this.named(s,c)))throw new RouteError(409,"Group already exists","GROUP_EXISTS");await this.addGroup(this.groupDn(s,o),[],n,e);const u=await y(()=>this.readGroup(s,o));r.status(201).json(u)}async updateRoute(t,e,r){const s=bodyOf(t),i=t.params.groupId,o=Object.keys(s).filter(t=>!["name","description","color"].includes(t));if(o.length)throw invalid(`Only "name", "description" and "color" can be updated; unsupported field(s): ${o.join(", ")}`);const{name:a,description:n,color:c}=s;if(void 0===a&&void 0===n&&void 0===c)throw invalid('at least one of "name", "description" or "color" must be provided');if(void 0!==a&&!isDisplayName(a))throw invalid(N);if(void 0!==n&&"string"!=typeof n)throw invalid("description must be a string");if(void 0!==c&&""!==c&&!isColor(c))throw invalid("color must be a hex code (e.g. #RRGGBB or #RGB) or empty to clear it");const u={};if(void 0!==a&&(u[this.displayName]=a),void 0!==n&&(u.description=n||[]),void 0!==c&&(u[this.color]=c||[]),await this.ldap.authorize("ldapmodifyrequest",[this.groupDn(r,i),{replace:u},this.opNumber(),t]),void 0!==a){const t=await y(()=>this.named(r,a));if(t&&t!==i)throw new RouteError(409,"Group already exists","GROUP_EXISTS")}await this.onGroup(()=>this.modifyGroup(this.groupDn(r,i),{replace:u},t)),e.json({success:!0})}async membersRoute(t,e,r){const s=this.page(t,$),i=t.params.groupId,o=await this.readGroup(r,i,t);if(!o)throw notFound("group");e.json({organizationId:r,id:i,...await this.memberPage(s,r,o.members,void 0,t)})}async memberPage(t,e,r,s=()=>({}),i){const o=[...new Map(r.map(t=>[t.toLowerCase(),t])).values()],a=await this.profiles(e,o,i);let n=o.map(t=>({...a.get(t.toLowerCase())??{uid:t},...s(t)}));if(t.search){const e=t.search.toLowerCase();n=n.filter(t=>[t.uid,t.displayName||t.cn,t.mail].some(t=>text(t).toLowerCase().includes(e)))}n=this.sorted(n,t.sortBy??"uid",t.desc);const c=Math.ceil(n.length/t.limit);return{members:n.slice(t.offset,t.offset+t.limit),pagination:{page:t.page,limit:t.limit,total:n.length,totalPages:c,hasNextPage:t.page<c,hasPreviousPage:t.page>1}}}async users(t,e,r,s){const{deleted:o,deletedValue:a}=this.attrs,n=o?`(!(${o}=${c(a)}))`:"",d=[];for(let o=0;o<e.length;o+=this.maxPage){const a=`(&(|${e.slice(o,o+this.maxPage).map(t=>`(${this.userAttribute}=${c(t)})`).join("")})${n})`;try{const{searchEntries:e}=await this.ldap.search({paged:!1,scope:"one",filter:a,attributes:r},this.userBaseOf(t),s);d.push(...e)}catch(t){if(32!==i(t)&&!u(t))throw t}}return d}async profiles(t,e,r){const s=[...new Set([...Object.values(this.memberFields),...Object.values(I),j])],i=new Map;for(const o of await this.users(t,e,s,r)){const t=read(o,this.userAttribute);if(!t)continue;const e={};for(const[t,r]of Object.entries(this.memberFields)){const s=read(o,r);void 0!==s&&(e[t]=s)}const r={};for(const[t,e]of Object.entries(I)){const s=read(o,e);void 0!==s&&(r[t]=s)}Object.keys(r).length&&(e.name=r),e.isTechnical="TRUE"===read(o,j)?.toUpperCase(),i.set(t.toLowerCase(),e)}return i}async addMembersRoute(t,e,r){const{usernames:s}=bodyOf(t);if(!Array.isArray(s)||0===s.length)throw invalid("usernames must be a non-empty array");if(!s.every(t=>"string"==typeof t&&t.length>0))throw invalid("usernames must be an array of non-empty strings");if(s.length>this.maxPage)throw invalid(`usernames cannot exceed ${this.maxPage} entries per request`);const i=t.params.groupId,o=[...new Map(s.map(t=>[t.toLowerCase(),t])).keys()];await this.ldap.authorize("ldapmodifyrequest",[this.groupDn(r,i),{add:{member:o.map(t=>this.userDn(r,t))}},this.opNumber(),t]);const a=await this.readGroup(r,i,t);if(!a)throw notFound("group");const n=new Map((await this.users(r,o,[this.userAttribute],t)).map(t=>{const e=read(t,this.userAttribute)||"";return[e.toLowerCase(),e]}));if(o.some(t=>!n.has(t)))throw notFound("user");const c=new Set(a.members.map(t=>t.toLowerCase())),u=o.filter(t=>!c.has(t)).map(t=>this.userDn(r,n.get(t))),d=this.groupDn(r,i);u.length&&(await this.onGroup(()=>this.ldap.modify(d,{add:{member:u}},t)),await this.dropPlaceholder(d,t)),e.json({success:!0})}async dropPlaceholder(t,e){const r=this.config.group_dummy_user;if(r)try{await this.ldap.modify(t,{delete:{member:r}},e)}catch(t){if(![16,65].includes(i(t)??0))throw t}}async removeMemberRoute(t,e,r){const s=this.groupDn(r,t.params.groupId),o=this.userDn(r,t.params.userId);try{await this.ldap.modify(s,{delete:{member:o}},t)}catch(e){const r=i(e);if(32===r)throw notFound("group");if(16===r)throw notFound("member");if(65!==r||!this.config.group_dummy_user)throw e;await this.ldap.modify(s,{replace:{member:[this.config.group_dummy_user]}},t)}e.json({success:!0})}}export{N as NAME_RULE,v as ORG,RouteError,bodyOf,branchPattern,TwakeGroups as default,dnKey,invalid,isDisplayName,movedInto,notFound,organizationIn,parentOf,rdnValue,read,values};
|