ldap-rest 0.2.2 → 0.3.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/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/expressFormatedResponses.js +1 -1
- package/dist/plugins/auth/authzDynamic.js +1 -0
- package/dist/plugins/auth/authzDynamicHash.js +1 -0
- package/dist/plugins/scim/baseResolver.js +1 -0
- package/dist/plugins/scim/bulk.js +1 -0
- package/dist/plugins/scim/discovery.js +1 -0
- package/dist/plugins/scim/errors.js +1 -0
- package/dist/plugins/scim/filter.js +1 -0
- package/dist/plugins/scim/groups.js +1 -0
- package/dist/plugins/scim/mapping.js +1 -0
- package/dist/plugins/scim/patch.js +1 -0
- package/dist/plugins/scim/scim.js +1 -0
- package/dist/plugins/scim/types.js +1 -0
- package/dist/plugins/scim/users.js +1 -0
- package/dist/src/abstract/ldapFlat.d.ts +20 -0
- package/dist/src/config/args.d.ts +24 -0
- package/dist/src/plugins/auth/authzDynamic.d.ts +117 -0
- package/dist/src/plugins/auth/authzDynamicHash.d.ts +27 -0
- package/dist/src/plugins/scim/baseResolver.d.ts +23 -0
- package/dist/src/plugins/scim/bulk.d.ts +43 -0
- package/dist/src/plugins/scim/discovery.d.ts +27 -0
- package/dist/src/plugins/scim/errors.d.ts +37 -0
- package/dist/src/plugins/scim/filter.d.ts +13 -0
- package/dist/src/plugins/scim/groups.d.ts +61 -0
- package/dist/src/plugins/scim/mapping.d.ts +52 -0
- package/dist/src/plugins/scim/patch.d.ts +39 -0
- package/dist/src/plugins/scim/scim.d.ts +23 -0
- package/dist/src/plugins/scim/types.d.ts +222 -0
- package/dist/src/plugins/scim/users.d.ts +58 -0
- package/docs/usage/plugins/auth/authz-dynamic.md +183 -0
- package/docs/usage/plugins/integrations/scim.md +567 -0
- package/package.json +65 -1
- package/static/schemas/scim/Group.json +31 -0
- package/static/schemas/scim/User.json +147 -0
- package/static/schemas/scim/default-mapping.json +48 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"fs";import{wantJson as e,jsonBody as i,created as s,tryMethod as a}from"../lib/expressFormatedResponses.js";import{transformSchemas as r,asyncHandler as n,escapeDnValue as o,validateDnValue as h,launchHooksChained as d,launchHooks as c,getCompiledRegex as u,escapeRegex as l,escapeLdapFilter as f}from"../lib/utils.js";import{NotFoundError as b,BadRequestError as m,ConflictError as p}from"../lib/errors.js";import g from"./plugin.js";class LdapFlat extends g{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(i.schemaPath)try{const e=t.readFileSync(i.schemaPath,"utf8");this.schema=JSON.parse(r(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}`)}}api(t){t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,n(async(t,i)=>{if(!e(t,i))return;const s={};if(t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute){if(!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(t.query.attribute))throw new m("Invalid LDAP attribute name");const e=f(t.query.match);s.filter=`(${t.query.attribute}=*${e}*)`}t.query.attributes&&"string"==typeof t.query.attributes&&(s.attributes=t.query.attributes.split(","));const a=await this.listEntries(s);i.json(a)})),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,n(async(t,e)=>this.apiGet(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,n(async(t,e)=>this.apiAdd(t,e))),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,n(async(t,e)=>this.apiDelete(t,e))),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,n(async(t,e)=>this.apiModify(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,n(async(t,e)=>this.apiMove(t,e)))}async apiGet(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);try{const t=/,/.test(s)?s:`${this.mainAttribute}=${o(s)},${this.base}`,e=await this.ldap.search({paged:!1,scope:"base"},t);if(0===e.searchEntries.length)throw new b(`${this.singularName} not found`);i.json(e.searchEntries[0])}catch(t){if(t.code&&32===t.code)throw new b(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const a=i(t,e,this.mainAttribute);if(!a)return;const r=a[this.mainAttribute],n={...a};delete n[this.mainAttribute],delete n.dn,await this.addEntry(r,n,t);const o=await this.searchEntriesByName(r,!1);return s(e,o[r])}async apiDelete(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);await a(i,this.deleteEntry.bind(this),s)}async apiModify(t,e){const s=i(t,e);if(!s)return;const r=decodeURIComponent(t.params.id);await a(e,this.modifyEntry.bind(this),r,s)}async apiMove(t,s){if(!e(t,s))return;const a=i(t,s,"targetOrgDn");if(!a)return;const r=decodeURIComponent(t.params.id),{targetOrgDn:n}=a;if(!n||"string"!=typeof n)throw new m("Missing or invalid targetOrgDn in request body");const o=await this.moveEntry(r,n,t);s.json({success:!0,...o})}async addEntry(t,e={},i){let s;if(new RegExp(`^${this.mainAttribute}=`).test(t)){s=t;const e=`,${this.base}`;if(!s.endsWith(e))throw new m(`DN must be in the flat branch "${this.base}". Provided DN "${s}" does not end with "${e}"`);t=t.replace(new RegExp(`^${this.mainAttribute}=((?:\\\\.|[^,])+)(?:,.*)?$`),"$1")}else h(t,this.mainAttribute),s=`${this.mainAttribute}=${o(t)},${this.base}`;await this.validateNewEntry(s,{objectClass:this.objectClass,[this.mainAttribute]:t,...e});let a,r={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,r]=await d(this.registeredHooks[`${this.hookPrefix}add`],[s,r]),this.logger.debug("Adding LDAP entry:",{dn:s,entry:JSON.stringify(r,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(r))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);try{a=await this.ldap.add(s,r,i)}catch(t){throw this.logger.error("LDAP add failed:",{dn:s,entry:JSON.stringify(r,null,2),error:t}),new Error(`Failed to add ${this.singularName} ${s}: ${t}`)}return c(this.registeredHooks[`${this.hookPrefix}adddone`],[s,r]),a}async modifyEntry(t,e){let i=/,/.test(t)?t:`${this.mainAttribute}=${o(t)},${this.base}`;const s=this.opNumber();if([i,e]=await d(this.registeredHooks[`${this.hookPrefix}modify`],[i,e,s]),e.add&&e.add[this.mainAttribute])throw new p(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new m(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new m(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new m(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(i,e);const a=await this.ldap.modify(i,e);return c(this.registeredHooks[`${this.hookPrefix}modifydone`],[i,e,s]),a}async renameEntry(t,e){/,/.test(t)||h(t,this.mainAttribute),/,/.test(e)||h(e,this.mainAttribute);let i=/,/.test(t)?t:`${this.mainAttribute}=${o(t)},${this.base}`,s=/,/.test(e)?e:`${this.mainAttribute}=${o(e)},${this.base}`;[i,s]=await d(this.registeredHooks[`${this.hookPrefix}rename`],[i,s]);const a=await this.ldap.rename(i,s);return c(this.registeredHooks[`${this.hookPrefix}renamedone`],[i,s]),a}async deleteEntry(t){let e=/,/.test(t)?t:`${this.mainAttribute}=${o(t)},${this.base}`;e=await d(this.registeredHooks[`${this.hookPrefix}delete`],e);const i=await this.ldap.delete(e);return c(this.registeredHooks[`${this.hookPrefix}deletedone`],e),i}async moveEntry(t,e,i){const s=/,/.test(t)?t:`${this.mainAttribute}=${o(t)},${this.base}`,a=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",r=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[a]||!this.schema?.attributes[r])throw new m(`Schema for ${this.singularName} does not support move operation (missing ${a} or ${r})`);const n=await this.ldap.search({paged:!1,scope:"base",attributes:[a]},s,i);if(!n.searchEntries||0===n.searchEntries.length)throw new b(`Entry ${s} not found`);const h=await this.getDepartmentPath(e,i);[,e]=await d(this.registeredHooks[`${this.hookPrefix}move`],[s,e,i]);const c={replace:{[a]:e,[r]:h}};return await this.modifyEntry(t,c),{departmentPath:h,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 b(`Organization ${t} not found`);const a=s.searchEntries[0];if(a[i]){const t=a[i];return Array.isArray(t)?String(t[0]):String(t)}const r=a.ou||a.o;if(r){return`/${Array.isArray(r)?String(r[0]):String(r)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries({filter:t,attributes:e}){const i={paged:!0,filter:t=t||"(objectClass=*)"};e&&e.length>0&&(i.attributes=e);const s=await this.ldap.search(i,this.base),a={};for await(const t of s)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=e?`(${this.mainAttribute}=*${t}*)`:`(${this.mainAttribute}=${t})`;return await this.listEntries({filter:s,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 m(`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 m(`Attribute "${t}" is fixed and cannot be modified. Expected: ${s}`)}if(!await this._validateOneChange(t,i))throw new m(`Invalid value for attribute "${t}"`);if(e.required&&!i)throw new m(`Attribute "${t}" is required`)}for(const[t,i]of Object.entries(this.schema.attributes))if(i.required&&!e[t])throw new m(`Attribute "${t}" 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 m(`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 m(`Invalid value for attribute "${t}"`);if(e.replace)for(const[t,i]of Object.entries(e.replace))if(checkFixed(t),!await this._validateOneChange(t,i))throw new m(`Invalid value for attribute "${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 m(`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 m(`Field ${t} must be a string (DN pointer)`);const s=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>u(`,?${l(t)}$`,"i").test(s)))throw new m(`Field ${t} must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.search({paged:!1,scope:"base"},s);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new m(`Field ${t} points to non-existent DN: ${s}`)}catch(e){throw new m(`Field ${t} points to invalid or non-existent DN: ${s}`)}}if(i.test){const t="string"==typeof i.test?u(i.test):i.test;return Array.isArray(e)?e.every(e=>t.test(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}from"../lib/expressFormatedResponses.js";import{transformSchemas as a,asyncHandler as n,escapeRegex as o,getParentDn as h,escapeDnValue as d,validateDnValue as c,launchHooksChained as l,launchHooks as u,getCompiledRegex as f,escapeLdapFilter as m}from"../lib/utils.js";import{NotFoundError as b,BadRequestError as p,ConflictError as g}from"../lib/errors.js";import w from"./plugin.js";class LdapFlat extends w{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(i.schemaPath)try{const e=t.readFileSync(i.schemaPath,"utf8");this.schema=JSON.parse(a(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}`)}}api(t){t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,n(async(t,i)=>{if(!e(t,i))return;const s={};if(t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute){if(!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(t.query.attribute))throw new p("Invalid LDAP attribute name");const e=m(t.query.match);s.filter=`(${t.query.attribute}=*${e}*)`}t.query.attributes&&"string"==typeof t.query.attributes&&(s.attributes=t.query.attributes.split(","));const r=await this.listEntries(s);i.json(r)})),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,n(async(t,e)=>this.apiGet(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,n(async(t,e)=>this.apiAdd(t,e))),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,n(async(t,e)=>this.apiDelete(t,e))),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,n(async(t,e)=>this.apiModify(t,e))),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,n(async(t,e)=>this.apiMove(t,e)))}async apiGet(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);try{const t=this.resolveDn(s),e=await this.ldap.search({paged:!1,scope:"base"},t);if(0===e.searchEntries.length)throw new b(`${this.singularName} not found`);i.json(e.searchEntries[0])}catch(t){if(t.code&&32===t.code)throw new b(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const r=i(t,e,this.mainAttribute);if(!r)return;const a=r[this.mainAttribute],n={...r};delete n[this.mainAttribute],delete n.dn,await this.addEntry(a,n,t);const o=await this.searchEntriesByName(a,!1);return s(e,o[a])}async apiDelete(t,i){if(!e(t,i))return;const s=decodeURIComponent(t.params.id);await r(i,this.deleteEntry.bind(this),s)}async apiModify(t,e){const s=i(t,e);if(!s)return;const a=decodeURIComponent(t.params.id);await r(e,this.modifyEntry.bind(this),a,s)}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 p("Missing or invalid targetOrgDn in request body");const o=await this.moveEntry(a,n,t);s.json({success:!0,...o})}resolveDn(t){if(new RegExp(`^${o(this.mainAttribute)}=`,"i").test(t)){const e=h(t),i=e!==t;if(!i||e.toLowerCase()!==this.base.toLowerCase())throw new p(`DN must be a direct child of "${this.base}". Provided DN "${t}" has parent "${i?e:"<none>"}"`);return t}return`${this.mainAttribute}=${d(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")):(c(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 l(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){throw this.logger.error("LDAP add failed:",{dn:s,entry:JSON.stringify(a,null,2),error:t}),new Error(`Failed to add ${this.singularName} ${s}: ${t}`)}return u(this.registeredHooks[`${this.hookPrefix}adddone`],[s,a]),r}async modifyEntry(t,e){let i=this.resolveDn(t);const s=this.opNumber();if([i,e]=await l(this.registeredHooks[`${this.hookPrefix}modify`],[i,e,s]),e.add&&e.add[this.mainAttribute])throw new g(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new p(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new p(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new p(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(i,e);const r=await this.ldap.modify(i,e);return u(this.registeredHooks[`${this.hookPrefix}modifydone`],[i,e,s]),r}async renameEntry(t,e){/,/.test(t)||c(t,this.mainAttribute),/,/.test(e)||c(e,this.mainAttribute);let i=this.resolveDn(t),s=this.resolveDn(e);[i,s]=await l(this.registeredHooks[`${this.hookPrefix}rename`],[i,s]);const r=await this.ldap.rename(i,s);return u(this.registeredHooks[`${this.hookPrefix}renamedone`],[i,s]),r}async deleteEntry(t){let e=this.resolveDn(t);e=await l(this.registeredHooks[`${this.hookPrefix}delete`],e);const i=await this.ldap.delete(e);return u(this.registeredHooks[`${this.hookPrefix}deletedone`],e),i}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 p(`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 b(`Entry ${s} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await l(this.registeredHooks[`${this.hookPrefix}move`],[s,e,i]);const h={replace:{[r]:e,[a]:o}};return await this.modifyEntry(t,h),{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 b(`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){throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries({filter:t,attributes:e}){const i={paged:!0,filter:t=t||"(objectClass=*)"};e&&e.length>0&&(i.attributes=e);const s=await this.ldap.search(i,this.base),r={};for await(const t of s)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),r[i]=t}});return r}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const s=e?`(${this.mainAttribute}=*${t}*)`:`(${this.mainAttribute}=${t})`;return await this.listEntries({filter:s,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 p(`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 p(`Attribute "${t}" is fixed and cannot be modified. Expected: ${s}`)}if(!await this._validateOneChange(t,i))throw new p(`Invalid value for attribute "${t}"`);if(e.required&&!i)throw new p(`Attribute "${t}" is required`)}for(const[t,i]of Object.entries(this.schema.attributes))if(i.required&&!e[t])throw new p(`Attribute "${t}" 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 p(`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 p(`Invalid value for attribute "${t}"`);if(e.replace)for(const[t,i]of Object.entries(e.replace))if(checkFixed(t),!await this._validateOneChange(t,i))throw new p(`Invalid value for attribute "${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 p(`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 p(`Field ${t} must be a string (DN pointer)`);const s=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>f(`,?${o(t)}$`,"i").test(s)))throw new p(`Field ${t} must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.search({paged:!1,scope:"base"},s);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new p(`Field ${t} points to non-existent DN: ${s}`)}catch(e){throw new p(`Field ${t} points to invalid or non-existent DN: ${s}`)}}if(i.test){const t="string"==typeof i.test?f(i.test):i.test;return Array.isArray(e)?e.every(e=>t.test(e)):t.test(e)}return!0}}export{LdapFlat as default};
|
package/dist/bin/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fileURLToPath as e}from"url";import{join as r,dirname as i}from"path";import s from"express";import o from"body-parser";import{parseConfig as t}from"../lib/parseConfig.js";import n from"../config/args.js";import a from"../lib/ldapActions.js";import{buildLogger as l}from"../logger/winston.js";import{setLogger as d}from"../lib/expressFormatedResponses.js";import g from"../plugins/priority.json.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,launchHooks,launchHooksChained,parseDn,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this._errorMiddlewareSetup=!1,this.config=t(n),this.app=s(),this.app.use(o.json()),this.app.use(o.urlencoded({extended:!0})),this.logger=l(this.config),this.ldap=new a(this),d(this.logger),this.operationSequence=0;const e=[];if(this.config.plugin){const r=this.config.plugin.find(e=>e.includes("configApi"));let i=this.config.plugin.filter(e=>!e.includes("configApi"));const s=(async()=>{for(const e of g)i.includes(e)&&(i=i.filter(r=>r!==e),await this.loadPlugin(e))})();e.push(s),e.push(s.then(async()=>{const e=i.map(e=>this.loadPlugin(e));await Promise.all(e)})),r&&e.push(Promise.all(e).then(()=>this.loadPlugin(r)))}this.ready=new Promise((r,i)=>{e.length>0?Promise.all(e).then(()=>{this.setupErrorMiddleware(),r()}).catch(e=>i(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),r())})}setupErrorMiddleware(){if(this._errorMiddlewareSetup&&this._errorMiddleware){const e=this.app._router?.stack;if(e){const r=e.findIndex(e=>e.handle===this._errorMiddleware);-1!==r&&e.splice(r,1)}}this._errorMiddleware=(e,r,i,s)=>{
|
|
1
|
+
import{fileURLToPath as e}from"url";import{join as r,dirname as i}from"path";import s from"express";import o from"body-parser";import{parseConfig as t}from"../lib/parseConfig.js";import n from"../config/args.js";import a from"../lib/ldapActions.js";import{buildLogger as l}from"../logger/winston.js";import{setLogger as d}from"../lib/expressFormatedResponses.js";import g from"../plugins/priority.json.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,launchHooks,launchHooksChained,parseDn,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this._errorMiddlewareSetup=!1,this.config=t(n),this.app=s(),this.app.use(o.json()),this.app.use(o.urlencoded({extended:!0})),this.logger=l(this.config),this.ldap=new a(this),d(this.logger),this.operationSequence=0;const e=[];if(this.config.plugin){const r=this.config.plugin.find(e=>e.includes("configApi"));let i=this.config.plugin.filter(e=>!e.includes("configApi"));const s=(async()=>{for(const e of g)i.includes(e)&&(i=i.filter(r=>r!==e),await this.loadPlugin(e))})();e.push(s),e.push(s.then(async()=>{const e=i.map(e=>this.loadPlugin(e));await Promise.all(e)})),r&&e.push(Promise.all(e).then(()=>this.loadPlugin(r)))}this.ready=new Promise((r,i)=>{e.length>0?Promise.all(e).then(()=>{this.setupErrorMiddleware(),r()}).catch(e=>i(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),r())})}setupErrorMiddleware(){if(this._errorMiddlewareSetup&&this._errorMiddleware){const e=this.app._router?.stack;if(e){const r=e.findIndex(e=>e.handle===this._errorMiddleware);-1!==r&&e.splice(r,1)}}this._errorMiddleware=(e,r,i,s)=>{let o="statusCode"in e?e.statusCode:500,t=e.message;if(500===o&&/\[authz-forbidden\]/.test(e.message)?(o=403,t="Token does not have permission on this branch"):403===o&&/\[authz-forbidden\]/.test(e.message)&&(t="Token does not have permission on this branch"),o>=400&&o<500)return this.logger.warn(`Client error ${o} in request ${r.method} ${r.path}: ${t}`),i.headersSent?void 0:i.status(o).json({error:t});this.logger.error(`Server error ${o} in request ${r.method} ${r.path}: ${e.message}`,{stack:e.stack,url:r.url,method:r.method}),i.headersSent||i.status(o).json({error:"Internal Server Error",message:this.config.debug?e.message:"An error occurred"})},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}run(){return process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack})}),process.on("unhandledRejection",(e,r)=>{this.logger.error(`Unhandled promise rejection: ${e}`,{reason:e,promise:r})}),new Promise((e,r)=>{this.server=this.app.listen(this.config.port,i=>{i?(this.logger.error(`Error starting server: ${i}`),r(i)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}loadPlugin(s){let o,t;if(/:/.test(s)){let e=s.substring(s.indexOf(":")+1);if(s=s.substring(0,s.indexOf(":")),/:/.test(e)){o=e.substring(0,e.indexOf(":")),o||(o=void 0),e=e.substring(e.indexOf(":")+1);try{t=JSON.parse(e),"object"!=typeof t?(this.logger.error(`Overrides for plugin ${s} are not valid: ${e}`),t=void 0):this.logger.debug(`Overrides for plugin ${o||s}: ${e}`)}catch(r){this.logger.error(`Failed to parse overrides for plugin ${s}: ${r}, using ${e}`),t=void 0}}else o=e,o||(o=void 0)}else o=void 0;return this.logger.debug(`Loading plugin ${s}`),s.startsWith("core/")&&(s=s.replace("core/",r(i(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js")),new Promise((e,r)=>{import(s).then(async i=>{let n;if(i&&i.default&&(i=i.default),t){const e={...this.config,...t};n=new i({...this,config:e})}else n=new i(this);if(!n)return r(new Error(`Unable to load ${s}`));e(await this.registerPlugin(s,n,o)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(e=>r(new Error(`Failed to load plugin ${s}: ${e}`)))})}async registerPlugin(e,r,i){if(r.name||(r.name=e),i&&(r.name=i),this.loadedPlugins[r.name])return this.logger.info(`Plugin ${e} already loaded as ${r.name}`),!1;if(this.logger.debug(`Registering plugin ${e} as ${r.name}`),r.dependencies)for(const e in r.dependencies)this.loadedPlugins[e]||(this.logger.debug(`Plugin ${r.name} depends on ${e}, loading it first`),await this.loadPlugin(r.dependencies[e]));if(r.api&&(this.logger.debug(`Plugin ${r.name} has API, registering it`),await r.api(this.app),this._errorMiddlewareSetup&&this.setupErrorMiddleware()),r.hooks)for(const e in r.hooks){this.logger.debug(`Plugin ${r.name} has hook ${e}, registering it`);const i=r.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!i||"function"!=typeof i)throw new Error(`Plugin ${r.name}: hook ${e} is invalid`);this.hooks[e].push(i)}return this.loadedPlugins[r.name]=r,!0}}export{DM};
|
package/dist/config/args.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fileURLToPath as a}from"url";import{join as _,dirname as e}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",300,"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"],["--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"],["--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",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--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"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--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-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-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--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",""]];export{t as default};
|
|
1
|
+
import{fileURLToPath as a}from"url";import{join as _,dirname as e}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",300,"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"],["--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"],["--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",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--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"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--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-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-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-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-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-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-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-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 +1 @@
|
|
|
1
|
-
let r;const setLogger=t=>{r=t},getLogger=()=>r,tryMethod=async(r,t,...e)=>{try{return await t(...e),ok(r)}catch(t){return serverError(r,t)}},tryMethodData=async(r,t,...e)=>{try{const n=await t(...e);return ok(r,n)}catch(t){return serverError(r,t)}},_rejectResponse=(t,e,n)=>{r?.warn(`Client error ${t}: ${n}`),e.status(t).json({error:n})},ok=(r,t={success:!0})=>{r.status(200).json(t)},created=(r,t)=>{r.status(201).json(t)},badRequest=(r,t="Bad request")=>_rejectResponse(400,r,t),unauthorized=(r,t="Unauthorized")=>_rejectResponse(401,r,t),notFound=(r,t="Not found")=>_rejectResponse(404,r,t),serverError=(t,e)=>{const n=e instanceof Error&&"statusCode"in e?e.statusCode:500;if(n>=400&&
|
|
1
|
+
let r;const setLogger=t=>{r=t},getLogger=()=>r,tryMethod=async(r,t,...e)=>{try{return await t(...e),ok(r)}catch(t){return serverError(r,t)}},tryMethodData=async(r,t,...e)=>{try{const n=await t(...e);return ok(r,n)}catch(t){return serverError(r,t)}},_rejectResponse=(t,e,n)=>{r?.warn(`Client error ${t}: ${n}`),e.status(t).json({error:n})},ok=(r,t={success:!0})=>{r.status(200).json(t)},created=(r,t)=>{r.status(201).json(t)},badRequest=(r,t="Bad request")=>_rejectResponse(400,r,t),unauthorized=(r,t="Unauthorized")=>_rejectResponse(401,r,t),notFound=(r,t="Not found")=>_rejectResponse(404,r,t),serverError=(t,e)=>{const n=e instanceof Error?e.message:String(e);let s=e instanceof Error&&"statusCode"in e?e.statusCode:500;if(500===s&&/\[authz-forbidden\]/.test(n)&&(s=403),s>=400&&s<500){const e=403===s&&/\[authz-forbidden\]/.test(n)?"Token does not have permission on this branch":n;return r.warn(`Client error ${s}: ${e}`),void t.status(s).json({error:e})}e instanceof Error?r.error(e.message,e):"string"==typeof e?r.error(e):r.error("Server error",e),t.status(s).json({error:"check logs"})},wantJson=(r,t)=>!1!==r.accepts("json")||(badRequest(t),!1),jsonBody=(r,t,...e)=>{try{if(!wantJson(r,t))return!1;const n=r.body;if(e.length>0)for(let r=0;r<e.length;r++)if(null==n[e[r]])return badRequest(t,"Bad content"),!1;return n}catch(r){return badRequest(t,"Bad content"),!1}};export{badRequest,created,getLogger,jsonBody,notFound,ok,serverError,setLogger,tryMethod,tryMethodData,unauthorized,wantJson};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AsyncLocalStorage as t}from"async_hooks";import e from"../../lib/auth/base.js";import{ForbiddenError as i}from"../../lib/errors.js";import{unauthorized as n,ok as r}from"../../lib/expressFormatedResponses.js";import{getParentDn as s,asyncHandler as a}from"../../lib/utils.js";import{verifyLdapPassword as o}from"./authzDynamicHash.js";const h=new t;function asString(t){if(null!=t){if(Array.isArray(t)){const e=t[0];return null==e?void 0:Buffer.isBuffer(e)?e.toString():String(e)}return Buffer.isBuffer(t)?t.toString():String(t)}}function isAtOrUnder(t,e){const i=t.toLowerCase().trim(),n=e.toLowerCase().trim();return!(!i||!n)&&(i===n||i.endsWith(","+n))}class AuthzDynamic extends e{constructor(t){if(super(t),this.name="authzDynamic",this.roles=["auth","authz","configurable"],this.tokens=[],this.lastLoad=0,this.lastFailure=0,this.hooks={ldapsearchrequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,t,"read"),[t,e,i]):[t,e,i]},ldapaddrequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,s(t),"write"),[t,e,i]):[t,e,i]},ldapmodifyrequest:([t,e,i,n])=>{const r=this.activeToken(n);return r?(this.requireBranchPermission(r,s(t),"write"),[t,e,i,n]):[t,e,i,n]},ldapdeleterequest:t=>{const e=h.getStore();if(!e)return t;const i=Array.isArray(t)?t:[t];for(const t of i)this.requireBranchPermission(e,s(t),"delete");return t},ldaprenamerequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,s(t),"read"),this.requireBranchPermission(n,s(e),"write"),[t,e,i]):[t,e,i]}},this.base=this.config.authz_dynamic_base||"",this.cacheTtlMs=1e3*(this.config.authz_dynamic_cache_ttl||60),this.failureBackoffMs=Math.max(Math.floor(this.cacheTtlMs/4),5e3),this.tokenAttr=this.config.authz_dynamic_token_attribute||"userPassword",this.configAttr=this.config.authz_dynamic_config_attribute||"description",this.tenantAttr=this.config.authz_dynamic_tenant_attribute||"cn",this.reloadEndpoint=Boolean(this.config.authz_dynamic_reload_endpoint),!this.base)throw new Error("authzDynamic requires --authz-dynamic-base (the LDAP branch holding token entries)")}api(t){if(super.api(t),this.reloadEndpoint){const e=this.config.api_prefix||"/api";t.post(`${e}/v1/authz-dynamic/reload`,a(async(t,e)=>{t.authzToken?(await this.reload(),r(e,{success:!0,tokens:this.tokens.length})):n(e)})),this.logger.info(`authzDynamic reload endpoint registered at ${e}/v1/authz-dynamic/reload`)}}async reload(){try{const t=Array.from(new Set(["dn","cn",this.tokenAttr,this.configAttr,this.tenantAttr])),e=await this.server.ldap.search({filter:"(objectClass=*)",scope:"sub",paged:!1,attributes:t},this.base),i=[];for(const t of e.searchEntries||[]){const e="string"==typeof t.dn?t.dn:String(t.dn),n=asString(t.cn)||"",r=asString(t[this.tokenAttr])||"";if(!r)continue;let s,a=[];const o=asString(t[this.configAttr]);if(o)try{const t=JSON.parse(o);t&&"string"==typeof t.tenant&&t.tenant&&(s=t.tenant),t&&Array.isArray(t.bases)&&(a=t.bases.filter(t=>t&&"string"==typeof t.dn&&t.dn.length>0).map(t=>({dn:t.dn,read:Boolean(t.read),write:Boolean(t.write),delete:Boolean(t.delete)})))}catch(t){this.logger.warn(`authzDynamic: invalid JSON in ${this.configAttr} of ${e}: ${String(t)}`)}const h="cn"===this.tenantAttr?n:asString(t[this.tenantAttr])||n,c=s||h;i.push({dn:e,cn:n,tenant:c,hash:r,bases:a})}this.tokens=i,this.lastLoad=Date.now(),this.lastFailure=0,this.logger.info(`authzDynamic: loaded ${i.length} token(s) from ${this.base}`)}catch(t){this.lastFailure=Date.now(),this.logger.error(`authzDynamic: failed to load tokens from ${this.base}: ${String(t)}`)}}async ensureFresh(){const t=Date.now();if(!(this.lastLoad>0&&t-this.lastLoad<this.cacheTtlMs||this.lastFailure>0&&t-this.lastFailure<this.failureBackoffMs))return this.loading||(this.loading=this.reload().finally(()=>{this.loading=void 0})),this.loading}authMethod(t,e,i){t.user?i():this.ensureFresh().then(()=>{const r=t.headers.authorization;if(!r||!/^Bearer\s+/.test(r))return this.logger.warn("authzDynamic: missing or invalid Authorization header"),void n(e);const s=r.slice(r.indexOf(" ")+1).trim();if(!s)return void n(e);let a;for(const t of this.tokens)if(o(s,t.hash)){a=t;break}if(!a){const t=s.length>8?`${s.substring(0,8)}...`:"***";return this.logger.warn(`authzDynamic: unauthorized token ${t}`),void n(e)}t.user=a.tenant,t.authzToken=a,h.run(a,i)}).catch(t=>{this.logger.error(`authzDynamic: authMethod error: ${String(t)}`),n(e)})}activeToken(t){return h.getStore()||t?.authzToken}requireBranchPermission(t,e,n){for(const i of t.bases)if(isAtOrUnder(e,i.dn)&&i[n])return;throw this.logger.debug(`authzDynamic: token "${t.tenant}" denied ${n} on ${e}`),new i(`[authz-forbidden] Token does not have ${n} permission on this branch`)}getConfigApiData(){return{enabled:!0,base:this.base,cacheTtlSeconds:this.cacheTtlMs/1e3,tokenCount:this.tokens.length,reloadEndpoint:this.reloadEndpoint?`${this.config.api_prefix||"/api"}/v1/authz-dynamic/reload`:void 0,tokenAttribute:this.tokenAttr,configAttribute:this.configAttr,tenantAttribute:this.tenantAttr}}_tokens(){return this.tokens}}export{h as authzContext,AuthzDynamic as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"crypto";const t={sha1:20,sha256:32,sha512:64,md5:16};function timingSafeStringEqual(t,r){const a=Buffer.from(t,"utf8"),n=Buffer.from(r,"utf8");return a.length===n.length&&e.timingSafeEqual(a,n)}function isCanonicalBase64(e){return 0!==e.length&&e.length%4==0&&(!!/^[A-Za-z0-9+/]+={0,2}$/.test(e)&&Buffer.from(e,"base64").toString("base64")===e)}function verifySalted(r,a,n){if(!isCanonicalBase64(a))return!1;const s=Buffer.from(a,"base64"),f=t[r];if(s.length<=f)return!1;const i=s.subarray(0,f),u=s.subarray(f),o=e.createHash(r).update(Buffer.from(n,"utf8")).update(u).digest();return o.length===i.length&&e.timingSafeEqual(i,o)}function verifyUnsalted(t,r,a){if(!isCanonicalBase64(r))return!1;return timingSafeStringEqual(r,e.createHash(t).update(Buffer.from(a,"utf8")).digest("base64"))}function verifyLdapPassword(e,t){if("string"!=typeof e||0===e.length)return!1;if("string"!=typeof t||0===t.length)return!1;const r=/^\{([A-Za-z0-9-]+)\}(.*)$/.exec(t);if(!r)return timingSafeStringEqual(e,t);const a=r[1].toUpperCase(),n=r[2];switch(a){case"SSHA":return verifySalted("sha1",n,e);case"SHA":return verifyUnsalted("sha1",n,e);case"SSHA256":return verifySalted("sha256",n,e);case"SHA256":return verifyUnsalted("sha256",n,e);case"SSHA512":return verifySalted("sha512",n,e);case"SHA512":return verifyUnsalted("sha512",n,e);case"SMD5":return verifySalted("md5",n,e);case"MD5":return verifyUnsalted("md5",n,e);case"CLEARTEXT":case"PLAIN":return timingSafeStringEqual(e,n);default:return!1}}const r={sha1:"SSHA",sha256:"SSHA256",sha512:"SSHA512"};function sshaHash(t,a,n){const s=n||e.randomBytes(16),f=e.createHash(t).update(Buffer.from(a,"utf8")).update(s).digest();return`{${r[t]}}${Buffer.concat([f,s]).toString("base64")}`}function ssha(e,t){return sshaHash("sha1",e,t)}export{ssha,sshaHash,verifyLdapPassword};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"fs";import{escapeDnValue as s}from"../../lib/utils.js";class BaseResolver{constructor(s){const r=s.ldap_base||"";this.defaultUserBase=s.scim_user_base||r,this.defaultGroupBase=s.scim_group_base||r,this.userTemplate=s.scim_user_base_template||"",this.groupTemplate=s.scim_group_base_template||"";const t=s.scim_base_map||"";if(t)try{const s=e.readFileSync(t,"utf8");this.map=JSON.parse(s)}catch(e){throw new Error(`Failed to load SCIM base map from ${t}: ${e instanceof Error?e.message:String(e)}`)}}applyTemplate(e,r){const t=r?s(r):"";return e.replace(/\{user\}/g,t)}resolve(e,s){const r=s&&"object"==typeof s&&"user"in s?s.user:void 0;if(this.map&&r&&this.map[r]){const s=this.map[r];if("user"===e&&s.userBase)return s.userBase;if("group"===e&&s.groupBase)return s.groupBase}if(this.map&&this.map["*"]){const s=this.map["*"];if("user"===e&&s.userBase)return this.applyTemplate(s.userBase,r);if("group"===e&&s.groupBase)return this.applyTemplate(s.groupBase,r)}const t="user"===e?this.userTemplate:this.groupTemplate;return t?this.applyTemplate(t,r):"user"===e?this.defaultUserBase:this.defaultGroupBase}userBase(e){return this.resolve("user",e)}groupBase(e){return this.resolve("group",e)}}export{BaseResolver};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{launchHooks as t}from"../../lib/utils.js";import{SCHEMA_BULK_RESPONSE as s}from"./types.js";import{scimInvalidValue as e,ScimError as o}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";const r=/^bulkId:([A-Za-z0-9_-]+)$/;class ScimBulk{constructor(t){this.config=t.config,this.logger=t.logger,this.users=t.users,this.groups=t.groups,this.scimPrefix=t.scimPrefix,this.hooks=t.hooks}async execute(o,r){const i=this.config.scim_bulk_max_operations||100;if(!r.Operations||!Array.isArray(r.Operations))throw e("Missing Operations array");if(r.Operations.length>i)throw e(`Too many operations: ${r.Operations.length} (max ${i})`);const a=new Map,n=[];let c=0;const u=r.failOnErrors??0;for(const t of r.Operations){if(u>0&&c>=u)break;const s=await this.executeOne(o,t,a);n.push(s),parseInt(s.status,10)>=400&&c++}const h={schemas:[s],Operations:n};return t(this.hooks.scimbulkdone,[h]),h}async executeOne(t,s,i){const a={method:s.method,bulkId:s.bulkId,status:"200"};try{if(!s.method||!s.path)throw e("method and path are required");s.data&&"object"==typeof s.data&&this.resolveRefs(s.data,i);const o=s.path.replace(/^\//,"").split("/"),n=o[0],c=o[1];if("Users"!==n&&"Groups"!==n)throw e(`Unsupported bulk path '${s.path}'`);const u="Users"===n;let h=c;if(h&&r.test(h)){const t=r.exec(h),s=i.get(t[1]);if(!s)throw e(`Unknown bulkId reference '${t[1]}'`);h=s.id}switch(s.method){case"POST":{if(h)throw e("POST must not include an id");const o=u?await this.users.create(t,s.data):await this.groups.create(t,s.data);return a.status="201",a.location=this.buildLocation(n,o.id,t),a.version=o.meta?.version||void 0,s.bulkId&&i.set(s.bulkId,{id:o.id,type:u?"User":"Group"}),a}case"PUT":{if(!h)throw e("PUT requires an id");const o=u?await this.users.replace(t,h,s.data):await this.groups.replace(t,h,s.data);return a.status="200",a.location=this.buildLocation(n,o.id,t),a}case"PATCH":{if(!h)throw e("PATCH requires an id");const o=u?await this.users.patch(t,h,s.data):await this.groups.patch(t,h,s.data);return a.status="200",a.location=this.buildLocation(n,o.id,t),a}case"DELETE":if(!h)throw e("DELETE requires an id");return u?await this.users.delete(t,h):await this.groups.delete(t,h),a.status="204",a;default:throw e(`Unsupported method '${String(s.method)}'`)}}catch(t){const s=t instanceof o?t.statusCode:500;return a.status=String(s),a.response={schemas:["urn:ietf:params:scim:api:messages:2.0:Error"],status:String(s),...t instanceof o&&t.scimType?{scimType:t.scimType}:{},detail:t instanceof Error?t.message:String(t)},a}}resolveRefs(t,s){for(const[e,o]of Object.entries(t)){if("string"==typeof o){const i=r.exec(o);if(i){const o=s.get(i[1]);o&&(t[e]=o.id)}continue}if(Array.isArray(o)){const t=o;for(let e=0;e<t.length;e++){const o=t[e];if("string"==typeof o){const i=r.exec(o);if(i){const o=s.get(i[1]);o&&(t[e]=o.id)}}else o&&"object"==typeof o&&this.resolveRefs(o,s)}continue}o&&"object"==typeof o&&this.resolveRefs(o,s)}}buildLocation(t,s,e){const o=this.config.scim_base_url||"";return`${o?o.replace(/\/$/,""):e?.protocol&&e.get?`${e.protocol}://${String(e.get("host")||"")}`:""}${this.scimPrefix}/${t}/${encodeURIComponent(s)}`}}export{ScimBulk};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"fs";import t from"path";import{SCHEMA_SERVICE_PROVIDER_CONFIG as s,SCHEMA_USER as o,SCHEMA_RESOURCE_TYPE as i,SCHEMA_GROUP as r,SCHEMA_SCHEMA as n}from"./types.js";class ScimDiscovery{constructor(e){this.config=e.config,this.schemaDir=e.schemaDir,this.scimPrefix=e.scimPrefix,this.loadedPlugins=e.loadedPlugins}baseUrl(e){const t=this.config.scim_base_url||"";return t?t.replace(/\/$/,""):e?.protocol&&e.get?`${e.protocol}://${String(e.get("host")||"")}`:""}location(e,t){return`${this.baseUrl(e)}${this.scimPrefix}${t}`}authSchemes(){const e=[];return this.loadedPlugins.authToken&&e.push({name:"OAuth Bearer Token",description:'Bearer token configured via --auth-token. Send "Authorization: Bearer <token>".',type:"oauthbearertoken",primary:0===e.length}),this.loadedPlugins.openidconnect&&e.push({name:"OpenID Connect",description:"OIDC-issued access token.",type:"oauth2",specUri:"https://openid.net/specs/openid-connect-core-1_0.html",primary:0===e.length}),this.loadedPlugins.authHmac&&e.push({name:"HMAC",description:"Signed request via HMAC-SHA256.",type:"httpbasic",primary:0===e.length}),0===e.length&&e.push({name:"No authentication",description:"No authentication plugin loaded. NOT RECOMMENDED for production.",type:"httpbasic",primary:!0}),e}serviceProviderConfig(e){return{schemas:[s],patch:{supported:!0},bulk:{supported:!0,maxOperations:this.config.scim_bulk_max_operations||100,maxPayloadSize:this.config.scim_bulk_max_payload_size||1048576},filter:{supported:!0,maxResults:this.config.scim_max_results||200},changePassword:{supported:!1},sort:{supported:!1},etag:{supported:Boolean(this.config.scim_etag)},authenticationSchemes:this.authSchemes(),meta:{resourceType:"ServiceProviderConfig",location:this.location(e,"/ServiceProviderConfig")}}}resourceTypes(e){return[{schemas:[i],id:"User",name:"User",endpoint:"/Users",description:"User Account",schema:o,meta:{resourceType:"ResourceType",location:this.location(e,"/ResourceTypes/User")}},{schemas:[i],id:"Group",name:"Group",endpoint:"/Groups",description:"Group",schema:r,meta:{resourceType:"ResourceType",location:this.location(e,"/ResourceTypes/Group")}}]}resourceType(e,t){return this.resourceTypes(t).find(t=>t.id===e)}schemas(e){const t=[this.loadSchema("User.json",o),this.loadSchema("Group.json",r)].filter(e=>null!=e);for(const s of t)s.meta={resourceType:"Schema",location:this.location(e,`/Schemas/${s.id}`)};return t}schema(e,t){return this.schemas(t).find(t=>t.id===e)}loadSchema(s,o){const i=t.join(this.schemaDir,s);try{const t=e.readFileSync(i,"utf8"),s=JSON.parse(t);return{schemas:[n],id:o,name:s.name||o.split(":").pop()||o,description:s.description,attributes:s.attributes||[]}}catch{return}}}export{ScimDiscovery};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{HttpError as r}from"../../lib/errors.js";import{SCHEMA_ERROR as e}from"./types.js";const t="application/scim+json";class ScimError extends r{constructor(r,e,t){super(e,r),this.name="ScimError",this.scimType=t}}const scimInvalidFilter=(r="Invalid filter")=>new ScimError(400,r,"invalidFilter"),scimInvalidPath=(r="Invalid path")=>new ScimError(400,r,"invalidPath"),scimInvalidValue=(r="Invalid value")=>new ScimError(400,r,"invalidValue"),scimInvalidSyntax=(r="Invalid syntax")=>new ScimError(400,r,"invalidSyntax"),scimNoTarget=(r="No target")=>new ScimError(400,r,"noTarget"),scimMutability=(r="Immutable attribute")=>new ScimError(400,r,"mutability"),scimUniqueness=(r="Uniqueness violation")=>new ScimError(409,r,"uniqueness"),scimTooMany=(r="Too many results")=>new ScimError(400,r,"tooMany"),scimNotFound=(r="Resource not found")=>new ScimError(404,r);function writeScimError(r,o,i,n){const s={schemas:[e],status:String(o),...n?{scimType:n}:{},detail:i};r.status(o).type(t).json(s)}function extractLdapCode(r){if(null==r)return;if("object"==typeof r&&"code"in r){const e=r.code;if("number"==typeof e)return e}const e=r instanceof Error?r.message:"string"==typeof r?r:"";return/noSuchObject|No such object|code:?\s*(32|0x20)/i.test(e)?32:/entryAlreadyExists|Already[_ ]?Exists|code:?\s*(68|0x44)/i.test(e)?68:/noSuchAttribute|No such attribute|code:?\s*(16|0x10)/i.test(e)?16:void 0}function writeScimErrorFromException(e,t,o=500){const sanitize=r=>/\[authz-forbidden\]/.test(r)?"Token does not have permission on this branch":r;if(t instanceof ScimError)return void writeScimError(e,t.statusCode,sanitize(t.message),t.scimType);if(t instanceof r)return void writeScimError(e,t.statusCode,sanitize(t.message));const i=t instanceof Error?t.message:String(t);if(/\[authz-forbidden\]/.test(i))return void writeScimError(e,403,"Token does not have permission on this branch");const n=extractLdapCode(t);32!==n?68!==n?writeScimError(e,o,sanitize(i)||"Internal error"):writeScimError(e,409,sanitize(i),"uniqueness"):writeScimError(e,404,"Resource not found")}const scimAsyncHandler=r=>(e,t,o)=>{Promise.resolve().then(()=>r(e,t,o)).catch(r=>{t.headersSent?o(r):writeScimErrorFromException(t,r)})};export{t as SCIM_CONTENT_TYPE,ScimError,extractLdapCode,scimAsyncHandler,scimInvalidFilter,scimInvalidPath,scimInvalidSyntax,scimInvalidValue,scimMutability,scimNoTarget,scimNotFound,scimTooMany,scimUniqueness,writeScimError,writeScimErrorFromException};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{escapeLdapFilter as e}from"../../lib/utils.js";import{scimPathToLdapAttribute as t}from"./mapping.js";import{scimInvalidFilter as s}from"./errors.js";const o=new Set(["and","or","not","pr","eq","ne","co","sw","ew","gt","ge","lt","le","true","false","null"]);class Parser{constructor(e,t){this.tokens=e,this.mapping=t,this.pos=0}peek(e=0){return this.tokens[this.pos+e]}consume(){return this.tokens[this.pos++]}expect(e,t){const o=this.tokens[this.pos];if(o.kind!==e||t&&o.value.toLowerCase()!==t)throw s(`Expected ${t||e} at position ${o.pos}, got '${o.value}'`);return this.pos++,o}parse(){const e=this.parseOr();if("EOF"!==this.peek().kind)throw s(`Unexpected token '${this.peek().value}' at position ${this.peek().pos}`);return e}parseOr(){const e=[this.parseAnd()];for(;"WORD"===this.peek().kind&&"or"===this.peek().value.toLowerCase();)this.consume(),e.push(this.parseAnd());return 1===e.length?e[0]:`(|${e.join("")})`}parseAnd(){const e=[this.parseNot()];for(;"WORD"===this.peek().kind&&"and"===this.peek().value.toLowerCase();)this.consume(),e.push(this.parseNot());return 1===e.length?e[0]:`(&${e.join("")})`}parseNot(){if("WORD"===this.peek().kind&&"not"===this.peek().value.toLowerCase()){this.consume();return`(!${this.parsePrimary()})`}return this.parsePrimary()}parsePrimary(){const e=this.peek();if("LPAREN"===e.kind){this.consume();const e=this.parseOr();return this.expect("RPAREN"),e}if("WORD"===e.kind){const e=this.consume().value;if("LBRACK"===this.peek().kind)throw s(`Complex multi-valued filters ('${e}[...]') are not supported; use 'and' at top level instead`);const t=this.consume();if("WORD"!==t.kind)throw s(`Expected comparison operator at position ${t.pos}`);const n=t.value.toLowerCase();if("pr"===n){return`(${this.resolvePath(e)}=*)`}if(!o.has(n)||["and","or","not"].includes(n))throw s(`Unknown operator '${t.value}' at position ${t.pos}`);const i=this.consume(),r=this.tokenToValue(i);return this.emitComparison(e,n,r)}throw s(`Unexpected token '${e.value}' at position ${e.pos}`)}tokenToValue(e){if("STRING"===e.kind)return e.value;if("NUMBER"===e.kind){const t=Number(e.value);if(Number.isNaN(t))throw s(`Invalid number '${e.value}' at position ${e.pos}`);return t}if("WORD"===e.kind){const t=e.value.toLowerCase();return"true"===t||"false"!==t&&("null"===t?null:e.value)}throw s(`Unexpected value token at position ${e.pos}`)}resolvePath(e){if("id"===e)throw s("filter on 'id' only supports 'eq' with a string value");if("active"===e)return"active";const o=t(e,this.mapping);if(!o)throw s(`Unknown attribute path '${e}'`);return o}emitComparison(t,o,n){const i=this.resolvePath(t);if("active"===i){const e=!0===n||"string"==typeof n&&"true"===n.toLowerCase();if("eq"===o)return e?"(!(pwdAccountLockedTime=*))":"(pwdAccountLockedTime=*)";if("ne"===o)return e?"(pwdAccountLockedTime=*)":"(!(pwdAccountLockedTime=*))";throw s(`Operator '${o}' not supported for 'active'`)}if(null===n){if("eq"===o)return`(!(${i}=*))`;if("ne"===o)return`(${i}=*)`;throw s(`Operator '${o}' not supported with null`)}const r=String(n),a=e(r);switch(o){case"eq":return`(${i}=${a})`;case"ne":return`(!(${i}=${a}))`;case"co":return`(${i}=*${a}*)`;case"sw":return`(${i}=${a}*)`;case"ew":return`(${i}=*${a})`;case"gt":return`(&(${i}>=${a})(!(${i}=${a})))`;case"ge":return`(${i}>=${a})`;case"lt":return`(&(${i}<=${a})(!(${i}=${a})))`;case"le":return`(${i}<=${a})`;default:throw s(`Unknown operator '${o}'`)}}}function scimFilterToLdap(e,t){const o=e.trim();if(!o)return{ldapFilter:"(objectClass=*)",touchesId:!1};const n=/^id\s+eq\s+"([^"\\]*(?:\\.[^"\\]*)*)"\s*$/i.exec(o);if(n)return{ldapFilter:"(objectClass=*)",touchesId:!0,idEquals:n[1].replace(/\\(.)/g,"$1")};const i=function(e){const t=[];let o=0;for(;o<e.length;){const n=e[o];if(" "!==n&&"\t"!==n&&"\n"!==n&&"\r"!==n)if("("!==n)if(")"!==n)if("["!==n){if("]"!==n){if('"'===n){const n=o;o++;let i="";for(;o<e.length&&'"'!==e[o];){if("\\"===e[o]&&o+1<e.length){const t=e[o+1];if('"'===t){i+='"',o+=2;continue}if("\\"===t){i+="\\",o+=2;continue}if("n"===t){i+="\n",o+=2;continue}if("t"===t){i+="\t",o+=2;continue}}i+=e[o],o++}if(o>=e.length)throw s(`Unterminated string literal at position ${n}`);o++,t.push({kind:"STRING",value:i,pos:n});continue}if(n>="0"&&n<="9"){const s=o;for(;o<e.length&&/[0-9.+\-eE]/.test(e[o])&&!/[\s()[\]]/.test(e[o]);)o++;t.push({kind:"NUMBER",value:e.slice(s,o),pos:s});continue}if(/[A-Za-z_$:]/.test(n)){const s=o;for(;o<e.length&&/[A-Za-z0-9_.$:]/.test(e[o]);)o++;t.push({kind:"WORD",value:e.slice(s,o),pos:s});continue}throw s(`Unexpected character '${n}' at position ${o}`)}t.push({kind:"RBRACK",value:"]",pos:o}),o++}else t.push({kind:"LBRACK",value:"[",pos:o}),o++;else t.push({kind:"RPAREN",value:")",pos:o}),o++;else t.push({kind:"LPAREN",value:"(",pos:o}),o++;else o++}return t.push({kind:"EOF",value:"",pos:e.length}),t}(o);return{ldapFilter:new Parser(i,t).parse(),touchesId:/\bid\b/.test(o)}}export{scimFilterToLdap};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{escapeDnValue as t,launchHooksChained as e,validateDnValue as s,launchHooks as r,isChildOf as i,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as o}from"./types.js";import{mergeMapping as n,loadMappingFile as c,DEFAULT_GROUP_MAPPING as h,requiredLdapAttributes as u,ldapToScimGroup as m,scimGroupToLdap as l}from"./mapping.js";import{scimFilterToLdap as p}from"./filter.js";import{patchToModifyRequest as d}from"./patch.js";import{extractLdapCode as g,scimNotFound as f,ScimError as b,scimTooMany as y,scimInvalidValue as w,scimUniqueness as $}from"./errors.js";class ScimGroups{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_group_rdn_attribute||"cn",this.objectClass=this.config.scim_group_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=this.config.scim_group_mapping||"";this.mapping=n(h,e?c(e):void 0)}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}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const e=this.baseResolver.userBase(t),s=this.baseResolver.groupBase(t),r=this.scimPrefix,i=this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),a=this.config.scim_user_rdn_attribute||"uid",o=this.rdnAttribute,n=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(t.toLowerCase()===n.toLowerCase())return;const c=/^([^=]+)=((?:\\.|[^,])+)/.exec(t);if(!c)return;const h=c[2].replace(/\\(.)/g,"$1"),u=t.toLowerCase();let m;e&&u.endsWith(e.toLowerCase())?m="User":s&&u.endsWith(s.toLowerCase())?m="Group":c[1]===a?m="User":c[1]===o&&(m="Group");const l={value:h};if(m&&(l.type=m,i)){const t="User"===m?"Users":"Groups";l.$ref=`${i.replace(/\/$/,"")}${r}/${t}/${encodeURIComponent(h)}`}return l}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.search({paged:!1,scope:"base",attributes:[...u(this.mapping),"member"]},s)}catch(t){if(32===g(t))throw f(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw f(`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 a,n=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=p(e.filter,this.mapping);t.idEquals?a=t.idEquals:n=`(&${n}${t.ldapFilter})`}if(a)try{const e=await this.get(t,a);return{schemas:[o],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof b&&404===t.statusCode)return{schemas:[o],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const c=(await this.ldap.search({filter:n,scope:"sub",paged:!1,attributes:[...u(this.mapping),"member"],sizeLimit:this.maxResults+1},s)).searchEntries||[],h=c.length;if(h>this.maxResults)throw y(`Filter matched ${h} resources, exceeds max ${this.maxResults}`);const l=c.slice(r-1,r-1+i),d=this.buildMemberResolver(t),g=l.map(e=>m(e,this.mapping,this.ctx(t),d));return{schemas:[o],totalResults:h,startIndex:r,itemsPerPage:g.length,Resources:g}}async create(i,a){if(!a.displayName)throw w("displayName is required");const o=(await e(this.hooks.scimgroupcreate,[a,i]))[0],{rdn:n,attributes:c}=l(o,this.mapping,this.objectClass);if(!n)throw w("displayName is required");s(n,this.rdnAttribute),c[this.rdnAttribute]=n;const h=[];if(o.members&&Array.isArray(o.members))for(const t of o.members){if(!t||!t.value)continue;const e=await this.users.resolveRef(i,t.value);e&&h.push(e)}if(0===h.length){const t=this.config.group_dummy_user||"cn=fakeuser";h.push(t)}c.member=h;const u=this.baseResolver.groupBase(i),m=`${this.rdnAttribute}=${t(n)},${u}`;try{await this.ldap.add(m,c)}catch(t){if(68===g(t))throw $(`Group ${n} already exists`);throw t}const p=await this.get(i,n);return r(this.hooks.scimgroupcreatedone,[p]),p}async replace(t,s,i){await this.get(t,s);const a=(await e(this.hooks.scimgroupupdate,[s,i,t]))[1],{attributes:o}=l(a,this.mapping,this.objectClass),n=[];if(a.members&&Array.isArray(a.members))for(const e of a.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(s,t),h={replace:{}};for(const[t,e]of Object.entries(o))"objectClass"!==t&&t!==this.rdnAttribute&&(h.replace[t]=e);h.replace.member=n,await this.ldap.modify(c,h);const u=await this.get(t,s);return r(this.hooks.scimgroupupdatedone,[s,u]),u}async patch(t,e,s){await this.get(t,e);const i=await d(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e)});if(!i.add&&!i.replace&&(!i.delete||Array.isArray(i.delete)&&0===i.delete.length))return this.get(t,e);const a=this.config.group_dummy_user||"cn=fakeuser",o=this.dnForId(e,t);if(i.delete&&!Array.isArray(i.delete)&&i.delete.member){const t=await this.ldap.search({paged:!1,scope:"base",attributes:["member"]},o),e=t.searchEntries[0]?.member,s=Array.isArray(e)?e:e?[String(e)]:[],r=Array.isArray(i.delete.member)?i.delete.member:[String(i.delete.member)];0===s.filter(t=>!r.includes(t)).length&&(i.add||(i.add={}),i.add.member=[a])}await this.ldap.modify(o,i);const n=await this.get(t,e);return r(this.hooks.scimgroupupdatedone,[e,n]),n}async delete(t,s){await this.get(t,s);const i=(await e(this.hooks.scimgroupdelete,[s,t]))[0],a=this.dnForId(i,t);await this.ldap.delete(a),r(this.hooks.scimgroupdeletedone,[i])}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e);if(s.includes("=")&&s.includes(","))return i(s,r)?s:void 0;const o=`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},o);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}try{const t=await this.ldap.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{}}get groupMapping(){return this.mapping}}export{ScimGroups};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"fs";import{SCHEMA_GROUP as r,SCHEMA_USER as i}from"./types.js";const a=["createTimestamp","modifyTimestamp","entryUUID"],t={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"}]},n={resourceType:"Group",schemas:[r],entries:[{scim:"displayName",ldap:"cn"},{scim:"externalId",ldap:"entryUUID",operational:!0,readOnly:!0}]};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 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 i=e.readFileSync(r,"utf8");return JSON.parse(i)}function mergeMapping(e,r){if(!r)return e;const i=new Map;for(const r of e.entries)i.set(r.scim,r);for(const e of r.entries)i.set(e.scim,e);return{...e,entries:Array.from(i.values())}}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 i="User"===r.resourceType?"Users":"Groups";return`${r.baseUrl.replace(/\/$/,"")}${r.scimPrefix}/${i}/${encodeURIComponent(e)}`}function ldapToScimUser(e,r,a){const t={schemas:[i]},n=resolveScimId(e,a);n&&(t.id=n);for(const i of r.entries){if(i.sub){const r={};for(const[a,t]of Object.entries(i.sub)){const i=asString(e[t]);null!=i&&(r[a]=i)}Object.keys(r).length>0&&(t[i.scim]=r);continue}if(i.ldapPrimary||i.ldapSecondary){const r=[];if(i.ldapPrimary){const a=asString(e[i.ldapPrimary]);null!=a&&r.push({value:a,primary:!0})}if(i.ldapSecondary)for(const a of asArray(e[i.ldapSecondary]))r.push({value:a});r.length>0&&(t[i.scim]=r);continue}if(i.ldap)if("array"===i.multi){const r=asArray(e[i.ldap]);r.length>0&&(t[i.scim]=r)}else{const r=asString(e[i.ldap]);null!=r&&(t[i.scim]=r)}}return t.active=null==e.pwdAccountLockedTime,n&&(t.meta={resourceType:"User",created:asString(e.createTimestamp),lastModified:asString(e.modifyTimestamp),location:buildLocation(n,a)}),t}function scimUserToLdap(e,r,i,a){const t={objectClass:a},n=e.userName||e.id||"";for(const i of r.entries){if(i.readOnly||i.operational)continue;const r=e[i.scim];if(null!=r)if(i.sub&&"object"==typeof r)for(const[e,a]of Object.entries(i.sub)){const i=r[e];"string"==typeof i&&i.length>0&&(t[a]=i)}else{if(i.ldapPrimary||i.ldapSecondary){if(!Array.isArray(r))continue;const e=r,a=e.find(e=>!0===e.primary)||e[0],n=e.filter(e=>e!==a);i.ldapPrimary&&a&&a.value&&(t[i.ldapPrimary]=a.value),i.ldapSecondary&&n.length>0&&(t[i.ldapSecondary]=n.map(e=>e.value).filter(Boolean));continue}i.ldap&&(Array.isArray(r)?t[i.ldap]=r.map(e=>String(e)):"string"!=typeof r&&"number"!=typeof r||(t[i.ldap]=String(r)))}}if(!t.cn&&n&&(t.cn=n),!t.sn){const r=e.name?.familyName||e.displayName||n;r&&(t.sn=r)}return{rdn:n,attributes:t}}function ldapToScimGroup(e,i,a,t){const n={schemas:[r]},s=resolveScimId(e,a);s&&(n.id=s);for(const r of i.entries){if(!r.ldap)continue;const i=e[r.ldap];if(null!=i)if("array"===r.multi){const e=asArray(i);e.length>0&&(n[r.scim]=e)}else{const e=asString(i);null!=e&&(n[r.scim]=e)}}const o=asArray(e.member),l=[];for(const e of o)if(e)if(t){const r=t(e);r&&l.push(r)}else l.push({value:e,type:"User"});return l.length>0&&(n.members=l),s&&(n.meta={resourceType:"Group",created:asString(e.createTimestamp),lastModified:asString(e.modifyTimestamp),location:buildLocation(s,a)}),n}function scimGroupToLdap(e,r,i){const a={objectClass:i},t=e.displayName||e.id||"";for(const i of r.entries){if(i.readOnly||i.operational)continue;if(!i.ldap)continue;const r=e[i.scim];null!=r&&(Array.isArray(r)?a[i.ldap]=r.map(e=>"string"==typeof e?e:JSON.stringify(e)):"string"!=typeof r&&"number"!=typeof r||(a[i.ldap]=String(r)))}return{rdn:t,attributes:a}}function requiredLdapAttributes(e){const r=new Set(["objectClass",...a]);for(const i of e.entries)if(i.ldap&&r.add(i.ldap),i.ldapPrimary&&r.add(i.ldapPrimary),i.ldapSecondary&&r.add(i.ldapSecondary),i.sub)for(const e of Object.values(i.sub))r.add(e);return Array.from(r)}function scimPathToLdapAttribute(e,r){const i=e.split(".")[0],a=e.includes(".")?e.split(".").slice(1).join("."):"";for(const e of r.entries)if(e.scim===i){if(e.sub&&a)return e.sub[a];if((e.ldapPrimary||e.ldapSecondary)&&("value"===a||!a))return e.ldapPrimary||e.ldapSecondary;if(e.ldap)return e.ldap}}export{n as DEFAULT_GROUP_MAPPING,t as DEFAULT_USER_MAPPING,buildLocation,ldapToScimGroup,ldapToScimUser,loadMappingFile,mergeMapping,requiredLdapAttributes,resolveScimId,scimGroupToLdap,scimPathToLdapAttribute,scimUserToLdap};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{scimInvalidValue as e,scimInvalidPath as t,scimNoTarget as r}from"./errors.js";import{scimPathToLdapAttribute as o}from"./mapping.js";function normalizeOp(t){const r=t.toLowerCase();if("add"===r||"remove"===r||"replace"===r)return r;throw e(`Unknown patch op '${t}'`)}const a=new Set(["__proto__","constructor","prototype"]),n=/^[A-Za-z_$:][\w$:]*$/;function assertSafeKey(e){if(a.has(e))throw t(`Forbidden attribute name '${e}'`)}function parsePath(e){if("string"!=typeof e||0===e.length||e.length>512)throw t(`Malformed path '${String(e)}'`);let r,o,a;const i=e.indexOf("[");if(i>=0){const n=e.lastIndexOf("]");if(n<=i)throw t(`Malformed path '${e}'`);r=e.slice(0,i),o=e.slice(i+1,n);const l=e.slice(n+1);if(l.length>0){if("."!==l[0])throw t(`Malformed path '${e}'`);a=l.slice(1)}}else{const t=e.indexOf(".");t>=0?(r=e.slice(0,t),a=e.slice(t+1)):r=e}if(!n.test(r))throw t(`Malformed path '${e}'`);if(assertSafeKey(r),null!=a){if(!n.test(a))throw t(`Malformed sub-attribute in '${e}'`);assertSafeKey(a)}return{top:r,filter:o,sub:a}}function mergeAttr(e,t,r){const o=e[t];if(null==o)return void(e[t]=r);const a=[...(n=o,Array.isArray(n)?n.map(e=>"string"==typeof e?e:e.toString()):["string"==typeof n?n:n.toString()]),...Array.isArray(r)?r:[r]];var n;e[t]=a}async function applyOperation(a,n,i){const l=normalizeOp(a.op),s=i.memberAttribute||"member";if(!a.path){if(null==a.value||"object"!=typeof a.value||Array.isArray(a.value))throw r("PATCH without path requires an object value");const e=a.value;for(const[t,r]of Object.entries(e))await applyOperation({op:a.op,path:t,value:r},n,i);return}const{top:f,sub:p,filter:u}=parsePath(a.path);if("members"===f){if(!i.resolveMemberRef)throw r("Member operations require a member resolver");if("add"===l){const e=Array.isArray(a.value)?a.value:[a.value],t=[];for(const r of e){const e="string"==typeof r?r:"object"==typeof r&&null!=r&&"value"in r?String(r.value):"";if(!e)continue;const o=await i.resolveMemberRef(e);o&&t.push(o)}return void(t.length>0&&(n.add||(n.add={}),mergeAttr(n.add,s,t)))}if("remove"===l){const collectFromValue=()=>{if(null==a.value)return[];return(Array.isArray(a.value)?a.value:[a.value]).map(e=>"string"==typeof e?e:"object"==typeof e&&null!=e&&"value"in e?String(e.value):"").filter(Boolean)},e=[...(()=>{if(!u)return[];const e=/value\s+eq\s+"([^"]+)"/i.exec(u);return e?[e[1]]:[]})(),...collectFromValue()],t=[];for(const r of e){const e=await i.resolveMemberRef(r);e&&t.push(e)}return void(t.length>0?(n.delete||(n.delete={}),Array.isArray(n.delete)?n.delete={[s]:t}:mergeAttr(n.delete,s,t)):u||a.value||(n.delete||(n.delete={}),Array.isArray(n.delete)?n.delete.push(s):n.delete[s]=""))}if("replace"===l){const e=Array.isArray(a.value)?a.value:[a.value],t=[];for(const r of e){const e="string"==typeof r?r:"object"==typeof r&&null!=r&&"value"in r?String(r.value):"";if(!e)continue;const o=await i.resolveMemberRef(e);o&&t.push(o)}return void(t.length>0&&(n.replace||(n.replace={}),n.replace[s]=t))}}if(u)throw t(`Complex multi-valued filters are only supported on 'members' (got '${a.path}')`);const c=o(p?`${f}.${p}`:f,i.mapping);if(!c)throw t(`Unknown SCIM attribute path '${a.path}'`);if("remove"===l)return n.delete||(n.delete={}),void(Array.isArray(n.delete)?n.delete.push(c):n.delete[c]="");const y=function(t){if(null!=t)return"string"==typeof t?t:"number"==typeof t||"boolean"==typeof t?String(t):Array.isArray(t)?t.map(t=>{if("string"==typeof t)return t;if("number"==typeof t||"boolean"==typeof t)return String(t);if(t&&"object"==typeof t&&!Array.isArray(t)&&"value"in t&&("string"==typeof t.value||"number"==typeof t.value||"boolean"==typeof t.value))return String(t.value);throw e(`Unsupported array element in PATCH value: ${JSON.stringify(t)}`)}):void 0}(a.value);if(null==y)throw e(`PATCH ${a.op} ${a.path} missing value`);return"add"===l?(n.add||(n.add={}),void mergeAttr(n.add,c,y)):"replace"===l?(n.replace||(n.replace={}),void(n.replace[c]=y)):void 0}async function patchToModifyRequest(t,r){if(!t.Operations||!Array.isArray(t.Operations))throw e("Missing Operations array");const o={};for(const e of t.Operations)await applyOperation(e,o,r);return o}function applyPatchToResource(e,t){const r={...e};for(const e of t.Operations){const t=normalizeOp(e.op);if(!e.path){if(e.value&&"object"==typeof e.value&&!Array.isArray(e.value))for(const[t,o]of Object.entries(e.value))assertSafeKey(t),r[t]=o;continue}const{top:o,sub:a}=parsePath(e.path);if("remove"!==t)if(a){const t=r[o],n=t&&"object"==typeof t&&!Array.isArray(t)?{...t}:Object.create(null);n[a]=e.value,r[o]=n}else r[o]=e.value;else if(a){const e=r[o];e&&"object"==typeof e&&delete e[a]}else delete r[o]}return r}export{applyPatchToResource,patchToModifyRequest};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{fileURLToPath as s}from"url";import{join as i,dirname as e}from"path";import t from"body-parser";import o from"../../abstract/plugin.js";import{BaseResolver as r}from"./baseResolver.js";import{ScimUsers as a}from"./users.js";import{ScimGroups as n}from"./groups.js";import{ScimBulk as c}from"./bulk.js";import{ScimDiscovery as p}from"./discovery.js";import{SCIM_CONTENT_TYPE as m,scimAsyncHandler as d,writeScimError as u}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";class Scim extends o{constructor(t){super(t),this.name="scim",this.roles=["api","configurable"],this.ldap=t.ldap,this.scimPrefix=this.config.scim_prefix||"/scim/v2",this.baseResolver=new r(this.config),this.users=new a({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,hooks:this.registeredHooks}),this.groups=new n({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,users:this.users,hooks:this.registeredHooks}),this.bulk=new c({config:this.config,logger:this.logger,users:this.users,groups:this.groups,scimPrefix:this.scimPrefix,hooks:this.registeredHooks});const o=this.config.schemas_path||i(e(s(import.meta.url)),"..","..","..","static","schemas");this.discovery=new p({config:this.config,schemaDir:i(o,"scim"),scimPrefix:this.scimPrefix,loadedPlugins:t.loadedPlugins})}scimJson(s,i,e){s.status(i).type(m).json(e)}readScimBody(s){const i=String(s.headers["content-type"]||"");if(i.startsWith("application/scim+json")||i.startsWith("application/json"))return s.body}api(s){const i=this.scimPrefix;s.use(i,t.json({type:["application/json","application/scim+json"],limit:`${this.config.scim_bulk_max_payload_size||1048576}b`})),s.get(`${i}/Users`,d(async(s,i)=>{const e=this.parseListQuery(s),t=await this.users.list(s,e);this.scimJson(i,200,t)})),s.get(`${i}/Users/:id`,d(async(s,i)=>{const e=await this.users.get(s,decodeURIComponent(s.params.id));this.scimJson(i,200,e)})),s.post(`${i}/Users`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return u(i,400,"Missing body","invalidSyntax");const t=await this.users.create(s,e);this.scimJson(i,201,t)})),s.put(`${i}/Users/:id`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return u(i,400,"Missing body","invalidSyntax");const t=await this.users.replace(s,decodeURIComponent(s.params.id),e);this.scimJson(i,200,t)})),s.patch(`${i}/Users/:id`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return u(i,400,"Missing body","invalidSyntax");const t=await this.users.patch(s,decodeURIComponent(s.params.id),e);this.scimJson(i,200,t)})),s.delete(`${i}/Users/:id`,d(async(s,i)=>{await this.users.delete(s,decodeURIComponent(s.params.id)),i.status(204).end()})),s.get(`${i}/Groups`,d(async(s,i)=>{const e=this.parseListQuery(s),t=await this.groups.list(s,e);this.scimJson(i,200,t)})),s.get(`${i}/Groups/:id`,d(async(s,i)=>{const e=await this.groups.get(s,decodeURIComponent(s.params.id));this.scimJson(i,200,e)})),s.post(`${i}/Groups`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return u(i,400,"Missing body","invalidSyntax");const t=await this.groups.create(s,e);this.scimJson(i,201,t)})),s.put(`${i}/Groups/:id`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return u(i,400,"Missing body","invalidSyntax");const t=await this.groups.replace(s,decodeURIComponent(s.params.id),e);this.scimJson(i,200,t)})),s.patch(`${i}/Groups/:id`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return u(i,400,"Missing body","invalidSyntax");const t=await this.groups.patch(s,decodeURIComponent(s.params.id),e);this.scimJson(i,200,t)})),s.delete(`${i}/Groups/:id`,d(async(s,i)=>{await this.groups.delete(s,decodeURIComponent(s.params.id)),i.status(204).end()})),s.post(`${i}/Bulk`,d(async(s,i)=>{const e=this.readScimBody(s);if(!e||"object"!=typeof e)return u(i,400,"Missing body","invalidSyntax");const t=this.config.scim_bulk_max_payload_size||1048576,o=JSON.stringify(e).length;if(o>t)return u(i,413,`Bulk payload too large: ${o} > ${t}`,"invalidValue");const r=await this.bulk.execute(s,e);this.scimJson(i,200,r)})),s.get(`${i}/ServiceProviderConfig`,d((s,i)=>{this.scimJson(i,200,this.discovery.serviceProviderConfig(s))})),s.get(`${i}/ResourceTypes`,d((s,i)=>{const e=this.discovery.resourceTypes(s);this.scimJson(i,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:e.length,Resources:e,startIndex:1,itemsPerPage:e.length})})),s.get(`${i}/ResourceTypes/:name`,d((s,i)=>{const e=this.discovery.resourceType(s.params.name,s);if(!e)return u(i,404,"ResourceType not found");this.scimJson(i,200,e)})),s.get(`${i}/Schemas`,d((s,i)=>{const e=this.discovery.schemas(s);this.scimJson(i,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:e.length,Resources:e,startIndex:1,itemsPerPage:e.length})})),s.get(`${i}/Schemas/:id`,d((s,i)=>{const e=this.discovery.schema(decodeURIComponent(s.params.id),s);if(!e)return u(i,404,"Schema not found");this.scimJson(i,200,e)}))}parseListQuery(s){const i=s.query,toInt=s=>{if("string"!=typeof s)return;const i=parseInt(s,10);return Number.isFinite(i)?i:void 0},toCsv=s=>{if("string"==typeof s)return s.split(",").map(s=>s.trim()).filter(Boolean)},e="string"==typeof i.sortOrder?i.sortOrder:void 0;return{filter:"string"==typeof i.filter?i.filter:void 0,startIndex:toInt(i.startIndex),count:toInt(i.count),attributes:toCsv(i.attributes),excludedAttributes:toCsv(i.excludedAttributes),sortBy:"string"==typeof i.sortBy?i.sortBy:void 0,sortOrder:"ascending"===e||"descending"===e?e:void 0}}getConfigApiData(){const s=this.scimPrefix;let i;if(this.server.loadedPlugins.static){const s=this.config.static_name||"static";i={user:`/${s}/schemas/scim/User.json`,group:`/${s}/schemas/scim/Group.json`,defaultMapping:`/${s}/schemas/scim/default-mapping.json`}}const e=this.users.userMapping,t=this.groups.groupMapping,o=e.entries.map(s=>s.scim).concat(["id","active"]),r=t.entries.map(s=>s.scim).concat(["id"]);return{enabled:!0,version:"2.0",prefix:s,endpoints:{users:`${s}/Users`,user:`${s}/Users/:id`,groups:`${s}/Groups`,group:`${s}/Groups/:id`,bulk:`${s}/Bulk`,serviceProviderConfig:`${s}/ServiceProviderConfig`,resourceTypes:`${s}/ResourceTypes`,schemas:`${s}/Schemas`},schemaUrls:i,capabilities:{patch:!0,bulk:!0,filter:!0,sort:!0,etag:Boolean(this.config.scim_etag),changePassword:!1,maxResults:this.config.scim_max_results||200,bulkMaxOperations:this.config.scim_bulk_max_operations||100,bulkMaxPayloadSize:this.config.scim_bulk_max_payload_size||1048576},resourceTypes:[{id:"User",endpoint:`${s}/Users`,schema:"urn:ietf:params:scim:schemas:core:2.0:User",rdnAttribute:this.config.scim_user_rdn_attribute||"uid",objectClass:this.config.scim_user_object_class,filterableAttributes:o,mapping:e.entries},{id:"Group",endpoint:`${s}/Groups`,schema:"urn:ietf:params:scim:schemas:core:2.0:Group",rdnAttribute:this.config.scim_group_rdn_attribute||"cn",objectClass:this.config.scim_group_object_class,filterableAttributes:r,mapping:t.entries}],idStrategy:this.config.scim_id_attribute||"rdn",baseResolution:{userBaseTemplate:this.config.scim_user_base_template||void 0,groupBaseTemplate:this.config.scim_group_base_template||void 0,hasBaseMap:Boolean(this.config.scim_base_map)}}}}export{Scim as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const s="urn:ietf:params:scim:schemas:core:2.0:User",e="urn:ietf:params:scim:schemas:core:2.0:Group",a="urn:ietf:params:scim:api:messages:2.0:ListResponse",r="urn:ietf:params:scim:api:messages:2.0:Error",m="urn:ietf:params:scim:api:messages:2.0:PatchOp",i="urn:ietf:params:scim:api:messages:2.0:BulkRequest",c="urn:ietf:params:scim:api:messages:2.0:BulkResponse",p="urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig",t="urn:ietf:params:scim:schemas:core:2.0:ResourceType",u="urn:ietf:params:scim:schemas:core:2.0:Schema";export{i as SCHEMA_BULK_REQUEST,c as SCHEMA_BULK_RESPONSE,r as SCHEMA_ERROR,e as SCHEMA_GROUP,a as SCHEMA_LIST_RESPONSE,m as SCHEMA_PATCH_OP,t as SCHEMA_RESOURCE_TYPE,u as SCHEMA_SCHEMA,p as SCHEMA_SERVICE_PROVIDER_CONFIG,s as SCHEMA_USER};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{escapeDnValue as t,launchHooksChained as s,validateDnValue as e,launchHooks as i,isChildOf as r,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as h}from"./types.js";import{mergeMapping as n,loadMappingFile as o,DEFAULT_USER_MAPPING as c,requiredLdapAttributes as l,ldapToScimUser as d,scimUserToLdap as u}from"./mapping.js";import{scimFilterToLdap as p}from"./filter.js";import{patchToModifyRequest as m}from"./patch.js";import{extractLdapCode as g,scimNotFound as f,ScimError as b,scimTooMany as w,scimInvalidValue as y,scimUniqueness as x}from"./errors.js";class ScimUsers{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_user_rdn_attribute||"uid",this.objectClass=this.config.scim_user_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const s=this.config.scim_user_mapping||"";this.mapping=n(c,s?o(s):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"User",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix}}dnForId(s,e){const i=this.baseResolver.userBase(e);return`${this.rdnAttribute}=${t(s)},${i}`}async get(t,s){const e=this.dnForId(s,t);let i;try{i=await this.ldap.search({paged:!1,scope:"base",attributes:l(this.mapping)},e)}catch(t){if(32===g(t))throw f(`User ${s} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw f(`User ${s} not found`);return d(i.searchEntries[0],this.mapping,this.ctx(t))}async list(t,s){const e=this.baseResolver.userBase(t),i=Math.max(1,s.startIndex||1),r=Math.min(this.maxResults,Math.max(0,s.count??this.maxResults));let a,n=`(${this.objectClass.includes("inetOrgPerson")?"objectClass=inetOrgPerson":`objectClass=${this.objectClass[0]}`})`;if(s.filter){const t=p(s.filter,this.mapping);t.idEquals?a=t.idEquals:n=`(&${n}${t.ldapFilter})`}if(a)try{const s=await this.get(t,a);return{schemas:[h],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[s]}}catch(t){if(t instanceof b&&404===t.statusCode)return{schemas:[h],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const o=(await this.ldap.search({filter:n,scope:"sub",paged:!1,attributes:l(this.mapping),sizeLimit:this.maxResults+1},e)).searchEntries||[],c=o.length;if(c>this.maxResults)throw w(`Filter matched ${c} resources, exceeds max ${this.maxResults}`);const u=o.slice(i-1,i-1+r);s.sortBy,s.sortOrder;const m=u.map(s=>d(s,this.mapping,this.ctx(t)));return{schemas:[h],totalResults:c,startIndex:i,itemsPerPage:m.length,Resources:m}}async create(r,a){if(!a.userName)throw y("userName is required");const h=(await s(this.hooks.scimusercreate,[a,r]))[0],{rdn:n,attributes:o}=u(h,this.mapping,this.ctx(r),this.objectClass);if(!n)throw y("userName is required");e(n,this.rdnAttribute),o[this.rdnAttribute]=n;const c=this.baseResolver.userBase(r),l=`${this.rdnAttribute}=${t(n)},${c}`;try{await this.ldap.add(l,o)}catch(t){if(68===g(t))throw x(`User ${n} already exists`);throw t}const d=await this.get(r,n);return i(this.hooks.scimusercreatedone,[d]),d}async replace(t,e,r){const a=this.dnForId(e,t),h=await this.ldap.search({paged:!1,scope:"base",attributes:l(this.mapping)},a);if(!h.searchEntries?.length)throw f(`User ${e} not found`);const n=h.searchEntries[0],o=(await s(this.hooks.scimuserupdate,[e,r,t]))[1],{attributes:c}=u(o,this.mapping,this.ctx(t),this.objectClass),d={replace:{},delete:[]},p=new Set(["objectClass","entryUUID","createTimestamp","modifyTimestamp",this.rdnAttribute]),hasAttrValue=t=>null!=t&&("string"==typeof t?t.length>0:!Array.isArray(t)||t.length>0&&t.some(t=>null!=t));for(const t of l(this.mapping))p.has(t)||(null!=c[t]?d.replace[t]=c[t]:hasAttrValue(n[t])&&d.delete.push(t));if(0===Object.keys(d.replace||{}).length&&delete d.replace,0===d.delete.length&&delete d.delete,d.replace||d.delete)try{await this.ldap.modify(a,d)}catch(t){if(16!==g(t))throw t}const m=await this.get(t,e);return i(this.hooks.scimuserupdatedone,[e,m]),m}async patch(t,s,e){await this.get(t,s);const r=await m(e,{mapping:this.mapping});if(!r.add&&!r.replace&&(!r.delete||Array.isArray(r.delete)&&0===r.delete.length))return this.get(t,s);const a=this.dnForId(s,t);await this.ldap.modify(a,r);const h=await this.get(t,s);return i(this.hooks.scimuserupdatedone,[s,h]),h}async delete(t,e){await this.get(t,e);const r=(await s(this.hooks.scimuserdelete,[e,t]))[0],a=this.dnForId(r,t);await this.ldap.delete(a),i(this.hooks.scimuserdeletedone,[r])}async resolveRef(s,e){if(!e)return;const i=this.baseResolver.userBase(s);if(e.includes("=")&&e.includes(","))return r(e,i)?e:void 0;const h=`${this.rdnAttribute}=${t(e)},${i}`;try{const t=await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},h);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}try{const t=await this.ldap.search({filter:`(${this.rdnAttribute}=${a(e)})`,scope:"sub",paged:!1,attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch{}}get userMapping(){return this.mapping}}export{ScimUsers};
|
|
@@ -58,6 +58,26 @@ export default abstract class LdapFlat extends DmPlugin {
|
|
|
58
58
|
apiDelete(req: Request, res: Response): Promise<void>;
|
|
59
59
|
apiModify(req: Request, res: Response): Promise<void>;
|
|
60
60
|
apiMove(req: Request, res: Response): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Resolve an id (either an RDN value or a full DN) into a DN inside this
|
|
63
|
+
* instance's base branch.
|
|
64
|
+
*
|
|
65
|
+
* A value is treated as a full DN only when it starts with
|
|
66
|
+
* `mainAttribute=`; otherwise it is treated as a raw RDN value and escaped.
|
|
67
|
+
* This matters for cn-based branches where values like `Smith, John` are
|
|
68
|
+
* legal and must not be misclassified as DNs.
|
|
69
|
+
*
|
|
70
|
+
* When the id is a full DN we enforce that its parent equals this.base
|
|
71
|
+
* (LdapFlat entries are, by definition, direct children of the base).
|
|
72
|
+
* The comparison is done on the parsed RDN components so escape-aware
|
|
73
|
+
* payloads like `cn=pwn\,ou=titles,ou=nomenclature,dc=example,dc=com`
|
|
74
|
+
* cannot sneak past a naive textual suffix check: `\,` is a literal comma
|
|
75
|
+
* inside the first RDN's value, so the actual parent of that DN is
|
|
76
|
+
* `ou=nomenclature,dc=example,dc=com` — one level above the expected base.
|
|
77
|
+
*
|
|
78
|
+
* @throws BadRequestError if the provided DN is not a direct child of this.base
|
|
79
|
+
*/
|
|
80
|
+
protected resolveDn(id: string): string;
|
|
61
81
|
addEntry(id: string, additional?: AttributesList, req?: Request): Promise<boolean>;
|
|
62
82
|
modifyEntry(id: string, changes: ModifyRequest): Promise<boolean>;
|
|
63
83
|
renameEntry(id: string, newId: string): Promise<boolean>;
|
|
@@ -78,6 +78,12 @@ export interface Config {
|
|
|
78
78
|
base_url?: string;
|
|
79
79
|
authz_per_branch_config?: AuthConfig;
|
|
80
80
|
authz_per_branch_cache_ttl?: number;
|
|
81
|
+
authz_dynamic_base?: string;
|
|
82
|
+
authz_dynamic_cache_ttl?: number;
|
|
83
|
+
authz_dynamic_token_attribute?: string;
|
|
84
|
+
authz_dynamic_config_attribute?: string;
|
|
85
|
+
authz_dynamic_tenant_attribute?: string;
|
|
86
|
+
authz_dynamic_reload_endpoint?: boolean;
|
|
81
87
|
authz_local_admin_attribute?: string;
|
|
82
88
|
rate_limit_window_ms?: number;
|
|
83
89
|
rate_limit_max?: number;
|
|
@@ -129,6 +135,24 @@ export interface Config {
|
|
|
129
135
|
ppolicy_require_digit?: boolean;
|
|
130
136
|
ppolicy_require_special?: boolean;
|
|
131
137
|
ldap_users_base?: string;
|
|
138
|
+
scim_prefix?: string;
|
|
139
|
+
scim_user_base?: string;
|
|
140
|
+
scim_group_base?: string;
|
|
141
|
+
scim_user_base_template?: string;
|
|
142
|
+
scim_group_base_template?: string;
|
|
143
|
+
scim_base_map?: string;
|
|
144
|
+
scim_user_object_class?: string[];
|
|
145
|
+
scim_user_rdn_attribute?: string;
|
|
146
|
+
scim_group_object_class?: string[];
|
|
147
|
+
scim_group_rdn_attribute?: string;
|
|
148
|
+
scim_id_attribute?: string;
|
|
149
|
+
scim_user_mapping?: string;
|
|
150
|
+
scim_group_mapping?: string;
|
|
151
|
+
scim_max_results?: number;
|
|
152
|
+
scim_bulk_max_operations?: number;
|
|
153
|
+
scim_bulk_max_payload_size?: number;
|
|
154
|
+
scim_etag?: boolean;
|
|
155
|
+
scim_base_url?: string;
|
|
132
156
|
[key: string]: string | string[] | boolean | number | AttributesList | AuthConfig | undefined;
|
|
133
157
|
}
|
|
134
158
|
/**
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module plugins/auth/authzDynamic
|
|
3
|
+
* @author Xavier Guimard <xguimard@linagora.com>
|
|
4
|
+
*
|
|
5
|
+
* LDAP-backed authentication + per-branch authorization plugin.
|
|
6
|
+
*
|
|
7
|
+
* Tokens are stored as entries under a dedicated LDAP branch (one entry per
|
|
8
|
+
* bearer token). The plugin caches them and enforces read / write / delete
|
|
9
|
+
* permissions on subsequent LDAP operations — so each client is scoped to
|
|
10
|
+
* its own subtree without touching the process configuration.
|
|
11
|
+
*
|
|
12
|
+
* Entry shape (conventions, overridable via CLI):
|
|
13
|
+
* dn: cn=<token-name>,<authz-dynamic-base>
|
|
14
|
+
* cn: <token-name>
|
|
15
|
+
* userPassword: {SSHA}… ← secret, hashed
|
|
16
|
+
* description: {"tenant":"acme", ← JSON config
|
|
17
|
+
* "bases":[
|
|
18
|
+
* {"dn":"ou=users,ou=acme,dc=example,dc=com",
|
|
19
|
+
* "read":true,"write":true,"delete":true}
|
|
20
|
+
* ]}
|
|
21
|
+
*
|
|
22
|
+
* The entry is treated as "token for tenant `acme`, permitted to read/write/
|
|
23
|
+
* delete under `ou=users,ou=acme,…`". Multiple bases per token are allowed.
|
|
24
|
+
* Sub-branch matching is used (a permission on `ou=acme` covers
|
|
25
|
+
* `ou=users,ou=acme`).
|
|
26
|
+
*/
|
|
27
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
28
|
+
import type { Express, Response } from 'express';
|
|
29
|
+
import { type Role } from '../../abstract/plugin';
|
|
30
|
+
import type { DM } from '../../bin';
|
|
31
|
+
import AuthBase, { type DmRequest } from '../../lib/auth/base';
|
|
32
|
+
import type { Hooks } from '../../hooks';
|
|
33
|
+
export interface BranchAcl {
|
|
34
|
+
dn: string;
|
|
35
|
+
read?: boolean;
|
|
36
|
+
write?: boolean;
|
|
37
|
+
delete?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface TokenConfig {
|
|
40
|
+
tenant?: string;
|
|
41
|
+
bases?: BranchAcl[];
|
|
42
|
+
}
|
|
43
|
+
export interface TokenEntry {
|
|
44
|
+
dn: string;
|
|
45
|
+
cn: string;
|
|
46
|
+
tenant: string;
|
|
47
|
+
hash: string;
|
|
48
|
+
bases: BranchAcl[];
|
|
49
|
+
}
|
|
50
|
+
/** Extends DmRequest with the resolved token entry for downstream hooks. */
|
|
51
|
+
export interface AuthzDynamicRequest extends DmRequest {
|
|
52
|
+
authzToken?: TokenEntry;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Per-request async context holding the authenticated token.
|
|
56
|
+
*
|
|
57
|
+
* Core plugins (ldapGroups, ldapFlat, etc.) do not always forward the Express
|
|
58
|
+
* `req` object down to `ldapActions.search()` / `.modify()`. Relying solely
|
|
59
|
+
* on `req` would therefore let un-tracked operations bypass authorization.
|
|
60
|
+
* Using AsyncLocalStorage gives us a request-scoped, ambient handle that the
|
|
61
|
+
* authz hooks can read regardless of whether the caller threaded `req`.
|
|
62
|
+
*/
|
|
63
|
+
export declare const authzContext: AsyncLocalStorage<TokenEntry>;
|
|
64
|
+
export default class AuthzDynamic extends AuthBase {
|
|
65
|
+
name: string;
|
|
66
|
+
roles: Role[];
|
|
67
|
+
private tokens;
|
|
68
|
+
private lastLoad;
|
|
69
|
+
private lastFailure;
|
|
70
|
+
private readonly cacheTtlMs;
|
|
71
|
+
private readonly failureBackoffMs;
|
|
72
|
+
private readonly base;
|
|
73
|
+
private readonly tokenAttr;
|
|
74
|
+
private readonly configAttr;
|
|
75
|
+
private readonly tenantAttr;
|
|
76
|
+
private readonly reloadEndpoint;
|
|
77
|
+
private loading?;
|
|
78
|
+
constructor(server: DM);
|
|
79
|
+
api(app: Express): void;
|
|
80
|
+
/**
|
|
81
|
+
* Load token entries from LDAP. Swallows errors so a transient LDAP hiccup
|
|
82
|
+
* does not poison the cache — the previous snapshot stays in use.
|
|
83
|
+
*/
|
|
84
|
+
reload(): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Ensure the cache is fresh (blocks if reloading).
|
|
87
|
+
*
|
|
88
|
+
* On a bootstrap or successful reload the cache is valid for
|
|
89
|
+
* `cacheTtlMs`. If the load fails, we still refuse to retry on every
|
|
90
|
+
* request — `failureBackoffMs` (default 1/4 of TTL, min 5 s) keeps the
|
|
91
|
+
* previous snapshot in use and shields LDAP from a thundering-herd
|
|
92
|
+
* during an outage.
|
|
93
|
+
*/
|
|
94
|
+
private ensureFresh;
|
|
95
|
+
authMethod(req: DmRequest, res: Response, next: () => void): void;
|
|
96
|
+
/**
|
|
97
|
+
* Authorization hooks. These run after the auth middleware has populated
|
|
98
|
+
* `req.authzToken`, so we can read the allowed bases directly from the
|
|
99
|
+
* request.
|
|
100
|
+
*/
|
|
101
|
+
hooks: Hooks;
|
|
102
|
+
/** Read the active token from the async context, or (as a fallback) from `req`. */
|
|
103
|
+
private activeToken;
|
|
104
|
+
/**
|
|
105
|
+
* Throw a ForbiddenError (403) if the token cannot perform `permission` on
|
|
106
|
+
* the branch containing `branchOrDn`. Any ACL entry that is `branchOrDn`
|
|
107
|
+
* itself or a parent of it (sub-branch match) qualifies.
|
|
108
|
+
*
|
|
109
|
+
* The client-facing message omits the target DN to avoid leaking tenant
|
|
110
|
+
* topology through authorization failures; the full context is still
|
|
111
|
+
* logged server-side at debug level for operators.
|
|
112
|
+
*/
|
|
113
|
+
private requireBranchPermission;
|
|
114
|
+
/** Expose current cache (primarily for the configApi auto-discovery). */
|
|
115
|
+
getConfigApiData(): Record<string, unknown>;
|
|
116
|
+
_tokens(): TokenEntry[];
|
|
117
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify a provided bearer token against a stored LDAP-style hash.
|
|
3
|
+
*
|
|
4
|
+
* The stored hash is typically the contents of `userPassword`:
|
|
5
|
+
* either `{SCHEME}<data>` or cleartext.
|
|
6
|
+
*/
|
|
7
|
+
export declare function verifyLdapPassword(provided: string, stored: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Convenience helpers for tests / tooling to generate an LDAP-style hash.
|
|
10
|
+
*
|
|
11
|
+
* These are NOT used by the runtime verification path and NOT intended for
|
|
12
|
+
* use as a general password-hashing function: the primary use case is
|
|
13
|
+
* seeding LDAP token fixtures in tests. Production `userPassword` values
|
|
14
|
+
* should be generated by `slappasswd` (or another operator tool) with a
|
|
15
|
+
* scheme of your choice. The RFC 3112 `{SSHA}` scheme uses a single SHA-1
|
|
16
|
+
* round — adequate for LDAP bearer-token interop, not adequate for
|
|
17
|
+
* user-chosen passwords. Prefer `sshaHash('sha512', …)` (`{SSHA512}`) when
|
|
18
|
+
* generating new fixtures.
|
|
19
|
+
*/
|
|
20
|
+
export type SshaAlgo = 'sha1' | 'sha256' | 'sha512';
|
|
21
|
+
export declare function sshaHash(algo: SshaAlgo, password: string, salt?: Buffer): string;
|
|
22
|
+
/**
|
|
23
|
+
* Legacy alias: generate an {SSHA} (salted SHA-1) hash.
|
|
24
|
+
* Retained so existing tests keep working. Prefer `sshaHash('sha512', ...)`
|
|
25
|
+
* for new fixtures.
|
|
26
|
+
*/
|
|
27
|
+
export declare function ssha(password: string, salt?: Buffer): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Config } from '../../config/args';
|
|
2
|
+
import type { DmRequest } from '../../lib/auth/base';
|
|
3
|
+
export interface BaseMapEntry {
|
|
4
|
+
userBase?: string;
|
|
5
|
+
groupBase?: string;
|
|
6
|
+
}
|
|
7
|
+
export type BaseMap = Record<string, BaseMapEntry>;
|
|
8
|
+
export declare class BaseResolver {
|
|
9
|
+
private readonly defaultUserBase;
|
|
10
|
+
private readonly defaultGroupBase;
|
|
11
|
+
private readonly userTemplate;
|
|
12
|
+
private readonly groupTemplate;
|
|
13
|
+
private readonly map;
|
|
14
|
+
constructor(config: Config);
|
|
15
|
+
private applyTemplate;
|
|
16
|
+
private resolve;
|
|
17
|
+
userBase(req?: DmRequest | {
|
|
18
|
+
user?: string;
|
|
19
|
+
}): string;
|
|
20
|
+
groupBase(req?: DmRequest | {
|
|
21
|
+
user?: string;
|
|
22
|
+
}): string;
|
|
23
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module plugins/scim/bulk
|
|
3
|
+
* @author Xavier Guimard <xguimard@linagora.com>
|
|
4
|
+
*
|
|
5
|
+
* SCIM 2.0 Bulk operations (RFC 7644 §3.7).
|
|
6
|
+
*
|
|
7
|
+
* Executes operations sequentially. Supports `bulkId` cross-references:
|
|
8
|
+
* a POST with `"bulkId":"abc"` creates a resource; subsequent operations
|
|
9
|
+
* in the same request can reference it as "bulkId:abc" anywhere a SCIM
|
|
10
|
+
* id or member.value is expected (resolved to the concrete id before
|
|
11
|
+
* dispatch).
|
|
12
|
+
*/
|
|
13
|
+
import type winston from 'winston';
|
|
14
|
+
import type { Config } from '../../config/args';
|
|
15
|
+
import type { DmRequest } from '../../lib/auth/base';
|
|
16
|
+
import type { ScimUsers } from './users';
|
|
17
|
+
import type { ScimGroups } from './groups';
|
|
18
|
+
import { type BulkRequest, type BulkResponse } from './types';
|
|
19
|
+
import { writeScimErrorFromException } from './errors';
|
|
20
|
+
export interface ScimBulkOptions {
|
|
21
|
+
config: Config;
|
|
22
|
+
logger: winston.Logger;
|
|
23
|
+
users: ScimUsers;
|
|
24
|
+
groups: ScimGroups;
|
|
25
|
+
scimPrefix: string;
|
|
26
|
+
hooks: {
|
|
27
|
+
[K: string]: Function[] | undefined;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export declare class ScimBulk {
|
|
31
|
+
private readonly config;
|
|
32
|
+
private readonly logger;
|
|
33
|
+
private readonly users;
|
|
34
|
+
private readonly groups;
|
|
35
|
+
private readonly scimPrefix;
|
|
36
|
+
private readonly hooks;
|
|
37
|
+
constructor(opts: ScimBulkOptions);
|
|
38
|
+
execute(req: DmRequest, body: BulkRequest): Promise<BulkResponse>;
|
|
39
|
+
private executeOne;
|
|
40
|
+
private resolveRefs;
|
|
41
|
+
private buildLocation;
|
|
42
|
+
}
|
|
43
|
+
export { writeScimErrorFromException };
|