ldap-rest 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/errors.js +1 -1
- package/dist/lib/instanceProviders.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/plugins/auth/openidconnect.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/trash.js +1 -1
- package/dist/plugins/scim/filter.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/scim/mapping.js +1 -1
- package/dist/plugins/scim/patch.js +1 -1
- package/dist/plugins/scim/users.js +1 -1
- package/dist/plugins/twake/groups.js +1 -0
- package/dist/plugins/twake/instances.js +1 -1
- package/dist/plugins/twake/lifecycleAttributes.js +1 -1
- package/dist/plugins/twake/lifecycleEvents.js +1 -1
- package/dist/plugins/twake/tombstone.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +34 -6
- package/dist/src/config/args.d.ts +12 -0
- package/dist/src/lib/errors.d.ts +3 -0
- package/dist/src/lib/instanceProviders.d.ts +2 -0
- package/dist/src/plugins/auth/openidconnect.d.ts +15 -0
- package/dist/src/plugins/ldap/groups.d.ts +5 -0
- package/dist/src/plugins/scim/filter.d.ts +2 -1
- package/dist/src/plugins/scim/groups.d.ts +1 -0
- package/dist/src/plugins/scim/mapping.d.ts +18 -7
- package/dist/src/plugins/scim/patch.d.ts +2 -2
- package/dist/src/plugins/twake/groups.d.ts +89 -0
- package/dist/src/plugins/twake/instances.d.ts +14 -0
- package/dist/src/plugins/twake/lifecycleEvents.d.ts +20 -3
- package/docs/client-development/api/rest-api.md +32 -2
- package/docs/usage/configuration.md +22 -1
- package/docs/usage/plugins/README.md +1 -0
- package/docs/usage/plugins/auth/oidc.md +19 -0
- package/docs/usage/plugins/integrations/README.md +1 -0
- package/docs/usage/plugins/integrations/groups.md +225 -0
- package/docs/usage/plugins/integrations/instances.md +31 -3
- package/docs/usage/plugins/integrations/lifecycle-events.md +38 -5
- package/docs/usage/plugins/integrations/scim.md +44 -34
- package/docs/usage/plugins/integrations/tombstone.md +2 -10
- package/docs/usage/plugins/ldap/flat-generic.md +38 -2
- package/docs/usage/plugins/ldap/trash.md +4 -4
- package/docs/usage/upgrading.md +56 -0
- package/package.json +9 -1
- package/static/schemas/twake/organizationGroups.json +32 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"fs";import{wantJson as e,jsonBody as i,created as s,tryMethod as r,multiStatus as a}from"../lib/expressFormatedResponses.js";import{transformSchemas as n,getCompiledRegex as o,escapeDnValue as h,asyncHandler as d,escapeRegex as c,validateDnValue as u,isDummyMemberDn as l,normalizeDn as f,getParentDn as m,launchHooksChained as b,launchHooks as p,escapeLdapFilter as g,isDnInBranch as w,substringSearchFilter as y}from"../lib/utils.js";import{neverReturnAttributes as $,schemaAttribute as A,modifiedAttributeNames as v,missingRequiredAttribute as E,checkDnValues as x}from"../config/schema.js";import{BadRequestError as N,ConflictError as j,NotFoundError as C,HttpError as O}from"../lib/errors.js";import k from"./plugin.js";class LdapFlat extends k{constructor(e,i){if(super(e),this.ldap=e.ldap,this.base=i.base,this.mainAttribute=i.mainAttribute,this.objectClass=i.objectClass,this.defaultAttributes=i.defaultAttributes||{},this.singularName=i.singularName,this.pluralName=i.pluralName,this.hookPrefix=i.hookPrefix,!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(this.ldap.declareObjectClasses(this.base,this.objectClass),i.schemaPath)try{const e=t.readFileSync(i.schemaPath,"utf8");this.schema=JSON.parse(n(e,this.config)),this.logger.info(`${this.singularName} schema loaded from ${i.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${i.schemaPath}: ${t}`)}}clientForbiddenAttributes(){const t=new Set;if(!this.schema)return t;for(const[e,i]of Object.entries(this.schema.attributes))(e!==this.mainAttribute||i.generatedFrom)&&(i.generated||i.readOnly)&&t.add(e.toLowerCase());return t}hiddenAttributes(){return $([this.schema])}project(t){const e=this.hiddenAttributes();if(0===e.size)return t;const i={};for(const[s,r]of Object.entries(t))e.has(s.split(";")[0].toLowerCase())||(i[s]=r);return i}projectList(t){if(0===this.hiddenAttributes().size)return t;const e={};for(const[i,s]of Object.entries(t))e[i]=this.project(s);return e}rejectForbiddenInput(t){const e=this.clientForbiddenAttributes();if(0!==e.size)for(const i of t){if("dn"===i)continue;if(!e.has(i.split(";")[0].toLowerCase()))continue;const t=A(this.schema,i)?.[1];throw new N(t?.readOnly?`Attribute "${i}" is read-only and cannot be set`:`Attribute "${i}" is computed by the server and cannot be set`)}}async generateMainAttribute(t){const e=this.schema?.attributes[this.mainAttribute],i=e?.generatedFrom;if(!i||!e.generated)return;const s=t[i.attribute],r=Array.isArray(s)?s[0]:s;if(null==r||""===r)throw new N(`Attribute "${i.attribute}" is required to generate "${this.mainAttribute}"`);let a=String(r);if(i.extract){const t=o(i.extract).exec(a);t&&(a=void 0!==t[1]?t[1]:t[0])}if(i.lowercase&&(a=a.toLowerCase()),i.strip&&(a=a.replace(o(i.strip,"g"),"")),!a)throw new N(`Cannot generate "${this.mainAttribute}" from "${i.attribute}"`);if(!await this.rdnExists(a))return a;if("suffix"!==i.onCollision)throw new j(`${this.singularName} "${a}" already exists (generated from ${i.attribute})`);for(let t=2;t<1e3;t++){const e=`${a}-${t}`;if(!await this.rdnExists(e))return e}throw new j(`Cannot generate a free "${this.mainAttribute}" from "${i.attribute}"`)}async rdnExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base"},`${this.mainAttribute}=${h(t)},${this.base}`);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}invalidValueMessage(t){const e=this.schema?.attributes[t],i=e?.hint||e?.items?.hint;return i?`Invalid value for attribute "${t}": ${i}`:`Invalid value for attribute "${t}"`}api(t){t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,d(async(t,i)=>{if(!e(t,i))return;const s={};t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute&&(s.filter=y(t.query.match,t.query.attribute)),t.query.attributes&&"string"==typeof t.query.attributes&&(s.attributes=t.query.attributes.split(","));const r=await this.listEntries({...s,req:t});i.json(this.projectList(r))})),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,d(async(t,e)=>this.apiGet(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,d(async(t,e)=>this.apiAdd(t,e))),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,d(async(t,e)=>this.apiDelete(t,e))),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,d(async(t,e)=>this.apiModify(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,d(async(t,e)=>this.apiMove(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/rename`,d(async(t,e)=>this.apiRename(t,e)))}async apiGet(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);try{const e=this.resolveDn(s),r=await this.ldap.search({paged:!1,scope:"base"},e,t);if(0===r.searchEntries.length)throw new C(`${this.singularName} not found`);i.json(this.project(r.searchEntries[0]))}catch(t){if(t.code&&32===t.code)throw new C(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const r=this.schema?.attributes[this.mainAttribute],a=Boolean(r?.generated&&r.generatedFrom),n=a?i(t,e):i(t,e,this.mainAttribute);if(!n)return;this.rejectForbiddenInput(Object.keys(n));const o=a?await this.generateMainAttribute(n):n[this.mainAttribute],h={...n};delete h[this.mainAttribute],delete h.dn,await this.addEntry(o,h,t);const d=await this.searchEntriesByName(o,!1);return s(e,this.project(d[o]))}async apiDelete(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);await r(i,this.deleteEntry.bind(this),s,t)}async apiModify(t,e){const s=i(t,e);if(!s)return;this.rejectForbiddenInput(v(s));const a=decodeURIComponent(t.params.id);await r(e,this.modifyEntry.bind(this),a,s,t)}async apiMove(t,s){if(!e(t,s))return;const r=i(t,s,"targetOrgDn");if(!r)return;const a=decodeURIComponent(t.params.id),{targetOrgDn:n}=r;if(!n||"string"!=typeof n)throw new N("Missing or invalid targetOrgDn in request body");const o=await this.moveEntry(a,n,t);s.json({success:!0,...o})}async apiRename(t,s){if(!e(t,s))return;const r=i(t,s,"newId");if(!r)return;const n=r.newId;if("string"!=typeof n)throw new N("Missing or invalid newId in request body");if(this.rejectForbiddenInput(Object.keys(r).filter(t=>"newId"!==t)),new RegExp(`^${c(this.mainAttribute)}=`,"i").test(n))throw new N(`newId must be a ${this.mainAttribute} value, not a DN`);try{u(n,this.mainAttribute)}catch(t){throw new N(t instanceof Error?t.message:"Invalid newId")}if(!await this._validateOneChange(this.mainAttribute,n))throw new N(this.invalidValueMessage(this.mainAttribute));const o=decodeURIComponent(t.params.id),h=this.resolveDn(o),d=this.resolveDn(n),m=this.config.group_dummy_user;if(m&&(l(h,m)||l(d,m)))throw new j(`${m} is the configured placeholder member and cannot be renamed`);const b=f(h)===f(d),p=await this.entryExists(h),g=b?p:await this.entryExists(d),w=this.requirePlugin("ldapEnterpriseRules");let y=[];if(b){if(!p)throw new C(`${this.singularName} not found`)}else if(p){if(g)throw new j(`${this.singularName} "${n}" already exists`);y=await(w?.findReferences(h))||[],await this.renameEntry(h,d,t)}else{if(!g)throw new C(`${this.singularName} not found`);y=await(w?.findReferences(h))||[]}const $=w&&y.length>0?await w.rewriteReferences(y,h,d,t):{updated:0,failed:[]},A={success:0===$.failed.length,dn:d,referencesUpdated:$.updated};0!==$.failed.length?a(s,{...A,referencesFailed:$.failed}):s.json(A)}async entryExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}resolveDn(t){if(new RegExp(`^${c(this.mainAttribute)}=`,"i").test(t)){const e=m(t),i=e!==t;if(!i||e.toLowerCase()!==this.base.toLowerCase())throw new N(`DN must be a direct child of "${this.base}". Provided DN "${t}" has parent "${i?e:"<none>"}"`);return t}return`${this.mainAttribute}=${h(t)},${this.base}`}async addEntry(t,e={},i){let s;new RegExp(`^${this.mainAttribute}=`,"i").test(t)?(s=this.resolveDn(t),t=t.replace(new RegExp(`^${this.mainAttribute}=((?:\\\\.|[^,])+)(?:,.*)?$`,"i"),"$1")):(u(t,this.mainAttribute),s=this.resolveDn(t)),await this.validateNewEntry(s,{objectClass:this.objectClass,[this.mainAttribute]:t,...e});let r,a={objectClass:this.objectClass,...this.defaultAttributes,[this.mainAttribute]:t,...e};this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[s,a]=await b(this.registeredHooks[`${this.hookPrefix}add`],[s,a]),this.logger.debug("Adding LDAP entry:",{dn:s,entry:JSON.stringify(a,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(a))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);try{r=await this.ldap.add(s,a,i)}catch(t){const e=t instanceof O&&t.statusCode<500;if(this.logger[e?"debug":"error"]("LDAP add failed:",{dn:s,entry:JSON.stringify(a,null,2),error:t}),t instanceof O)throw t;throw new Error(`Failed to add ${this.singularName} ${s}: ${t}`)}return p(this.registeredHooks[`${this.hookPrefix}adddone`],[s,a]),r}async modifyEntry(t,e,i){let s=this.resolveDn(t);const r=this.opNumber();if([s,e]=await b(this.registeredHooks[`${this.hookPrefix}modify`],[s,e,r]),e.add&&e.add[this.mainAttribute])throw new j(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new N(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new N(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new N(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(s,e);const a=await this.ldap.modify(s,e,i);return p(this.registeredHooks[`${this.hookPrefix}modifydone`],[s,e,r]),a}async renameEntry(t,e,i){/,/.test(t)||u(t,this.mainAttribute),/,/.test(e)||u(e,this.mainAttribute);let s=this.resolveDn(t),r=this.resolveDn(e);[s,r]=await b(this.registeredHooks[`${this.hookPrefix}rename`],[s,r]);const a=await(i?this.ldap.forRequest(i).rename(s,r):this.ldap.rename(s,r));return p(this.registeredHooks[`${this.hookPrefix}renamedone`],[s,r]),a}async deleteEntry(t,e){let i=this.resolveDn(t);i=await b(this.registeredHooks[`${this.hookPrefix}delete`],i);const s=await this.ldap.delete(i,e);return p(this.registeredHooks[`${this.hookPrefix}deletedone`],i),s}async moveEntry(t,e,i){const s=this.resolveDn(t),r=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",a=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[r]||!this.schema?.attributes[a])throw new N(`Schema for ${this.singularName} does not support move operation (missing ${r} or ${a})`);const n=await this.ldap.search({paged:!1,scope:"base",attributes:[r]},s,i);if(!n.searchEntries||0===n.searchEntries.length)throw new C(`Entry ${s} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await b(this.registeredHooks[`${this.hookPrefix}move`],[s,e,i]);const h={replace:{[r]:e,[a]:o}};return await this.modifyEntry(t,h,i),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const s=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!s.searchEntries||0===s.searchEntries.length)throw new C(`Organization ${t} not found`);const r=s.searchEntries[0];if(r[i]){const t=r[i];return Array.isArray(t)?String(t[0]):String(t)}const a=r.ou||r.o;if(a){return`/${Array.isArray(a)?String(a[0]):String(a)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){if(e instanceof C)throw e;if(32===e.code)throw new C(`Organization ${t} not found`);throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries({filter:t,attributes:e,req:i}){const s={paged:!0,filter:t=t||"(objectClass=*)"};e&&e.length>0&&(s.attributes=e);const r=await this.ldap.search(s,this.base,i),a={};for await(const t of r)t.searchEntries.forEach(t=>{if(t[this.mainAttribute]){const e=t[this.mainAttribute];let i;i=Array.isArray(e)?"string"==typeof e[0]?e[0]:String(e[0]):"string"==typeof e?e:String(e),a[i]=t}});return a}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const s=g(t),r=e?`(${this.mainAttribute}=*${s}*)`:`(${this.mainAttribute}=${s})`;return await this.listEntries({filter:r,attributes:i})}async validateNewEntry(t,e){if(!this.schema)return!0;for(const[t,i]of Object.entries(this.schema.attributes))i.fixed&&void 0!==i.default&&(e[t]=i.default);for(const[t,i]of Object.entries(e)){if(!this.schema.attributes[t]){if(this.schema.strict)throw new N(`Unknown attribute "${t}" for ${this.singularName}`);continue}const e=this.schema.attributes[t];if(e.fixed&&void 0!==e.default){const s=JSON.stringify(e.default);if(s!==JSON.stringify(i))throw new N(`Attribute "${t}" is fixed and cannot be modified. Expected: ${s}`)}if(!await this._validateOneChange(t,i))throw new N(this.invalidValueMessage(t));if(e.required&&!i&&!e.generated)throw new N(`Attribute "${t}" is required`)}const i=E(this.schema,e,this.server.loadedPlugins);if(i)throw new N(`Attribute "${i}" is required`);return!0}async validateChanges(t,e){if(!this.schema)return!0;const checkFixed=(t,e)=>{const i=this.schema?.attributes[t];if(i?.fixed)throw new N(`Attribute "${t}" is fixed and cannot be modified`)};if(e.add)for(const[t,i]of Object.entries(e.add))if(checkFixed(t),!await this._validateOneChange(t,i))throw new N(this.invalidValueMessage(t));if(e.replace)for(const[t,i]of Object.entries(e.replace))if(checkFixed(t),!await this._validateOneChange(t,i))throw new N(this.invalidValueMessage(t));if(e.delete){const t=Array.isArray(e.delete)?e.delete:Object.keys(e.delete);for(const e of t){const t=this.schema.attributes[e];if(t?.fixed)throw new N(`Attribute "${e}" is fixed and cannot be deleted`)}}return!0}async _validateOneChange(t,e){if(!this.schema)return!0;const i=this.schema.attributes[t];if(!i)return!this.schema.strict;if(!e)return!0;if("pointer"===i.type){if("string"!=typeof e)throw new N(`Field ${t} must be a string (DN pointer)`);const s=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>w(s,t)))throw new N(`Field ${t} must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.system.search({paged:!1,scope:"base"},s);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new N(`Field ${t} points to non-existent DN: ${s}`)}catch(e){throw new N(`Field ${t} points to invalid or non-existent DN: ${s}`)}}"pointer"!==i.type&&await x(t,i,e,async t=>(await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0);const s=i.test??i.items?.test;if(s){const t="string"==typeof s?o(s):s;return function(t){return(Array.isArray(t)?t:[t]).map(t=>String(t))}(e).every(e=>t.test(e))}return!0}}export{LdapFlat as default};
|
|
1
|
+
import t from"fs";import{wantJson as e,jsonBody as i,created as s,tryMethod as r,multiStatus as a}from"../lib/expressFormatedResponses.js";import{transformSchemas as n,getCompiledRegex as o,escapeDnValue as h,asyncHandler as d,escapeRegex as c,validateDnValue as u,isDummyMemberDn as l,normalizeDn as f,getParentDn as m,launchHooksChained as b,launchHooks as p,escapeLdapFilter as w,isDnInBranch as g,substringSearchFilter as y}from"../lib/utils.js";import{neverReturnAttributes as $,schemaAttribute as A,modifiedAttributeNames as v,missingRequiredAttribute as E,checkDnValues as x}from"../config/schema.js";import{BadRequestError as N,ConflictError as j,NotFoundError as C,HttpError as O,UnprocessableEntityError as k}from"../lib/errors.js";import{extractLdapCode as D}from"../lib/ldapCodes.js";import P from"./plugin.js";class LdapFlat extends P{constructor(e,i){if(super(e),this.ldap=e.ldap,this.base=i.base,this.mainAttribute=i.mainAttribute,this.objectClass=i.objectClass,this.defaultAttributes=i.defaultAttributes||{},this.singularName=i.singularName,this.pluralName=i.pluralName,this.hookPrefix=i.hookPrefix,!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(this.ldap.declareObjectClasses(this.base,this.objectClass),i.schemaPath)try{const e=t.readFileSync(i.schemaPath,"utf8");this.schema=JSON.parse(n(e,this.config)),this.logger.info(`${this.singularName} schema loaded from ${i.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${i.schemaPath}: ${t}`)}}clientForbiddenAttributes(){const t=new Set;if(!this.schema)return t;for(const[e,i]of Object.entries(this.schema.attributes))(e!==this.mainAttribute||i.generatedFrom)&&(i.generated||i.readOnly)&&t.add(e.toLowerCase());return t}hiddenAttributes(){return $([this.schema])}project(t){const e=this.hiddenAttributes();if(0===e.size)return t;const i={};for(const[s,r]of Object.entries(t))e.has(s.split(";")[0].toLowerCase())||(i[s]=r);return i}projectList(t){if(0===this.hiddenAttributes().size)return t;const e={};for(const[i,s]of Object.entries(t))e[i]=this.project(s);return e}rejectForbiddenInput(t){const e=this.clientForbiddenAttributes();if(0!==e.size)for(const i of t){if("dn"===i)continue;if(!e.has(i.split(";")[0].toLowerCase()))continue;const t=A(this.schema,i)?.[1];throw new N(t?.readOnly?`Attribute "${i}" is read-only and cannot be set`:`Attribute "${i}" is computed by the server and cannot be set`)}}async generateMainAttribute(t){const e=this.schema?.attributes[this.mainAttribute],i=e?.generatedFrom;if(!i||!e.generated)return;const s=t[i.attribute],r=Array.isArray(s)?s[0]:s;if(null==r||""===r)throw new N(`Attribute "${i.attribute}" is required to generate "${this.mainAttribute}"`);let a=String(r);if(i.extract){const t=o(i.extract).exec(a);t&&(a=void 0!==t[1]?t[1]:t[0])}if(i.lowercase&&(a=a.toLowerCase()),i.strip&&(a=a.replace(o(i.strip,"g"),"")),!a)throw new N(`Cannot generate "${this.mainAttribute}" from "${i.attribute}"`);if(!await this.rdnExists(a))return a;if("suffix"!==i.onCollision)throw new j(`${this.singularName} "${a}" already exists (generated from ${i.attribute})`);for(let t=2;t<1e3;t++){const e=`${a}-${t}`;if(!await this.rdnExists(e))return e}throw new j(`Cannot generate a free "${this.mainAttribute}" from "${i.attribute}"`)}async rdnExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base"},`${this.mainAttribute}=${h(t)},${this.base}`);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}invalidValueMessage(t){const e=this.schema?.attributes[t],i=e?.hint||e?.items?.hint;return i?`Invalid value for attribute "${t}": ${i}`:`Invalid value for attribute "${t}"`}api(t){t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,d(async(t,i)=>{if(!e(t,i))return;const s={};let r,a;if(t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute&&(s.filter=y(t.query.match,t.query.attribute)),t.query.attributes&&"string"==typeof t.query.attributes&&(s.attributes=t.query.attributes.split(",")),void 0!==t.query.limit){const e=t.query.limit;if(r="string"==typeof e&&/^\d+$/.test(e)?+e:NaN,!Number.isSafeInteger(r)||r<1)throw new N('Query parameter "limit" must be a positive integer')}try{a=await this.listEntriesLimited({...s,limit:r,req:t})}catch(t){if(void 0!==r||4!==D(t))throw t;throw new k("Too many entries to list at once: the directory's size limit was exceeded. Narrow the search, or pass `limit`.")}a.truncated&&i.set("X-Result-Truncated","true"),i.json(this.projectList(a.entries))})),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,d(async(t,e)=>this.apiGet(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,d(async(t,e)=>this.apiAdd(t,e))),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,d(async(t,e)=>this.apiDelete(t,e))),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,d(async(t,e)=>this.apiModify(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,d(async(t,e)=>this.apiMove(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/rename`,d(async(t,e)=>this.apiRename(t,e)))}async apiGet(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);try{const e=this.resolveDn(s),r=await this.ldap.search({paged:!1,scope:"base"},e,t);if(0===r.searchEntries.length)throw new C(`${this.singularName} not found`);i.json(this.project(r.searchEntries[0]))}catch(t){if(t.code&&32===t.code)throw new C(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const r=this.schema?.attributes[this.mainAttribute],a=Boolean(r?.generated&&r.generatedFrom),n=a?i(t,e):i(t,e,this.mainAttribute);if(!n)return;this.rejectForbiddenInput(Object.keys(n));const o=a?await this.generateMainAttribute(n):n[this.mainAttribute],h={...n};delete h[this.mainAttribute],delete h.dn,await this.addEntry(o,h,t);const d=await this.searchEntriesByName(o,!1);return s(e,this.project(d[o]))}async apiDelete(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);await r(i,this.deleteEntry.bind(this),s,t)}async apiModify(t,e){const s=i(t,e);if(!s)return;this.rejectForbiddenInput(v(s));const a=decodeURIComponent(t.params.id);await r(e,this.modifyEntry.bind(this),a,s,t)}async apiMove(t,s){if(!e(t,s))return;const r=i(t,s,"targetOrgDn");if(!r)return;const a=decodeURIComponent(t.params.id),{targetOrgDn:n}=r;if(!n||"string"!=typeof n)throw new N("Missing or invalid targetOrgDn in request body");const o=await this.moveEntry(a,n,t);s.json({success:!0,...o})}async apiRename(t,s){if(!e(t,s))return;const r=i(t,s,"newId");if(!r)return;const n=r.newId;if("string"!=typeof n)throw new N("Missing or invalid newId in request body");if(this.rejectForbiddenInput(Object.keys(r).filter(t=>"newId"!==t)),new RegExp(`^${c(this.mainAttribute)}=`,"i").test(n))throw new N(`newId must be a ${this.mainAttribute} value, not a DN`);try{u(n,this.mainAttribute)}catch(t){throw new N(t instanceof Error?t.message:"Invalid newId")}if(!await this._validateOneChange(this.mainAttribute,n))throw new N(this.invalidValueMessage(this.mainAttribute));const o=decodeURIComponent(t.params.id),h=this.resolveDn(o),d=this.resolveDn(n),m=this.config.group_dummy_user;if(m&&(l(h,m)||l(d,m)))throw new j(`${m} is the configured placeholder member and cannot be renamed`);const b=f(h)===f(d),p=await this.entryExists(h),w=b?p:await this.entryExists(d),g=this.requirePlugin("ldapEnterpriseRules");let y=[];if(b){if(!p)throw new C(`${this.singularName} not found`)}else if(p){if(w)throw new j(`${this.singularName} "${n}" already exists`);y=await(g?.findReferences(h))||[],await this.renameEntry(h,d,t)}else{if(!w)throw new C(`${this.singularName} not found`);y=await(g?.findReferences(h))||[]}const $=g&&y.length>0?await g.rewriteReferences(y,h,d,t):{updated:0,failed:[]},A={success:0===$.failed.length,dn:d,referencesUpdated:$.updated};0!==$.failed.length?a(s,{...A,referencesFailed:$.failed}):s.json(A)}async entryExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}resolveDn(t){if(new RegExp(`^${c(this.mainAttribute)}=`,"i").test(t)){const e=m(t),i=e!==t;if(!i||e.toLowerCase()!==this.base.toLowerCase())throw new N(`DN must be a direct child of "${this.base}". Provided DN "${t}" has parent "${i?e:"<none>"}"`);return t}return`${this.mainAttribute}=${h(t)},${this.base}`}async addEntry(t,e={},i){let s;new RegExp(`^${this.mainAttribute}=`,"i").test(t)?(s=this.resolveDn(t),t=t.replace(new RegExp(`^${this.mainAttribute}=((?:\\\\.|[^,])+)(?:,.*)?$`,"i"),"$1")):(u(t,this.mainAttribute),s=this.resolveDn(t)),await this.validateNewEntry(s,{objectClass:this.objectClass,[this.mainAttribute]:t,...e});let r,a={objectClass:this.objectClass,...this.defaultAttributes,[this.mainAttribute]:t,...e};this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[s,a]=await b(this.registeredHooks[`${this.hookPrefix}add`],[s,a]),this.logger.debug("Adding LDAP entry:",{dn:s,entry:JSON.stringify(a,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(a))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);try{r=await this.ldap.add(s,a,i)}catch(t){const e=t instanceof O&&t.statusCode<500;if(this.logger[e?"debug":"error"]("LDAP add failed:",{dn:s,entry:JSON.stringify(a,null,2),error:t}),t instanceof O)throw t;throw new Error(`Failed to add ${this.singularName} ${s}: ${t}`)}return p(this.registeredHooks[`${this.hookPrefix}adddone`],[s,a]),r}async modifyEntry(t,e,i){let s=this.resolveDn(t);const r=this.opNumber();if([s,e]=await b(this.registeredHooks[`${this.hookPrefix}modify`],[s,e,r]),e.add&&e.add[this.mainAttribute])throw new j(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new N(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new N(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new N(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(s,e);const a=await this.ldap.modify(s,e,i);return p(this.registeredHooks[`${this.hookPrefix}modifydone`],[s,e,r]),a}async renameEntry(t,e,i){/,/.test(t)||u(t,this.mainAttribute),/,/.test(e)||u(e,this.mainAttribute);let s=this.resolveDn(t),r=this.resolveDn(e);[s,r]=await b(this.registeredHooks[`${this.hookPrefix}rename`],[s,r]);const a=await(i?this.ldap.forRequest(i).rename(s,r):this.ldap.rename(s,r));return p(this.registeredHooks[`${this.hookPrefix}renamedone`],[s,r]),a}async deleteEntry(t,e){let i=this.resolveDn(t);i=await b(this.registeredHooks[`${this.hookPrefix}delete`],i);const s=await this.ldap.delete(i,e);return p(this.registeredHooks[`${this.hookPrefix}deletedone`],i),s}async moveEntry(t,e,i){const s=this.resolveDn(t),r=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",a=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[r]||!this.schema?.attributes[a])throw new N(`Schema for ${this.singularName} does not support move operation (missing ${r} or ${a})`);const n=await this.ldap.search({paged:!1,scope:"base",attributes:[r]},s,i);if(!n.searchEntries||0===n.searchEntries.length)throw new C(`Entry ${s} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await b(this.registeredHooks[`${this.hookPrefix}move`],[s,e,i]);const h={replace:{[r]:e,[a]:o}};return await this.modifyEntry(t,h,i),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const s=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!s.searchEntries||0===s.searchEntries.length)throw new C(`Organization ${t} not found`);const r=s.searchEntries[0];if(r[i]){const t=r[i];return Array.isArray(t)?String(t[0]):String(t)}const a=r.ou||r.o;if(a){return`/${Array.isArray(a)?String(a[0]):String(a)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){if(e instanceof C)throw e;if(32===e.code)throw new C(`Organization ${t} not found`);throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries(t){return(await this.listEntriesLimited(t)).entries}async listEntriesLimited({filter:t,attributes:e,req:i,limit:s}){const r={paged:void 0===s||{pageSize:Math.min(s+2,100)},filter:t||"(objectClass=*)"};if(e&&e.length>0){const t=this.mainAttribute.toLowerCase();r.attributes=e.some(e=>e.toLowerCase()===t)?e:[...e,this.mainAttribute]}const a=function(t,e){const i={},s=new Set;return{entries:i,add(r){for(const a of r){const r=a[t],n=Array.isArray(r)?r[0]:r;if(null==n||""===n)continue;const o=String(n);if(!s.has(o)){if(void 0!==e&&s.size>=e)return!1;s.add(o)}i[o]=a}return!0}}}(this.mainAttribute,s);try{const t=await this.ldap.search(r,this.base,i);for await(const e of t)if(!a.add(e.searchEntries))return{entries:a.entries,truncated:!0};return{entries:a.entries,truncated:!1}}catch(t){if(void 0===s||4!==D(t))throw t}try{const t=await this.ldap.search({...r,paged:!1,sizeLimit:Math.min(s+1,2147483647)},this.base,i);a.add(t.searchEntries)}catch(t){if(4!==D(t))throw t}return{entries:a.entries,truncated:!0}}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const s=w(t),r=e?`(${this.mainAttribute}=*${s}*)`:`(${this.mainAttribute}=${s})`;return await this.listEntries({filter:r,attributes:i})}async validateNewEntry(t,e){if(!this.schema)return!0;for(const[t,i]of Object.entries(this.schema.attributes))i.fixed&&void 0!==i.default&&(e[t]=i.default);for(const[t,i]of Object.entries(e)){if(!this.schema.attributes[t]){if(this.schema.strict)throw new N(`Unknown attribute "${t}" for ${this.singularName}`);continue}const e=this.schema.attributes[t];if(e.fixed&&void 0!==e.default){const s=JSON.stringify(e.default);if(s!==JSON.stringify(i))throw new N(`Attribute "${t}" is fixed and cannot be modified. Expected: ${s}`)}if(!await this._validateOneChange(t,i))throw new N(this.invalidValueMessage(t));if(e.required&&!i&&!e.generated)throw new N(`Attribute "${t}" is required`)}const i=E(this.schema,e,this.server.loadedPlugins);if(i)throw new N(`Attribute "${i}" is required`);return!0}async validateChanges(t,e){if(!this.schema)return!0;const checkFixed=(t,e)=>{const i=this.schema?.attributes[t];if(i?.fixed)throw new N(`Attribute "${t}" is fixed and cannot be modified`)};if(e.add)for(const[t,i]of Object.entries(e.add))if(checkFixed(t),!await this._validateOneChange(t,i))throw new N(this.invalidValueMessage(t));if(e.replace)for(const[t,i]of Object.entries(e.replace))if(checkFixed(t),!await this._validateOneChange(t,i))throw new N(this.invalidValueMessage(t));if(e.delete){const t=Array.isArray(e.delete)?e.delete:Object.keys(e.delete);for(const e of t){const t=this.schema.attributes[e];if(t?.fixed)throw new N(`Attribute "${e}" is fixed and cannot be deleted`)}}return!0}async _validateOneChange(t,e){if(!this.schema)return!0;const i=this.schema.attributes[t];if(!i)return!this.schema.strict;if(!e)return!0;if("pointer"===i.type){if("string"!=typeof e)throw new N(`Field ${t} must be a string (DN pointer)`);const s=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>g(s,t)))throw new N(`Field ${t} must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.system.search({paged:!1,scope:"base"},s);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new N(`Field ${t} points to non-existent DN: ${s}`)}catch(e){throw new N(`Field ${t} points to invalid or non-existent DN: ${s}`)}}"pointer"!==i.type&&await x(t,i,e,async t=>(await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0);const s=i.test??i.items?.test;if(s){const t="string"==typeof s?o(s):s;return function(t){return(Array.isArray(t)?t:[t]).map(t=>String(t))}(e).every(e=>t.test(e))}return!0}}export{LdapFlat as default};
|
package/dist/config/args.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fileURLToPath as _}from"url";import{join as e,dirname as a}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",e(a(_(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--organization-schema","DM_ORGANIZATION_SCHEMA",""],["--enterprise-domain-name-attribute","DM_ENTERPRISE_DOMAIN_NAME_ATTRIBUTE","associatedDomain"],["--enterprise-domain-link-attribute","DM_ENTERPRISE_DOMAIN_LINK_ATTRIBUTE",""],["--storage-backend","DM_STORAGE_BACKEND",""],["--storage-sweep-interval","DM_STORAGE_SWEEP_INTERVAL",600,"number"],["--storage-ldap-base","DM_STORAGE_LDAP_BASE",""],["--storage-ldap-object-class","DM_STORAGE_LDAP_OBJECT_CLASS","applicationProcess"],["--storage-file-directory","DM_STORAGE_FILE_DIRECTORY",""],["--storage-postgres-url","DM_STORAGE_POSTGRES_URL",""],["--storage-postgres-table","DM_STORAGE_POSTGRES_TABLE","ldap_rest_storage"],["--storage-valkey-url","DM_STORAGE_VALKEY_URL",""],["--storage-valkey-prefix","DM_STORAGE_VALKEY_PREFIX","ldap-rest:"],["--bcl-retention","DM_BCL_RETENTION",604800,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",e(a(_(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",e(a(_(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-flat-auto-repair","DM_LDAP_FLAT_AUTO_REPAIR",!0,"boolean"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--twake-lifecycle-role-attribute","DM_TWAKE_LIFECYCLE_ROLE_ATTRIBUTE",""],["--twake-lifecycle-lock-attribute","DM_TWAKE_LIFECYCLE_LOCK_ATTRIBUTE",""],["--twake-lifecycle-lock-value","DM_TWAKE_LIFECYCLE_LOCK_VALUE",""],["--twake-lifecycle-deleted-attribute","DM_TWAKE_LIFECYCLE_DELETED_ATTRIBUTE",""],["--twake-lifecycle-deleted-value","DM_TWAKE_LIFECYCLE_DELETED_VALUE","TRUE"],["--twake-lifecycle-deleted-at-attribute","DM_TWAKE_LIFECYCLE_DELETED_AT_ATTRIBUTE",""],["--twake-lifecycle-deleted-at-format","DM_TWAKE_LIFECYCLE_DELETED_AT_FORMAT","iso8601"],["--twake-lifecycle-reason-attribute","DM_TWAKE_LIFECYCLE_REASON_ATTRIBUTE",""],["--twake-lifecycle-rules","DM_TWAKE_LIFECYCLE_RULES",""],["--twake-tombstone-dn","DM_TWAKE_TOMBSTONE_DN",[],"array"],["--twake-tombstone-default-reason","DM_TWAKE_TOMBSTONE_DEFAULT_REASON","deleted"],["--twake-tombstone-reason-header","DM_TWAKE_TOMBSTONE_REASON_HEADER","x-deletion-reason"],["--twake-tombstone-reasons","DM_TWAKE_TOMBSTONE_REASONS",[],"array"],["--twake-tombstone-clear-attributes","DM_TWAKE_TOMBSTONE_CLEAR_ATTRIBUTES",[],"array"],["--twake-tombstone-erase-min-age","DM_TWAKE_TOMBSTONE_ERASE_MIN_AGE",2592e3,"number"],["--twake-tombstone-group-bases","DM_TWAKE_TOMBSTONE_GROUP_BASES",[],"array"],["--twake-instance-dn","DM_TWAKE_INSTANCE_DN",[],"array"],["--twake-instance-skip-attribute","DM_TWAKE_INSTANCE_SKIP_ATTRIBUTE",""],["--twake-instance-skip-value","DM_TWAKE_INSTANCE_SKIP_VALUE",""],["--twake-instance-provider","DM_TWAKE_INSTANCE_PROVIDER","cloudery"],["--twake-instance-id","DM_TWAKE_INSTANCE_ID","{uid}"],["--twake-instance-fqdn-attribute","DM_TWAKE_INSTANCE_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--twake-instance-sent-attribute","DM_TWAKE_INSTANCE_SENT_ATTRIBUTE","twakeCreatedEventAt"],["--twake-instance-locale","DM_TWAKE_INSTANCE_LOCALE",""],["--twake-instance-timeout","DM_TWAKE_INSTANCE_TIMEOUT",3e4,"number"],["--twake-instance-cloudery-url","DM_TWAKE_INSTANCE_CLOUDERY_URL",""],["--twake-instance-cloudery-token","DM_TWAKE_INSTANCE_CLOUDERY_TOKEN",""],["--twake-instance-cloudery-offer","DM_TWAKE_INSTANCE_CLOUDERY_OFFER","b2b_twake_default"],["--twake-instance-cloudery-domain","DM_TWAKE_INSTANCE_CLOUDERY_DOMAIN",""],["--twake-instance-cloudery-organization-offer","DM_TWAKE_INSTANCE_CLOUDERY_ORGANIZATION_OFFER",""],["--twake-instance-cozy-url","DM_TWAKE_INSTANCE_COZY_URL",""],["--twake-instance-cozy-user","DM_TWAKE_INSTANCE_COZY_USER","admin"],["--twake-instance-cozy-passphrase","DM_TWAKE_INSTANCE_COZY_PASSPHRASE",""],["--twake-instance-cozy-domain","DM_TWAKE_INSTANCE_COZY_DOMAIN",""],["--twake-instance-cozy-context","DM_TWAKE_INSTANCE_COZY_CONTEXT","default"],["--twake-instance-cozy-apps","DM_TWAKE_INSTANCE_COZY_APPS","home,drive,settings,notes,dataproxy"],["--twake-instance-cozy-org-id","DM_TWAKE_INSTANCE_COZY_ORG_ID",""],["--twake-instance-cozy-org-domain","DM_TWAKE_INSTANCE_COZY_ORG_DOMAIN",""],["--twake-instance-auth-exchange","DM_TWAKE_INSTANCE_AUTH_EXCHANGE","auth"],["--twake-instance-b2b-exchange","DM_TWAKE_INSTANCE_B2B_EXCHANGE","b2b"],["--twake-instance-user-created-key","DM_TWAKE_INSTANCE_USER_CREATED_KEY","user.created"],["--twake-instance-organization-created-key","DM_TWAKE_INSTANCE_ORGANIZATION_CREATED_KEY","organization.created"],["--twake-instance-workplace-created-key","DM_TWAKE_INSTANCE_WORKPLACE_CREATED_KEY","workplace.created"],["--twake-instance-queue","DM_TWAKE_INSTANCE_QUEUE","workplace.created.ldap-rest"],["--twake-instance-organization-base","DM_TWAKE_INSTANCE_ORGANIZATION_BASE",""],["--twake-instance-organization-id-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_ID_ATTRIBUTE","ou"],["--twake-instance-organization-domain-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_DOMAIN_ATTRIBUTE","twakeDomain"],["--twake-instance-organization-fqdn-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_FQDN_ATTRIBUTE",""],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--llng-username-header","DM_LLNG_USERNAME_HEADER",""],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-fake-user","DM_AUTH_FAKE_USER",""],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-transit-branch","DM_AUTHZ_TRANSIT_BRANCH",""],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-bypass","DM_AUTHZ_DYNAMIC_BYPASS",[],"array"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-username-claim","DM_OIDC_USERNAME_CLAIM","sub"],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
|
|
1
|
+
import{fileURLToPath as _}from"url";import{join as e,dirname as a}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",0,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",e(a(_(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--organization-schema","DM_ORGANIZATION_SCHEMA",""],["--enterprise-domain-name-attribute","DM_ENTERPRISE_DOMAIN_NAME_ATTRIBUTE","associatedDomain"],["--enterprise-domain-link-attribute","DM_ENTERPRISE_DOMAIN_LINK_ATTRIBUTE",""],["--storage-backend","DM_STORAGE_BACKEND",""],["--storage-sweep-interval","DM_STORAGE_SWEEP_INTERVAL",600,"number"],["--storage-ldap-base","DM_STORAGE_LDAP_BASE",""],["--storage-ldap-object-class","DM_STORAGE_LDAP_OBJECT_CLASS","applicationProcess"],["--storage-file-directory","DM_STORAGE_FILE_DIRECTORY",""],["--storage-postgres-url","DM_STORAGE_POSTGRES_URL",""],["--storage-postgres-table","DM_STORAGE_POSTGRES_TABLE","ldap_rest_storage"],["--storage-valkey-url","DM_STORAGE_VALKEY_URL",""],["--storage-valkey-prefix","DM_STORAGE_VALKEY_PREFIX","ldap-rest:"],["--bcl-retention","DM_BCL_RETENTION",604800,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",e(a(_(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",e(a(_(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--ldap-flat-auto-repair","DM_LDAP_FLAT_AUTO_REPAIR",!0,"boolean"],["--ldap-raw-base","DM_LDAP_RAW_BASE",[],"array","--ldap-raw-bases"],["--ldap-raw-hidden-attribute","DM_LDAP_RAW_HIDDEN_ATTRIBUTES",[],"array","--ldap-raw-hidden-attributes"],["--ldap-raw-show-secrets","DM_LDAP_RAW_SHOW_SECRETS",!1,"boolean"],["--ldap-raw-max-results","DM_LDAP_RAW_MAX_RESULTS",200,"number"],["--ldap-raw-schema-cache-ttl","DM_LDAP_RAW_SCHEMA_CACHE_TTL",3600,"number"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--calendar-firstname-attribute","DM_CALENDAR_FIRSTNAME_ATTRIBUTE","givenName"],["--calendar-lastname-attribute","DM_CALENDAR_LASTNAME_ATTRIBUTE","sn"],["--cozy-admin-url","DM_COZY_ADMIN_URL",""],["--cozy-admin-user","DM_COZY_ADMIN_USER","admin"],["--cozy-admin-passphrase","DM_COZY_ADMIN_PASSPHRASE",""],["--cozy-org-id","DM_COZY_ORG_ID",""],["--cozy-org-domain","DM_COZY_ORG_DOMAIN",""],["--cozy-default-locale","DM_COZY_DEFAULT_LOCALE","fr"],["--cozy-context-name","DM_COZY_CONTEXT_NAME","default"],["--cozy-apps","DM_COZY_APPS","home,drive,settings,notes,dataproxy"],["--cozy-auth-exchange","DM_COZY_AUTH_EXCHANGE","auth"],["--cozy-b2b-exchange","DM_COZY_B2B_EXCHANGE","b2b"],["--cozy-user-created-routing-key","DM_COZY_USER_CREATED_ROUTING_KEY","user.created"],["--cozy-user-deleted-routing-key","DM_COZY_USER_DELETED_ROUTING_KEY","domain.user.deleted"],["--cloudery-manager-url","DM_CLOUDERY_MANAGER_URL",""],["--cloudery-manager-token","DM_CLOUDERY_MANAGER_TOKEN",""],["--cloudery-offer","DM_CLOUDERY_OFFER","b2b_twake_default"],["--cloudery-domain","DM_CLOUDERY_DOMAIN",""],["--cloudery-user-branch","DM_CLOUDERY_USER_BRANCH",""],["--cloudery-org-id-header","DM_CLOUDERY_ORG_ID_HEADER","x-cloudery-org-id"],["--cloudery-org-role-header","DM_CLOUDERY_ORG_ROLE_HEADER","x-cloudery-org-role"],["--cloudery-default-org-role","DM_CLOUDERY_DEFAULT_ORG_ROLE","member"],["--cloudery-fqdn-attribute","DM_CLOUDERY_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--cloudery-org-id-attribute","DM_CLOUDERY_ORG_ID_ATTRIBUTE","twakeOrganizationId"],["--cloudery-org-role-attribute","DM_CLOUDERY_ORG_ROLE_ATTRIBUTE","twakeOrganizationRole"],["--cloudery-phones-attribute","DM_CLOUDERY_PHONES_ATTRIBUTE","twakePhones"],["--cloudery-invited-attribute","DM_CLOUDERY_INVITED_ATTRIBUTE","twakeInvited"],["--cloudery-default-locale","DM_CLOUDERY_DEFAULT_LOCALE","en"],["--cloudery-workflow-poll-interval-ms","DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",2e3,"number"],["--cloudery-workflow-max-attempts","DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",60,"number"],["--twake-lifecycle-role-attribute","DM_TWAKE_LIFECYCLE_ROLE_ATTRIBUTE",""],["--twake-lifecycle-lock-attribute","DM_TWAKE_LIFECYCLE_LOCK_ATTRIBUTE",""],["--twake-lifecycle-lock-value","DM_TWAKE_LIFECYCLE_LOCK_VALUE",""],["--twake-lifecycle-deleted-attribute","DM_TWAKE_LIFECYCLE_DELETED_ATTRIBUTE",""],["--twake-lifecycle-deleted-value","DM_TWAKE_LIFECYCLE_DELETED_VALUE","TRUE"],["--twake-lifecycle-deleted-at-attribute","DM_TWAKE_LIFECYCLE_DELETED_AT_ATTRIBUTE",""],["--twake-lifecycle-deleted-at-format","DM_TWAKE_LIFECYCLE_DELETED_AT_FORMAT","iso8601"],["--twake-lifecycle-reason-attribute","DM_TWAKE_LIFECYCLE_REASON_ATTRIBUTE",""],["--twake-lifecycle-rules","DM_TWAKE_LIFECYCLE_RULES",""],["--twake-lifecycle-member-attribute","DM_TWAKE_LIFECYCLE_MEMBER_ATTRIBUTE","member"],["--twake-group-base","DM_TWAKE_GROUP_BASE",""],["--twake-group-user-base","DM_TWAKE_GROUP_USER_BASE",""],["--twake-group-organization-dn","DM_TWAKE_GROUP_ORGANIZATION_DN",""],["--twake-group-organization-status-attribute","DM_TWAKE_GROUP_ORGANIZATION_STATUS_ATTRIBUTE","twakeOrgStatus"],["--twake-group-organization-deleted-value","DM_TWAKE_GROUP_ORGANIZATION_DELETED_VALUE","deleted"],["--twake-group-display-name-attribute","DM_TWAKE_GROUP_DISPLAY_NAME_ATTRIBUTE","twakeDisplayName"],["--twake-group-color-attribute","DM_TWAKE_GROUP_COLOR_ATTRIBUTE","twakeGroupColor"],["--twake-group-created-at-attribute","DM_TWAKE_GROUP_CREATED_AT_ATTRIBUTE","twakeCreatedAt"],["--twake-group-max-page-limit","DM_TWAKE_GROUP_MAX_PAGE_LIMIT",1e3,"number"],["--twake-group-member-fields","DM_TWAKE_GROUP_MEMBER_FIELDS",{},"json"],["--twake-tombstone-dn","DM_TWAKE_TOMBSTONE_DN",[],"array"],["--twake-tombstone-default-reason","DM_TWAKE_TOMBSTONE_DEFAULT_REASON","deleted"],["--twake-tombstone-reason-header","DM_TWAKE_TOMBSTONE_REASON_HEADER","x-deletion-reason"],["--twake-tombstone-reasons","DM_TWAKE_TOMBSTONE_REASONS",[],"array"],["--twake-tombstone-clear-attributes","DM_TWAKE_TOMBSTONE_CLEAR_ATTRIBUTES",[],"array"],["--twake-tombstone-erase-min-age","DM_TWAKE_TOMBSTONE_ERASE_MIN_AGE",2592e3,"number"],["--twake-tombstone-group-bases","DM_TWAKE_TOMBSTONE_GROUP_BASES",[],"array"],["--twake-instance-dn","DM_TWAKE_INSTANCE_DN",[],"array"],["--twake-instance-skip-attribute","DM_TWAKE_INSTANCE_SKIP_ATTRIBUTE",""],["--twake-instance-skip-value","DM_TWAKE_INSTANCE_SKIP_VALUE",""],["--twake-instance-provider","DM_TWAKE_INSTANCE_PROVIDER","cloudery"],["--twake-instance-id","DM_TWAKE_INSTANCE_ID","{uid}"],["--twake-instance-fqdn-attribute","DM_TWAKE_INSTANCE_FQDN_ATTRIBUTE","twakeWorkspaceUrl"],["--twake-instance-sent-attribute","DM_TWAKE_INSTANCE_SENT_ATTRIBUTE","twakeCreatedEventAt"],["--twake-instance-locale","DM_TWAKE_INSTANCE_LOCALE",""],["--twake-instance-timeout","DM_TWAKE_INSTANCE_TIMEOUT",3e4,"number"],["--twake-instance-cloudery-url","DM_TWAKE_INSTANCE_CLOUDERY_URL",""],["--twake-instance-cloudery-token","DM_TWAKE_INSTANCE_CLOUDERY_TOKEN",""],["--twake-instance-cloudery-offer","DM_TWAKE_INSTANCE_CLOUDERY_OFFER","b2b_twake_default"],["--twake-instance-cloudery-domain","DM_TWAKE_INSTANCE_CLOUDERY_DOMAIN",""],["--twake-instance-cloudery-organization-offer","DM_TWAKE_INSTANCE_CLOUDERY_ORGANIZATION_OFFER",""],["--twake-instance-cozy-url","DM_TWAKE_INSTANCE_COZY_URL",""],["--twake-instance-cozy-user","DM_TWAKE_INSTANCE_COZY_USER","admin"],["--twake-instance-cozy-passphrase","DM_TWAKE_INSTANCE_COZY_PASSPHRASE",""],["--twake-instance-cozy-domain","DM_TWAKE_INSTANCE_COZY_DOMAIN",""],["--twake-instance-cozy-context","DM_TWAKE_INSTANCE_COZY_CONTEXT","default"],["--twake-instance-cozy-apps","DM_TWAKE_INSTANCE_COZY_APPS","home,drive,settings,notes,dataproxy"],["--twake-instance-cozy-org-id","DM_TWAKE_INSTANCE_COZY_ORG_ID",""],["--twake-instance-cozy-org-domain","DM_TWAKE_INSTANCE_COZY_ORG_DOMAIN",""],["--twake-instance-auth-exchange","DM_TWAKE_INSTANCE_AUTH_EXCHANGE","auth"],["--twake-instance-b2b-exchange","DM_TWAKE_INSTANCE_B2B_EXCHANGE","b2b"],["--twake-instance-user-created-key","DM_TWAKE_INSTANCE_USER_CREATED_KEY","user.created"],["--twake-instance-organization-created-key","DM_TWAKE_INSTANCE_ORGANIZATION_CREATED_KEY","organization.created"],["--twake-instance-workplace-created-key","DM_TWAKE_INSTANCE_WORKPLACE_CREATED_KEY","workplace.created"],["--twake-instance-queue","DM_TWAKE_INSTANCE_QUEUE","workplace.created.ldap-rest"],["--twake-instance-organization-base","DM_TWAKE_INSTANCE_ORGANIZATION_BASE",""],["--twake-instance-organization-id-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_ID_ATTRIBUTE","ou"],["--twake-instance-organization-domain-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_DOMAIN_ATTRIBUTE","twakeDomain"],["--twake-instance-organization-name-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_NAME_ATTRIBUTE","description"],["--twake-instance-organization-fqdn-attribute","DM_TWAKE_INSTANCE_ORGANIZATION_FQDN_ATTRIBUTE",""],["--rabbitmq-url","DM_RABBITMQ_URL",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--applicative-account-attribute","DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",["objectClass","cn","sn","givenName","displayName","description"],"array","--applicative-account-attributes"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--llng-username-header","DM_LLNG_USERNAME_HEADER",""],["--auth-path-prefix","DM_AUTH_PATH_PREFIX",[],"array","--auth-path-prefixes"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-fake-user","DM_AUTH_FAKE_USER",""],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-route","DM_AUTHZ_PER_ROUTES",[],"array","--authz-per-routes"],["--authz-unresolved-user","DM_AUTHZ_UNRESOLVED_USER","deny"],["--authz-identity","DM_AUTHZ_IDENTITY","req.user"],["--authz-for","DM_AUTHZ_FOR",[],"array"],["--authz-combine","DM_AUTHZ_COMBINE",!1,"boolean"],["--authz-scope-source","DM_AUTHZ_SCOPE_SOURCE",""],["--authz-filter-attached-entries","DM_AUTHZ_FILTER_ATTACHED_ENTRIES",!1,"boolean"],["--authz-transit-branch","DM_AUTHZ_TRANSIT_BRANCH",""],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-dynamic-bypass","DM_AUTHZ_DYNAMIC_BYPASS",[],"array"],["--authz-dynamic-base","DM_AUTHZ_DYNAMIC_BASE",""],["--authz-dynamic-cache-ttl","DM_AUTHZ_DYNAMIC_CACHE_TTL",60,"number"],["--authz-dynamic-token-attribute","DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE","userPassword"],["--authz-dynamic-config-attribute","DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE","description"],["--authz-dynamic-tenant-attribute","DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE","cn"],["--authz-dynamic-reload-endpoint","DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",!1,"boolean"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-username-claim","DM_OIDC_USERNAME_CLAIM","sub"],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""],["--scim-prefix","DM_SCIM_PREFIX","/scim/v2"],["--scim-user-base","DM_SCIM_USER_BASE",""],["--scim-group-base","DM_SCIM_GROUP_BASE",""],["--scim-user-base-template","DM_SCIM_USER_BASE_TEMPLATE",""],["--scim-group-base-template","DM_SCIM_GROUP_BASE_TEMPLATE",""],["--scim-base-map","DM_SCIM_BASE_MAP",""],["--scim-user-base-header","DM_SCIM_USER_BASE_HEADER",""],["--scim-group-base-header","DM_SCIM_GROUP_BASE_HEADER",""],["--scim-base-header-root","DM_SCIM_BASE_HEADER_ROOT",""],["--scim-user-object-class","DM_SCIM_USER_OBJECT_CLASSES",["top","inetOrgPerson","organizationalPerson","person"],"array","--scim-user-object-classes"],["--scim-user-rdn-attribute","DM_SCIM_USER_RDN_ATTRIBUTE","uid"],["--scim-user-lock-attribute","DM_SCIM_USER_LOCK_ATTRIBUTE","pwdAccountLockedTime"],["--scim-user-lock-value","DM_SCIM_USER_LOCK_VALUE",""],["--scim-group-object-class","DM_SCIM_GROUP_OBJECT_CLASSES",["top","groupOfNames"],"array","--scim-group-object-classes"],["--scim-group-rdn-attribute","DM_SCIM_GROUP_RDN_ATTRIBUTE","cn"],["--scim-group-external-id-attribute","DM_SCIM_GROUP_EXTERNAL_ID_ATTRIBUTE",""],["--scim-id-attribute","DM_SCIM_ID_ATTRIBUTE","rdn"],["--scim-user-mapping","DM_SCIM_USER_MAPPING",""],["--scim-group-mapping","DM_SCIM_GROUP_MAPPING",""],["--scim-max-results","DM_SCIM_MAX_RESULTS",200,"number"],["--scim-max-scanned","DM_SCIM_MAX_SCANNED",1e4,"number"],["--scim-bulk-max-operations","DM_SCIM_BULK_MAX_OPERATIONS",100,"number"],["--scim-bulk-max-payload-size","DM_SCIM_BULK_MAX_PAYLOAD_SIZE",1048576,"number"],["--scim-etag","DM_SCIM_ETAG",!1,"boolean"],["--scim-base-url","DM_SCIM_BASE_URL",""]];export{t as default};
|
package/dist/lib/errors.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
class HttpError extends Error{constructor(r,
|
|
1
|
+
class HttpError extends Error{constructor(r,t=500){super(r),this.statusCode=t,this.name="HttpError"}}class BadRequestError extends HttpError{constructor(r="Bad request"){super(r,400),this.name="BadRequestError"}}class UnauthorizedError extends HttpError{constructor(r="Unauthorized"){super(r,401),this.name="UnauthorizedError"}}class ForbiddenError extends HttpError{constructor(r="Forbidden"){super(r,403),this.name="ForbiddenError"}}class AmbiguousIdentityError extends ForbiddenError{constructor(r,t){super(`[authz-forbidden] ${r} names ${t} directory entries, so no single one can be taken for the caller`),this.identity=r,this.count=t,this.name="AmbiguousIdentityError"}}class NotFoundError extends HttpError{constructor(r="Not found"){super(r,404),this.name="NotFoundError"}}class ConflictError extends HttpError{constructor(r="Conflict"){super(r,409),this.name="ConflictError"}}class UriTooLongError extends HttpError{constructor(r="URI Too Long"){super(r,414),this.name="UriTooLongError"}}class UnprocessableEntityError extends HttpError{constructor(r="Unprocessable Entity"){super(r,422),this.name="UnprocessableEntityError"}}class TooManyRequestsError extends HttpError{constructor(r="Too Many Requests"){super(r,429),this.name="TooManyRequestsError"}}class BadGatewayError extends HttpError{constructor(r="Bad Gateway"){super(r,502),this.name="BadGatewayError"}}class ServiceUnavailableError extends HttpError{constructor(r="Service Unavailable"){super(r,503),this.name="ServiceUnavailableError"}}class GatewayTimeoutError extends HttpError{constructor(r="Gateway Timeout"){super(r,504),this.name="GatewayTimeoutError"}}export{AmbiguousIdentityError,BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UnprocessableEntityError,UriTooLongError};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node-fetch";import{ConflictError as e,GatewayTimeoutError as i,BadGatewayError as a}from"./errors.js";async function refused(t,e){const i=(await e.text().catch(()=>"")).slice(0,500);return new a(`${t}: HTTP ${e.status}${i?` ${i}`:""}`)}class HttpProvider{constructor(t,e){this.domain=t,this.timeout=e}address(t){return`${t.id}.${this.domain}`.toLowerCase()}async call(e,i={}){try{return await t(e,{...i,signal:AbortSignal.timeout(this.timeout)})}catch(t){throw this.timedOut(t,e)}}async json(t){try{return await t.json()}catch(e){throw this.timedOut(e,t.url)}}timedOut(t,e){return["AbortError","TimeoutError"].includes(t.name)?new i(`${new URL(e).host} did not answer within ${this.timeout} ms`):t}}class ClouderyProvider extends HttpProvider{constructor(t,e,i,a,s){super(i,s),this.url=t,this.token=e,this.offer=a}headers(){return{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json"}}async create(t){const e=await this.call(`${this.url}/api/v1/instances`,{method:"POST",headers:this.headers(),body:JSON.stringify({email:t.email,internal_email:t.email,publicName:t.publicName,public_name:t.publicName,locale:t.locale,oidc:t.id,slug:t.id,phone:t.phone||"",offer:t.offer||this.offer,domain:this.domain,skip_email_validation:!0,...t.orgId?{org_id:t.orgId}:{},...t.orgDomain?{org_domain:t.orgDomain}:{}})});if(!e.ok)throw await refused("Cloudery refused the instance",e)}async find(t,i){const a=(i??this.address(t)).toLowerCase(),s=new URL(`${this.url}/api/v2/instances`);s.searchParams.set("fqdn",a),s.searchParams.set("limit","1");const
|
|
1
|
+
import t from"node-fetch";import{ConflictError as e,GatewayTimeoutError as i,BadGatewayError as a}from"./errors.js";async function refused(t,e){const i=(await e.text().catch(()=>"")).slice(0,500);return new a(`${t}: HTTP ${e.status}${i?` ${i}`:""}`)}class HttpProvider{constructor(t,e){this.domain=t,this.timeout=e}address(t){return`${t.id}.${this.domain}`.toLowerCase()}async call(e,i={}){try{return await t(e,{...i,signal:AbortSignal.timeout(this.timeout)})}catch(t){throw this.timedOut(t,e)}}async json(t){try{return await t.json()}catch(e){throw this.timedOut(e,t.url)}}timedOut(t,e){return["AbortError","TimeoutError"].includes(t.name)?new i(`${new URL(e).host} did not answer within ${this.timeout} ms`):t}}class ClouderyProvider extends HttpProvider{constructor(t,e,i,a,s){super(i,s),this.url=t,this.token=e,this.offer=a}headers(){return{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json"}}async create(t){const e=await this.call(`${this.url}/api/v1/instances`,{method:"POST",headers:this.headers(),body:JSON.stringify({email:t.email,internal_email:t.email,publicName:t.publicName,public_name:t.publicName,locale:t.locale,oidc:t.id,slug:t.id,phone:t.phone||"",offer:t.offer||this.offer,domain:this.domain,skip_email_validation:!0,...t.orgId?{org_id:t.orgId}:{},...t.orgDomain?{org_domain:t.orgDomain}:{}})});if(!e.ok)throw await refused("Cloudery refused the instance",e)}async find(t,i){const a=(i??this.address(t)).toLowerCase(),s=new URL(`${this.url}/api/v2/instances`);s.searchParams.set("fqdn",a),s.searchParams.set("limit","1");const o=await this.call(s.toString(),{headers:this.headers()});if(!o.ok)throw await refused("Cloudery instance search failed",o);const{items:r}=await this.json(o),n=r?.[0];if(n){if(n.internal_email&&n.internal_email.toLowerCase()!==t.email.toLowerCase()||n.oidc&&n.oidc!==t.id)throw new e(`${a} already exists and belongs to another user`);return{fqdn:n.fqdn,ready:Boolean(n.instantiated_at)}}}async createOrganization(t){const e=await this.call(`${this.url}/api/v2/organizations`,{method:"POST",headers:this.headers(),body:JSON.stringify({ldap_branch:t.id,name:t.name,custom_domain:t.domain})});if(!e.ok&&409!==e.status)throw await refused("Cloudery refused the organization",e)}async linkOrganization(t,e){const i=await this.call(`${this.url}/api/v2/organizations/${encodeURIComponent(t)}`,{method:"PATCH",headers:this.headers(),body:JSON.stringify({instance_fqdn:e})});if(!i.ok)throw await refused("Cloudery refused to link the organization",i)}}class CozyStackProvider extends HttpProvider{constructor(t,e,i,a,s,o,r){super(a,r),this.url=t,this.context=s,this.apps=o,this.auth=`Basic ${Buffer.from(`${e}:${i}`).toString("base64")}`}headers(){return{Authorization:this.auth,Accept:"application/json"}}async create(t){const i=this.address(t),a=new URLSearchParams({Domain:i,Email:t.email,PublicName:t.publicName,Locale:t.locale,OIDCID:t.oidc??t.id});t.phone&&a.set("Phone",t.phone),t.orgId&&a.set("OrgID",t.orgId),t.orgDomain&&a.set("OrgDomain",t.orgDomain),this.context&&a.set("ContextName",this.context),this.apps&&a.set("Apps",this.apps);const s=await this.call(`${this.url}/instances?${a.toString()}`,{method:"POST",headers:this.headers()});if(409===s.status){if(!await this.find(t))throw new e(`${i} already exists`);return i}if(!s.ok)throw await refused("cozy-stack refused the instance",s);return await this.finishOnboarding(i),i}async find(t,i){const a=(i??this.address(t)).toLowerCase(),s=await this.read(a);if(s){if(s.email?.toLowerCase()!==t.email.toLowerCase()||s.oidc_id!==(t.oidc??t.id))throw new e(`${a} already exists and belongs to another user`);return s.onboarding_finished||await this.finishOnboarding(a),{fqdn:a,ready:!0}}}async destroy(t,i){const a=this.address({id:t}),s=await this.read(a);if(!s)return;if(s.oidc_id!==i)throw new e(`${a} belongs to another user, left in place`);const o=await this.call(`${this.url}/instances/${encodeURIComponent(a)}`,{method:"DELETE",headers:this.headers()});if(!o.ok&&404!==o.status)throw await refused("cozy-stack refused to destroy the instance",o)}async read(t){const e=await this.call(`${this.url}/instances/${encodeURIComponent(t)}`,{headers:this.headers()});if(404===e.status)return;if(!e.ok)throw await refused("cozy-stack instance read failed",e);const{data:i}=await this.json(e);return i?.attributes??{}}async finishOnboarding(t){const e=await this.call(`${this.url}/instances/${encodeURIComponent(t)}?OnboardingFinished=true`,{method:"PATCH",headers:this.headers()});if(!e.ok)throw await refused("cozy-stack refused to finish onboarding",e)}}export{ClouderyProvider,CozyStackProvider};
|
package/dist/lib/ldapActions.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash as e}from"node:crypto";import{Client as t,Attribute as i,Change as s,Control as n}from"ldapts";import{LRUCache as a}from"lru-cache";import r from"p-limit";import{isDnInBranch as o,normalizeDn as c,launchHooksChained as l,launchHooks as h,escapeDnValue as d}from"./utils.js";import{changeContext as p}from"./changeContext.js";import{NotFoundError as u,ConflictError as f}from"./errors.js";import{SchemaIndex as g,parseSchema as y}from"./ldapSchema.js";const m={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}};function ldapError(e,t){const i=new Error(`${e}: ${String(t)}`),s=t?.code;return"number"==typeof s&&(i.code=s),i}const digestKey=t=>e("sha256").update(t).digest("hex").slice(0,12),foldDn=e=>e.trim().toLowerCase().split(",").map(e=>e.trim()).join(","),cloneSearchResult=e=>({searchEntries:e.searchEntries.map(e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?[...t]:t]))),searchReferences:[...e.searchReferences]});class ldapActions{constructor(e){this.cacheGeneration=0,this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.declaredClasses=[],this.schemaUnavailable=!1,this.classWarnings=new a({max:1e4}),this.parent=e,this.logger=e.logger,this.config=e.config,this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=r(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const i="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,s=this.config.ldap_cache_ttl,n="string"==typeof s?parseInt(s,10)||0:s??0,o=1e3*Math.max(n,0);if(this.cacheEnabled=o>0,this.searchCache=new a({max:i,ttl:this.cacheEnabled?o:void 0,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(this.cacheEnabled?`LDAP search cache initialized: max=${i}, ttl=${o/1e3}s`:"LDAP search cache disabled (--ldap-cache-ttl=0)"),this.attrSignatureCache=new a({max:1e3}),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP DN is not defined");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined");if(!e.config.ldap_base)throw new Error("LDAP base is not defined, please set --ldap-base (or DM_LDAP_BASE)");this.base=e.config.ldap_base,this.ldapUrls=e.config.ldap_url,this.logger.info(`LDAP failover configured with ${this.ldapUrls.length} URL(s): ${this.ldapUrls.join(", ")}`),this.options={url:this.ldapUrls[0],timeout:0,connectTimeout:0,strictDN:!1},this.ldapUrls[0].startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const e=[];for(let i=0;i<this.ldapUrls.length;i++){const s=(this.currentUrlIndex+i)%this.ldapUrls.length,n=this.ldapUrls[s];try{this.logger.debug(`Attempting connection to ${n}`);const e={...this.options,url:n,tlsOptions:n.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},i=new t(e);return await i.bind(this.dn,this.pwd),s!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${n}`),this.currentUrlIndex=s),i}catch(t){this.logger.warn(`Failed to connect to ${n}: ${String(t)}`),e.push(t)}}throw this.logger.error(`LDAP connection failed for all ${this.ldapUrls.length} URL(s)`),new Error(`LDAP connection failed for all URLs: ${e.map(e=>e.message).join(", ")}`)}cleanupExpiredConnections(){if(!this.isCleaningUp){this.isCleaningUp=!0;try{const e=Date.now(),t=[];for(let i=this.availableConnections.length-1;i>=0;i--){const s=this.availableConnections[i];if(e-s.createdAt>this.connectionTtl){t.push(s),this.availableConnections.splice(i,1);const e=this.connectionPool.indexOf(s);-1!==e&&this.connectionPool.splice(e,1)}}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}finally{this.isCleaningUp=!1}}}async acquireConnection(){if(this.cleanupExpiredConnections(),this.availableConnections.length>0){const e=this.availableConnections.pop();return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e}if(this.connectionPool.length<this.poolSize){const e={client:await this.createConnection(),createdAt:Date.now(),inUse:!0};return this.connectionPool.push(e),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),e}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((e,t)=>{const i=globalThis.setTimeout(()=>{const e=this.waitingResolvers.indexOf(resolveWrapper);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),resolveWrapper=t=>{globalThis.clearTimeout(i),e(t)};this.waitingResolvers.push(resolveWrapper)})}releaseConnection(e){if(Date.now()-e.createdAt>this.connectionTtl){const t=this.connectionPool.indexOf(e);return-1!==t&&this.connectionPool.splice(t,1),e.client.unbind().catch(()=>{}),void this.logger.debug("Released expired LDAP connection (discarded)")}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t(e)}e.inUse=!1,this.availableConnections.push(e),this.logger.debug("Released LDAP connection back to pool")}getAttributeSignature(e){if(!e||0===e.length)return"*";const t=e.join("|");let i=this.attrSignatureCache.get(t);return i||(i=[...e].sort().join(","),this.attrSignatureCache.set(t,i)),i}getCacheKey(e,t){const i=this.getAttributeSignature(t.attributes),s=t.explicitBufferAttributes?.length?[...t.explicitBufferAttributes].sort().join(","):"-",n="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return[e,t.scope||"sub",n,i,s,!1===t.returnAttributeValues?"novalues":"values",t.derefAliases??"-",String(t.sizeLimit??"-")].join("\0")}invalidateCache(e){if(!this.cacheEnabled)return;this.cacheGeneration++;const t=foldDn(e),i=`,${t}`,s=[];for(const e of this.searchCache.keys()){const n=foldDn(e.split("\0")[0]);(n===t||n.endsWith(i))&&s.push(e)}for(const e of s)this.searchCache.delete(e)}get system(){return this}forRequest(e){return{search:(t,i)=>this.search(t,i??this.base,e),add:(t,i)=>this.add(t,i,e),modify:(t,i,s)=>this.modify(t,i,e,s),rename:(t,i)=>this.rename(t,i,e),delete:t=>this.delete(t,e)}}declareObjectClasses(e,t,i=!1){const s=(Array.isArray(t)?t:[t]).filter(e=>"string"==typeof e&&""!==e);e&&0!==s.length&&this.declaredClasses.push({base:e,classes:s,includeBase:i})}declaredFor(e){let t=0,i=[];for(const s of this.declaredClasses){if(!o(e,s.base))continue;const n=c(s.base);if(!s.includeBase&&c(e)===n)continue;const a=n.split(",").length;a>t&&(t=a,i=[]),a===t&&i.push(...s.classes)}const s=new Set;return i.filter(e=>{const t=e.toLowerCase();return!s.has(t)&&(s.add(t),!0)})}warnOnce(e){this.classWarnings.has(e)||(this.classWarnings.set(e,!0),this.logger.warn(e))}async classRepair(e,t){if(!1===this.config.ldap_flat_auto_repair)return[];const i=this.declaredFor(e);if(0===i.length)return[];const s=Array.isArray(t.delete)?t.delete:Object.entries(t.delete||{}).filter(([,e])=>!e||Array.isArray(e)&&!e.length).map(([e])=>e);if([...Object.keys(t.add||{}),...Object.keys(t.replace||{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete||{})].some(e=>"objectclass"===e.toLowerCase()))return[];let n;try{n=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){return this.schemaUnavailable||this.logger.warn(`Object classes are not repaired: the directory schema could not be read (${String(e)})`),this.schemaUnavailable=!0,[]}const attributeKey=e=>n.getAttributeType(e)?.oid??e.toLowerCase(),allowedBy=e=>{const{must:t,may:i}=n.resolveAttributes(e);return new Set([...t,...i].map(attributeKey))},valued=e=>Object.entries(e||{}).filter(([,e])=>Array.isArray(e)?e.length>0:null!=e&&""!==e).map(([e])=>e),a=[...valued(t.add),...valued(t.replace)],r=i.filter(e=>{const t=n.getObjectClass(e);if("AUXILIARY"!==t?.kind)return!1;const i=allowedBy([e]);return a.some(e=>i.has(attributeKey(e)))});if(0===r.length)return[];let o;try{o=(await this.system.search({paged:!1,scope:"base",attributes:["objectClass",...n.resolveAttributes(r).must]},e)).searchEntries[0]}catch{return[]}if(!o)return[];const c=o.objectClass??o.objectclass,l=(Array.isArray(c)?c:c?[c]:[]).map(String),h=allowedBy(l);let d=a.filter(e=>n.getAttributeType(e)&&!h.has(attributeKey(e)));if(0===d.length)return[];const p=new Set(s.map(attributeKey)),u=new Set([...valued(o).filter(e=>!p.has(attributeKey(e))),...a].map(attributeKey)),{auxiliary:f,structural:g}=n.missingClasses(i,l),y=[];for(const t of f){const i=t.names[0]??t.oid,s=allowedBy([i]),a=d.filter(e=>s.has(attributeKey(e)));if(0===a.length)continue;const r=n.resolveAttributes([i]).must.filter(e=>!u.has(attributeKey(e)));r.length>0?this.warnOnce(`${e} needs ${i} for ${a.join(", ")}, and cannot be given it without ${r.join(", ")}`):(y.push(i),d=d.filter(e=>!a.includes(e)))}const m=g.filter(e=>{const t=allowedBy([e]);return d.some(e=>t.has(attributeKey(e)))});return m.length>0&&this.warnOnce(`${e} cannot hold ${d.join(", ")}: its schema declares ${m.join(", ")}, a structural class an existing entry cannot be given`),y.length>0&&this.logger.info(`${e}: adding ${y.join(", ")}, which the change needs and its schema declares`),y}async subschemaDn(){try{const e=await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["subschemaSubentry"]},""),t=e.searchEntries[0]?.subschemaSubentry,i=Array.isArray(t)?t[0]:t;if(i)return String(i)}catch(e){this.logger.warn(`Unable to read subschemaSubentry from root DSE: ${String(e)}`)}return"cn=Subschema"}async schemaIndex(){const e=1e3*(this.config.ldap_raw_schema_cache_ttl??3600);if(this.schemaCache&&Date.now()-this.schemaCache.fetchedAt<e)return this.schemaCache.index;if(this.schemaLoading)return this.schemaLoading;this.schemaLoading=(async()=>{const e=await this.subschemaDn(),t=(await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["objectClasses","attributeTypes","ldapSyntaxes","matchingRules"]},e).catch(t=>{if(32===t?.code)throw new u(`Subschema entry ${e} not found`);throw t})).searchEntries[0];if(!t)throw new u(`Subschema entry ${e} not found`);const list=e=>{if(void 0===e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString("utf8"):e)},i=new g(y({objectClasses:list(t.objectClasses),attributeTypes:list(t.attributeTypes),ldapSyntaxes:list(t.ldapSyntaxes),matchingRules:list(t.matchingRules)}));return this.schemaCache={index:i,fetchedAt:Date.now()},this.logger.info(`LDAP schema loaded from ${e}: ${i.schema.objectClasses.length} object classes, ${i.schema.attributeTypes.length} attribute types`),i})();try{return await this.schemaLoading}finally{this.schemaLoading=void 0}}filterForCaller(e,t,i){if(!this.parent.hooks.ldapsearchfilter)return e;const s=this.parent.hooks.ldapsearchfilter,one=async e=>{const[n]=await l(s,[e,t,i]);return n??e};if("function"==typeof e[Symbol.asyncIterator]){const t=e;return async function*(){for await(const e of t)yield await one(e)}()}return one(e)}async search(e,t=this.base,i){let s={...m,...e};s=await l(this.parent.hooks.ldapsearchopts,s),[t,s]=await l(this.parent.hooks.ldapsearchrequest,[t,s,i]);const n=this.cacheEnabled&&!s.paged&&"base"===s.scope,a=n?this.getCacheKey(t,s):"",r=this.cacheGeneration;if(n){const e=this.searchCache.get(a);if(e)return this.logger.debug(`LDAP search cache hit: ${digestKey(a)}`),this.filterForCaller(cloneSearchResult(e),i,s)}const o=await this.acquireConnection();try{let e=s.paged?o.client.searchPaginated(t,s):o.client.search(t,s);if(e=await l(this.parent.hooks.ldapsearchresult,e),n){const t=e;return r===this.cacheGeneration?(this.searchCache.set(a,cloneSearchResult(t)),this.logger.debug(`LDAP search cached: ${digestKey(a)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${digestKey(a)}`),this.filterForCaller(t,i,s)}return s.paged?this.filterForCaller(this.wrapPaginatedSearch(e,o),i,s):this.filterForCaller(e,i,s)}finally{s.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,t,s){e=this.setDn(e),t.objectClass&&0!==t.objectClass.length||!this.config.user_class||(t.objectClass=this.config.user_class);const n={};for(const[e,i]of Object.entries(t))Buffer.isBuffer(i)?n[e]=i.toString():Array.isArray(i)&&i.length>0&&Buffer.isBuffer(i[0])?n[e]=i.map(e=>e.toString()):n[e]=i;[e,t]=await l(this.parent.hooks.ldapaddrequest,[e,n,s]);const a=[];for(const[e,t]of Object.entries(n)){const s=Array.isArray(t)?t:[t];a.push(new i({type:e,values:s}))}const r=await this.acquireConnection();try{this.invalidateCache(e),await r.client.add(e,a),this.invalidateCache(e),h(this.parent.hooks.ldapadddone,[e,t],p(s))}catch(t){throw function(e,t){const i=ldapError("LDAP add error",t);if(68!==i.code)return i;const s=new f(`Entry ${e} already exists`);return s.code=68,s}(e,t)}finally{this.releaseConnection(r)}return await h(this.parent.hooks.ldapaddafter,[e,t],p(s)),!0}async modify(e,t,a,r={}){e=this.setDn(e);const o=[],c=this.opNumber();if([e,t]=await l(this.parent.hooks.ldapmodifyrequest,[e,t,c,a]),t.add)for(const[e,n]of Object.entries(t.add))o.push(new s({operation:"add",modification:new i({type:e,values:Array.isArray(n)?n:[n]})}));if(t.replace)for(const[e,n]of Object.entries(t.replace))o.push(new s({operation:"replace",modification:new i({type:e,values:Array.isArray(n)?n:[n]})}));if(t.delete)if(Array.isArray(t.delete))for(const e of t.delete)e&&o.push(new s({operation:"delete",modification:new i({type:e,values:[]})}));else for(const[e,n]of Object.entries(t.delete)){const t=new s({operation:"delete",modification:new i(n?{type:e,values:Array.isArray(n)?n:[n]}:{type:e})});o.push(t)}if(0===o.length){return Object.keys(t.add||{}).length>0||Object.keys(t.replace||{}).length>0||(Array.isArray(t.delete)?t.delete.length>0:Object.keys(t.delete||{}).length>0)?this.logger.warn(`Modify on ${e} asked for changes but emitted none; nothing was written`):this.logger.debug(`Modify on ${e} had nothing to apply`),h(this.parent.hooks.ldapmodifydone,[e,{},c],p(a)),!1}{const l=await this.classRepair(e,t),d=l.length>0?new s({operation:"add",modification:new i({type:"objectClass",values:l})}):void 0;d&&o.push(d);const u=await this.acquireConnection();try{this.invalidateCache(e);const i=r.permissive?[new n("1.2.840.113556.1.4.1413",{critical:!1})]:void 0;try{await u.client.modify(e,o,i)}catch(t){if(!d||20!==t?.code)throw t;o.splice(o.indexOf(d),1),await u.client.modify(e,o,i)}return this.invalidateCache(e),h(this.parent.hooks.ldapmodifydone,[e,t,c],p(a)),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(o)}`),function(e,t){const i=ldapError("LDAP modify error",t);if(65!==i.code)return i;const s=new f(`Entry ${e} cannot hold this change: ${String(t)}`);return s.code=65,s}(e,t)}finally{this.releaseConnection(u)}}}async rename(e,t,i){e=this.setDn(e),t=this.setDn(t),[e,t]=await l(this.parent.hooks.ldaprenamerequest,[e,t,i]);const s=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await s.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),h(this.parent.hooks.ldaprenamedone,[e,t],p(i)),!0}catch(e){throw ldapError("LDAP rename error",e)}finally{this.releaseConnection(s)}}async move(e,t,i){e=this.setDn(e),t=this.setDn(t);const s=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await s.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),h(this.parent.hooks.ldaprenamedone,[e,t],p(i)),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(e){throw ldapError("LDAP move error",e)}finally{this.releaseConnection(s)}}async delete(e,t){e=Array.isArray(e)?e.map(e=>this.setDn(e)):this.setDn(e),Array.isArray(e)||(e=[e]),[e]=await l(this.parent?.hooks.ldapdeleterequest,[e,t]);const i=await this.acquireConnection();try{for(const s of e){try{this.invalidateCache(s),await i.client.del(s),this.invalidateCache(s)}catch(e){throw ldapError("LDAP delete error",e)}h(this.parent.hooks.ldapdeletedone,s,p(t))}return!0}finally{this.releaseConnection(i)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${d(e)},${this.base}`,e}opNumber(){return this.parent.operationSequence++}}export{ldapActions as default};
|
|
1
|
+
import{createHash as e}from"node:crypto";import{Client as t,Attribute as i,Change as s,Control as n}from"ldapts";import{LRUCache as a}from"lru-cache";import r from"p-limit";import{normalizeDn as o,isDnInBranch as c,launchHooksChained as l,launchHooks as h,escapeDnValue as d}from"./utils.js";import{changeContext as p}from"./changeContext.js";import{NotFoundError as f,ConflictError as u}from"./errors.js";import{SchemaIndex as g,parseSchema as y}from"./ldapSchema.js";const m={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}};function ldapError(e,t){const i=new Error(`${e}: ${String(t)}`),s=t?.code;return"number"==typeof s&&(i.code=s),i}const digestKey=t=>e("sha256").update(t).digest("hex").slice(0,12),foldDn=e=>e.trim().toLowerCase().split(",").map(e=>e.trim()).join(","),cloneSearchResult=e=>({searchEntries:e.searchEntries.map(e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?[...t]:t]))),searchReferences:[...e.searchReferences]});class ldapActions{constructor(e){this.cacheGeneration=0,this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.declaredClasses=new Map,this.schemaUnavailable=!1,this.classWarnings=new a({max:1e4}),this.parent=e,this.logger=e.logger,this.config=e.config,this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=r(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const i="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,s=this.config.ldap_cache_ttl,n="string"==typeof s?parseInt(s,10)||0:s??0,o=1e3*Math.max(n,0);if(this.cacheEnabled=o>0,this.searchCache=new a({max:i,ttl:this.cacheEnabled?o:void 0,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(this.cacheEnabled?`LDAP search cache initialized: max=${i}, ttl=${o/1e3}s`:"LDAP search cache disabled (--ldap-cache-ttl=0)"),this.attrSignatureCache=new a({max:1e3}),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP DN is not defined");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined");if(!e.config.ldap_base)throw new Error("LDAP base is not defined, please set --ldap-base (or DM_LDAP_BASE)");this.base=e.config.ldap_base,this.ldapUrls=e.config.ldap_url,this.logger.info(`LDAP failover configured with ${this.ldapUrls.length} URL(s): ${this.ldapUrls.join(", ")}`),this.options={url:this.ldapUrls[0],timeout:0,connectTimeout:0,strictDN:!1},this.ldapUrls[0].startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const e=[];for(let i=0;i<this.ldapUrls.length;i++){const s=(this.currentUrlIndex+i)%this.ldapUrls.length,n=this.ldapUrls[s];try{this.logger.debug(`Attempting connection to ${n}`);const e={...this.options,url:n,tlsOptions:n.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},i=new t(e);return await i.bind(this.dn,this.pwd),s!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${n}`),this.currentUrlIndex=s),i}catch(t){this.logger.warn(`Failed to connect to ${n}: ${String(t)}`),e.push(t)}}throw this.logger.error(`LDAP connection failed for all ${this.ldapUrls.length} URL(s)`),new Error(`LDAP connection failed for all URLs: ${e.map(e=>e.message).join(", ")}`)}cleanupExpiredConnections(){if(!this.isCleaningUp){this.isCleaningUp=!0;try{const e=Date.now(),t=[];for(let i=this.availableConnections.length-1;i>=0;i--){const s=this.availableConnections[i];if(e-s.createdAt>this.connectionTtl){t.push(s),this.availableConnections.splice(i,1);const e=this.connectionPool.indexOf(s);-1!==e&&this.connectionPool.splice(e,1)}}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}finally{this.isCleaningUp=!1}}}async acquireConnection(){if(this.cleanupExpiredConnections(),this.availableConnections.length>0){const e=this.availableConnections.pop();return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e}if(this.connectionPool.length<this.poolSize){const e={client:await this.createConnection(),createdAt:Date.now(),inUse:!0};return this.connectionPool.push(e),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),e}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((e,t)=>{const i=globalThis.setTimeout(()=>{const e=this.waitingResolvers.indexOf(resolveWrapper);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),resolveWrapper=t=>{globalThis.clearTimeout(i),e(t)};this.waitingResolvers.push(resolveWrapper)})}releaseConnection(e){if(Date.now()-e.createdAt>this.connectionTtl){const t=this.connectionPool.indexOf(e);return-1!==t&&this.connectionPool.splice(t,1),e.client.unbind().catch(()=>{}),void this.logger.debug("Released expired LDAP connection (discarded)")}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t(e)}e.inUse=!1,this.availableConnections.push(e),this.logger.debug("Released LDAP connection back to pool")}getAttributeSignature(e){if(!e||0===e.length)return"*";const t=e.join("|");let i=this.attrSignatureCache.get(t);return i||(i=[...e].sort().join(","),this.attrSignatureCache.set(t,i)),i}getCacheKey(e,t){const i=this.getAttributeSignature(t.attributes),s=t.explicitBufferAttributes?.length?[...t.explicitBufferAttributes].sort().join(","):"-",n="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return[e,t.scope||"sub",n,i,s,!1===t.returnAttributeValues?"novalues":"values",t.derefAliases??"-",String(t.sizeLimit??"-")].join("\0")}invalidateCache(e){if(!this.cacheEnabled)return;this.cacheGeneration++;const t=foldDn(e),i=`,${t}`,s=[];for(const e of this.searchCache.keys()){const n=foldDn(e.split("\0")[0]);(n===t||n.endsWith(i))&&s.push(e)}for(const e of s)this.searchCache.delete(e)}get system(){return this}forRequest(e){return{search:(t,i)=>this.search(t,i??this.base,e),add:(t,i)=>this.add(t,i,e),modify:(t,i,s)=>this.modify(t,i,e,s),rename:(t,i)=>this.rename(t,i,e),delete:t=>this.delete(t,e)}}declareObjectClasses(e,t,i=!1){const s=(Array.isArray(t)?t:[t]).filter(e=>"string"==typeof e&&""!==e);if(!e||0===s.length)return;const n=JSON.stringify([o(e),s.map(e=>e.toLowerCase()).sort(),i]);this.declaredClasses.set(n,{base:e,classes:s,includeBase:i})}declaredFor(e){let t=0,i=[];for(const s of this.declaredClasses.values()){if(!c(e,s.base))continue;const n=o(s.base);if(!s.includeBase&&o(e)===n)continue;const a=n.split(",").length;a>t&&(t=a,i=[]),a===t&&i.push(...s.classes)}const s=new Set;return i.filter(e=>{const t=e.toLowerCase();return!s.has(t)&&(s.add(t),!0)})}warnOnce(e){this.classWarnings.has(e)||(this.classWarnings.set(e,!0),this.logger.warn(e))}async classRepair(e,t){if(!1===this.config.ldap_flat_auto_repair)return[];const i=this.declaredFor(e);if(0===i.length)return[];const s=Array.isArray(t.delete)?t.delete:Object.entries(t.delete||{}).filter(([,e])=>!e||Array.isArray(e)&&!e.length).map(([e])=>e);if([...Object.keys(t.add||{}),...Object.keys(t.replace||{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete||{})].some(e=>"objectclass"===e.toLowerCase()))return[];let n;try{n=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){return this.schemaUnavailable||this.logger.warn(`Object classes are not repaired: the directory schema could not be read (${String(e)})`),this.schemaUnavailable=!0,[]}const attributeKey=e=>n.getAttributeType(e)?.oid??e.toLowerCase(),allowedBy=e=>{const{must:t,may:i}=n.resolveAttributes(e);return new Set([...t,...i].map(attributeKey))},valued=e=>Object.entries(e||{}).filter(([,e])=>Array.isArray(e)?e.length>0:null!=e&&""!==e).map(([e])=>e),a=[...valued(t.add),...valued(t.replace)],r=i.filter(e=>{const t=n.getObjectClass(e);if("AUXILIARY"!==t?.kind)return!1;const i=allowedBy([e]);return a.some(e=>i.has(attributeKey(e)))});if(0===r.length)return[];let o;try{o=(await this.system.search({paged:!1,scope:"base",attributes:["objectClass",...n.resolveAttributes(r).must]},e)).searchEntries[0]}catch{return[]}if(!o)return[];const c=o.objectClass??o.objectclass,l=(Array.isArray(c)?c:c?[c]:[]).map(String),h=allowedBy(l);let d=a.filter(e=>n.getAttributeType(e)&&!h.has(attributeKey(e)));if(0===d.length)return[];const p=new Set(s.map(attributeKey)),f=new Set([...valued(o).filter(e=>!p.has(attributeKey(e))),...a].map(attributeKey)),{auxiliary:u,structural:g}=n.missingClasses(i,l),y=[];for(const t of u){const i=t.names[0]??t.oid,s=allowedBy([i]),a=d.filter(e=>s.has(attributeKey(e)));if(0===a.length)continue;const r=n.resolveAttributes([i]).must.filter(e=>!f.has(attributeKey(e)));r.length>0?this.warnOnce(`${e} needs ${i} for ${a.join(", ")}, and cannot be given it without ${r.join(", ")}`):(y.push(i),d=d.filter(e=>!a.includes(e)))}const m=g.filter(e=>{const t=allowedBy([e]);return d.some(e=>t.has(attributeKey(e)))});return m.length>0&&this.warnOnce(`${e} cannot hold ${d.join(", ")}: its schema declares ${m.join(", ")}, a structural class an existing entry cannot be given`),y.length>0&&this.logger.info(`${e}: adding ${y.join(", ")}, which the change needs and its schema declares`),y}async subschemaDn(){try{const e=await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["subschemaSubentry"]},""),t=e.searchEntries[0]?.subschemaSubentry,i=Array.isArray(t)?t[0]:t;if(i)return String(i)}catch(e){this.logger.warn(`Unable to read subschemaSubentry from root DSE: ${String(e)}`)}return"cn=Subschema"}async schemaIndex(){const e=1e3*(this.config.ldap_raw_schema_cache_ttl??3600);if(this.schemaCache&&Date.now()-this.schemaCache.fetchedAt<e)return this.schemaCache.index;if(this.schemaLoading)return this.schemaLoading;this.schemaLoading=(async()=>{const e=await this.subschemaDn(),t=(await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["objectClasses","attributeTypes","ldapSyntaxes","matchingRules"]},e).catch(t=>{if(32===t?.code)throw new f(`Subschema entry ${e} not found`);throw t})).searchEntries[0];if(!t)throw new f(`Subschema entry ${e} not found`);const list=e=>{if(void 0===e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString("utf8"):e)},i=new g(y({objectClasses:list(t.objectClasses),attributeTypes:list(t.attributeTypes),ldapSyntaxes:list(t.ldapSyntaxes),matchingRules:list(t.matchingRules)}));return this.schemaCache={index:i,fetchedAt:Date.now()},this.logger.info(`LDAP schema loaded from ${e}: ${i.schema.objectClasses.length} object classes, ${i.schema.attributeTypes.length} attribute types`),i})();try{return await this.schemaLoading}finally{this.schemaLoading=void 0}}filterForCaller(e,t,i){if(!this.parent.hooks.ldapsearchfilter)return e;const s=this.parent.hooks.ldapsearchfilter,one=async e=>{const[n]=await l(s,[e,t,i]);return n??e};if("function"==typeof e[Symbol.asyncIterator]){const t=e;return async function*(){for await(const e of t)yield await one(e)}()}return one(e)}async search(e,t=this.base,i){let s={...m,...e};s=await l(this.parent.hooks.ldapsearchopts,s),[t,s]=await l(this.parent.hooks.ldapsearchrequest,[t,s,i]);const n=this.cacheEnabled&&!s.paged&&"base"===s.scope,a=n?this.getCacheKey(t,s):"",r=this.cacheGeneration;if(n){const e=this.searchCache.get(a);if(e)return this.logger.debug(`LDAP search cache hit: ${digestKey(a)}`),this.filterForCaller(cloneSearchResult(e),i,s)}const o=await this.acquireConnection();try{let e=s.paged?o.client.searchPaginated(t,s):o.client.search(t,s);if(e=await l(this.parent.hooks.ldapsearchresult,e),n){const t=e;return r===this.cacheGeneration?(this.searchCache.set(a,cloneSearchResult(t)),this.logger.debug(`LDAP search cached: ${digestKey(a)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${digestKey(a)}`),this.filterForCaller(t,i,s)}return s.paged?this.filterForCaller(this.wrapPaginatedSearch(e,o),i,s):this.filterForCaller(e,i,s)}finally{s.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,t,s){e=this.setDn(e),t.objectClass&&0!==t.objectClass.length||!this.config.user_class||(t.objectClass=this.config.user_class);const n={};for(const[e,i]of Object.entries(t))Buffer.isBuffer(i)?n[e]=i.toString():Array.isArray(i)&&i.length>0&&Buffer.isBuffer(i[0])?n[e]=i.map(e=>e.toString()):n[e]=i;[e,t]=await l(this.parent.hooks.ldapaddrequest,[e,n,s]);const a=[];for(const[e,s]of Object.entries(t)){const t=Array.isArray(s)?s:[s];a.push(new i({type:e,values:t}))}const r=await this.acquireConnection();try{this.invalidateCache(e),await r.client.add(e,a),this.invalidateCache(e),h(this.parent.hooks.ldapadddone,[e,t],p(s))}catch(t){throw function(e,t){const i=ldapError("LDAP add error",t);if(68!==i.code)return i;const s=new u(`Entry ${e} already exists`);return s.code=68,s}(e,t)}finally{this.releaseConnection(r)}return await h(this.parent.hooks.ldapaddafter,[e,t],p(s)),!0}async modify(e,t,a,r={}){e=this.setDn(e);const o=[],c=this.opNumber();if([e,t]=await l(this.parent.hooks.ldapmodifyrequest,[e,t,c,a]),t.add)for(const[e,n]of Object.entries(t.add))o.push(new s({operation:"add",modification:new i({type:e,values:Array.isArray(n)?n:[n]})}));if(t.replace)for(const[e,n]of Object.entries(t.replace))o.push(new s({operation:"replace",modification:new i({type:e,values:Array.isArray(n)?n:[n]})}));if(t.delete)if(Array.isArray(t.delete))for(const e of t.delete)e&&o.push(new s({operation:"delete",modification:new i({type:e,values:[]})}));else for(const[e,n]of Object.entries(t.delete)){const t=new s({operation:"delete",modification:new i(n?{type:e,values:Array.isArray(n)?n:[n]}:{type:e})});o.push(t)}if(0===o.length){return Object.keys(t.add||{}).length>0||Object.keys(t.replace||{}).length>0||(Array.isArray(t.delete)?t.delete.length>0:Object.keys(t.delete||{}).length>0)?this.logger.warn(`Modify on ${e} asked for changes but emitted none; nothing was written`):this.logger.debug(`Modify on ${e} had nothing to apply`),h(this.parent.hooks.ldapmodifydone,[e,{},c],p(a)),!1}{const l=await this.classRepair(e,t),d=l.length>0?new s({operation:"add",modification:new i({type:"objectClass",values:l})}):void 0;d&&o.push(d);const f=await this.acquireConnection();try{this.invalidateCache(e);const i=r.permissive?[new n("1.2.840.113556.1.4.1413",{critical:!1})]:void 0;try{await f.client.modify(e,o,i)}catch(t){if(!d||20!==t?.code)throw t;o.splice(o.indexOf(d),1),await f.client.modify(e,o,i)}return this.invalidateCache(e),h(this.parent.hooks.ldapmodifydone,[e,t,c],p(a)),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(o)}`),function(e,t){const i=ldapError("LDAP modify error",t);if(65!==i.code)return i;const s=new u(`Entry ${e} cannot hold this change: ${String(t)}`);return s.code=65,s}(e,t)}finally{this.releaseConnection(f)}}}async rename(e,t,i){e=this.setDn(e),t=this.setDn(t),[e,t]=await l(this.parent.hooks.ldaprenamerequest,[e,t,i]);const s=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await s.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),h(this.parent.hooks.ldaprenamedone,[e,t],p(i)),!0}catch(e){throw ldapError("LDAP rename error",e)}finally{this.releaseConnection(s)}}async move(e,t,i){e=this.setDn(e),t=this.setDn(t);const s=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),await s.client.modifyDN(e,t),this.invalidateCache(e),this.invalidateCache(t),h(this.parent.hooks.ldaprenamedone,[e,t],p(i)),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(e){throw ldapError("LDAP move error",e)}finally{this.releaseConnection(s)}}async delete(e,t){e=Array.isArray(e)?e.map(e=>this.setDn(e)):this.setDn(e),Array.isArray(e)||(e=[e]),[e]=await l(this.parent?.hooks.ldapdeleterequest,[e,t]);const i=await this.acquireConnection();try{for(const s of e){try{this.invalidateCache(s),await i.client.del(s),this.invalidateCache(s)}catch(e){throw ldapError("LDAP delete error",e)}h(this.parent.hooks.ldapdeletedone,s,p(t))}return!0}finally{this.releaseConnection(i)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${d(e)},${this.base}`,e}opNumber(){return this.parent.operationSequence++}}export{ldapActions as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"express-openid-connect";import o from"../../lib/auth/base.js";import{launchHooksChained as i}from"../../lib/utils.js";import{serverError as t}from"../../lib/expressFormatedResponses.js";const{auth:
|
|
1
|
+
import e from"express-openid-connect";import o from"../../lib/auth/base.js";import{launchHooksChained as i}from"../../lib/utils.js";import{serverError as t,unauthorized as s}from"../../lib/expressFormatedResponses.js";const{auth:n,requiresAuth:r}=e,c=["/login","/logout","/callback","/backchannel-logout"],a=/^end_session_endpoint must be configured/,h=/^\/logout\/?$/i,walkSubscribers=async(e,o)=>{let i;for(const t of e??[])if(t)try{await t(o)}catch(e){i??(i=e instanceof Error?e:new Error(String(e)))}if(i)throw i};class OpenIDConnect extends o{constructor(e){super(e),this.name="openidconnect",this.configApiKey="openidconnect",this.roles=["auth","configurable"],this.warnedNoEndSession=!1;for(const e of["oidc_server","oidc_client_id","oidc_client_secret","base_url"])if(!this.config[e])throw new Error(`Missing config parameter ${e}`)}buildConfig(){return{authRequired:!1,issuerBaseURL:this.config.oidc_server,clientID:this.config.oidc_client_id,secret:this.config.oidc_client_secret,baseURL:this.config.base_url,clientSecret:this.config.oidc_client_secret,authorizationParams:{response_type:"code",scope:"openid profile email"},idpLogout:!0,routes:{logout:!1},backchannelLogout:{onLogoutToken:async e=>{this.server.hooks.oidclogouttoken?await walkSubscribers(this.server.hooks.oidclogouttoken,e):this.logger.warn("openidconnect: a logout token arrived but no Back-Channel Logout plugin is loaded, so nothing records it")},onLogin:async e=>{if(!this.server.hooks.oidclogin)return;const o=e.oidc?.idTokenClaims;if(o)try{await walkSubscribers(this.server.hooks.oidclogin,o)}catch(o){const i=e.appSession;if(i)for(const e of Object.keys(i))delete i[e];throw o}},isLoggedOut:async e=>{if(!this.server.hooks.oidcsessionvalid)return!1;const o=e.oidc.idTokenClaims,[,t]=await i(this.server.hooks.oidcsessionvalid,[o,!0]);return!t}}}}identitySource(){const e=this.config.oidc_username_claim||"sub";return`req.user: the OIDC sub, an opaque provider identifier; req.userName: the "${e}" claim`+("sub"===e?" — the same value, until --oidc-username-claim names another":"")}getConfigApiData(){return{enabled:!0,endpoints:{logout:"/logout"}}}get pathPrefixes(){const e=super.pathPrefixes;return 0===e.length?e:[...e,...c]}api(e){this.router=n(this.buildConfig()),super.api(e)}authMethod(e,o,i){if(!this.router)return t(o,new Error(`${this.name}: api() has not built the router`));this.router(e,o,n=>{if(n&&h.test(e.path)&&this.noEndSession(n))return o.redirect(this.config.base_url);if(n)return t(o,n);if(("GET"===e.method||"HEAD"===e.method)&&h.test(e.path))return void o.oidc.logout({logoutParams:{post_logout_redirect_uri:void 0}});const guard=()=>{r()(e,o,t=>{if(t)return s(o);const n=e.oidc.user,r=this.config.oidc_username_claim||"sub",c=n[r];"sub"!==r&&"string"!=typeof c&&this.logger.warn(`${this.name}: no "${r}" claim on this session, so req.userName falls back to the sub. Check --oidc-username-claim against the scopes the provider is asked for`),this.publishIdentity(e,String(n.sub),"string"==typeof c?c:String(n.sub)),i()})},c=this.renewExpired(e);c?c.then(guard):guard()})}renewExpired(e){const o=e.oidc,i=o.accessToken;if(!i?.isExpired())return;const end=()=>{e.appSession=void 0};if(o.refreshToken)return i.refresh().then(()=>{},e=>{this.logger.info(`${this.name}: the provider refused to renew an access token, so the session ends: ${String(e)}`),end()});end()}noEndSession(e){return!!(e instanceof Error&&a.test(e.message))&&(this.warnedNoEndSession||(this.warnedNoEndSession=!0,this.logger.warn(`${this.name}: the provider publishes no end_session_endpoint, so /logout ends the session here only, and the provider may log the caller back in`)),!0)}}export{OpenIDConnect as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,jsonBody as i,tryMethod as s,ok as a}from"../../lib/expressFormatedResponses.js";import{extractLdapCode as o}from"../../lib/ldapCodes.js";import{transformSchemas as n,asyncHandler as h,isDummyMemberDn as d,escapeDnValue as c,validateDnValue as p,launchHooksChained as m,launchHooks as l,escapeLdapFilter as u,organizationLink as f,substringSearchFilter as g}from"../../lib/utils.js";import{NotFoundError as b,BadRequestError as w,HttpError as y}from"../../lib/errors.js";import{assertClientMaySet as $,modifiedAttributeNames as v,missingRequiredAttribute as _,checkDnValues as E,matchesPattern as G}from"../../config/schema.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeletedone:async e=>{const t=Array.isArray(e)?e:[e];this.logger.debug(`User deletion detected, removing from groups: ${t.join(", ")}`),await Promise.all(t.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)})}},this.ldap=t.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");this.config.ldap_group_base&&t.ldap.declareObjectClasses(this.base,this.config.group_class||[]),this.config.group_schema&&e.readFile(this.config.group_schema,(e,t)=>{if(e)this.logger.error(`Failed to load group schema from ${this.config.group_schema}: ${e}`);else try{this.schema=JSON.parse(n(t.toString(),this.config)),this.logger.debug("Group schema loaded")}catch(e){this.logger.error(`Failed to parse ${this.config.schemas_path}/group.json: ${e}`)}})}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,h(async(e,t)=>{if(!r(e,t))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new w("Invalid match query");if("string"==typeof e.query.attribute&&e.query.attribute)i.filter=g(e.query.match,e.query.attribute);else if(/=/.test(e.query.match)){if(!/^[\w*=()&|, -]+$/.test(e.query.match))throw new w("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=u(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const s=await this.listGroups({...i,req:e});return a(t,this.hideNeverReturn(s))})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,h(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,h(async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)throw new w("cn is required");const a=i(e,t,"member");if(!a)return;if((Array.isArray(a.member)?a.member:[a.member]).some(e=>"string"!=typeof e))throw new w("member must be a string or string array");await s(t,this.addMember.bind(this),r,a.member)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,h(async(e,t)=>{if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!i||!a)throw new w("cn and member are required");await s(t,this.deleteMember.bind(this),i,a)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,h(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new w("cn is required");const o=i(e,t,"targetOrgDn");if(o){if("string"!=typeof o.targetOrgDn)throw new w("targetOrgDn must be a string");await s(t,this.moveGroup.bind(this),a,o.targetOrgDn,e)}})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,h(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new w("cn is required");const o=i(e,t,"newCn");if(o){if("string"!=typeof o.newCn)throw new w("newCn must be a string");await s(t,this.renameGroup.bind(this),a,o.newCn,e)}}))}isDummyMember(e){return d(e,this.config.group_dummy_user)}withoutDummyMember(e){const t=e.member;if(void 0===t)return e;const r=Array.isArray(t)?t:[t];return{...e,member:r.filter(e=>!this.isDummyMember(e))}}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const r=/,/.test(i)?i:`${this.cn}=${c(i)},${this.base}`,s=await this.ldap.search({paged:!1,scope:"base"},r,e);if(0===s.searchEntries.length)throw new b("Group not found");t.json(this.hideNeverReturn(this.withoutDummyMember(s.searchEntries[0])))}catch(e){if(e.code&&32===e.code)throw new b("Group not found");throw e}}async apiAdd(e,t,...r){const a=i(e,t,...r);if(!a)return;$(this.schema,Object.keys(a),this.cn);const o=a[this.cn],n=a.member?a.member:[],h=Object.fromEntries(Object.entries(a).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.addGroup.bind(this),o,n,h,e)}async apiDelete(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);if(!i)throw new w("cn is required");await s(t,this.deleteGroup.bind(this),i,e)}async apiModify(e,t){const r=i(e,t);if(!r)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new w("cn is required");$(this.schema,v(r));const o=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.modifyGroup.bind(this),a,o,e)}async addGroup(e,t=[],r={},i){let s;new RegExp(`^${this.cn}=`).test(e)?(s=e,e=e.replace(new RegExp(`^${this.cn}=((?:\\\\.|[^,])+)(?:,.*)?$`),"$1")):(p(e,this.cn),s=`${this.cn}=${c(e)},${this.base}`),await this.validateMembers(s,t),await this.validateNewGroup(s,{objectClass:this.config.group_class,cn:e,member:t,...r});const a=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||a.includes("twakeGroup")||a.includes("twakeStaticGroup")||a.push("twakeGroup");let o,n={objectClass:a,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&n.member.push(...t),[s,n]=await m(this.registeredHooks.ldapgroupadd,[s,n]);try{o=await this.ldap.add(s,n,i)}catch(e){if(e instanceof y)throw e;throw new Error(`Failed to add group ${s}: ${e}`)}return l(this.registeredHooks.ldapgroupadddone,[s,n]),o}async modifyGroup(e,t,r){let i=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;const s=this.opNumber();if([i,t]=await m(this.registeredHooks.ldapgroupmodify,[i,t,s]),t.add){if(t.add.member)throw new Error("Use dedicated API to add members");if(t.add[this.cn])throw new Error(`${this.cn} attribute iq unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new Error("Use dedicated API to delete members");if(t.delete[this.cn])throw new Error(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new Error("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new Error(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new Error("Use dedicated API to replace members");if(t.replace[this.cn])throw new Error(`Use dedicated API to change ${this.cn} attribute`)}await this.validateChanges(i,t);const a=await this.ldap.modify(i,t,r);return l(this.registeredHooks.ldapgroupmodifydone,[i,t,s]),a}async renameGroup(e,t,r){/,/.test(e)||p(e,this.cn),/,/.test(t)||p(t,this.cn);let i=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`,s=/,/.test(t)?t:`${this.cn}=${c(t)},${this.base}`;[i,s]=await m(this.registeredHooks.ldapgrouprename,[i,s]);const a=await this.ldap.rename(i,s,r);return l(this.registeredHooks.ldapgrouprenamedone,[i,s]),a}async deleteGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;r=await m(this.registeredHooks.ldapgroupdelete,r);const i=await this.ldap.delete(r,t);return l(this.registeredHooks.ldapgroupdeletedone,r),i}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await m(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{if(e instanceof y)throw e;throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;if([e,t]=await m(this.registeredHooks.ldapgroupdeletemember,[e,t]),this.isDummyMember(t))throw new w("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{if(e instanceof y)throw e;throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${u(e)}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.ldap.modify(t.dn,{delete:{member:e}}).catch(r=>{if(65===o(r)&&this.config.group_dummy_user)return this.ldap.modify(t.dn,{add:{member:this.config.group_dummy_user},delete:{member:e}});throw r}).catch(r=>{const i=o(r);16!==i&&32!==i&&this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r)})))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0],h=f(n[i]);if(!h)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(h===t)throw new Error("Group is already in the target organization");let d;try{d=await this.ldap.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===d.searchEntries.length)throw new Error(`Target organization ${t} not found`);const p=d.searchEntries[0][s];if(!p)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:p}},r),this.logger.info(`Group ${a} moved from ${h} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t,req:r}={}){const i=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base,r).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let s={};for await(const e of i)for(const t of e.searchEntries){const e=t[this.cn];if(Array.isArray(e)&&0===e.length)continue;e&&(s[e]=this.withoutDummyMember(t))}return s=await m(this.registeredHooks._ldapgrouplist,s),s}async searchGroupsByName(e,t=!1,r=[this.cn,"member"],i){const s=u(e),a=t?`(${this.cn}=*${s}*)`:`(${this.cn}=${s})`;return await this.listGroups({filter:a,attributes:r,req:i})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`)}async validateMembers(e,t){if([e,t]=await m(this.server.hooks.ldapgroupvalidatemembers,[e,t]),!this.config.groups_allow_unexistent_members&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{try{await this.ldap.search({paged:!1,scope:"base"},e)}catch(t){throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await m(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new w(`Invalid value for field ${e}`);const r=_(this.schema,t,this.server.loadedPlugins);if(r)throw new w(`Missing required field ${r}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await m(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))await this._validateOneChange(e,r);if(t.replace)for(const[e,r]of Object.entries(t.replace))await this._validateOneChange(e,r);if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=this.schema.attributes[e];if(!r){if(this.schema.strict)throw new w(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new w(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new w(`Field ${e} must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test&&"pointer"!==r.items.type)for(const i of t)if(typeof i!==r.items.type)throw new w(`Field ${e} must be of type ${r.items.type}`)}else if("pointer"!==r.type&&typeof t!==r.type)return!1;if(await E(e,r,t,async e=>(await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},e)).searchEntries.length>0),!G(r,t))throw new w(`Field ${e} has invalid value ${String(t)}`);return!0}getConfigApiData(){const e=this.config.api_prefix||"/api";let t;if(this.server.loadedPlugins.static&&this.config.group_schema){const e=this.config.static_name||"static",r=this.config.group_schema.indexOf("/schemas/");if(-1!==r){t=`/${e}${this.config.group_schema.substring(r)}`}}return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};
|
|
1
|
+
import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,jsonBody as i,tryMethod as s,ok as a}from"../../lib/expressFormatedResponses.js";import{extractLdapCode as o}from"../../lib/ldapCodes.js";import{transformSchemas as n,asyncHandler as h,isDummyMemberDn as d,escapeDnValue as c,validateDnValue as p,launchHooksChained as m,launchHooks as l,escapeLdapFilter as u,organizationLink as f,substringSearchFilter as g}from"../../lib/utils.js";import{NotFoundError as b,BadRequestError as w,HttpError as y}from"../../lib/errors.js";import{assertClientMaySet as $,modifiedAttributeNames as v,missingRequiredAttribute as _,checkDnValues as E,matchesPattern as G}from"../../config/schema.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeletedone:async e=>{const t=Array.isArray(e)?e:[e];this.logger.debug(`User deletion detected, removing from groups: ${t.join(", ")}`),await Promise.all(t.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)})}},this.ldap=t.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");this.config.ldap_group_base&&t.ldap.declareObjectClasses(this.base,this.config.group_class||[]),this.config.group_schema&&e.readFile(this.config.group_schema,(e,t)=>{if(e)this.logger.error(`Failed to load group schema from ${this.config.group_schema}: ${e}`);else try{this.schema=this.adaptSchema(JSON.parse(n(t.toString(),this.config))),this.logger.debug("Group schema loaded")}catch(e){this.logger.error(`Failed to parse ${this.config.schemas_path}/group.json: ${e}`)}})}adaptSchema(e){return e}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,h(async(e,t)=>{if(!r(e,t))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new w("Invalid match query");if("string"==typeof e.query.attribute&&e.query.attribute)i.filter=g(e.query.match,e.query.attribute);else if(/=/.test(e.query.match)){if(!/^[\w*=()&|, -]+$/.test(e.query.match))throw new w("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=u(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const s=await this.listGroups({...i,req:e});return a(t,this.hideNeverReturn(s))})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,h(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,h(async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)throw new w("cn is required");const a=i(e,t,"member");if(!a)return;if((Array.isArray(a.member)?a.member:[a.member]).some(e=>"string"!=typeof e))throw new w("member must be a string or string array");await s(t,this.addMember.bind(this),r,a.member)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,h(async(e,t)=>{if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!i||!a)throw new w("cn and member are required");await s(t,this.deleteMember.bind(this),i,a)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,h(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new w("cn is required");const o=i(e,t,"targetOrgDn");if(o){if("string"!=typeof o.targetOrgDn)throw new w("targetOrgDn must be a string");await s(t,this.moveGroup.bind(this),a,o.targetOrgDn,e)}})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,h(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new w("cn is required");const o=i(e,t,"newCn");if(o){if("string"!=typeof o.newCn)throw new w("newCn must be a string");await s(t,this.renameGroup.bind(this),a,o.newCn,e)}}))}isDummyMember(e){return d(e,this.config.group_dummy_user)}withoutDummyMember(e){const t=e.member;if(void 0===t)return e;const r=Array.isArray(t)?t:[t];return{...e,member:r.filter(e=>!this.isDummyMember(e))}}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const r=/,/.test(i)?i:`${this.cn}=${c(i)},${this.base}`,s=await this.ldap.search({paged:!1,scope:"base"},r,e);if(0===s.searchEntries.length)throw new b("Group not found");t.json(this.hideNeverReturn(this.withoutDummyMember(s.searchEntries[0])))}catch(e){if(e.code&&32===e.code)throw new b("Group not found");throw e}}async apiAdd(e,t,...r){const a=i(e,t,...r);if(!a)return;$(this.schema,Object.keys(a),this.cn);const o=a[this.cn],n=a.member?a.member:[],h=Object.fromEntries(Object.entries(a).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.addGroup.bind(this),o,n,h,e)}async apiDelete(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);if(!i)throw new w("cn is required");await s(t,this.deleteGroup.bind(this),i,e)}async apiModify(e,t){const r=i(e,t);if(!r)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new w("cn is required");$(this.schema,v(r));const o=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.modifyGroup.bind(this),a,o,e)}async addGroup(e,t=[],r={},i){let s;new RegExp(`^${this.cn}=`).test(e)?(s=e,e=e.replace(new RegExp(`^${this.cn}=((?:\\\\.|[^,])+)(?:,.*)?$`),"$1")):(p(e,this.cn),s=`${this.cn}=${c(e)},${this.base}`),await this.validateMembers(s,t),await this.validateNewGroup(s,{objectClass:this.config.group_class,cn:e,member:t,...r});const a=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||a.includes("twakeGroup")||a.includes("twakeStaticGroup")||a.push("twakeGroup");let o,n={objectClass:a,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&n.member.push(...t),[s,n]=await m(this.registeredHooks.ldapgroupadd,[s,n]);try{o=await this.ldap.add(s,n,i)}catch(e){if(e instanceof y)throw e;throw new Error(`Failed to add group ${s}: ${e}`)}return l(this.registeredHooks.ldapgroupadddone,[s,n]),o}async modifyGroup(e,t,r){let i=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;const s=this.opNumber();if([i,t]=await m(this.registeredHooks.ldapgroupmodify,[i,t,s]),t.add){if(t.add.member)throw new Error("Use dedicated API to add members");if(t.add[this.cn])throw new Error(`${this.cn} attribute iq unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new Error("Use dedicated API to delete members");if(t.delete[this.cn])throw new Error(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new Error("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new Error(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new Error("Use dedicated API to replace members");if(t.replace[this.cn])throw new Error(`Use dedicated API to change ${this.cn} attribute`)}await this.validateChanges(i,t);const a=await this.ldap.modify(i,t,r);return l(this.registeredHooks.ldapgroupmodifydone,[i,t,s]),a}async renameGroup(e,t,r){/,/.test(e)||p(e,this.cn),/,/.test(t)||p(t,this.cn);let i=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`,s=/,/.test(t)?t:`${this.cn}=${c(t)},${this.base}`;[i,s]=await m(this.registeredHooks.ldapgrouprename,[i,s]);const a=await this.ldap.rename(i,s,r);return l(this.registeredHooks.ldapgrouprenamedone,[i,s]),a}async deleteGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;r=await m(this.registeredHooks.ldapgroupdelete,r);const i=await this.ldap.delete(r,t);return l(this.registeredHooks.ldapgroupdeletedone,r),i}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await m(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{if(e instanceof y)throw e;throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;if([e,t]=await m(this.registeredHooks.ldapgroupdeletemember,[e,t]),this.isDummyMember(t))throw new w("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{if(e instanceof y)throw e;throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${u(e)}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.ldap.modify(t.dn,{delete:{member:e}}).catch(r=>{if(65===o(r)&&this.config.group_dummy_user)return this.ldap.modify(t.dn,{add:{member:this.config.group_dummy_user},delete:{member:e}});throw r}).catch(r=>{const i=o(r);16!==i&&32!==i&&this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r)})))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0],h=f(n[i]);if(!h)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(h===t)throw new Error("Group is already in the target organization");let d;try{d=await this.ldap.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===d.searchEntries.length)throw new Error(`Target organization ${t} not found`);const p=d.searchEntries[0][s];if(!p)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:p}},r),this.logger.info(`Group ${a} moved from ${h} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t,req:r}={}){const i=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base,r).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let s={};for await(const e of i)for(const t of e.searchEntries){const e=t[this.cn];if(Array.isArray(e)&&0===e.length)continue;e&&(s[e]=this.withoutDummyMember(t))}return s=await m(this.registeredHooks._ldapgrouplist,s),s}async searchGroupsByName(e,t=!1,r=[this.cn,"member"],i){const s=u(e),a=t?`(${this.cn}=*${s}*)`:`(${this.cn}=${s})`;return await this.listGroups({filter:a,attributes:r,req:i})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`)}async validateMembers(e,t){if([e,t]=await m(this.server.hooks.ldapgroupvalidatemembers,[e,t]),!this.config.groups_allow_unexistent_members&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{try{await this.ldap.search({paged:!1,scope:"base"},e)}catch(t){throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await m(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new w(`Invalid value for field ${e}`);const r=_(this.schema,t,this.server.loadedPlugins);if(r)throw new w(`Missing required field ${r}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await m(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))await this._validateOneChange(e,r);if(t.replace)for(const[e,r]of Object.entries(t.replace))await this._validateOneChange(e,r);if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=this.schema.attributes[e];if(!r){if(this.schema.strict)throw new w(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new w(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new w(`Field ${e} must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test&&"pointer"!==r.items.type)for(const i of t)if(typeof i!==r.items.type)throw new w(`Field ${e} must be of type ${r.items.type}`)}else if("pointer"!==r.type&&typeof t!==r.type)return!1;if(await E(e,r,t,async e=>(await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},e)).searchEntries.length>0),!G(r,t))throw new w(`Field ${e} has invalid value ${String(t)}`);return!0}getConfigApiData(){const e=this.config.api_prefix||"/api";let t;if(this.server.loadedPlugins.static&&this.config.group_schema){const e=this.config.static_name||"static",r=this.config.group_schema.indexOf("/schemas/");if(-1!==r){t=`/${e}${this.config.group_schema.substring(r)}`}}return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../../abstract/plugin.js";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 a=Array.isArray(t)?t:[t],e=[];for(const t of a){if(!this.isWatched(t)){this.logger.debug(`Trash plugin: ${t} not in watched branches, allowing native delete`),e.push(t);continue}this.logger.info(`Trash plugin: Intercepting delete of ${t}`),await this.ensureTrashExists();const a=this.buildTrashDN(t);await this.removeOldTrashEntry(a);try{this.logger.debug(`Moving ${t} to ${a} using LDAP move (atomic)`),await this.server.ldap.move(t,a,s),this.logger.info(`Entry ${t} moved to trash at ${a}`),await this.addMetadataToTrash(a,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)?e:e[0]??void 0,s]}};const
|
|
1
|
+
import t from"../../abstract/plugin.js";import{parseDn as s,normalizeDn as a,isDnInBranch as e}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 a=Array.isArray(t)?t:[t],e=[];for(const t of a){if(!this.isWatched(t)){this.logger.debug(`Trash plugin: ${t} not in watched branches, allowing native delete`),e.push(t);continue}this.logger.info(`Trash plugin: Intercepting delete of ${t}`),await this.ensureTrashExists();const a=this.buildTrashDN(t);await this.removeOldTrashEntry(a);try{this.logger.debug(`Moving ${t} to ${a} using LDAP move (atomic)`),await this.server.ldap.move(t,a,s),this.logger.info(`Entry ${t} moved to trash at ${a}`),await this.addMetadataToTrash(a,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)?e:e[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(a(h)),holdsBase=s=>s.some((a,e)=>t.every((t,a)=>s[e+a]===t));for(const r of this.watchedBases)e(r,h)&&!holdsBase(s(a(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!e(t,this.trashBase)&&(0===this.watchedBases.length||this.watchedBases.some(s=>e(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.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.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.search({paged:!1},t)).searchEntries.length>0&&(this.logger.info(`Removing old trash entry ${t} before overwrite`),await this.server.ldap.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 a=`Deleted on ${(new Date).toISOString()} from ${s}`;await this.server.ldap.modify(t,{replace:{description:a}}),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};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{escapeLdapFilter as t}from"../../lib/utils.js";import{scimPathToLdapAttribute as e,DEFAULT_LOCK_ATTRIBUTE as s}from"./mapping.js";import{scimInvalidFilter as
|
|
1
|
+
import{escapeLdapFilter as t}from"../../lib/utils.js";import{scimPathToLdapAttribute as e,DEFAULT_LOCK_ATTRIBUTE as s,DEFAULT_LOCK_VALUE as o}from"./mapping.js";import{scimInvalidFilter as n}from"./errors.js";const r=new Set(["and","or","not","pr","eq","ne","co","sw","ew","gt","ge","lt","le","true","false","null"]);const i="\0active";class Parser{constructor(t,e,s={}){this.tokens=t,this.mapping=e,this.opts=s,this.pos=0}peek(t=0){return this.tokens[this.pos+t]}consume(){return this.tokens[this.pos++]}expect(t,e){const s=this.tokens[this.pos];if(s.kind!==t||e&&s.value.toLowerCase()!==e)throw n(`Expected ${e||t} at position ${s.pos}, got '${s.value}'`);return this.pos++,s}parse(){const t=this.parseOr();if("EOF"!==this.peek().kind)throw n(`Unexpected token '${this.peek().value}' at position ${this.peek().pos}`);return t}parseOr(){const t=[this.parseAnd()];for(;"WORD"===this.peek().kind&&"or"===this.peek().value.toLowerCase();)this.consume(),t.push(this.parseAnd());return 1===t.length?t[0]:`(|${t.join("")})`}parseAnd(){const t=[this.parseNot()];for(;"WORD"===this.peek().kind&&"and"===this.peek().value.toLowerCase();)this.consume(),t.push(this.parseNot());return 1===t.length?t[0]:`(&${t.join("")})`}parseNot(){if("WORD"===this.peek().kind&&"not"===this.peek().value.toLowerCase()){this.consume();return`(!${this.parsePrimary()})`}return this.parsePrimary()}parsePrimary(){const t=this.peek();if("LPAREN"===t.kind){this.consume();const t=this.parseOr();return this.expect("RPAREN"),t}if("WORD"===t.kind){const t=this.consume().value;if("LBRACK"===this.peek().kind)throw n(`Complex multi-valued filters ('${t}[...]') are not supported; use 'and' at top level instead`);const e=this.consume();if("WORD"!==e.kind)throw n(`Expected comparison operator at position ${e.pos}`);const s=e.value.toLowerCase();if("pr"===s){const e=this.resolvePath(t);return e===i?"(objectClass=*)":`(${e}=*)`}if(!r.has(s)||["and","or","not"].includes(s))throw n(`Unknown operator '${e.value}' at position ${e.pos}`);const o=this.consume(),a=this.tokenToValue(o);return this.emitComparison(t,s,a)}throw n(`Unexpected token '${t.value}' at position ${t.pos}`)}tokenToValue(t){if("STRING"===t.kind)return t.value;if("NUMBER"===t.kind){const e=Number(t.value);if(Number.isNaN(e))throw n(`Invalid number '${t.value}' at position ${t.pos}`);return e}if("WORD"===t.kind){const e=t.value.toLowerCase();return"true"===e||"false"!==e&&("null"===e?null:t.value)}throw n(`Unexpected value token at position ${t.pos}`)}resolvePath(t){if("id"===t)throw n("filter on 'id' only supports 'eq' with a string value");if("active"===t&&this.opts.supportsActive)return i;const s=e(t,this.mapping);if(!s)throw n(`Unknown attribute path '${t}'`);return s}emitComparison(e,r,a){const u=this.resolvePath(e);if(u===i){const e=!0===a||"string"==typeof a&&"true"===a.toLowerCase(),i=this.opts.lockAttribute||s;if(!this.opts.lockValue&&i.toLowerCase()!==s.toLowerCase())throw new Error(`No lock value given for '${i}'`);const u=`(${i}=${t(this.opts.lockValue||o)})`,p=`(!${u})`;if("eq"===r)return e?p:u;if("ne"===r)return e?u:p;throw n(`Operator '${r}' not supported for 'active'`)}if(null===a){if("eq"===r)return`(!(${u}=*))`;if("ne"===r)return`(${u}=*)`;throw n(`Operator '${r}' not supported with null`)}const p=String(a),l=t(p);switch(r){case"eq":return`(${u}=${l})`;case"ne":return`(!(${u}=${l}))`;case"co":return`(${u}=*${l}*)`;case"sw":return`(${u}=${l}*)`;case"ew":return`(${u}=*${l})`;case"gt":return`(&(${u}>=${l})(!(${u}=${l})))`;case"ge":return`(${u}>=${l})`;case"lt":return`(&(${u}<=${l})(!(${u}=${l})))`;case"le":return`(${u}<=${l})`;default:throw n(`Unknown operator '${r}'`)}}}function scimFilterToLdap(t,e,s={}){const o=t.trim();if(!o)return{ldapFilter:"(objectClass=*)",touchesId:!1};const r=/^id\s+eq\s+"([^"\\]*(?:\\.[^"\\]*)*)"\s*$/i.exec(o);if(r)return{ldapFilter:"(objectClass=*)",touchesId:!0,idEquals:r[1].replace(/\\(.)/g,"$1")};const i=function(t){const e=[];let s=0;for(;s<t.length;){const o=t[s];if(" "!==o&&"\t"!==o&&"\n"!==o&&"\r"!==o)if("("!==o)if(")"!==o)if("["!==o){if("]"!==o){if('"'===o){const o=s;s++;let r="";for(;s<t.length&&'"'!==t[s];){if("\\"===t[s]&&s+1<t.length){const e=t[s+1];if('"'===e){r+='"',s+=2;continue}if("\\"===e){r+="\\",s+=2;continue}if("n"===e){r+="\n",s+=2;continue}if("t"===e){r+="\t",s+=2;continue}}r+=t[s],s++}if(s>=t.length)throw n(`Unterminated string literal at position ${o}`);s++,e.push({kind:"STRING",value:r,pos:o});continue}if(o>="0"&&o<="9"){const o=s;for(;s<t.length&&/[0-9.+\-eE]/.test(t[s])&&!/[\s()[\]]/.test(t[s]);)s++;e.push({kind:"NUMBER",value:t.slice(o,s),pos:o});continue}if(/[A-Za-z_$:]/.test(o)){const o=s;for(;s<t.length&&/[A-Za-z0-9_.$:]/.test(t[s]);)s++;e.push({kind:"WORD",value:t.slice(o,s),pos:o});continue}throw n(`Unexpected character '${o}' at position ${s}`)}e.push({kind:"RBRACK",value:"]",pos:s}),s++}else e.push({kind:"LBRACK",value:"[",pos:s}),s++;else e.push({kind:"RPAREN",value:")",pos:s}),s++;else e.push({kind:"LPAREN",value:"(",pos:s}),s++;else s++}return e.push({kind:"EOF",value:"",pos:t.length}),e}(o);return{ldapFilter:new Parser(i,e,s).parse(),touchesId:/\bid\b/.test(o)}}export{scimFilterToLdap};
|
|
@@ -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
|
|
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);if(s.includes("=")&&s.includes(","))return o(s,r)?s:void 0;const i=`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==v(t))throw t}try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},r);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==v(t))throw t}}get groupMapping(){return this.mapping}}export{ScimGroups};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"fs";import{scimInvalidValue as r}from"./errors.js";import{SCHEMA_GROUP as t,SCHEMA_USER as i}from"./types.js";const a=["createTimestamp","modifyTimestamp","entryUUID"],n={resourceType:"User",schemas:[i],entries:[{scim:"userName",ldap:"uid"},{scim:"externalId",ldap:"employeeNumber"},{scim:"name",sub:{familyName:"sn",givenName:"givenName",formatted:"cn",middleName:"initials"}},{scim:"displayName",ldap:"displayName"},{scim:"nickName",ldap:"displayName"},{scim:"title",ldap:"title"},{scim:"preferredLanguage",ldap:"preferredLanguage"},{scim:"emails",ldapPrimary:"mail",ldapSecondary:"mailAlternateAddress",multi:"array"},{scim:"phoneNumbers",ldapPrimary:"telephoneNumber",ldapSecondary:"mobile",multi:"array"}]},o={resourceType:"Group",schemas:[t],entries:[{scim:"displayName",ldap:"cn"}]};function withExternalId(e,r){const t=(r||"").trim();return t?e.entries.some(e=>"externalId"===e.scim)?e:{...e,entries:[...e.entries,{scim:"externalId",ldap:t}]}:e}function asString(e){if(null!=e){if(Array.isArray(e)){const r=e[0];return null==r?void 0:String(r)}return Buffer.isBuffer(e)?e.toString():String(e)}}function ldapTimeToIso(e){if(!e)return;const r=/^(\d{4})(\d{2})(\d{2})(\d{2})(?:(\d{2})(?:(\d{2}))?)?(?:[.,](\d+))?(Z|[+-]\d{2}(?:\d{2})?)?$/.exec(e);if(!r)return e;const[,t,i,a,n,o,s,c,l]=r;let d=0;if(c){const e=s?1e3:o?6e4:36e5;d=Math.round(Number(`0.${c}`)*e)}let u=1e3*(60*(60*Number(n)+Number(o||0))+Number(s||0))+d;const m=Math.floor(u/36e5);u-=36e5*m;const
|
|
1
|
+
import e from"fs";import{scimInvalidValue as r}from"./errors.js";import{SCHEMA_GROUP as t,SCHEMA_USER as i}from"./types.js";const a=["createTimestamp","modifyTimestamp","entryUUID"],n={resourceType:"User",schemas:[i],entries:[{scim:"userName",ldap:"uid"},{scim:"externalId",ldap:"employeeNumber"},{scim:"name",sub:{familyName:"sn",givenName:"givenName",formatted:"cn",middleName:"initials"}},{scim:"displayName",ldap:"displayName"},{scim:"nickName",ldap:"displayName"},{scim:"title",ldap:"title"},{scim:"preferredLanguage",ldap:"preferredLanguage"},{scim:"emails",ldapPrimary:"mail",ldapSecondary:"mailAlternateAddress",multi:"array"},{scim:"phoneNumbers",ldapPrimary:"telephoneNumber",ldapSecondary:"mobile",multi:"array"}]},o={resourceType:"Group",schemas:[t],entries:[{scim:"displayName",ldap:"cn"}]};function withExternalId(e,r){const t=(r||"").trim();return t?e.entries.some(e=>"externalId"===e.scim)?e:{...e,entries:[...e.entries,{scim:"externalId",ldap:t}]}:e}function asString(e){if(null!=e){if(Array.isArray(e)){const r=e[0];return null==r?void 0:String(r)}return Buffer.isBuffer(e)?e.toString():String(e)}}function ldapTimeToIso(e){if(!e)return;const r=/^(\d{4})(\d{2})(\d{2})(\d{2})(?:(\d{2})(?:(\d{2}))?)?(?:[.,](\d+))?(Z|[+-]\d{2}(?:\d{2})?)?$/.exec(e);if(!r)return e;const[,t,i,a,n,o,s,c,l]=r;let d=0;if(c){const e=s?1e3:o?6e4:36e5;d=Math.round(Number(`0.${c}`)*e)}let u=1e3*(60*(60*Number(n)+Number(o||0))+Number(s||0))+d;const m=Math.floor(u/36e5);u-=36e5*m;const f=Math.floor(u/6e4);u-=6e4*f;const p=Math.floor(u/1e3),y=u-1e3*p;let b="Z";l&&"Z"!==l&&(b=`${l[0]}${l.slice(1,3)}:${l.slice(3,5)||"00"}`);const pad=e=>String(e).padStart(2,"0"),g=`${t}-${i}-${a}T${pad(m)}:${pad(f)}:${pad(p)}${y?`.${String(y).padStart(3,"0")}`:""}${b}`;return Number.isNaN(Date.parse(g))?e:g}function asArray(e){return null==e?[]:Array.isArray(e)?e.map(e=>Buffer.isBuffer(e)?e.toString():String(e)):Buffer.isBuffer(e)?[e.toString()]:[String(e)]}function loadMappingFile(r){if(!r)return;const t=e.readFileSync(r,"utf8");return JSON.parse(t)}function mergeMapping(e,r){if(!r)return e;const t=new Map;for(const r of e.entries)t.set(r.scim,r);for(const e of r.entries)t.set(e.scim,e);return{...e,entries:Array.from(t.values())}}const s="pwdAccountLockedTime",c="000001010000Z",l=/^\d{10,14}(\.\d+)?(Z|[+-]\d{2,4})$/;let warn=e=>{console.warn(`WARNING: ${e}`)};function setLockConfigWarn(e){const r=warn;return warn=e,r}const d=/^([A-Za-z][A-Za-z0-9-]*|\d+(\.\d+)+)$/;function resolveLockConfig(e,r,t={attribute:"--scim-user-lock-attribute",value:"--scim-user-lock-value"}){const i=(e||"").trim()||s;if(!d.test(i))throw new Error(`${t.attribute} must be an LDAP attribute name, got '${i}'`);const a=i.toLowerCase()===s.toLowerCase(),n=(r||"").trim();if(!n){if(a)return{attribute:i,value:c};throw new Error(`${t.attribute} is '${i}', so ${t.value} must say what marks an account locked (for nsAccountLock on 389-ds, 'TRUE'). The default '${c}' only means anything to ${s}.`)}return a||n!==c||warn(`${t.value} is the ppolicy convention '${c}' but ${t.attribute} is '${i}', not ${s}. Unless '${i}' is a GeneralizedTime your directory reads as a lock, every deactivation will answer 200 and read back as active:false while the account keeps binding. Verify a deactivation actually prevents a bind.`),a&&!l.test(n)&&warn(`${t.value} '${n}' is not a GeneralizedTime, and ${s} holds one. The directory will refuse the write, or — where the attribute was redefined locally — store a value it does not read as a lock. The ppolicy convention is '${c}'.`),{attribute:i,value:n}}function readActive(e){if(void 0!==e){if("boolean"==typeof e)return e;if("string"==typeof e){const r=e.trim().toLowerCase();if("true"===r)return!0;if("false"===r)return!1}throw r(`Cannot read ${JSON.stringify(e)} as active`)}}function isLocked(e,r){return holdsValue(e,r.lockAttribute,r.lockValue)}function holdsValue(e,r,t){if(!r)return!1;const i=r.toLowerCase(),a=Object.keys(e).find(e=>e.toLowerCase()===i),n=void 0===a?void 0:e[a];if(null==n)return!1;const o=t.toLowerCase();return(Array.isArray(n)?n:[n]).some(e=>null!=e&&e.toString().toLowerCase()===o)}function resolveScimId(e,r){if("rdn"===r.idAttribute)return asString(e[r.rdnAttribute]);return asString(e[r.idAttribute])??asString(e[r.rdnAttribute])}function buildLocation(e,r){if(!r.baseUrl)return;const t="User"===r.resourceType?"Users":"Groups";return`${r.baseUrl.replace(/\/$/,"")}${r.scimPrefix}/${t}/${encodeURIComponent(e)}`}function ldapToScimUser(e,r,t){const a={schemas:[i]},n=resolveScimId(e,t);n&&(a.id=n);for(const t of r.entries){if(t.sub){const r={};for(const[i,a]of Object.entries(t.sub)){const t=asString(e[a]);null!=t&&(r[i]=t)}Object.keys(r).length>0&&(a[t.scim]=r);continue}if(t.ldapPrimary||t.ldapSecondary){const r=[];if(t.ldapPrimary){const i=asString(e[t.ldapPrimary]);null!=i&&r.push({value:i,primary:!0})}if(t.ldapSecondary)for(const i of asArray(e[t.ldapSecondary]))r.push({value:i});r.length>0&&(a[t.scim]=r);continue}if(t.ldap)if("array"===t.multi){const r=asArray(e[t.ldap]);r.length>0&&(a[t.scim]=r)}else{const r=asString(e[t.ldap]);null!=r&&(a[t.scim]=r)}}return a.active=!isLocked(e,t),n&&(a.meta={resourceType:"User",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(n,t)}),a}function scimUserToLdap(e,r,t,i){const a={objectClass:i},n=e.userName||e.id||"";for(const t of r.entries){if(t.readOnly||t.operational)continue;const r=e[t.scim];if(null!=r)if(t.sub&&"object"==typeof r)for(const[e,i]of Object.entries(t.sub)){const t=r[e];"string"==typeof t&&t.length>0&&(a[i]=t)}else{if(t.ldapPrimary||t.ldapSecondary){if(!Array.isArray(r))continue;const e=r,i=e.find(e=>!0===e.primary)||e[0],n=e.filter(e=>e!==i);t.ldapPrimary&&i&&i.value&&(a[t.ldapPrimary]=i.value),t.ldapSecondary&&n.length>0&&(a[t.ldapSecondary]=n.map(e=>e.value).filter(Boolean));continue}t.ldap&&(Array.isArray(r)?a[t.ldap]=r.map(e=>String(e)):"string"!=typeof r&&"number"!=typeof r||(a[t.ldap]=String(r)))}}const o=readActive(e.active);if(!1===o&&(a[t.lockAttribute]=t.lockValue||c),!a.cn&&n&&(a.cn=n),!a.sn){const r=e.name?.familyName||e.displayName||n;r&&(a.sn=r)}return{rdn:n,attributes:a,active:o}}function ldapToScimGroup(e,r,i,a){const n={schemas:[t]},o=resolveScimId(e,i);o&&(n.id=o);for(const t of r.entries){if(!t.ldap)continue;const r=e[t.ldap];if(null!=r)if("array"===t.multi){const e=asArray(r);e.length>0&&(n[t.scim]=e)}else{const e=asString(r);null!=e&&(n[t.scim]=e)}}const s=asArray(e.member),c=[];for(const e of s)if(e)if(a){const r=a(e);r&&c.push(r)}else c.push({value:e,type:"User"});return c.length>0&&(n.members=c),o&&(n.meta={resourceType:"Group",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(o,i)}),n}function scimGroupToLdap(e,r,t){const i={objectClass:t},a=e.displayName||e.id||"";for(const t of r.entries){if(t.readOnly||t.operational)continue;if(!t.ldap)continue;const r=e[t.scim];null!=r&&(Array.isArray(r)?i[t.ldap]=r.map(e=>"string"==typeof e?e:JSON.stringify(e)):"string"!=typeof r&&"number"!=typeof r||(i[t.ldap]=String(r)))}return{rdn:a,attributes:i}}function requiredLdapAttributes(e,r=[]){const t=new Set(["objectClass",...a,...r]);for(const r of e.entries)if(r.ldap&&t.add(r.ldap),r.ldapPrimary&&t.add(r.ldapPrimary),r.ldapSecondary&&t.add(r.ldapSecondary),r.sub)for(const e of Object.values(r.sub))t.add(e);return Array.from(t)}function scimPathToLdapAttribute(e,r){const t=e.split(".")[0],i=e.includes(".")?e.split(".").slice(1).join("."):"";for(const e of r.entries)if(e.scim===t){if(e.sub&&i)return e.sub[i];if((e.ldapPrimary||e.ldapSecondary)&&("value"===i||!i))return e.ldapPrimary||e.ldapSecondary;if(e.ldap)return e.ldap}}export{o as DEFAULT_GROUP_MAPPING,s as DEFAULT_LOCK_ATTRIBUTE,c as DEFAULT_LOCK_VALUE,n as DEFAULT_USER_MAPPING,buildLocation,holdsValue,isLocked,ldapTimeToIso,ldapToScimGroup,ldapToScimUser,loadMappingFile,mergeMapping,readActive,requiredLdapAttributes,resolveLockConfig,resolveScimId,scimGroupToLdap,scimPathToLdapAttribute,scimUserToLdap,setLockConfigWarn,warn,withExternalId};
|