ldap-rest 2.0.2 → 2.0.4
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/lib/authz/base.js +1 -1
- package/dist/lib/entitySchema/validator.js +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/auth/auth.options.js +1 -1
- package/dist/plugins/auth/authzDynamic.js +1 -1
- package/dist/plugins/auth/openidconnect.js +1 -1
- package/dist/plugins/auth/trustedProxy.js +1 -1
- package/dist/plugins/ldap/departmentSync.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/scim/baseResolver.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +6 -6
- package/dist/src/abstract/plugin.d.ts +11 -11
- package/dist/src/abstract/twakePlugin.d.ts +4 -4
- package/dist/src/bin/index.d.ts +10 -10
- package/dist/src/browser/ldap-browser/LdapBrowser.d.ts +1 -1
- package/dist/src/browser/ldap-browser/api/RawApiClient.d.ts +1 -1
- package/dist/src/browser/ldap-browser/components/EntryTree.d.ts +2 -2
- package/dist/src/browser/ldap-browser/components/EntryView.d.ts +2 -2
- package/dist/src/browser/ldap-browser/index.d.ts +4 -5
- package/dist/src/browser/ldap-browser/schema.d.ts +1 -1
- package/dist/src/browser/ldap-group-editor/LdapGroupEditor.d.ts +2 -2
- package/dist/src/browser/ldap-group-editor/api/GroupApiClient.d.ts +1 -1
- package/dist/src/browser/ldap-group-editor/components/CreateGroupModal.d.ts +2 -2
- package/dist/src/browser/ldap-group-editor/components/GroupPropertyEditor.d.ts +1 -1
- package/dist/src/browser/ldap-group-editor/components/MoveGroupModal.d.ts +1 -1
- package/dist/src/browser/ldap-group-editor/types.d.ts +1 -1
- package/dist/src/browser/ldap-resource-editor/api/ResourceApiClient.d.ts +2 -2
- package/dist/src/browser/ldap-resource-editor/index.d.ts +2 -2
- package/dist/src/browser/ldap-resource-editor/types.d.ts +1 -1
- package/dist/src/browser/ldap-tree-viewer/LdapTreeViewer.d.ts +1 -1
- package/dist/src/browser/ldap-tree-viewer/components/TreeNode.d.ts +2 -2
- package/dist/src/browser/ldap-tree-viewer/components/TreeRoot.d.ts +2 -2
- package/dist/src/browser/ldap-tree-viewer/index.d.ts +2 -3
- package/dist/src/browser/ldap-tree-viewer/store/actions.d.ts +2 -2
- package/dist/src/browser/ldap-tree-viewer/store/reducers.d.ts +2 -2
- package/dist/src/browser/ldap-unit-editor/LdapUnitEditor.d.ts +1 -1
- package/dist/src/browser/ldap-unit-editor/api/UnitApiClient.d.ts +1 -1
- package/dist/src/browser/ldap-unit-editor/components/MoveUnitModal.d.ts +1 -1
- package/dist/src/browser/ldap-unit-editor/components/UnitPropertyEditor.d.ts +1 -1
- package/dist/src/browser/ldap-unit-editor/types.d.ts +1 -1
- package/dist/src/browser/ldap-user-editor/LdapUserEditor.d.ts +2 -2
- package/dist/src/browser/ldap-user-editor/api/UserApiClient.d.ts +2 -2
- package/dist/src/browser/ldap-user-editor/components/MoveUserModal.d.ts +1 -1
- package/dist/src/browser/ldap-user-editor/components/PointerField.d.ts +2 -2
- package/dist/src/browser/ldap-user-editor/components/UserEditor.d.ts +1 -1
- package/dist/src/browser/ldap-user-editor/components/UserList.d.ts +1 -1
- package/dist/src/browser/ldap-user-editor/components/UserTree.d.ts +1 -1
- package/dist/src/browser/ldap-user-editor/index.d.ts +2 -3
- package/dist/src/browser/ldap-user-editor/types.d.ts +1 -1
- package/dist/src/browser/shared/components/Modal.d.ts +1 -1
- package/dist/src/browser/shared/index.d.ts +7 -7
- package/dist/src/browser/shared/utils/form.d.ts +1 -1
- package/dist/src/browser/shared/utils/schema.d.ts +1 -1
- package/dist/src/config/args.d.ts +3 -3
- package/dist/src/config/options.d.ts +2 -2
- package/dist/src/config/schema.d.ts +1 -1
- package/dist/src/hooks.d.ts +5 -5
- package/dist/src/lib/auth/base.d.ts +2 -2
- package/dist/src/lib/authz/base.d.ts +15 -20
- package/dist/src/lib/authz/composition.d.ts +4 -4
- package/dist/src/lib/entitySchema/validator.d.ts +23 -2
- package/dist/src/lib/expressFormatedResponses.d.ts +1 -1
- package/dist/src/lib/health.d.ts +1 -1
- package/dist/src/lib/hookBus.d.ts +1 -1
- package/dist/src/lib/ldap/schema.d.ts +2 -2
- package/dist/src/lib/ldapActions.d.ts +5 -5
- package/dist/src/lib/mailDomains.d.ts +1 -1
- package/dist/src/lib/parseConfig.d.ts +1 -1
- package/dist/src/lib/schemaFile.d.ts +1 -1
- package/dist/src/lib/staticFiles.d.ts +1 -1
- package/dist/src/lib/storage/store.d.ts +1 -1
- package/dist/src/lib/utils.d.ts +3 -3
- package/dist/src/logger/winston.d.ts +1 -1
- package/dist/src/plugins/auth/auth.options.d.ts +39 -38
- package/dist/src/plugins/auth/authzDynamic.d.ts +24 -11
- package/dist/src/plugins/auth/authzLinid1.d.ts +7 -7
- package/dist/src/plugins/auth/authzPerBranch.d.ts +8 -8
- package/dist/src/plugins/auth/authzPerRoute.d.ts +2 -2
- package/dist/src/plugins/auth/authzScope.d.ts +1 -1
- package/dist/src/plugins/auth/crowdsec.d.ts +4 -4
- package/dist/src/plugins/auth/fake.d.ts +3 -3
- package/dist/src/plugins/auth/hmac.d.ts +4 -4
- package/dist/src/plugins/auth/llng.d.ts +20 -5
- package/dist/src/plugins/auth/openidconnect.d.ts +29 -8
- package/dist/src/plugins/auth/rateLimit.d.ts +3 -3
- package/dist/src/plugins/auth/token.d.ts +3 -3
- package/dist/src/plugins/auth/totp.d.ts +5 -5
- package/dist/src/plugins/auth/trustedProxy.d.ts +16 -32
- package/dist/src/plugins/bcl.d.ts +4 -4
- package/dist/src/plugins/bcl.options.d.ts +3 -3
- package/dist/src/plugins/configApi.d.ts +1 -1
- package/dist/src/plugins/demo/helloworld.d.ts +1 -1
- package/dist/src/plugins/ldap/accountLifecycle.d.ts +1 -1
- package/dist/src/plugins/ldap/bulkImport.d.ts +5 -5
- package/dist/src/plugins/ldap/departmentSync.d.ts +14 -3
- package/dist/src/plugins/ldap/enterpriseRules.d.ts +7 -7
- package/dist/src/plugins/ldap/externalUsersInGroups.d.ts +6 -6
- package/dist/src/plugins/ldap/flatGeneric.d.ts +6 -6
- package/dist/src/plugins/ldap/groups.d.ts +8 -8
- package/dist/src/plugins/ldap/ldap.options.d.ts +45 -45
- package/dist/src/plugins/ldap/onChange.d.ts +4 -4
- package/dist/src/plugins/ldap/organization.d.ts +1 -1
- package/dist/src/plugins/ldap/organizations.d.ts +12 -12
- package/dist/src/plugins/ldap/passwordPolicy.d.ts +11 -11
- package/dist/src/plugins/ldap/raw.d.ts +8 -8
- package/dist/src/plugins/ldap/trash.d.ts +6 -6
- package/dist/src/plugins/metrics.d.ts +3 -3
- package/dist/src/plugins/metrics.options.d.ts +3 -3
- package/dist/src/plugins/rabbitmq.d.ts +4 -4
- package/dist/src/plugins/rabbitmq.options.d.ts +3 -3
- package/dist/src/plugins/scim/baseResolver.d.ts +3 -3
- package/dist/src/plugins/scim/bulk.d.ts +7 -7
- package/dist/src/plugins/scim/discovery.d.ts +4 -4
- package/dist/src/plugins/scim/errors.d.ts +3 -3
- package/dist/src/plugins/scim/filter.d.ts +1 -1
- package/dist/src/plugins/scim/groups.d.ts +8 -8
- package/dist/src/plugins/scim/list.d.ts +1 -1
- package/dist/src/plugins/scim/mapping.d.ts +2 -2
- package/dist/src/plugins/scim/patch.d.ts +2 -2
- package/dist/src/plugins/scim/scim.d.ts +30 -30
- package/dist/src/plugins/scim/scim.options.d.ts +27 -27
- package/dist/src/plugins/scim/users.d.ts +6 -6
- package/dist/src/plugins/static.d.ts +6 -6
- package/dist/src/plugins/static.options.d.ts +6 -6
- package/dist/src/plugins/storage/file.d.ts +2 -2
- package/dist/src/plugins/storage/ldap.d.ts +2 -2
- package/dist/src/plugins/storage/postgres.d.ts +2 -2
- package/dist/src/plugins/storage/valkey.d.ts +2 -2
- package/dist/src/plugins/storage.d.ts +13 -13
- package/dist/src/plugins/storage.options.d.ts +11 -11
- package/dist/src/plugins/twake/appAccounts.options.d.ts +6 -6
- package/dist/src/plugins/twake/appAccountsApi.d.ts +7 -7
- package/dist/src/plugins/twake/appAccountsConsistency.d.ts +9 -9
- package/dist/src/plugins/twake/calendar.d.ts +14 -14
- package/dist/src/plugins/twake/calendar.options.d.ts +11 -11
- package/dist/src/plugins/twake/calendarResources.d.ts +1 -1
- package/dist/src/plugins/twake/cloudery.options.d.ts +18 -18
- package/dist/src/plugins/twake/clouderyProvision.d.ts +19 -19
- package/dist/src/plugins/twake/cozy.options.d.ts +14 -14
- package/dist/src/plugins/twake/cozyProvision.d.ts +15 -15
- package/dist/src/plugins/twake/drive.d.ts +9 -9
- package/dist/src/plugins/twake/drive.options.d.ts +7 -7
- package/dist/src/plugins/twake/groups.d.ts +16 -16
- package/dist/src/plugins/twake/instances.d.ts +38 -38
- package/dist/src/plugins/twake/james.d.ts +18 -18
- package/dist/src/plugins/twake/james.options.d.ts +13 -13
- package/dist/src/plugins/twake/lifecycleAttributes.d.ts +2 -2
- package/dist/src/plugins/twake/lifecycleEvents.d.ts +13 -13
- package/dist/src/plugins/twake/migrationProxy.d.ts +11 -11
- package/dist/src/plugins/twake/migrationProxy.options.d.ts +8 -8
- package/dist/src/plugins/twake/spaces.d.ts +12 -12
- package/dist/src/plugins/twake/tombstone.d.ts +10 -10
- package/dist/src/plugins/twake/twakeGroups.options.d.ts +12 -12
- package/dist/src/plugins/twake/twakeInstances.options.d.ts +35 -35
- package/dist/src/plugins/twake/twakeLifecycle.options.d.ts +12 -12
- package/dist/src/plugins/twake/twakeSpaces.options.d.ts +11 -11
- package/dist/src/plugins/twake/twakeTombstone.options.d.ts +9 -9
- package/dist/src/plugins/weblogs.d.ts +1 -1
- package/docs/client-development/schemas/README.md +6 -1
- package/docs/development/contracts.md +22 -0
- package/docs/presentations/en.md +1 -1
- package/docs/presentations/fr.md +1 -1
- package/docs/usage/configuration.md +1 -0
- package/docs/usage/plugins/auth/authz-dynamic.md +3 -1
- package/docs/usage/plugins/auth/authz-linid1.md +10 -5
- package/docs/usage/plugins/auth/oidc.md +11 -0
- package/docs/usage/plugins/auth/trusted-proxy.md +11 -1
- package/docs/usage/upgrading.md +61 -0
- package/package.json +4 -3
- package/static/browser/ldap-user-editor.esm.js.map +1 -1
- package/static/browser/ldap-user-editor.js.map +1 -1
- package/static/browser/ldap-user-editor.min.js.map +1 -1
- package/static/browser/shared/utils/schema.js.map +1 -1
- package/static/browser/shared.esm.js.map +1 -1
- package/static/browser/shared.js.map +1 -1
- package/static/browser/shared.min.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Type as t}from"@sinclair/typebox";import{assertModifyRequest as e}from"../lib/ldapActions.js";import{jsonBody as i,created as a,tryMethod as s,multiStatus as r,WriteResponse as n}from"../lib/expressFormatedResponses.js";import{getCompiledRegex as o,escapeDnValue as h,escapeRegex as d,validateDnValue as c,isDummyMemberDn as l,normalizeDn as u,namesByDn as m,getParentDn as p,isDirectChildOf as b,assertRfc4514Escapes as f,assertedRdnValue as w,escapeLdapFilter as g,substringSearchFilter as y}from"../lib/utils.js";import{neverReturnAttributes as A,assertClientMaySet as $,modifiedAttributeNames as v,toDirectoryEntry as E}from"../config/schema.js";import{applyFixedDefaults as j,EntityValidator as N}from"../lib/entitySchema/validator.js";import{ConflictError as x,BadRequestError as D,NotFoundError as O,HttpError as k,isAuthzRefusal as C,UnprocessableEntityError as S}from"../lib/errors.js";import{extractLdapCode as P}from"../lib/ldapCodes.js";import{loadSchemaFile as L}from"../lib/schemaFile.js";import _ from"./plugin.js";import{defineRoute as q}from"../lib/route.js";const z=t.Object({match:t.Optional(t.String()),attribute:t.Optional(t.String()),attributes:t.Optional(t.String()),limit:t.Optional(t.Integer({minimum:1,maximum:Number.MAX_SAFE_INTEGER}))}),F=t.Object({id:t.String({minLength:1})}),I=t.Object({targetOrgDn:t.String({minLength:1})}),M=t.Object({success:t.Boolean(),departmentPath:t.String(),departmentLink:t.String()}),H=t.Object({newId:t.String({minLength:1})});class LdapFlat extends _{base;ldap;mainAttribute;objectClass;defaultAttributes;schema;singularName;pluralName;hookPrefix;ownHooks;constructor(t,e){if(super(t),this.ldap=t.ldap,this.base=e.base,this.mainAttribute=e.mainAttribute,this.objectClass=e.objectClass,this.defaultAttributes=e.defaultAttributes||{},this.singularName=e.singularName,this.pluralName=e.pluralName,this.hookPrefix=e.hookPrefix,this.ownHooks=this.hookBus.prefixed(this.hookPrefix),!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(this.ldap.declareObjectClasses(this.base,this.objectClass),e.schemaPath)try{this.schema=L(e.schemaPath,{config:this.config}),this.ldap.knowAttributeNames(Object.keys(this.schema.attributes)),this.logger.info(`${this.singularName} schema loaded from ${e.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${e.schemaPath}: ${t}`)}}hiddenAttributes(){return A([this.schema])}entryAttributes(){return["*",...Object.keys(this.schema?.attributes??{})]}project(t){const e=this.hiddenAttributes();if(0===e.size)return t;const i={};for(const[a,s]of Object.entries(t))e.has(a.split(";")[0].toLowerCase())||(i[a]=s);return i}projectList(t){if(0===this.hiddenAttributes().size)return t;const e={};for(const[i,a]of Object.entries(t))e[i]=this.project(a);return e}rejectForbiddenInput(t){$(this.schema,t,this.mainAttribute)}async generateMainAttribute(t){const e=this.deriveMainAttribute(t),i=this.schema?.attributes[this.mainAttribute]?.generatedFrom;if(void 0!==e&&i){if(!await this.rdnExists(e))return e;if("suffix"!==i.onCollision)throw new x(`${this.singularName} "${e}" already exists (generated from ${i.attribute})`);for(let t=2;t<1e3;t++){const i=`${e}-${t}`;if(!await this.rdnExists(i))return i}throw new x(`Cannot generate a free "${this.mainAttribute}" from "${i.attribute}"`)}}deriveMainAttribute(t){const e=this.schema?.attributes[this.mainAttribute],i=e?.generatedFrom;if(!i||!e.generated)return;const a=t[i.attribute],s=Array.isArray(a)?a[0]:a;if(null==s||""===s)throw new D(`Attribute "${i.attribute}" is required to generate "${this.mainAttribute}"`);let r=String(s);if(i.extract){const t=o(i.extract).exec(r);t&&(r=void 0!==t[1]?t[1]:t[0])}if(i.lowercase&&(r=r.toLowerCase()),i.strip&&(r=r.replace(o(i.strip,"g"),"")),!r)throw new D(`Cannot generate "${this.mainAttribute}" from "${i.attribute}"`);return r}candidateEntry(t,e){const i={...this.defaultAttributes,[this.mainAttribute]:t,...e,objectClass:this.objectClass};return j(this.schema,i),i}async rdnExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base"},`${this.mainAttribute}=${h(t)},${this.base}`);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}validator(){return new N(this.schema,{entryExists:async t=>{try{return(await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}},loadedPlugins:this.server.loadedPlugins,singularName:this.singularName})}api(t){q(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,query:z,handler:async({req:t,res:e,query:i})=>{const a={};i.match&&i.attribute&&(a.filter=y(i.match,i.attribute)),i.attributes&&(a.attributes=i.attributes.split(","));const s=i.limit;let r;try{r=await this.listEntriesLimited({...a,limit:s,req:t})}catch(t){if(void 0!==s||4!==P(t))throw t;throw new S("Too many entries to list at once: the directory's size limit was exceeded. Narrow the search, or pass `limit`.")}r.truncated&&e.set("X-Result-Truncated","true"),e.json(this.projectList(r.entries))}}),q(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,params:F,handler:async({req:t,res:e})=>this.apiGet(t,e)}),q(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,handler:async({req:t,res:e})=>this.apiAdd(t,e)}),q(t,{method:"delete",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,params:F,responses:{200:n},handler:async({req:t,res:e})=>this.apiDelete(t,e)}),q(t,{method:"put",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,params:F,responses:{200:n},handler:async({req:t,res:e})=>this.apiModify(t,e)}),q(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,params:F,body:I,responses:{200:M},handler:async({req:t,res:e})=>this.apiMove(t,e)}),q(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/rename`,params:F,body:H,handler:async({req:t,res:e})=>this.apiRename(t,e)})}async apiGet(t,e){const i=t.params.id;try{const a=this.resolveDn(i),s=await this.ldap.search({paged:!1,scope:"base",attributes:this.entryAttributes()},a,t);if(0===s.searchEntries.length)throw new O(`${this.singularName} not found`);e.json(this.project(s.searchEntries[0]))}catch(t){if(t.code&&32===t.code)throw new O(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const s=this.schema?.attributes[this.mainAttribute],r=Boolean(s?.generated&&s.generatedFrom),n=r?i(t,e):i(t,e,this.mainAttribute);if(!n)return;this.rejectForbiddenInput(Object.keys(n));const o={...n};delete o[this.mainAttribute],delete o.dn;const h=r?this.deriveMainAttribute(n):void 0;void 0!==h&&await this.ldap.authorize("ldapaddrequest",[this.resolveDn(h),this.candidateEntry(h,o),t]);const d=r?await this.generateMainAttribute(n):n[this.mainAttribute];await this.addEntry(d,o,t);const c=await this.ldap.system.search({paged:!1,scope:"base",attributes:[this.mainAttribute]},this.creationOf(d).dn);return a(e,this.project(c.searchEntries[0]))}async apiDelete(t,e){const i=t.params.id;await s(e,this.deleteEntry.bind(this),i,t)}async apiModify(t,a){const r=i(t,a);if(!r)return;e(r),this.rejectForbiddenInput(v(r));const n=t.params.id;await s(a,this.modifyEntry.bind(this),n,r,t)}async apiMove(t,e){const i=t.body,a=t.params.id,{targetOrgDn:s}=i,r=await this.moveEntry(a,s,t);e.json({success:!0,...r})}async apiRename(t,e){const i=t.body,a=i.newId;if(this.rejectForbiddenInput(Object.keys(i).filter(t=>"newId"!==t)),new RegExp(`^${d(this.mainAttribute)}=`,"i").test(a))throw new D(`newId must be a ${this.mainAttribute} value, not a DN`);try{c(a,this.mainAttribute)}catch(t){throw new D(t instanceof Error?t.message:"Invalid newId")}await this.validator().validateValue(this.mainAttribute,a);const s=t.params.id,n=this.resolveDn(s),o=this.resolveDn(a),h=this.config.group_dummy_user;if(h&&(l(n,h)||l(o,h)))throw new x(`${h} is the configured placeholder member and cannot be renamed`);await this.ldap.authorize("ldaprenamerequest",[n,o,t]);const m=u(n)===u(o),p=await this.entryExists(n),b=m?p:await this.entryExists(o),f=this.requirePlugin("ldapEnterpriseRules");let w=[];if(m){if(!p)throw new O(`${this.singularName} not found`)}else if(p){if(b)throw new x(`${this.singularName} "${a}" already exists`);w=await(f?.findReferences(n))||[],await this.renameEntry(n,o,t)}else{if(!b)throw new O(`${this.singularName} not found`);w=await(f?.findReferences(n))||[]}const g=f&&w.length>0?await f.rewriteReferences(w,n,o,t):{updated:0,failed:[]},y={success:0===g.failed.length,dn:o,referencesUpdated:g.updated};0!==g.failed.length?r(e,{...y,referencesFailed:g.failed}):e.json(y)}async entryExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}resolveDn(t){if(m(t,this.mainAttribute)){const e=p(t),i=e!==t;if(!b(t,this.base))throw new D(`DN must be a direct child of "${this.base}". Provided DN "${t}" has parent "${i?e:"<none>"}"`);return t}return`${this.mainAttribute}=${h(t)},${this.base}`}creationOf(t){if(!m(t,this.mainAttribute))return{name:t,dn:this.resolveDn(t)};f(t,`${this.mainAttribute} DN`),this.resolveDn(t);const e=w(t,this.mainAttribute,this.base);if(null===e)throw new D(`${this.mainAttribute} must be a value, or a "${this.mainAttribute}=…,${this.base}" DN`);return{name:e,dn:`${this.mainAttribute}=${h(e)},${this.base}`}}async addEntry(t,e={},i){if("string"!=typeof t)throw new D(`${this.mainAttribute} must be a string`);await this.ldap.canonicalAttributes(e);const a=this.creationOf(t);t=a.name;let s=a.dn;try{c(t,this.mainAttribute)}catch(t){throw new D(t instanceof Error?t.message:"Invalid identifier")}let r=this.candidateEntry(t,e);await this.ldap.authorize("ldapaddrequest",[s,{...r},i]);const n=await this.validateNewEntry(s,{[this.mainAttribute]:t,...e,objectClass:this.objectClass});r={...this.candidateEntry(t,{}),...n},E(this.schema,r),this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[s,r]=await this.ownHooks.chain("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)}`);let o;try{o=await this.ldap.add(s,r,i)}catch(t){const e=t instanceof k&&t.statusCode<500;if(this.logger[e?"debug":"error"]("LDAP add failed:",{dn:s,entry:JSON.stringify(r,null,2),error:t}),t instanceof k||C(t))throw t;throw new Error(`Failed to add ${this.singularName} ${s}: ${String(t)}`,{cause:t})}return this.ownHooks.notify("adddone",s,r),o}async modifyEntry(t,e,i){await this.ldap.canonicalChanges(e);let a=this.resolveDn(t);const s=this.opNumber();if([a,e]=await this.ownHooks.chain("modify",[a,e,s]),e.add&&e.add[this.mainAttribute])throw new x(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new D(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new D(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new D(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.ldap.authorize("ldapmodifyrequest",[a,e,s,i]),await this.validateChanges(a,e);const r=await this.ldap.modify(a,e,i);return this.ownHooks.notify("modifydone",a,e,s),r}async renameEntry(t,e,i){/,/.test(t)||c(t,this.mainAttribute),/,/.test(e)||c(e,this.mainAttribute);let a=this.resolveDn(t),s=this.resolveDn(e);[a,s]=await this.ownHooks.chain("rename",[a,s]);const r=await this.ldap.rename(a,s,i);return this.ownHooks.notify("renamedone",a,s),r}async deleteEntry(t,e){let i=this.resolveDn(t);i=await this.ownHooks.chain("delete",i);const a=await this.ldap.delete(i,e);return this.ownHooks.notify("deletedone",i),a}async moveEntry(t,e,i){f(e,"targetOrgDn");const a=this.resolveDn(t),s=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",r=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[s]||!this.schema?.attributes[r])throw new D(`Schema for ${this.singularName} does not support move operation (missing ${s} or ${r})`);let n;try{n=await this.ldap.search({paged:!1,scope:"base",attributes:[s]},a,i)}catch(t){if(32===P(t))throw new O(`Entry ${a} not found`);throw t}if(!n.searchEntries||0===n.searchEntries.length)throw new O(`Entry ${a} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await this.ownHooks.chain("move",[a,e,i]);const h={replace:{[s]:e,[r]:o}};return await this.modifyEntry(t,h,i),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const a=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!a.searchEntries||0===a.searchEntries.length)throw new O(`Organization ${t} not found`);const s=a.searchEntries[0];if(s[i]){const t=s[i];return Array.isArray(t)?String(t[0]):String(t)}const r=s.ou||s.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){if(e instanceof O)throw e;const i=P(e);if(32===i)throw new O(`Organization ${t} not found`);if(34===i)throw new D(`${t} is not a valid DN`);throw new Error(`Failed to fetch organization ${t}: ${String(e)}`,{cause:e})}}async listEntries(t){return(await this.listEntriesLimited(t)).entries}async listEntriesLimited({filter:t,attributes:e,req:i,limit:a}){const s={paged:void 0===a||{pageSize:Math.min(a+2,100)},filter:t||"(objectClass=*)"};if(e&&e.length>0){const t=this.mainAttribute.toLowerCase();s.attributes=e.some(e=>e.toLowerCase()===t)?e:[...e,this.mainAttribute]}const r=function(t,e){const i={},a=new Set;return{entries:i,add(s){for(const r of s){const s=r[t],n=Array.isArray(s)?s[0]:s;if(null==n||""===n)continue;const o=String(n);if(!a.has(o)){if(void 0!==e&&a.size>=e)return!1;a.add(o)}i[o]=r}return!0}}}(this.mainAttribute,a);try{const t=await this.ldap.search(s,this.base,i);for await(const e of t)if(!r.add(e.searchEntries))return{entries:r.entries,truncated:!0};return{entries:r.entries,truncated:!1}}catch(t){if(void 0===a||4!==P(t))throw t}try{const t=await this.ldap.search({...s,paged:!1,sizeLimit:Math.min(a+1,2147483647)},this.base,i);r.add(t.searchEntries)}catch(t){if(4!==P(t))throw t}return{entries:r.entries,truncated:!0}}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const a=g(t),s=e?`(${this.mainAttribute}=*${a}*)`:`(${this.mainAttribute}=${a})`;return await this.listEntries({filter:s,attributes:i})}async validateNewEntry(t,e){return await this.validator().validateNew(e)}async validateChanges(t,e){return await this.validator().validateChanges(e),!0}}export{F as FlatEntryParams,z as FlatListQuery,I as FlatMoveBody,M as FlatMoveResponse,H as FlatRenameBody,LdapFlat as default};
|
|
1
|
+
import{Type as t}from"@sinclair/typebox";import{assertModifyRequest as e}from"../lib/ldapActions.js";import{jsonBody as i,created as a,tryMethod as s,multiStatus as r,WriteResponse as n}from"../lib/expressFormatedResponses.js";import{getCompiledRegex as o,escapeDnValue as h,escapeRegex as d,validateDnValue as c,isDummyMemberDn as l,normalizeDn as u,namesByDn as m,getParentDn as p,isDirectChildOf as b,assertRfc4514Escapes as f,assertedRdnValue as w,escapeLdapFilter as g,substringSearchFilter as y}from"../lib/utils.js";import{neverReturnAttributes as A,assertClientMaySet as $,modifiedAttributeNames as v,toDirectoryEntry as E}from"../config/schema.js";import{applyFixedDefaults as j,EntityValidator as N}from"../lib/entitySchema/validator.js";import{ConflictError as x,BadRequestError as D,NotFoundError as O,HttpError as k,isAuthzRefusal as C,UnprocessableEntityError as S}from"../lib/errors.js";import{extractLdapCode as P}from"../lib/ldapCodes.js";import{loadSchemaFile as L}from"../lib/schemaFile.js";import _ from"./plugin.js";import{defineRoute as q}from"../lib/route.js";const z=t.Object({match:t.Optional(t.String()),attribute:t.Optional(t.String()),attributes:t.Optional(t.String()),limit:t.Optional(t.Integer({minimum:1,maximum:Number.MAX_SAFE_INTEGER}))}),F=t.Object({id:t.String({minLength:1})}),I=t.Object({targetOrgDn:t.String({minLength:1})}),M=t.Object({success:t.Boolean(),departmentPath:t.String(),departmentLink:t.String()}),H=t.Object({newId:t.String({minLength:1})});class LdapFlat extends _{base;ldap;mainAttribute;objectClass;defaultAttributes;schema;singularName;pluralName;hookPrefix;ownHooks;constructor(t,e){if(super(t),this.ldap=t.ldap,this.base=e.base,this.mainAttribute=e.mainAttribute,this.objectClass=e.objectClass,this.defaultAttributes=e.defaultAttributes||{},this.singularName=e.singularName,this.pluralName=e.pluralName,this.hookPrefix=e.hookPrefix,this.ownHooks=this.hookBus.prefixed(this.hookPrefix),!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(this.ldap.declareObjectClasses(this.base,this.objectClass),e.schemaPath)try{this.schema=L(e.schemaPath,{config:this.config}),this.ldap.knowAttributeNames(Object.keys(this.schema.attributes)),this.logger.info(`${this.singularName} schema loaded from ${e.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${e.schemaPath}: ${t}`)}}hiddenAttributes(){return A([this.schema])}entryAttributes(){return["*",...Object.keys(this.schema?.attributes??{})]}project(t){const e=this.hiddenAttributes();if(0===e.size)return t;const i={};for(const[a,s]of Object.entries(t))e.has(a.split(";")[0].toLowerCase())||(i[a]=s);return i}projectList(t){if(0===this.hiddenAttributes().size)return t;const e={};for(const[i,a]of Object.entries(t))e[i]=this.project(a);return e}rejectForbiddenInput(t){$(this.schema,t,this.mainAttribute)}async generateMainAttribute(t){const e=this.deriveMainAttribute(t),i=this.schema?.attributes[this.mainAttribute]?.generatedFrom;if(void 0!==e&&i){if(!await this.rdnExists(e))return e;if("suffix"!==i.onCollision)throw new x(`${this.singularName} "${e}" already exists (generated from ${i.attribute})`);for(let t=2;t<1e3;t++){const i=`${e}-${t}`;if(!await this.rdnExists(i))return i}throw new x(`Cannot generate a free "${this.mainAttribute}" from "${i.attribute}"`)}}deriveMainAttribute(t){const e=this.schema?.attributes[this.mainAttribute],i=e?.generatedFrom;if(!i||!e.generated)return;const a=t[i.attribute],s=Array.isArray(a)?a[0]:a;if(null==s||""===s)throw new D(`Attribute "${i.attribute}" is required to generate "${this.mainAttribute}"`);let r=String(s);if(i.extract){const t=o(i.extract).exec(r);t&&(r=void 0!==t[1]?t[1]:t[0])}if(i.lowercase&&(r=r.toLowerCase()),i.strip&&(r=r.replace(o(i.strip,"g"),"")),!r)throw new D(`Cannot generate "${this.mainAttribute}" from "${i.attribute}"`);return r}candidateEntry(t,e){const i={...this.defaultAttributes,[this.mainAttribute]:t,...e,objectClass:this.objectClass};return j(this.schema,i),i}async rdnExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base"},`${this.mainAttribute}=${h(t)},${this.base}`);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}validator(){return new N(this.schema,{entryExists:async t=>{try{return(await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}},loadedPlugins:this.server.loadedPlugins,singularName:this.singularName})}api(t){q(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,query:z,handler:async({req:t,res:e,query:i})=>{const a={};i.match&&i.attribute&&(a.filter=y(i.match,i.attribute)),i.attributes&&(a.attributes=i.attributes.split(","));const s=i.limit;let r;try{r=await this.listEntriesLimited({...a,limit:s,req:t})}catch(t){if(void 0!==s||4!==P(t))throw t;throw new S("Too many entries to list at once: the directory's size limit was exceeded. Narrow the search, or pass `limit`.")}r.truncated&&e.set("X-Result-Truncated","true"),e.json(this.projectList(r.entries))}}),q(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,params:F,handler:async({req:t,res:e})=>this.apiGet(t,e)}),q(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,handler:async({req:t,res:e})=>this.apiAdd(t,e)}),q(t,{method:"delete",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,params:F,responses:{200:n},handler:async({req:t,res:e})=>this.apiDelete(t,e)}),q(t,{method:"put",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,params:F,responses:{200:n},handler:async({req:t,res:e})=>this.apiModify(t,e)}),q(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,params:F,body:I,responses:{200:M},handler:async({req:t,res:e})=>this.apiMove(t,e)}),q(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/rename`,params:F,body:H,handler:async({req:t,res:e})=>this.apiRename(t,e)})}async apiGet(t,e){const i=t.params.id;try{const a=this.resolveDn(i),s=await this.ldap.search({paged:!1,scope:"base",attributes:this.entryAttributes()},a,t);if(0===s.searchEntries.length)throw new O(`${this.singularName} not found`);e.json(this.project(s.searchEntries[0]))}catch(t){if(t.code&&32===t.code)throw new O(`${this.singularName} not found`);throw t}}async apiAdd(t,e){const s=this.schema?.attributes[this.mainAttribute],r=Boolean(s?.generated&&s.generatedFrom),n=r?i(t,e):i(t,e,this.mainAttribute);if(!n)return;this.rejectForbiddenInput(Object.keys(n));const o={...n};delete o[this.mainAttribute],delete o.dn;const h=r?this.deriveMainAttribute(n):void 0;void 0!==h&&await this.ldap.authorize("ldapaddrequest",[this.resolveDn(h),this.candidateEntry(h,o),t]);const d=r?await this.generateMainAttribute(n):n[this.mainAttribute];await this.addEntry(d,o,t);const c=await this.ldap.system.search({paged:!1,scope:"base",attributes:[this.mainAttribute]},this.creationOf(d).dn);return a(e,this.project(c.searchEntries[0]))}async apiDelete(t,e){const i=t.params.id;await s(e,this.deleteEntry.bind(this),i,t)}async apiModify(t,a){const r=i(t,a);if(!r)return;e(r),this.rejectForbiddenInput(v(r));const n=t.params.id;await s(a,this.modifyEntry.bind(this),n,r,t)}async apiMove(t,e){const i=t.body,a=t.params.id,{targetOrgDn:s}=i,r=await this.moveEntry(a,s,t);e.json({success:!0,...r})}async apiRename(t,e){const i=t.body,a=i.newId;if(this.rejectForbiddenInput(Object.keys(i).filter(t=>"newId"!==t)),new RegExp(`^${d(this.mainAttribute)}=`,"i").test(a))throw new D(`newId must be a ${this.mainAttribute} value, not a DN`);try{c(a,this.mainAttribute)}catch(t){throw new D(t instanceof Error?t.message:"Invalid newId")}await this.validator().validateValue(this.mainAttribute,a);const s=t.params.id,n=this.resolveDn(s),o=this.resolveDn(a),h=this.config.group_dummy_user;if(h&&(l(n,h)||l(o,h)))throw new x(`${h} is the configured placeholder member and cannot be renamed`);await this.ldap.authorize("ldaprenamerequest",[n,o,t]);const m=u(n)===u(o),p=await this.entryExists(n),b=m?p:await this.entryExists(o),f=this.requirePlugin("ldapEnterpriseRules");let w=[];if(m){if(!p)throw new O(`${this.singularName} not found`)}else if(p){if(b)throw new x(`${this.singularName} "${a}" already exists`);w=await(f?.findReferences(n))||[],await this.renameEntry(n,o,t)}else{if(!b)throw new O(`${this.singularName} not found`);w=await(f?.findReferences(n))||[]}const g=f&&w.length>0?await f.rewriteReferences(w,n,o,t):{updated:0,failed:[]},y={success:0===g.failed.length,dn:o,referencesUpdated:g.updated};0!==g.failed.length?r(e,{...y,referencesFailed:g.failed}):e.json(y)}async entryExists(t){try{const e=await this.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t);return(e?.searchEntries?.length??0)>0}catch(t){return!1}}resolveDn(t){if(m(t,this.mainAttribute)){const e=p(t),i=e!==t;if(!b(t,this.base))throw new D(`DN must be a direct child of "${this.base}". Provided DN "${t}" has parent "${i?e:"<none>"}"`);return t}return`${this.mainAttribute}=${h(t)},${this.base}`}creationOf(t){if(!m(t,this.mainAttribute))return{name:t,dn:this.resolveDn(t)};f(t,`${this.mainAttribute} DN`),this.resolveDn(t);const e=w(t,this.mainAttribute,this.base);if(null===e)throw new D(`${this.mainAttribute} must be a value, or a "${this.mainAttribute}=…,${this.base}" DN`);return{name:e,dn:`${this.mainAttribute}=${h(e)},${this.base}`}}async addEntry(t,e={},i){if("string"!=typeof t)throw new D(`${this.mainAttribute} must be a string`);await this.ldap.canonicalAttributes(e);const a=this.creationOf(t);t=a.name;let s=a.dn;try{c(t,this.mainAttribute)}catch(t){throw new D(t instanceof Error?t.message:"Invalid identifier")}let r=this.candidateEntry(t,e);await this.ldap.authorize("ldapaddrequest",[s,{...r},i]);const n=await this.validateNewEntry(s,{[this.mainAttribute]:t,...e,objectClass:this.objectClass});r={...this.candidateEntry(t,{}),...n},E(this.schema,r),this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[s,r]=await this.ownHooks.chain("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)}`);let o;try{o=await this.ldap.add(s,r,i)}catch(t){const e=t instanceof k&&t.statusCode<500;if(this.logger[e?"debug":"error"]("LDAP add failed:",{dn:s,entry:JSON.stringify(r,null,2),error:t}),t instanceof k||C(t))throw t;throw new Error(`Failed to add ${this.singularName} ${s}: ${String(t)}`,{cause:t})}return this.ownHooks.notify("adddone",s,r),o}async modifyEntry(t,e,i){await this.ldap.canonicalChanges(e);let a=this.resolveDn(t);const s=this.opNumber();if([a,e]=await this.ownHooks.chain("modify",[a,e,s]),e.add&&e.add[this.mainAttribute])throw new x(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new D(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new D(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new D(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.ldap.authorize("ldapmodifyrequest",[a,e,s,i]),await this.validateChanges(a,e);const r=await this.ldap.modify(a,e,i);return this.ownHooks.notify("modifydone",a,e,s),r}async renameEntry(t,e,i){/,/.test(t)||c(t,this.mainAttribute),/,/.test(e)||c(e,this.mainAttribute);let a=this.resolveDn(t),s=this.resolveDn(e);[a,s]=await this.ownHooks.chain("rename",[a,s]);const r=await this.ldap.rename(a,s,i);return this.ownHooks.notify("renamedone",a,s),r}async deleteEntry(t,e){let i=this.resolveDn(t);i=await this.ownHooks.chain("delete",i);const a=await this.ldap.delete(i,e);return this.ownHooks.notify("deletedone",i),a}async moveEntry(t,e,i){f(e,"targetOrgDn");const a=this.resolveDn(t),s=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",r=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[s]||!this.schema?.attributes[r])throw new D(`Schema for ${this.singularName} does not support move operation (missing ${s} or ${r})`);let n;try{n=await this.ldap.search({paged:!1,scope:"base",attributes:[s]},a,i)}catch(t){if(32===P(t))throw new O(`Entry ${a} not found`);throw t}if(!n.searchEntries||0===n.searchEntries.length)throw new O(`Entry ${a} not found`);const o=await this.getDepartmentPath(e);[,e]=await this.ownHooks.chain("move",[a,e,i]);const h={replace:{[s]:e,[r]:o}};return await this.modifyEntry(t,h,i),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t){const e=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const i=await this.ldap.system.search({paged:!1,scope:"base",attributes:[e,"ou","o"]},t);if(!i.searchEntries||0===i.searchEntries.length)throw new O(`Organization ${t} not found`);const a=i.searchEntries[0];if(a[e]){const t=a[e];return Array.isArray(t)?String(t[0]):String(t)}const s=a.ou||a.o;if(s){return`/${Array.isArray(s)?String(s[0]):String(s)}`}return this.logger.warn(`Organization ${t} has no ${e} attribute, using DN`),t}catch(e){if(e instanceof O)throw e;const i=P(e);if(32===i)throw new O(`Organization ${t} not found`);if(34===i)throw new D(`${t} is not a valid DN`);throw new Error(`Failed to fetch organization ${t}: ${String(e)}`,{cause:e})}}async listEntries(t){return(await this.listEntriesLimited(t)).entries}async listEntriesLimited({filter:t,attributes:e,req:i,limit:a}){const s={paged:void 0===a||{pageSize:Math.min(a+2,100)},filter:t||"(objectClass=*)"};if(e&&e.length>0){const t=this.mainAttribute.toLowerCase();s.attributes=e.some(e=>e.toLowerCase()===t)?e:[...e,this.mainAttribute]}const r=function(t,e){const i={},a=new Set;return{entries:i,add(s){for(const r of s){const s=r[t],n=Array.isArray(s)?s[0]:s;if(null==n||""===n)continue;const o=String(n);if(!a.has(o)){if(void 0!==e&&a.size>=e)return!1;a.add(o)}i[o]=r}return!0}}}(this.mainAttribute,a);try{const t=await this.ldap.search(s,this.base,i);for await(const e of t)if(!r.add(e.searchEntries))return{entries:r.entries,truncated:!0};return{entries:r.entries,truncated:!1}}catch(t){if(void 0===a||4!==P(t))throw t}try{const t=await this.ldap.search({...s,paged:!1,sizeLimit:Math.min(a+1,2147483647)},this.base,i);r.add(t.searchEntries)}catch(t){if(4!==P(t))throw t}return{entries:r.entries,truncated:!0}}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const a=g(t),s=e?`(${this.mainAttribute}=*${a}*)`:`(${this.mainAttribute}=${a})`;return await this.listEntries({filter:s,attributes:i})}async validateNewEntry(t,e){return await this.validator().validateNew(e)}async validateChanges(t,e){return await this.validator().validateChanges(e),!0}}export{F as FlatEntryParams,z as FlatListQuery,I as FlatMoveBody,M as FlatMoveResponse,H as FlatRenameBody,LdapFlat as default};
|
package/dist/lib/authz/base.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../../abstract/plugin.js";import{roleAttribute as e}from"../../config/schema.js";import{assertIdentityMode as r,identityFor as s}from"../auth/base.js";import{BadRequestError as i,ServiceUnavailableError as n,AmbiguousIdentityError as a,AuthzRefusedError as o}from"../errors.js";import{getParentDn as h,isDnInBranch as c,getRdn as u,organizationLink as l,normalizeDn as d,escapeLdapFilter as f,parseDn as g}from"../utils.js";import{authzFor as w,servesRequest as p}from"./composition.js";function mostSpecificBranchRule(t,e){let r=null,s=-1;for(const[i,n]of t){if(!c(e,i))continue;const t=g(i).length;t>s&&([r,s]=[n,t])}return r}class AuthzBase extends t{static widenedForLink=Symbol("widened for the link");static topOrganizationRead=Symbol("top organization");roles=["authz"];cacheTTL;static UNRESOLVED_POLICIES=["deny","allow"];constructor(t){super(t),r(this.config,this.constructor.name),w(this.config,this.constructor.name);const e=this.config.authz_unresolved_user??"deny";if(!AuthzBase.UNRESOLVED_POLICIES.includes(e))throw new Error(`${this.constructor.name}: unknown --authz-unresolved-user "${e}". Known: ${AuthzBase.UNRESOLVED_POLICIES.join(", ")}.`);this.config.authz_transit_branch&&!this.filtersAttachedEntries()&&this.logger.warn(`${this.constructor.name}: --authz-transit-branch is ignored, entries are not judged by their attachment here (--authz-filter-attached-entries)`)}static wantsAttribute(t,e){const r=t?.attributes;if(void 0===r)return!0;const s=(Array.isArray(r)?r:[r]).map(t=>String(t).toLowerCase());return 0===s.length||(!!s.includes("*")||s.includes(e.toLowerCase()))}extractBranchDn(t){return h(t)}filtersAttachedEntries(){return Boolean(this.config.authz_filter_attached_entries)}judgesByAttachment(){return this.filtersAttachedEntries()}globalBranches(){return[]}async searchRefusal(t,e){if(this.filtersAttachedEntries()){if(0===(await this.getAuthorizedBranches(t)).length)return"administers no branch";if(!this.globalBranches().some(t=>c(e,t)))return}return(await this.getUserPermissions(t,e)).read?void 0:`does not have read permission for branch ${e}`}sourcePermission(t){return"read"}async storedDn(t){try{const e=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:["1.1"]},t),r=e.searchEntries?.[0]?.dn;if(r)return String(r)}catch(e){if(34===e?.code)throw new i(`Invalid DN: ${t}`);if(32!==e?.code)throw this.logger.error(`${this.name}: ${t} could not be read to be authorized: ${String(e)}`),new n("Directory unavailable")}return t}async storedParent(t){const e=h(t);return e===t?t:`${u(t)},${await this.storedDn(e)}`}async effectiveBranch(t){const e=this.config.ldap_organization_link_attribute;if(!e||!this.filtersAttachedEntries())return this.extractBranchDn(t);try{const r=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[e]},t),s=l(r.searchEntries?.[0]?.[e]);if(s)return s}catch{}return this.extractBranchDn(t)}transitBranch(){if(!this.filtersAttachedEntries())return;const t=String(this.config.authz_transit_branch??"").trim();return t.length>0?t:void 0}inTransit(t,e=this.transitKey()){return void 0===t||void 0!==e&&d(t)===e}transitOriginAttribute(){if(void 0===this.transitBranch())return;const t=String(this.config.authz_transit_origin_attribute??"").trim();return t.length>0?t:void 0}async recordTransitOrigin(t,e){const r=this.transitOriginAttribute(),s=this.config.ldap_organization_link_attribute;if(!r||!s)return e;const same=(t,e)=>t.split(";")[0].toLowerCase()===e.toLowerCase(),named=(t,e)=>void 0!==t&&Object.keys(t).some(t=>same(t,e)),deleted=t=>Array.isArray(e.delete)?e.delete.some(e=>same(e,t)):named(e.delete,t);let i,n;if(named(e.replace,s))i=l(AuthzBase.valuesOf(e.replace,s));else if(named(e.add,s))i=l(AuthzBase.valuesOf(e.add,s));else if(!deleted(s))return e;if(named(e.replace,r)||named(e.add,r)||deleted(r))return e;try{n=(await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[s,r]},t)).searchEntries[0]}catch{}if(!n)return e;const a=l(AuthzBase.valuesOf(n,s)),o=this.inTransit(a),h=this.inTransit(i);return!o&&h&&a?{...e,replace:{...e.replace,[r]:a}}:o&&!h&&AuthzBase.valuesOf(n,r).length>0?{...e,delete:Array.isArray(e.delete)?[...e.delete,r]:{...e.delete,[r]:[]}}:e}transitKey(){const t=this.transitBranch();return void 0===t?void 0:d(t)}async findUserDn(t){const e=`(${this.config.ldap_user_main_attribute||"uid"}=${f(t)})`,r=(await this.server.ldap.system.search({paged:!1,filter:e,attributes:["dn"],scope:"sub"},this.server.ldap.base)).searchEntries??[];if(r.length>1)throw new a(t,r.length);if(0===r.length)return null;const s=r[0].dn;return"string"==typeof s?s:String(s)}onlyRelinks(t){const e=[this.config.ldap_organization_link_attribute,this.config.ldap_organization_path_attribute].filter(t=>"string"==typeof t).map(t=>t.toLowerCase());return[...Object.keys(t.add??{}),...Object.keys(t.replace??{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete??{})].every(t=>e.includes(String(t).split(";")[0].toLowerCase()))}async refusingRule(t,e,r,s){const i=d(e);for(const n of await this.ruleBranches(t)){if(!(d(n)===i||s&&c(n,e)))continue;const a=await this.getUserPermissions(t,n);if(r.some(t=>!a[t]))return n}}shouldSkipAuthorization(t){return!t?.user||!p(t,this.config)}resolutionCache=new Map;static RESOLUTION_CACHE_MAX=1e4;resolutionCacheFullWarned=!1;fallbackWarned=!1;async resolveCaller(t){const{value:e,fellBack:r}=s(t,this.config);r&&!this.fallbackWarned&&(this.fallbackWarned=!0,this.logger.warn(`${this.name}: --authz-identity asks for req.userName and the authenticator published none, so permissions are read for req.user instead. Rules written on logins will not match`));const i=e,n=Date.now(),a=this.resolutionCache.get(i);let h;if(a&&n-a.at<this.cacheTTL)h=a.user;else{if(h=await this.resolveUser(i),this.resolutionCache.size>=AuthzBase.RESOLUTION_CACHE_MAX)for(const[t,e]of this.resolutionCache)n-e.at>=this.cacheTTL&&this.resolutionCache.delete(t);this.resolutionCache.size<AuthzBase.RESOLUTION_CACHE_MAX?this.resolutionCache.set(i,{user:h,at:n}):this.resolutionCacheFullWarned||(this.resolutionCacheFullWarned=!0,this.logger.warn(`${this.name}: more than ${AuthzBase.RESOLUTION_CACHE_MAX} identities resolved within one cache window, so resolutions are no longer cached and each request costs a lookup`))}if(h)return h;if("allow"===this.config.authz_unresolved_user)return this.logger.warn(`User ${i} could not be resolved by ${this.name}; allowed by --authz-unresolved-user allow`),null;throw new o(`User ${i} could not be resolved by ${this.name}, so no permission can be read for them`)}domainLinkAttribute(){const t=this.server.loadedPlugins.ldapOrganizations;return e(t?.schema,"domainLink")||this.config.enterprise_domain_link_attribute||void 0}static organizationLinks(t,e){if(e.some(t=>""===t.trim()))throw new i(`${t} cannot hold an empty value`);const r=[...new Map(e.map(t=>[d(t),t])).values()];if(r.length>1)throw new i(`${t} attaches an entry to one organization, not ${r.length}`);return r}static valuesOf(t,e){if(!t)return[];const r=e.toLowerCase(),s=[];for(const[e,i]of Object.entries(t))if(e.split(";")[0].toLowerCase()===r&&null!=i)for(const t of Array.isArray(i)?i:[i])s.push(String(t));return s}async storedValues(t,e){try{const r=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[e]},t);return AuthzBase.valuesOf(r.searchEntries?.[0],e)}catch{return[]}}async guardDomainLinks(t,e,r,s,i,n){if(0===s.length)return;const a=new Set((n?[]:await this.storedValues(r,i)).map(d)),u=s.filter(t=>!a.has(d(t)));if(0===u.length)return;const l=this.config.ldap_top_organization,f=d(l||this.server.ldap.base),g=new Set;let w=h(r);const p=new Set;for(;w&&!p.has(d(w));){const t=d(w);if(p.add(t),t!==f&&!c(w,f))break;for(const t of await this.storedValues(w,i))g.add(d(t));if(t===f)break;const e=h(w);if(e===w)break;w=e}const m=u.filter(t=>!g.has(d(t)));if(0!==m.length&&!await this.mayLinkAnyDomain(t))throw new o(`User ${e.user} may not link ${r} to domain ${m[0]}: no organization above it is linked to it`)}async mayLinkAnyDomain(t){const e=this.config.ldap_top_organization;return!!e&&!0===(await this.getUserPermissions(t,e)).write}clearResolutionCache(){this.resolutionCache.clear()}hooks={ldaprenamedone:()=>{this.resolutionCache.clear()},ldapdeletedone:()=>{this.resolutionCache.clear()},ldapmodifyrequest:async([t,e,r,s])=>{if(this.shouldSkipAuthorization(s))return[t,await this.recordTransitOrigin(t,e),r,s];const i=await this.resolveCaller(s);if(!i)return[t,await this.recordTransitOrigin(t,e),r,s];const n=await this.storedDn(t),a=this.config.ldap_organization_link_attribute,h=a?AuthzBase.organizationLinks(a,[...AuthzBase.valuesOf(e.replace,a),...AuthzBase.valuesOf(e.add,a)]):[];if(a&&h.length>0){let r,c=this.extractBranchDn(n),u=!1;try{const e=(await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[a]},t)).searchEntries[0];e&&(u=!0,r=l(e[a]),r&&(c=r))}catch{}const d=await this.storedDn(h[0]),f=this.filtersAttachedEntries(),g=f&&u&&this.inTransit(r),w=f&&this.inTransit(d);if(!g){const t=w?"write":this.sourcePermission(c);if(!(await this.getUserPermissions(i,c))[t])throw new o(`User ${s.user} does not have ${t} permission for source branch ${c}`)}if(w){if(g){if(0===(await this.getAuthorizedBranches(i)).length)throw new o(`User ${s.user} administers no branch`)}}else{if(!(await this.getUserPermissions(i,d)).write)throw new o(`User ${s.user} does not have write permission for destination branch ${d}`)}if(!g&&!this.onlyRelinks(e)){const t=f?c:this.extractBranchDn(n);if(!(await this.getUserPermissions(i,t)).write)throw new o(`User ${s.user} does not have write permission for branch ${t}`)}}else{const t=await this.effectiveBranch(n);if(!(await this.getUserPermissions(i,t)).write)throw new o(`User ${s.user} does not have write permission for branch ${t}`)}const c=await this.refusingRule(i,n,["write"],!1);if(c)throw new o(`User ${s.user} does not have write permission for branch ${c}`);const u=this.domainLinkAttribute();return u&&await this.guardDomainLinks(i,s,t,[...AuthzBase.valuesOf(e.add,u),...AuthzBase.valuesOf(e.replace,u)],u,!1),[t,await this.recordTransitOrigin(t,e),r,s]},ldapaddrequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];let i;const n=this.config.ldap_organization_link_attribute,a=n?AuthzBase.organizationLinks(n,AuthzBase.valuesOf(e,n)):[],h=await this.storedParent(t);i=a.length>0?await this.storedDn(a[0]):this.extractBranchDn(h);if(!(await this.getUserPermissions(s,i)).write)throw new o(`User ${r.user} does not have write permission for branch ${i}`);const c=await this.refusingRule(s,h,["write"],!1);if(c)throw new o(`User ${r.user} does not have write permission for branch ${c}`);const u=this.domainLinkAttribute();return u&&await this.guardDomainLinks(s,r,t,AuthzBase.valuesOf(e,u),u,!0),[t,e,r]},ldapsearchrequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];if(t===this.config.ldap_top_organization&&"base"===e.scope)return[t,{...e,[AuthzBase.topOrganizationRead]:!0},r];const i=await this.searchRefusal(s,t);if(i)throw new o(`User ${r.user} ${i}`);if(this.filtersAttachedEntries()){const s=this.config.ldap_organization_link_attribute;if(s&&!AuthzBase.wantsAttribute(e,s)){const t=e.attributes,r=Array.isArray(t)?t.map(String):[String(t)];e={...e,attributes:[...r,s],[AuthzBase.widenedForLink]:!0}}return[t,e,r]}return[t,e,r]},ldapsearchfilter:async([t,e,r])=>{const s=[t,e,r];if(this.shouldSkipAuthorization(e))return s;if(!t?.searchEntries?.length)return s;const i=await this.resolveCaller(e);if(!i)return s;if(!this.filtersAttachedEntries()){if(r?.[AuthzBase.topOrganizationRead])return s;const n=await this.ruleBranches(i),a=[],o=new Map,c=new Set(n.map(d));for(const r of t.searchEntries){const t=String(r.dn),s=d(h(t)),n=c.has(d(t));let u=n?void 0:o.get(s);void 0===u&&(u=!0===(await this.getUserPermissions(i,t)).read,n||o.set(s,u)),u?a.push(r):this.logger.debug(`${this.name}: hiding ${t} from ${e.user}, refused by a narrower branch rule`)}return t.searchEntries=a,s}const n=this.globalBranches();if(n.length){const r=new Map,s=[];for(const a of t.searchEntries){const t=n.find(t=>c(a.dn,t));void 0===t||(r.has(t)||r.set(t,!0===(await this.getUserPermissions(i,t)).read),r.get(t))?s.push(a):this.logger.debug(`${this.name}: hiding ${String(a.dn)} from ${e.user}, in the global branch ${t}`)}t.searchEntries=s}const a=this.config.ldap_organization_link_attribute;if(!a)return s;const o=await this.getAuthorizedBranches(i);if(0===o.length)return s;const u=this.transitKey(),l=new Map,reads=async t=>{if(e=t,!o.some(t=>c(e,t)))return!1;var e;const r=d(t);return l.has(r)||l.set(r,!0===(await this.getUserPermissions(i,t)).read),l.get(r)},f=[];for(const r of t.searchEntries){const t=r[a],s=null==t?[]:(Array.isArray(t)?t:[t]).map(String);if(0===s.length){f.push(r);continue}let i=!1;for(const t of s)if(await reads(t)){i=!0;break}i||s.some(t=>this.inTransit(t,u))?f.push(r):this.logger.debug(`${this.name}: hiding ${String(r.dn)} from ${e.user}, attached to ${s[0]}`)}if(t.searchEntries=f,r?.[AuthzBase.widenedForLink])for(const e of t.searchEntries)delete e[a];return[t,e,r]},getOrganisationTop:async([t,e])=>{if(this.shouldSkipAuthorization(t))return[t,e];const r=await this.resolveCaller(t);if(!r)return[t,e];const s=await this.getAuthorizedBranches(r);if(s.length>0){const e=[];for(const r of s)try{const s=await this.server.ldap.search({paged:!1,scope:"base"},r,t);1===s.searchEntries.length&&e.push(s.searchEntries[0])}catch(t){this.logger.warn(`Failed to fetch authorized branch ${r}: ${t}`)}if(1===e.length)return[t,e[0]];if(e.length>1)return[t,e[0]]}return[t,e]},ldaprenamerequest:async([t,e,r])=>{if(this.shouldSkipAuthorization(r))return[t,e,r];const s=await this.resolveCaller(r);if(!s)return[t,e,r];const i=await this.storedDn(t),n=await this.storedParent(e),a=this.extractBranchDn(i),h=this.extractBranchDn(n),c=await this.getUserPermissions(s,a),u=this.sourcePermission(a);if(!c[u])throw new o(`User ${r.user} does not have ${u} permission for source branch ${a}`);const l=await this.refusingRule(s,i,["read","write"],!0);if(l)throw new o(`User ${r.user} does not have read and write permission for branch ${l}`);if(!(await this.getUserPermissions(s,h)).write)throw new o(`User ${r.user} does not have write permission for destination branch ${h}`);const d=await this.refusingRule(s,n,["write"],!1);if(d)throw new o(`User ${r.user} does not have write permission for branch ${d}`);return[t,e,r]},ldapdeleterequest:async([t,e])=>{if(this.shouldSkipAuthorization(e))return[t,e];const r=await this.resolveCaller(e);if(!r)return[t,e];const s=Array.isArray(t)?t:[t];for(const t of s){const s=await this.storedDn(t),i=await this.effectiveBranch(s);if(!(await this.getUserPermissions(r,i)).delete)throw new o(`User ${e.user} does not have delete permission for branch ${i}`);const n=await this.refusingRule(r,s,["delete"],!0);if(n)throw new o(`User ${e.user} does not have delete permission for branch ${n}`)}return[t,e]}}}export{AuthzBase as default,mostSpecificBranchRule};
|
|
1
|
+
import t from"../../abstract/plugin.js";import{roleAttribute as e}from"../../config/schema.js";import{assertIdentityMode as s,identityFor as r}from"../auth/base.js";import{BadRequestError as i,ServiceUnavailableError as n,AmbiguousIdentityError as a,AuthzRefusedError as o}from"../errors.js";import{getParentDn as h,isDnInBranch as c,getRdn as u,organizationLink as l,normalizeDn as d,escapeLdapFilter as f,parseDn as w}from"../utils.js";import{authzFor as g,servesRequest as p}from"./composition.js";function mostSpecificBranchRule(t,e){let s=null,r=-1;for(const[i,n]of t){if(!c(e,i))continue;const t=w(i).length;t>r&&([s,r]=[n,t])}return s}class AuthzBase extends t{static widenedForLink=Symbol("widened for the link");static widenedForObjectClass=Symbol("widened for object classes");roles=["authz"];cacheTTL;static UNRESOLVED_POLICIES=["deny","allow"];constructor(t){super(t),s(this.config,this.constructor.name),g(this.config,this.constructor.name);const e=this.config.authz_unresolved_user??"deny";if(!AuthzBase.UNRESOLVED_POLICIES.includes(e))throw new Error(`${this.constructor.name}: unknown --authz-unresolved-user "${e}". Known: ${AuthzBase.UNRESOLVED_POLICIES.join(", ")}.`);this.config.authz_transit_branch&&!this.filtersAttachedEntries()&&this.logger.warn(`${this.constructor.name}: --authz-transit-branch is ignored, entries are not judged by their attachment here (--authz-filter-attached-entries)`)}static wantsAttribute(t,e){const s=t?.attributes;if(void 0===s)return!0;const r=(Array.isArray(s)?s:[s]).map(t=>String(t).toLowerCase());return 0===r.length||(!!r.includes("*")||r.includes(e.toLowerCase()))}extractBranchDn(t){return h(t)}filtersAttachedEntries(){return Boolean(this.config.authz_filter_attached_entries)}judgesByAttachment(){return this.filtersAttachedEntries()}globalBranches(){return[]}async searchRefusal(t,e){if(this.filtersAttachedEntries()){if(0===(await this.getAuthorizedBranches(t)).length)return"administers no branch";if(!this.globalBranches().some(t=>c(e,t)))return}return(await this.getUserPermissions(t,e)).read?void 0:`does not have read permission for branch ${e}`}sourcePermission(t){return"read"}async storedDn(t){try{const e=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:["1.1"]},t),s=e.searchEntries?.[0]?.dn;if(s)return String(s)}catch(e){if(34===e?.code)throw new i(`Invalid DN: ${t}`);if(32!==e?.code)throw this.logger.error(`${this.name}: ${t} could not be read to be authorized: ${String(e)}`),new n("Directory unavailable")}return t}async storedParent(t){const e=h(t);return e===t?t:`${u(t)},${await this.storedDn(e)}`}async effectiveBranch(t){const e=this.config.ldap_organization_link_attribute;if(!e||!this.filtersAttachedEntries())return this.extractBranchDn(t);try{const s=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[e]},t),r=l(s.searchEntries?.[0]?.[e]);if(r)return r}catch{}return this.extractBranchDn(t)}transitBranch(){if(!this.filtersAttachedEntries())return;const t=String(this.config.authz_transit_branch??"").trim();return t.length>0?t:void 0}inTransit(t,e=this.transitKey()){return void 0===t||void 0!==e&&d(t)===e}transitOriginAttribute(){if(void 0===this.transitBranch())return;const t=String(this.config.authz_transit_origin_attribute??"").trim();return t.length>0?t:void 0}async recordTransitOrigin(t,e){const s=this.transitOriginAttribute(),r=this.config.ldap_organization_link_attribute;if(!s||!r)return e;const same=(t,e)=>t.split(";")[0].toLowerCase()===e.toLowerCase(),named=(t,e)=>void 0!==t&&Object.keys(t).some(t=>same(t,e)),deleted=t=>Array.isArray(e.delete)?e.delete.some(e=>same(e,t)):named(e.delete,t);let i,n;if(named(e.replace,r))i=l(AuthzBase.valuesOf(e.replace,r));else if(named(e.add,r))i=l(AuthzBase.valuesOf(e.add,r));else if(!deleted(r))return e;if(named(e.replace,s)||named(e.add,s)||deleted(s))return e;try{n=(await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[r,s]},t)).searchEntries[0]}catch{}if(!n)return e;const a=l(AuthzBase.valuesOf(n,r)),o=this.inTransit(a),h=this.inTransit(i);return!o&&h&&a?{...e,replace:{...e.replace,[s]:a}}:o&&!h&&AuthzBase.valuesOf(n,s).length>0?{...e,delete:Array.isArray(e.delete)?[...e.delete,s]:{...e.delete,[s]:[]}}:e}transitKey(){const t=this.transitBranch();return void 0===t?void 0:d(t)}async findUserDn(t){const e=`(${this.config.ldap_user_main_attribute||"uid"}=${f(t)})`,s=(await this.server.ldap.system.search({paged:!1,filter:e,attributes:["dn"],scope:"sub"},this.server.ldap.base)).searchEntries??[];if(s.length>1)throw new a(t,s.length);if(0===s.length)return null;const r=s[0].dn;return"string"==typeof r?r:String(r)}onlyRelinks(t){const e=[this.config.ldap_organization_link_attribute,this.config.ldap_organization_path_attribute].filter(t=>"string"==typeof t).map(t=>t.toLowerCase());return[...Object.keys(t.add??{}),...Object.keys(t.replace??{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete??{})].every(t=>e.includes(String(t).split(";")[0].toLowerCase()))}async refusingRule(t,e,s,r){const i=d(e);for(const n of await this.ruleBranches(t)){if(!(d(n)===i||r&&c(n,e)))continue;const a=await this.getUserPermissions(t,n);if(s.some(t=>!a[t]))return n}}shouldSkipAuthorization(t){return!t?.user||!p(t,this.config)}resolutionCache=new Map;static RESOLUTION_CACHE_MAX=1e4;resolutionCacheFullWarned=!1;fallbackWarned=!1;async resolveCaller(t){const{value:e,fellBack:s}=r(t,this.config);s&&!this.fallbackWarned&&(this.fallbackWarned=!0,this.logger.warn(`${this.name}: --authz-identity asks for req.userName and the authenticator published none, so permissions are read for req.user instead. Rules written on logins will not match`));const i=e,n=Date.now(),a=this.resolutionCache.get(i);let h;if(a&&n-a.at<this.cacheTTL)h=a.user;else{if(h=await this.resolveUser(i),this.resolutionCache.size>=AuthzBase.RESOLUTION_CACHE_MAX)for(const[t,e]of this.resolutionCache)n-e.at>=this.cacheTTL&&this.resolutionCache.delete(t);this.resolutionCache.size<AuthzBase.RESOLUTION_CACHE_MAX?this.resolutionCache.set(i,{user:h,at:n}):this.resolutionCacheFullWarned||(this.resolutionCacheFullWarned=!0,this.logger.warn(`${this.name}: more than ${AuthzBase.RESOLUTION_CACHE_MAX} identities resolved within one cache window, so resolutions are no longer cached and each request costs a lookup`))}if(h)return h;if("allow"===this.config.authz_unresolved_user)return this.logger.warn(`User ${i} could not be resolved by ${this.name}; allowed by --authz-unresolved-user allow`),null;throw new o(`User ${i} could not be resolved by ${this.name}, so no permission can be read for them`)}domainLinkAttribute(){const t=this.server.loadedPlugins.ldapOrganizations;return e(t?.schema,"domainLink")||this.config.enterprise_domain_link_attribute||void 0}static organizationLinks(t,e){if(e.some(t=>""===t.trim()))throw new i(`${t} cannot hold an empty value`);const s=[...new Map(e.map(t=>[d(t),t])).values()];if(s.length>1)throw new i(`${t} attaches an entry to one organization, not ${s.length}`);return s}static valuesOf(t,e){if(!t)return[];const s=e.toLowerCase(),r=[];for(const[e,i]of Object.entries(t))if(e.split(";")[0].toLowerCase()===s&&null!=i)for(const t of Array.isArray(i)?i:[i])r.push(String(t));return r}async storedValues(t,e){try{const s=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[e]},t);return AuthzBase.valuesOf(s.searchEntries?.[0],e)}catch{return[]}}async guardDomainLinks(t,e,s,r,i,n){if(0===r.length)return;const a=new Set((n?[]:await this.storedValues(s,i)).map(d)),u=r.filter(t=>!a.has(d(t)));if(0===u.length)return;const l=this.config.ldap_top_organization,f=d(l||this.server.ldap.base),w=new Set;let g=h(s);const p=new Set;for(;g&&!p.has(d(g));){const t=d(g);if(p.add(t),t!==f&&!c(g,f))break;for(const t of await this.storedValues(g,i))w.add(d(t));if(t===f)break;const e=h(g);if(e===g)break;g=e}const m=u.filter(t=>!w.has(d(t)));if(0!==m.length&&!await this.mayLinkAnyDomain(t))throw new o(`User ${e.user} may not link ${s} to domain ${m[0]}: no organization above it is linked to it`)}async mayLinkAnyDomain(t){const e=this.config.ldap_top_organization;return!!e&&!0===(await this.getUserPermissions(t,e)).write}clearResolutionCache(){this.resolutionCache.clear()}hooks={ldaprenamedone:()=>{this.resolutionCache.clear()},ldapdeletedone:()=>{this.resolutionCache.clear()},ldapmodifyrequest:async([t,e,s,r])=>{if(this.shouldSkipAuthorization(r))return[t,await this.recordTransitOrigin(t,e),s,r];const i=await this.resolveCaller(r);if(!i)return[t,await this.recordTransitOrigin(t,e),s,r];const n=await this.storedDn(t),a=this.config.ldap_organization_link_attribute,h=a?AuthzBase.organizationLinks(a,[...AuthzBase.valuesOf(e.replace,a),...AuthzBase.valuesOf(e.add,a)]):[];if(a&&h.length>0){let s,c=this.extractBranchDn(n),u=!1;try{const e=(await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[a]},t)).searchEntries[0];e&&(u=!0,s=l(e[a]),s&&(c=s))}catch{}const d=await this.storedDn(h[0]),f=this.filtersAttachedEntries(),w=f&&u&&this.inTransit(s),g=f&&this.inTransit(d);if(!w){const t=g?"write":this.sourcePermission(c);if(!(await this.getUserPermissions(i,c))[t])throw new o(`User ${r.user} does not have ${t} permission for source branch ${c}`)}if(g){if(w){if(0===(await this.getAuthorizedBranches(i)).length)throw new o(`User ${r.user} administers no branch`)}}else{if(!(await this.getUserPermissions(i,d)).write)throw new o(`User ${r.user} does not have write permission for destination branch ${d}`)}if(!w&&!this.onlyRelinks(e)){const t=f?c:this.extractBranchDn(n);if(!(await this.getUserPermissions(i,t)).write)throw new o(`User ${r.user} does not have write permission for branch ${t}`)}}else{const t=await this.effectiveBranch(n);if(!(await this.getUserPermissions(i,t)).write)throw new o(`User ${r.user} does not have write permission for branch ${t}`)}const c=await this.refusingRule(i,n,["write"],!1);if(c)throw new o(`User ${r.user} does not have write permission for branch ${c}`);const u=this.domainLinkAttribute();return u&&await this.guardDomainLinks(i,r,t,[...AuthzBase.valuesOf(e.add,u),...AuthzBase.valuesOf(e.replace,u)],u,!1),[t,await this.recordTransitOrigin(t,e),s,r]},ldapaddrequest:async([t,e,s])=>{if(this.shouldSkipAuthorization(s))return[t,e,s];const r=await this.resolveCaller(s);if(!r)return[t,e,s];let i;const n=this.config.ldap_organization_link_attribute,a=n?AuthzBase.organizationLinks(n,AuthzBase.valuesOf(e,n)):[],h=await this.storedParent(t);i=a.length>0?await this.storedDn(a[0]):this.extractBranchDn(h);if(!(await this.getUserPermissions(r,i)).write)throw new o(`User ${s.user} does not have write permission for branch ${i}`);const c=await this.refusingRule(r,h,["write"],!1);if(c)throw new o(`User ${s.user} does not have write permission for branch ${c}`);const u=this.domainLinkAttribute();return u&&await this.guardDomainLinks(r,s,t,AuthzBase.valuesOf(e,u),u,!0),[t,e,s]},ldapsearchrequest:async([t,e,s])=>{if(this.shouldSkipAuthorization(s))return[t,e,s];const r=await this.resolveCaller(s);if(!r)return[t,e,s];const i=await this.searchRefusal(r,t);if(i)throw new o(`User ${s.user} ${i}`);if(this.filtersAttachedEntries()){const r=this.config.ldap_organization_link_attribute;if(r&&!AuthzBase.wantsAttribute(e,r)){const t=e.attributes,s=Array.isArray(t)?t.map(String):[String(t)];e={...e,attributes:[...s,r],[AuthzBase.widenedForLink]:!0}}if(r&&!AuthzBase.wantsAttribute(e,"objectClass")){const t=e.attributes,s=Array.isArray(t)?t.map(String):[String(t)];e={...e,attributes:[...s,"objectClass"],[AuthzBase.widenedForObjectClass]:!0}}return[t,e,s]}return[t,e,s]},ldapsearchfilter:async([t,e,s])=>{const r=[t,e,s];if(this.shouldSkipAuthorization(e))return r;if(!t?.searchEntries?.length)return r;const i=await this.resolveCaller(e);if(!i)return r;if(!this.filtersAttachedEntries()){const s=await this.ruleBranches(i),n=[],a=new Map,o=new Set(s.map(d));for(const s of t.searchEntries){const t=String(s.dn),r=d(h(t)),c=o.has(d(t));let u=c?void 0:a.get(r);void 0===u&&(u=!0===(await this.getUserPermissions(i,t)).read,c||a.set(r,u)),u?n.push(s):this.logger.debug(`${this.name}: hiding ${t} from ${e.user}, refused by a narrower branch rule`)}return t.searchEntries=n,r}const n=this.globalBranches();if(n.length){const s=new Map,r=[];for(const a of t.searchEntries){const t=n.find(t=>c(a.dn,t));void 0===t||(s.has(t)||s.set(t,!0===(await this.getUserPermissions(i,t)).read),s.get(t))?r.push(a):this.logger.debug(`${this.name}: hiding ${String(a.dn)} from ${e.user}, in the global branch ${t}`)}t.searchEntries=r}const a=this.config.ldap_organization_link_attribute;if(!a)return r;const o=await this.getAuthorizedBranches(i);if(0===o.length)return r;const u=this.transitKey(),l=new Map,reads=async t=>{if(e=t,!o.some(t=>c(e,t)))return!1;var e;const s=d(t);return l.has(s)||l.set(s,!0===(await this.getUserPermissions(i,t)).read),l.get(s)},readsOrganisation=async t=>{const e=d(t);return l.has(e)||l.set(e,!0===(await this.getUserPermissions(i,t)).read),l.get(e)},f=this.config.ldap_top_organization,w=(this.config.ldap_organization_class??[]).filter(t=>"top"!==t.toLowerCase()).map(t=>t.toLowerCase()),organization=(t,e)=>{if(u&&d(t)===u)return!1;if(!f||!c(t,f))return!1;const s=[].concat(e.objectClass??[]).map(t=>String(t).toLowerCase());return w.some(t=>s.includes(t))},g=[];for(const s of t.searchEntries){const t=s[a],r=null==t?[]:(Array.isArray(t)?t:[t]).map(String);if(0===r.length){const t=String(s.dn);if(organization(t,s)&&!await readsOrganisation(t)){this.logger.debug(`${this.name}: hiding ${t} from ${e.user}, outside the branches they hold`);continue}g.push(s);continue}let i=!1;for(const t of r)if(await reads(t)){i=!0;break}i||r.some(t=>this.inTransit(t,u))?g.push(s):this.logger.debug(`${this.name}: hiding ${String(s.dn)} from ${e.user}, attached to ${r[0]}`)}if(t.searchEntries=g,s?.[AuthzBase.widenedForLink])for(const e of t.searchEntries)delete e[a];if(s?.[AuthzBase.widenedForObjectClass])for(const e of t.searchEntries)delete e.objectClass;return[t,e,s]},getOrganisationTop:async([t,e])=>{if(this.shouldSkipAuthorization(t))return[t,e];const s=await this.resolveCaller(t);if(!s)return[t,e];const r=await this.getAuthorizedBranches(s);if(r.length>0){const e=[];for(const s of r)try{const r=await this.server.ldap.search({paged:!1,scope:"base"},s,t);1===r.searchEntries.length&&e.push(r.searchEntries[0])}catch(t){this.logger.warn(`Failed to fetch authorized branch ${s}: ${t}`)}if(1===e.length)return[t,e[0]];if(e.length>1)return[t,e[0]]}return[t,e]},ldaprenamerequest:async([t,e,s])=>{if(this.shouldSkipAuthorization(s))return[t,e,s];const r=await this.resolveCaller(s);if(!r)return[t,e,s];const i=await this.storedDn(t),n=await this.storedParent(e),a=this.extractBranchDn(i),h=this.extractBranchDn(n),c=await this.getUserPermissions(r,a),u=this.sourcePermission(a);if(!c[u])throw new o(`User ${s.user} does not have ${u} permission for source branch ${a}`);const l=await this.refusingRule(r,i,["read","write"],!0);if(l)throw new o(`User ${s.user} does not have read and write permission for branch ${l}`);if(!(await this.getUserPermissions(r,h)).write)throw new o(`User ${s.user} does not have write permission for destination branch ${h}`);const d=await this.refusingRule(r,n,["write"],!1);if(d)throw new o(`User ${s.user} does not have write permission for branch ${d}`);return[t,e,s]},ldapdeleterequest:async([t,e])=>{if(this.shouldSkipAuthorization(e))return[t,e];const s=await this.resolveCaller(e);if(!s)return[t,e];const r=Array.isArray(t)?t:[t];for(const t of r){const r=await this.storedDn(t),i=await this.effectiveBranch(r);if(!(await this.getUserPermissions(s,i)).delete)throw new o(`User ${e.user} does not have delete permission for branch ${i}`);const n=await this.refusingRule(s,r,["delete"],!0);if(n)throw new o(`User ${e.user} does not have delete permission for branch ${n}`)}return[t,e]}}}export{AuthzBase as default,mostSpecificBranchRule};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BadRequestError as e}from"../errors.js";import{knownAttributeName as t}from"../utils.js";import{toDirectoryEntry as r,missingRequiredAttribute as i,toDirectoryChanges as s,schemaAttribute as a,tooLongValue as n,tooLongMessage as o,maxLengthOf as d,checkDnValues as c,matchesPattern as u,invalidValueMessage as
|
|
1
|
+
import{BadRequestError as e}from"../errors.js";import{knownAttributeName as t}from"../utils.js";import{toDirectoryEntry as r,missingRequiredAttribute as i,toDirectoryChanges as s,schemaAttribute as a,tooLongValue as n,tooLongMessage as o,maxLengthOf as d,checkDnValues as c,matchesPattern as u,invalidValueMessage as f}from"../../config/schema.js";class EntityValidator{schema;deps;constructor(e,t){this.schema=e,this.deps=t}async validateNew(t){const s=this.renameDeclared(t);if(this.dropUndeclaredEmpty(s),!this.schema)return r(void 0,s),s;this.refuseFixedOptions(s),applyFixedDefaults(this.schema,s),await this.checkValues(s);const a=i(this.schema,s,this.deps.loadedPlugins);if(a)throw new e(`Attribute "${a}" is required`);return r(this.schema,s),s}async validateChanges(t){t.add&&(t.add=this.renameDeclared(t.add)),t.replace&&(t.replace=this.renameDeclared(t.replace)),t.delete&&(t.delete=Array.isArray(t.delete)?this.renameDeclaredNames(t.delete):this.renameDeclared(t.delete));for(const e of["add","replace"]){const r=t[e];r&&("add"===e&&this.dropUndeclaredEmpty(r),await this.checkValues(r,e))}if(!this.schema)return s(void 0,t),t;if(t.delete){const whole=e=>{if(Array.isArray(t.delete))return!0;const r=t.delete[e];return!r||isNoValue(r)},r=Array.isArray(t.delete)?t.delete:Object.keys(t.delete);for(const t of r){const r=this.attribute(t);if(r?.fixed)throw new e(`Attribute "${t}" is fixed and cannot be deleted`);if(r?.required&&whole(t))throw new e(`Attribute "${t}" is required`)}}return s(this.schema,t),t}renameDeclared(e){const t=Object.keys(e),r=this.renameDeclaredNames(t);if(t.every((e,t)=>e===r[t]))return e;const i=Object.create(null);for(const[s,a]of r.entries())i[a]=e[t[s]];return i}renameDeclaredNames(t){const r=new Map;return t.map(t=>{const i=this.attributeIdentity(t),s=r.get(i);if(void 0!==s&&s!==t)throw new e(`"${s}" and "${t}" name the same attribute`);return r.set(i,t),this.schemaSpelling(t)})}attributeIdentity(e){const r=split(e)[0];return(a(this.schema,r)?.[0]??t(r)??r).toLowerCase()}schemaSpelling(e){const[t,r]=split(e),i=a(this.schema,t)?.[0];return void 0===i?e:r?`${i};${r}`:i}refuseFixedOptions(t){for(const r of Object.keys(t)){const[,t]=split(r),i=t?this.declaredAttribute(r):void 0;if(i?.fixed&&void 0!==i.default)throw new e(`Attribute "${r}" is fixed and cannot be modified`)}}declaredAttribute(e){const r=this.attributeIdentity(e);for(const[e,i]of Object.entries(this.schema?.attributes??{}))if((t(e)??e).toLowerCase()===r)return i}dropUndeclaredEmpty(e){for(const[t,r]of Object.entries(e))null!=r||this.attribute(t)||delete e[t]}async checkValues(t,r){for(const[i,s]of Object.entries(t)){const a=this.attribute(i);if(a?.fixed&&r)throw new e(`Attribute "${i}" is fixed and cannot be modified`);if(a?.required&&!r&&isMissing(s)&&!a.generated)throw new e(`Attribute "${i}" is required`);(a||r)&&(t[i]=asStored(await this.validateValue(i,s)))}}async validateValue(t,r){if(!this.schema)return r;const i=this.attribute(t);if(!i)return r;if(isNoValue(r)){if(i.required)throw new e(`Attribute "${t}" is required`);return r}let s=r;if("array"===i.type){Array.isArray(r)||(s=[r]);for(const e of s)assertScalar(t,e,i.items?.type)}else{if(Array.isArray(r)){if(r.length>1)throw new e(`Attribute "${t}" must be a single value`);s=r[0]}i.normalize&&"string"==typeof s||assertScalar(t,s,i.type)}if(void 0!==n(i,s))throw new e(o(t,d(i)));if(await c(t,i,s,this.deps.entryExists),!u(i,s))throw new e(f(t,i));return s}attribute(t){const r=a(this.schema,t)?.[1];if(!r&&this.schema?.strict)throw new e(`Unknown attribute "${t}" for ${this.deps.singularName}`);return r}}function applyFixedDefaults(e,t){if(e)for(const[r,i]of Object.entries(e.attributes))i.fixed&&void 0!==i.default&&"objectclass"!==r.toLowerCase()&&(t[r]=i.default)}const split=e=>{const[t,...r]=e.split(";");return[t,r.find(Boolean)?.toLowerCase()]},isNoValue=e=>null==e||Array.isArray(e)&&0===e.length,isMissing=e=>isNoValue(e)||""===e,asStored=e=>isNoValue(e)?[]:e,l=/^-?\d+(\.\d+)?$/,h=/^-?\d+$/;function assertScalar(t,r,i){const s="number"==typeof r&&Number.isFinite(r)?String(r):void 0,reads=e=>void 0!==s?e.test(s):"string"==typeof r&&e.test(r);if(!(()=>{switch(i){case"number":return reads(l)&&("number"!=typeof r||!Number.isInteger(r)||Number.isSafeInteger(r));case"integer":return reads(h)&&("number"!=typeof r||Number.isSafeInteger(r));case"boolean":return"boolean"==typeof r||"string"==typeof r&&("TRUE"===r||"FALSE"===r);default:return"string"==typeof r||Buffer.isBuffer(r)}})()){throw new e(`Attribute "${t}" must be ${"number"===i||"integer"===i||"boolean"===i?`a${"integer"===i?"n":""} ${i}`:"a string"}`)}}export{EntityValidator,applyFixedDefaults};
|
package/dist/lib/utils.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BadRequestError as e}from"./errors.js";import"./expressFormatedResponses.js";import"./metrics.js";import"./requestContext.js";const n=new Map;function getCompiledRegex(e,t){const r=t?`${e}:${t}`:e;let i=n.get(r);return i||(i=new RegExp(e,t),n.set(r,i)),i}function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const transformSchemas=(e,n)=>e.toString().replace(/__(\S+)__/g,(e,t)=>{if(!t||"string"!=typeof t)return e;const r=t.trim().toLowerCase(),i=n;return i[r]?"object"!=typeof i[r]?i[r]:JSON.stringify(i[r]):e});function escapeLdapFilter(e){return e.replace(/\\/g,"\\5c").replace(/\*/g,"\\2a").replace(/\(/g,"\\28").replace(/\)/g,"\\29").replace(/\0/g,"\\00")}function substringSearchFilter(n,t){const r=/^[a-zA-Z][a-zA-Z0-9-]*$/,i=t.split(",").map(e=>e.trim()).filter(Boolean);if(0===i.length||!i.every(e=>r.test(e)))throw new e("Invalid LDAP attribute name");const o=escapeLdapFilter(n),s=i.map(e=>`(${e}=*${o}*)`);return 1===s.length?s[0]:`(|${s.join("")})`}function fillPattern(e,n,t){return e.split(n).join(t)}function escapeDnValue(e){let n=e.replace(/[\\,+"<>;=]/g,e=>`\\${e}`).replace(/[\0-\x1f\x7f]/g,e=>`\\${hexByte(e.charCodeAt(0))}`);return e.startsWith(" ")&&(n=`\\${n}`),e.length>1&&e.endsWith(" ")&&(n=`${n.slice(0,-1)}\\ `),e.startsWith("#")&&(n=`\\${n}`),n}const t=new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0});function unescapeDnValue(n){if("string"!=typeof n)throw new e("DN value must be a string");let t="",r=0,i=[];const flushBytes=()=>{if(i.length){for(const e of utf8Chars(i))t+="number"==typeof e?String.fromCharCode(e):e;i=[]}};for(;r<n.length;)if("\\"===n[r]&&r+1<n.length){const e=n[r+1];if(/[0-9a-fA-F]/.test(e)&&r+2<n.length){const e=n.substring(r+1,r+3);if(/^[0-9a-fA-F]{2}$/.test(e)){i.push(parseInt(e,16)),r+=3;continue}}flushBytes(),t+=e,r+=2}else flushBytes(),t+=n[r],r++;return flushBytes(),t}function validateDnValue(e,n){if(null==e||"string"!=typeof e)throw new Error(`${n} must be a string`);if(0===e.trim().length)throw new Error(`${n} must be a non-empty string`);if(/[\x00-\x1F\x7F]/.test(e))throw new Error(`${n} contains invalid control characters`);if(/[\u200B-\u200F\uFEFF]/.test(e))throw new Error(`${n} contains invalid invisible characters`)}function asciiSpace(e){return" "===e||e>="\t"&&e<="\r"}function trimUnescaped(e){let n=0,t=e.length;for(;n<t&&asciiSpace(e[n]);)n++;for(;t>n&&asciiSpace(e[t-1]);)t--;if(t<e.length){let r=0;for(let i=t-1;i>=n&&"\\"===e[i];i--)r++;r%2==1&&t++}return e.slice(n,t)}function parseDn(n){if("string"!=typeof n)throw new e("DN must be a string");const t=[];let r="",i=!1,o=!1;for(let e=0;e<n.length;e++){const s=n[e];i?(r+=s,i=!1):"\\"===s?(r+=s,i=!0):'"'===s&&(o||opensValue(r))?(r+=s,o=!o):o||","!==s&&";"!==s?r+=s:(t.push(trimUnescaped(r)),r="")}if(o)throw new e("DN has an unbalanced quote");return r&&t.push(trimUnescaped(r)),t}function opensValue(e){const n=e.trimEnd();if(!n.endsWith("="))return!1;let t=0;for(let e=n.length-2;e>=0&&"\\"===n[e];e--)t++;return t%2==0}function isDnSyntax(e){let n;try{n=parseDn(e)}catch{return!1}if(0===n.length)return!1;const t=/^\s*(?:[A-Za-z][A-Za-z0-9-]*|(?:OID\.)?\d+(?:\.\d+)+)\s*=/i;return n.every(e=>{if(!e)return!1;const n=[];let r="",i=!1;for(const t of e){if(i)i=!1;else if("\\"===t)i=!0;else if("+"===t){n.push(r),r="";continue}r+=t}return n.push(r),n.every(e=>t.test(e))})}const r=new Set(['"',"+",",",";","<",">","\\","#","="," "]);function hasOnlyRfc4514Escapes(e){for(let n=0;n<e.length;n++){if("\\"!==e[n])continue;const t=e[n+1];if(void 0===t)return!1;if(r.has(t))n++;else{if(!/^[0-9a-fA-F]{2}$/.test(e.slice(n+1,n+3)))return!1;n+=2}}return!0}function assertRfc4514Escapes(n,t){if(!hasOnlyRfc4514Escapes(n))throw new e(`${t} uses an escape RFC 4514 does not define`)}function getParentDn(e){const n=parseDn(e);return n.length<=1?e:n.slice(1).join(",")}function getRdn(e){return parseDn(e)[0]||""}function rdnValue(e){const[n]=parseDn(e);return rdnValueOf(n)}function unquotedValue(e){let n=0;for(let t=e.length-2;t>0&&"\\"===e[t];t--)n++;return e.length>=2&&e.startsWith('"')&&e.endsWith('"')&&n%2==0?e.slice(1,-1):e}function rdnValueOf(e){if(!e)return"";const n=e.indexOf("=");if(n<=0)return"";let t=n+1;for(;t<e.length&&asciiSpace(e[t]);)t++;return unescapeDnValue(unquotedValue(e.slice(t)))}function dropSpaces(e){const n=e.filter((n,t)=>" "!==n||" "!==e[t-1]);let t=0,r=n.length;for(;t<r&&" "===n[t];)t++;for(;r>t&&" "===n[r-1];)r--;return n.slice(t,r)}function assertedRdnValue(e,n,t){const[r]=parseDn(e),i=r?r.indexOf("="):-1;return i<=0||r.slice(0,i).trim().toLowerCase()!==n.toLowerCase()||splitRdn(r).length>1?null:void 0===t||isDirectChildOf(e,t)?dropSpaces([...rdnValueOf(r).normalize("NFKC")]).join(""):null}function namesByDn(e,n){try{const[t]=parseDn(e),r=t?t.indexOf("="):-1;return r>0&&t.slice(0,r).trim().toLowerCase()===n.toLowerCase()}catch{return!1}}function isChildOf(e,n){try{return isDnInBranch(e,n)&&!isDnInBranch(n,e)}catch{return!1}}function isDirectChildOf(e,n){try{const[,...t]=parseDn(e);return t.length>0&&normalizeDn(t.join(","))===normalizeDn(n)}catch{return!1}}const i=/((?:\\[0-9a-fA-F]{2})+)|\\([^])/g;function hexByte(e){return e.toString(16).padStart(2,"0")}function utf8Chars(e){const n=[],r=Uint8Array.from(e);for(let i=0;i<e.length;){const o=utf8Length(e[i]);let s;if(o&&i+o<=e.length)try{s=t.decode(r.subarray(i,i+o))}catch{}void 0===s?n.push(e[i++]):(n.push(...s),i+=o)}return n}function utf8Length(e){return e<128?1:e>>5==6?2:e>>4==14?3:e>>3==30?4:0}const o=new Map,s=new Map;function registerAttributeTypes(e){for(const{oid:n,names:t}of e){const e=(t[0]??n).toLowerCase();for(const r of[n,...t])o.set(r.toLowerCase(),e),s.set(r.toLowerCase(),t[0]??n)}}function knownAttributeName(e){return s.get(e.toLowerCase().replace(/^oid\./,""))}function rdnTypes(e){return splitRdn(parseDn(e)[0]??"").map(e=>{const n=e.indexOf("=");return-1===n?"":canonicalType(e.slice(0,n).trim())})}function canonicalType(e){const n=e.toLowerCase().replace(/^oid\./,"");return o.get(n)??n}function splitRdn(e){const n=[];let t="",r=!1,i=!1;for(const o of e){if(r)r=!1;else if("\\"===o)r=!0;else if('"'===o&&(i||opensValue(t)))i=!i;else if("+"===o&&!i){n.push(t),t="";continue}t+=o}return n.push(t),n}function normalizeDn(e){return normalizedRdns(e).join(",")}function canonicalDn(e){return normalizedRdns(e,!1).join(",")}function normalizedRdns(e,n=!0){return parseDn(e).map(e=>splitRdn(e).map(e=>{const t=e.indexOf("=");if(-1===t)return e.trim().toLowerCase();const r=function(e,n=!0){const t=trimUnescaped(e);if(/^#(?:[0-9a-fA-F]{2})+$/.test(t))return t;e=unquotedValue(t);const r=[];let o=0;const plain=e=>{for(const n of e)r.push(n)};for(const n of e.matchAll(i)){plain(e.slice(o,n.index)),o=n.index+n[0].length;const[,t,i]=n;if(void 0!==i){r.push(i);continue}const s=t.split("\\").filter(Boolean).map(e=>parseInt(e,16));for(const e of utf8Chars(s))"number"==typeof e?r.push(`\\${hexByte(e)}`):plain(e)}o<e.length&&plain(e.slice(o));const kept=e=>3===e.length&&"\\"===e[0],fold=e=>(n?e.toLowerCase():e).normalize("NFKC"),s=[];let a="";for(const e of r)kept(e)?(s.push(...fold(a),e),a=""):a+=e;s.push(...fold(a));const c=dropSpaces(s);let u="";for(const[e,n]of c.entries())kept(n)?u+=n:n.charCodeAt(0)<32||127===n.charCodeAt(0)?u+=`\\${hexByte(n.charCodeAt(0))}`:',+"\\<>;='.includes(n)||0===e&&"#"===n?u+=`\\${n}`:u+=n;return u}(e.slice(t+1),n);return`${canonicalType(e.slice(0,t).trim())}=${n?r.toLowerCase():r}`}).sort().join("+")).filter(e=>e.length>0)}function isDummyMemberDn(e,n){if(!n||"string"!=typeof e)return!1;if(e===n)return!0;try{return normalizeDn(e)===normalizeDn(n)}catch{return!1}}function organizationLink(e){for(const n of Array.isArray(e)?e:[e]){if(null==n)continue;const e=String(n).trim();if(e.length>0)return e}}function isDnInBranch(e,n){const t=normalizedRdns(n);if(0===t.length)return!1;const r=normalizedRdns(e);if(r.length<t.length)return!1;return r.slice(r.length-t.length).every((e,n)=>e===t[n])}function movedToTrash(e,n,t){return!!e&&isDnInBranch(t,e)&&!isDnInBranch(n,e)}function parseDirectoryDate(e){const n=String(e).trim(),t=/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})?(\d{2})?(?:[.,](\d+))?(Z|[+-]\d{2}(?::?\d{2})?)?$/.exec(n);if(!t){const e=new Date(n);return isNaN(e.getTime())?null:e}const[,r,i,o,s,a,c,u,l]=t,[f,d,p,m,h,g]=[r,i,o,s,a,c].map(e=>void 0===e?0:Number(e));if(d<1||d>12||m>23||h>59||g>60)return null;const D=new Date(0);if(D.setUTCFullYear(f,d-1,p),D.getUTCMonth()!==d-1||D.getUTCDate()!==p)return null;D.setUTCHours(m,h,g);let y=D.getTime();if(void 0!==u){const e=void 0!==c?1e3:void 0!==a?6e4:36e5;y+=Math.round(Number(`0.${u}`)*e)}if(l&&"Z"!==l){const e=l.slice(1).replace(":",""),n=Number(e.slice(0,2)),t=e.length>2?Number(e.slice(2,4)):0;if(n>23||t>59)return null;y-=("-"===l[0]?-1:1)*(60*n+t)*6e4}return new Date(y)}registerAttributeTypes([{oid:"2.5.4.0",names:["objectClass"]},{oid:"2.5.4.3",names:["cn","commonName"]},{oid:"2.5.4.4",names:["sn","surname"]},{oid:"2.5.4.5",names:["serialNumber"]},{oid:"2.5.4.6",names:["c","countryName"]},{oid:"2.5.4.7",names:["l","localityName"]},{oid:"2.5.4.8",names:["st","stateOrProvinceName"]},{oid:"2.5.4.9",names:["street","streetAddress"]},{oid:"2.5.4.10",names:["o","organizationName"]},{oid:"2.5.4.11",names:["ou","organizationalUnitName"]},{oid:"2.5.4.12",names:["title"]},{oid:"2.5.4.42",names:["givenName","gn"]},{oid:"2.5.4.43",names:["initials"]},{oid:"2.5.4.46",names:["dnQualifier"]},{oid:"2.5.4.65",names:["pseudonym"]},{oid:"0.9.2342.19200300.100.1.1",names:["uid","userid"]},{oid:"0.9.2342.19200300.100.1.3",names:["mail","rfc822Mailbox"]},{oid:"0.9.2342.19200300.100.1.25",names:["dc","domainComponent"]},{oid:"2.16.840.1.113730.3.1.241",names:["displayName"]},{oid:"2.16.840.1.113730.3.1.3",names:["employeeNumber"]},{oid:"1.3.6.1.1.1.1.0",names:["uidNumber"]},{oid:"1.3.6.1.1.1.1.1",names:["gidNumber"]}]);const asyncHandler=e=>(n,t,r)=>{Promise.resolve(e(n,t,r)).catch(r)};export{assertRfc4514Escapes,assertedRdnValue,asyncHandler,canonicalDn,escapeDnValue,escapeLdapFilter,escapeRegex,fillPattern,getCompiledRegex,getParentDn,getRdn,hasOnlyRfc4514Escapes,isChildOf,isDirectChildOf,isDnInBranch,isDnSyntax,isDummyMemberDn,knownAttributeName,movedToTrash,namesByDn,normalizeDn,organizationLink,parseDirectoryDate,parseDn,rdnTypes,rdnValue,registerAttributeTypes,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue};
|
|
1
|
+
import{BadRequestError as e}from"./errors.js";import"./expressFormatedResponses.js";import"./metrics.js";import"./requestContext.js";const n=new Map;function getCompiledRegex(e,t){const r=t?`${e}:${t}`:e;let i=n.get(r);return i||(i=new RegExp(e,t),n.set(r,i)),i}function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const transformSchemas=(e,n)=>e.toString().replace(/__(\S+)__/g,(e,t)=>{if(!t||"string"!=typeof t)return e;const r=t.trim().toLowerCase(),i=n;return i[r]?"object"!=typeof i[r]?i[r]:JSON.stringify(i[r]):e});function escapeLdapFilter(e){return e.replace(/\\/g,"\\5c").replace(/\*/g,"\\2a").replace(/\(/g,"\\28").replace(/\)/g,"\\29").replace(/\0/g,"\\00")}function substringSearchFilter(n,t){const r=/^[a-zA-Z][a-zA-Z0-9-]*$/,i=t.split(",").map(e=>e.trim()).filter(Boolean);if(0===i.length||!i.every(e=>r.test(e)))throw new e("Invalid LDAP attribute name");const s=escapeLdapFilter(n),o=i.map(e=>`(${e}=*${s}*)`);return 1===o.length?o[0]:`(|${o.join("")})`}function fillPattern(e,n,t){return e.split(n).join(t)}function escapeDnValue(e){let n=e.replace(/[\\,+"<>;=]/g,e=>`\\${e}`).replace(/[\0-\x1f\x7f]/g,e=>`\\${hexByte(e.charCodeAt(0))}`);return e.startsWith(" ")&&(n=`\\${n}`),e.length>1&&e.endsWith(" ")&&(n=`${n.slice(0,-1)}\\ `),e.startsWith("#")&&(n=`\\${n}`),n}const t=new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0});function unescapeDnValue(n){if("string"!=typeof n)throw new e("DN value must be a string");let t="",r=0,i=[];const flushBytes=()=>{if(i.length){for(const e of utf8Chars(i))t+="number"==typeof e?String.fromCharCode(e):e;i=[]}};for(;r<n.length;)if("\\"===n[r]&&r+1<n.length){const e=n[r+1];if(/[0-9a-fA-F]/.test(e)&&r+2<n.length){const e=n.substring(r+1,r+3);if(/^[0-9a-fA-F]{2}$/.test(e)){i.push(parseInt(e,16)),r+=3;continue}}flushBytes(),t+=e,r+=2}else flushBytes(),t+=n[r],r++;return flushBytes(),t}function validateDnValue(e,n){if(null==e||"string"!=typeof e)throw new Error(`${n} must be a string`);if(0===e.trim().length)throw new Error(`${n} must be a non-empty string`);if(/[\x00-\x1F\x7F]/.test(e))throw new Error(`${n} contains invalid control characters`);if(/[\u200B-\u200F\uFEFF]/.test(e))throw new Error(`${n} contains invalid invisible characters`)}function asciiSpace(e){return" "===e||e>="\t"&&e<="\r"}function trimUnescaped(e){let n=0,t=e.length;for(;n<t&&asciiSpace(e[n]);)n++;for(;t>n&&asciiSpace(e[t-1]);)t--;if(t<e.length){let r=0;for(let i=t-1;i>=n&&"\\"===e[i];i--)r++;r%2==1&&t++}return e.slice(n,t)}function parseDn(n){if("string"!=typeof n)throw new e("DN must be a string");const t=[];let r="",i=!1,s=!1;for(let e=0;e<n.length;e++){const o=n[e];i?(r+=o,i=!1):"\\"===o?(r+=o,i=!0):'"'===o&&(s||opensValue(r))?(r+=o,s=!s):s||","!==o&&";"!==o?r+=o:(t.push(trimUnescaped(r)),r="")}if(s)throw new e("DN has an unbalanced quote");return r&&t.push(trimUnescaped(r)),t}function opensValue(e){const n=e.trimEnd();if(!n.endsWith("="))return!1;let t=0;for(let e=n.length-2;e>=0&&"\\"===n[e];e--)t++;return t%2==0}function isDnSyntax(e){let n;try{n=parseDn(e)}catch{return!1}if(0===n.length)return!1;const t=/^\s*(?:[A-Za-z][A-Za-z0-9-]*|(?:OID\.)?\d+(?:\.\d+)+)\s*=/i;return n.every(e=>{if(!e)return!1;const n=[];let r="",i=!1;for(const t of e){if(i)i=!1;else if("\\"===t)i=!0;else if("+"===t){n.push(r),r="";continue}r+=t}return n.push(r),n.every(e=>{if(!t.test(e))return!1;return!trimUnescaped(e.slice(e.indexOf("=")+1)).startsWith("#")})})}const r=new Set(['"',"+",",",";","<",">","\\","#","="," "]);function hasOnlyRfc4514Escapes(e){for(let n=0;n<e.length;n++){if("\\"!==e[n])continue;const t=e[n+1];if(void 0===t)return!1;if(r.has(t))n++;else{if(!/^[0-9a-fA-F]{2}$/.test(e.slice(n+1,n+3)))return!1;n+=2}}return!0}function assertRfc4514Escapes(n,t){if(!hasOnlyRfc4514Escapes(n))throw new e(`${t} uses an escape RFC 4514 does not define`)}function getParentDn(e){const n=parseDn(e);return n.length<=1?e:n.slice(1).join(",")}function getRdn(e){return parseDn(e)[0]||""}function rdnValue(e){const[n]=parseDn(e);return rdnValueOf(n)}function unquotedValue(e){let n=0;for(let t=e.length-2;t>0&&"\\"===e[t];t--)n++;return e.length>=2&&e.startsWith('"')&&e.endsWith('"')&&n%2==0?e.slice(1,-1):e}function rdnValueOf(e){if(!e)return"";const n=e.indexOf("=");if(n<=0)return"";let t=n+1;for(;t<e.length&&asciiSpace(e[t]);)t++;return unescapeDnValue(unquotedValue(e.slice(t)))}function dropSpaces(e){const n=e.filter((n,t)=>" "!==n||" "!==e[t-1]);let t=0,r=n.length;for(;t<r&&" "===n[t];)t++;for(;r>t&&" "===n[r-1];)r--;return n.slice(t,r)}function assertedRdnValue(e,n,t){const[r]=parseDn(e),i=r?r.indexOf("="):-1;return i<=0||r.slice(0,i).trim().toLowerCase()!==n.toLowerCase()||splitRdn(r).length>1?null:void 0===t||isDirectChildOf(e,t)?dropSpaces([...rdnValueOf(r).normalize("NFKC")]).join(""):null}function namesByDn(e,n){try{const[t]=parseDn(e),r=t?t.indexOf("="):-1;return r>0&&t.slice(0,r).trim().toLowerCase()===n.toLowerCase()}catch{return!1}}function isChildOf(e,n){try{return isDnInBranch(e,n)&&!isDnInBranch(n,e)}catch{return!1}}function isDirectChildOf(e,n){try{const[,...t]=parseDn(e);return t.length>0&&normalizeDn(t.join(","))===normalizeDn(n)}catch{return!1}}const i=/((?:\\[0-9a-fA-F]{2})+)|\\([^])/g;function hexByte(e){return e.toString(16).padStart(2,"0")}function utf8Chars(e){const n=[],r=Uint8Array.from(e);for(let i=0;i<e.length;){const s=utf8Length(e[i]);let o;if(s&&i+s<=e.length)try{o=t.decode(r.subarray(i,i+s))}catch{}void 0===o?n.push(e[i++]):(n.push(...o),i+=s)}return n}function utf8Length(e){return e<128?1:e>>5==6?2:e>>4==14?3:e>>3==30?4:0}const s=new Map,o=new Map;function registerAttributeTypes(e){for(const{oid:n,names:t}of e){const e=(t[0]??n).toLowerCase();for(const r of[n,...t])s.set(r.toLowerCase(),e),o.set(r.toLowerCase(),t[0]??n)}}function knownAttributeName(e){return o.get(e.toLowerCase().replace(/^oid\./,""))}function rdnTypes(e){return splitRdn(parseDn(e)[0]??"").map(e=>{const n=e.indexOf("=");return-1===n?"":canonicalType(e.slice(0,n).trim())})}function canonicalType(e){const n=e.toLowerCase().replace(/^oid\./,"");return s.get(n)??n}function splitRdn(e){const n=[];let t="",r=!1,i=!1;for(const s of e){if(r)r=!1;else if("\\"===s)r=!0;else if('"'===s&&(i||opensValue(t)))i=!i;else if("+"===s&&!i){n.push(t),t="";continue}t+=s}return n.push(t),n}function normalizeDn(e){return normalizedRdns(e).join(",")}function canonicalDn(e){return normalizedRdns(e,!1).join(",")}function normalizedRdns(e,n=!0){return parseDn(e).map(e=>splitRdn(e).map(e=>{const t=e.indexOf("=");if(-1===t)return e.trim().toLowerCase();const r=function(e,n=!0){const t=trimUnescaped(e);if(/^#(?:[0-9a-fA-F]{2})+$/.test(t))return t;e=unquotedValue(t);const r=[];let s=0;const plain=e=>{for(const n of e)r.push(n)};for(const n of e.matchAll(i)){plain(e.slice(s,n.index)),s=n.index+n[0].length;const[,t,i]=n;if(void 0!==i){r.push(i);continue}const o=t.split("\\").filter(Boolean).map(e=>parseInt(e,16));for(const e of utf8Chars(o))"number"==typeof e?r.push(`\\${hexByte(e)}`):plain(e)}s<e.length&&plain(e.slice(s));const kept=e=>3===e.length&&"\\"===e[0],fold=e=>(n?e.toLowerCase():e).normalize("NFKC"),o=[];let a="";for(const e of r)kept(e)?(o.push(...fold(a),e),a=""):a+=e;o.push(...fold(a));const c=dropSpaces(o);let u="";for(const[e,n]of c.entries())kept(n)?u+=n:n.charCodeAt(0)<32||127===n.charCodeAt(0)?u+=`\\${hexByte(n.charCodeAt(0))}`:',+"\\<>;='.includes(n)||0===e&&"#"===n?u+=`\\${n}`:u+=n;return u}(e.slice(t+1),n);return`${canonicalType(e.slice(0,t).trim())}=${n?r.toLowerCase():r}`}).sort().join("+")).filter(e=>e.length>0)}function isDummyMemberDn(e,n){if(!n||"string"!=typeof e)return!1;if(e===n)return!0;try{return normalizeDn(e)===normalizeDn(n)}catch{return!1}}function organizationLink(e){for(const n of Array.isArray(e)?e:[e]){if(null==n)continue;const e=String(n).trim();if(e.length>0)return e}}function isDnInBranch(e,n){const t=normalizedRdns(n);if(0===t.length)return!1;const r=normalizedRdns(e);if(r.length<t.length)return!1;return r.slice(r.length-t.length).every((e,n)=>e===t[n])}function movedToTrash(e,n,t){return!!e&&isDnInBranch(t,e)&&!isDnInBranch(n,e)}function parseDirectoryDate(e){const n=String(e).trim(),t=/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})?(\d{2})?(?:[.,](\d+))?(Z|[+-]\d{2}(?::?\d{2})?)?$/.exec(n);if(!t){const e=new Date(n);return isNaN(e.getTime())?null:e}const[,r,i,s,o,a,c,u,l]=t,[f,d,p,m,h,g]=[r,i,s,o,a,c].map(e=>void 0===e?0:Number(e));if(d<1||d>12||m>23||h>59||g>60)return null;const D=new Date(0);if(D.setUTCFullYear(f,d-1,p),D.getUTCMonth()!==d-1||D.getUTCDate()!==p)return null;D.setUTCHours(m,h,g);let y=D.getTime();if(void 0!==u){const e=void 0!==c?1e3:void 0!==a?6e4:36e5;y+=Math.round(Number(`0.${u}`)*e)}if(l&&"Z"!==l){const e=l.slice(1).replace(":",""),n=Number(e.slice(0,2)),t=e.length>2?Number(e.slice(2,4)):0;if(n>23||t>59)return null;y-=("-"===l[0]?-1:1)*(60*n+t)*6e4}return new Date(y)}registerAttributeTypes([{oid:"2.5.4.0",names:["objectClass"]},{oid:"2.5.4.3",names:["cn","commonName"]},{oid:"2.5.4.4",names:["sn","surname"]},{oid:"2.5.4.5",names:["serialNumber"]},{oid:"2.5.4.6",names:["c","countryName"]},{oid:"2.5.4.7",names:["l","localityName"]},{oid:"2.5.4.8",names:["st","stateOrProvinceName"]},{oid:"2.5.4.9",names:["street","streetAddress"]},{oid:"2.5.4.10",names:["o","organizationName"]},{oid:"2.5.4.11",names:["ou","organizationalUnitName"]},{oid:"2.5.4.12",names:["title"]},{oid:"2.5.4.42",names:["givenName","gn"]},{oid:"2.5.4.43",names:["initials"]},{oid:"2.5.4.46",names:["dnQualifier"]},{oid:"2.5.4.65",names:["pseudonym"]},{oid:"0.9.2342.19200300.100.1.1",names:["uid","userid"]},{oid:"0.9.2342.19200300.100.1.3",names:["mail","rfc822Mailbox"]},{oid:"0.9.2342.19200300.100.1.25",names:["dc","domainComponent"]},{oid:"2.16.840.1.113730.3.1.241",names:["displayName"]},{oid:"2.16.840.1.113730.3.1.3",names:["employeeNumber"]},{oid:"1.3.6.1.1.1.1.0",names:["uidNumber"]},{oid:"1.3.6.1.1.1.1.1",names:["gidNumber"]}]);const asyncHandler=e=>(n,t,r)=>{Promise.resolve(e(n,t,r)).catch(r)};export{assertRfc4514Escapes,assertedRdnValue,asyncHandler,canonicalDn,escapeDnValue,escapeLdapFilter,escapeRegex,fillPattern,getCompiledRegex,getParentDn,getRdn,hasOnlyRfc4514Escapes,isChildOf,isDirectChildOf,isDnInBranch,isDnSyntax,isDummyMemberDn,knownAttributeName,movedToTrash,namesByDn,normalizeDn,organizationLink,parseDirectoryDate,parseDn,rdnTypes,rdnValue,registerAttributeTypes,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{defineOptions as _,opt as t,readOptions as e}from"../../config/options.js";const n=_({auth_token:t.array({env:"DM_AUTH_TOKENS",default:[],plural:"auth-tokens"})}),a=_({auth_fake_user:t.string({env:"DM_AUTH_FAKE_USER",default:""})}),r=_({auth_totp:t.array({env:"DM_AUTH_TOTP",default:[],plural:"auth-totps",kind:"phrases"}),auth_totp_window:t.number({env:"DM_AUTH_TOTP_WINDOW",default:1}),auth_totp_step:t.number({env:"DM_AUTH_TOTP_STEP",default:30,min:1})}),i=_({auth_hmac:t.array({env:"DM_AUTH_HMAC",default:[],plural:"auth-hmacs",kind:"phrases"}),auth_hmac_window:t.number({env:"DM_AUTH_HMAC_WINDOW",default:12e4,min:1})}),u=_({llng_ini:t.string({env:"DM_LLNG_INI",default:"/etc/lemonldap-ng/lemonldap-ng.ini"}),llng_username_header:t.string({env:"DM_LLNG_USERNAME_HEADER",default:""})}),l=_({authz_per_route:t.array({env:"DM_AUTHZ_PER_ROUTES",default:[],plural:"authz-per-routes"})}),d=_({authz_per_branch_config:t.json({env:"DM_AUTHZ_PER_BRANCH_CONFIG",default:{default:{read:!0,write:!1,delete:!1}}}),authz_per_branch_cache_ttl:t.number({env:"DM_AUTHZ_PER_BRANCH_CACHE_TTL",default:60,min:1}),ldap_group_member_attribute:t.string({env:"DM_LDAP_GROUP_MEMBER_ATTRIBUTE",default:"member"})}),
|
|
1
|
+
import{defineOptions as _,opt as t,readOptions as e}from"../../config/options.js";const n=_({auth_token:t.array({env:"DM_AUTH_TOKENS",default:[],plural:"auth-tokens"})}),a=_({auth_fake_user:t.string({env:"DM_AUTH_FAKE_USER",default:""})}),r=_({auth_totp:t.array({env:"DM_AUTH_TOTP",default:[],plural:"auth-totps",kind:"phrases"}),auth_totp_window:t.number({env:"DM_AUTH_TOTP_WINDOW",default:1}),auth_totp_step:t.number({env:"DM_AUTH_TOTP_STEP",default:30,min:1})}),i=_({auth_hmac:t.array({env:"DM_AUTH_HMAC",default:[],plural:"auth-hmacs",kind:"phrases"}),auth_hmac_window:t.number({env:"DM_AUTH_HMAC_WINDOW",default:12e4,min:1})}),u=_({llng_ini:t.string({env:"DM_LLNG_INI",default:"/etc/lemonldap-ng/lemonldap-ng.ini"}),llng_username_header:t.string({env:"DM_LLNG_USERNAME_HEADER",default:""})}),l=_({authz_per_route:t.array({env:"DM_AUTHZ_PER_ROUTES",default:[],plural:"authz-per-routes"})}),d=_({authz_per_branch_config:t.json({env:"DM_AUTHZ_PER_BRANCH_CONFIG",default:{default:{read:!0,write:!1,delete:!1}}}),authz_per_branch_cache_ttl:t.number({env:"DM_AUTHZ_PER_BRANCH_CACHE_TTL",default:60,min:1}),ldap_group_member_attribute:t.string({env:"DM_LDAP_GROUP_MEMBER_ATTRIBUTE",default:"member"})}),o=_({authz_dynamic_bypass:t.array({env:"DM_AUTHZ_DYNAMIC_BYPASS",default:[],kind:"identifiers"}),authz_dynamic_base:t.string({env:"DM_AUTHZ_DYNAMIC_BASE",default:""}),authz_dynamic_cache_ttl:t.number({env:"DM_AUTHZ_DYNAMIC_CACHE_TTL",default:60,min:1}),authz_dynamic_token_attribute:t.string({env:"DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE",default:"userPassword"}),authz_dynamic_config_attribute:t.string({env:"DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE",default:"description"}),authz_dynamic_tenant_attribute:t.string({env:"DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE",default:"cn"}),authz_dynamic_reload_endpoint:t.boolean({env:"DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",default:!1})}),s=_({authz_local_admin_attribute:t.string({env:"DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE",default:"twakeLocalAdminLink"}),authz_global_branch:t.array({env:"DM_AUTHZ_GLOBAL_BRANCHES",default:[],plural:"authz-global-branches",kind:"dns"}),authz_global_write_branch:t.array({env:"DM_AUTHZ_GLOBAL_WRITE_BRANCHES",default:[],plural:"authz-global-write-branches",kind:"dns"}),authz_global_organization:t.string({env:"DM_AUTHZ_GLOBAL_ORGANIZATION",default:""})}),T=_({oidc_server:t.string({env:"DM_OIDC_SERVER",default:""}),oidc_username_claim:t.string({env:"DM_OIDC_USERNAME_CLAIM",default:"sub"}),oidc_client_id:t.string({env:"DM_OIDC_CLIENT_ID",default:""}),oidc_client_secret:t.string({env:"DM_OIDC_CLIENT_SECRET",default:""}),base_url:t.string({env:"DM_BASE_URL",default:""}),oidc_logout_origins:t.array({env:"DM_OIDC_LOGOUT_ORIGINS",default:[],plural:"oidc-logout-origins"})}),A=_({rate_limit_window_ms:t.number({env:"DM_RATE_LIMIT_WINDOW_MS",default:9e5,min:1}),rate_limit_max:t.number({env:"DM_RATE_LIMIT_MAX",default:100,min:1})}),c=_({crowdsec_url:t.string({env:"DM_CROWDSEC_URL",default:"http://localhost:8080/v1/decisions"}),crowdsec_api_key:t.string({env:"DM_CROWDSEC_API_KEY",default:""}),crowdsec_cache_ttl:t.number({env:"DM_CROWDSEC_CACHE_TTL",default:60,min:1})}),D=_({trusted_proxy:t.array({env:"DM_TRUSTED_PROXIES",default:[],plural:"trusted-proxies",kind:"identifiers"}),trusted_proxy_auth_header:t.string({env:"DM_TRUSTED_PROXY_AUTH_HEADER",default:"Auth-User"})});function totpOptionsOf(_){return e(r,_)}function hmacOptionsOf(_){return e(i,_)}function perBranchOptionsOf(_){return e(d,_)}function dynamicOptionsOf(_){return e(o,_)}function rateLimitOptionsOf(_){return e(A,_)}function crowdsecOptionsOf(_){return e(c,_)}export{c as crowdsecOptions,crowdsecOptionsOf,o as dynamicOptions,dynamicOptionsOf,a as fakeOptions,i as hmacOptions,hmacOptionsOf,s as linid1Options,u as llngOptions,T as openidconnectOptions,d as perBranchOptions,perBranchOptionsOf,l as perRouteOptions,A as rateLimitOptions,rateLimitOptionsOf,n as tokenOptions,r as totpOptions,totpOptionsOf,D as trustedProxyOptions};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import"../../abstract/plugin.js";import t from"../../lib/auth/base.js";import{runTogether as e}from"../../lib/authz/composition.js";import{AuthzRefusedError as i}from"../../lib/errors.js";import{unauthorized as n,ok as s}from"../../lib/expressFormatedResponses.js";import{getParentDn as a,isDnInBranch as r}from"../../lib/utils.js";import{dynamicOptions as o}from"./auth.options.js";import{verifyLdapPassword as h}from"./authzDynamicHash.js";import{defineRoute as u}from"../../lib/route.js";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)}}class AuthzDynamic extends t{static optionDefinitions=o;name="authzDynamic";roles=["auth","authz","configurable"];tokens=[];lastLoad=0;lastFailure=0;cacheTtlMs;failureBackoffMs;base;tokenAttr;configAttr;tenantAttr;reloadEndpoint;loading;constructor(t){super(t);const e=this.options(o);if(this.base=e.authz_dynamic_base,this.cacheTtlMs=1e3*e.authz_dynamic_cache_ttl,this.failureBackoffMs=Math.max(Math.floor(this.cacheTtlMs/4),5e3),this.tokenAttr=e.authz_dynamic_token_attribute,this.configAttr=e.authz_dynamic_config_attribute,this.tenantAttr=e.authz_dynamic_tenant_attribute,this.reloadEndpoint=e.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";u(t,{method:"post",path:`${e}/v1/authz-dynamic/reload`,handler:async({req:t,res:e})=>{t.authzToken?(await this.reload(),s(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.system.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)||"",s=asString(t[this.tokenAttr])||"";if(!s)continue;let a,r=[];const o=asString(t[this.configAttr]);if(o)try{const t=JSON.parse(o);t&&"string"==typeof t.tenant&&t.tenant&&(a=t.tenant),t&&Array.isArray(t.bases)&&(r=t.bases.filter(t=>t&&"string"==typeof t.dn&&t.dn.length>0).map(t=>({dn:t.dn,read:
|
|
1
|
+
import"../../abstract/plugin.js";import t from"../../lib/auth/base.js";import{runTogether as e}from"../../lib/authz/composition.js";import{AuthzRefusedError as i}from"../../lib/errors.js";import{unauthorized as n,ok as s}from"../../lib/expressFormatedResponses.js";import{getParentDn as a,isDnInBranch as r}from"../../lib/utils.js";import{dynamicOptions as o}from"./auth.options.js";import{verifyLdapPassword as h}from"./authzDynamicHash.js";import{defineRoute as u}from"../../lib/route.js";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)}}class AuthzDynamic extends t{static optionDefinitions=o;name="authzDynamic";roles=["auth","authz","configurable"];tokens=[];lastLoad=0;lastFailure=0;cacheTtlMs;failureBackoffMs;base;tokenAttr;configAttr;tenantAttr;reloadEndpoint;loading;constructor(t){super(t);const e=this.options(o);if(this.base=e.authz_dynamic_base,this.cacheTtlMs=1e3*e.authz_dynamic_cache_ttl,this.failureBackoffMs=Math.max(Math.floor(this.cacheTtlMs/4),5e3),this.tokenAttr=e.authz_dynamic_token_attribute,this.configAttr=e.authz_dynamic_config_attribute,this.tenantAttr=e.authz_dynamic_tenant_attribute,this.reloadEndpoint=e.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";u(t,{method:"post",path:`${e}/v1/authz-dynamic/reload`,handler:async({req:t,res:e})=>{t.authzToken?(await this.reload(),s(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.system.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)||"",s=asString(t[this.tokenAttr])||"";if(!s)continue;let a,r=[];const o=asString(t[this.configAttr]);if(o)try{const t=JSON.parse(o);t&&"string"==typeof t.tenant&&t.tenant&&(a=t.tenant),t&&Array.isArray(t.bases)&&(r=t.bases.filter(t=>t&&"string"==typeof t.dn&&t.dn.length>0).map(t=>({dn:t.dn,read:this.aclFlag(t.read,"read",t.dn,e),write:this.aclFlag(t.write,"write",t.dn,e),delete:this.aclFlag(t.delete,"delete",t.dn,e)})))}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,u=a||h;i.push({dn:e,cn:n,tenant:u,hash:s,bases:r})}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)}`)}}aclFlag(t,e,i,n){return"boolean"==typeof t?t:(void 0!==t&&this.logger.warn(`authzDynamic: ${e} flag of ${i} in ${n} is ${String(JSON.stringify(t))}, not a boolean: permission refused`),!1)}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}bypassReason(t,e){if(0===e.length)return;const i=t;return e.includes("trusted-proxy")&&!0===i.trustedProxy&&i.proxyAuthUser?`trusted-proxy vouching for ${i.proxyAuthUser}`:t.user?e.includes("any-authenticated")?"any-authenticated":e.includes(t.user)?`identity ${t.user}`:void 0:void 0}identitySource(){return"req.user and req.userName: the token's tenant"}afterLoad(){super.afterLoad();const t=Object.values(this.server.loadedPlugins).filter(t=>t!==this&&t.roles?.includes("auth")&&e(this,{name:t.name,pathPrefixes:t.pathPrefixes||[]})).map(t=>t.name);if(0===t.length)return;const i=this.options(o).authz_dynamic_bypass;this.logger.warn(`authzDynamic: ${t.join(", ")} authenticate the same paths as this plugin, so a request can arrive already identified. It is `+(0===i.length?"checked against the ACLs of the token it carries, and refused without one — set --authz-dynamic-bypass to let an identity through without a token.":`let through without a token when it matches ${i.join(", ")} (--authz-dynamic-bypass), and checked against its token ACLs otherwise.`))}afterChain(t,e,i){const s=t;if(!s.authzDynamicPending)return i();s.authzDynamicPending=!1;const a=s.authzDynamicBypass??this.options(o).authz_dynamic_bypass,r=this.bypassReason(s,a);if(r)return this.logger.info(`authzDynamic: request allowed past the token ACLs (${r})`),i();this.logger.warn(s.authzDynamicPendingReason||"authzDynamic: no token on a request no bypass covers"),n(e)}refuseOrDefer(t,e,i,s,a){const r=this.bypassReason(t,a);if(r)return this.logger.info(`authzDynamic: request allowed past the token ACLs (${r})`),void this.passThrough(t,i);if(!t.user&&a.length>0){const e=t;return e.authzDynamicPending=!0,e.authzDynamicPendingReason=s,e.authzDynamicBypass=a,void this.passThrough(t,i)}this.logger.warn(s),n(e)}authMethod(t,e,i){const s=this.options(o).authz_dynamic_bypass,a=this.bypassReason(t,s);if(a)return this.logger.info(`authzDynamic: request allowed past the token ACLs (${a})`),void this.passThrough(t,i);this.ensureFresh().then(()=>{const n=t.headers.authorization;if(!n||!/^Bearer\s+/.test(n))return void this.refuseOrDefer(t,e,i,"authzDynamic: missing or invalid Authorization header",s);const a=n.slice(n.indexOf(" ")+1).trim();if(!a)return void this.refuseOrDefer(t,e,i,"authzDynamic: empty bearer token",s);let r;for(const t of this.tokens)if(h(a,t.hash)){r=t;break}if(!r){const n=a.length>8?`${a.substring(0,8)}...`:"***";return void this.refuseOrDefer(t,e,i,`authzDynamic: unauthorized token ${n}`,s)}t.user||this.publishIdentity(t,r.tenant),t.authzToken=r,i()}).catch(t=>{this.logger.error(`authzDynamic: authMethod error: ${String(t)}`),n(e)})}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,a(t),"write"),[t,e,i]):[t,e,i]},ldapmodifyrequest:([t,e,i,n])=>{const s=this.activeToken(n);return s?(this.requireBranchPermission(s,a(t),"write"),[t,e,i,n]):[t,e,i,n]},ldapdeleterequest:([t,e])=>{const i=this.activeToken(e);if(!i)return[t,e];const n=Array.isArray(t)?t:[t];for(const t of n)this.requireBranchPermission(i,a(t),"delete");return[t,e]},ldaprenamerequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,a(t),"read"),this.requireBranchPermission(n,a(e),"write"),[t,e,i]):[t,e,i]}};activeToken(t){return t?.authzToken}requireBranchPermission(t,e,n){for(const i of t.bases){let t;try{t=r(e,i.dn)}catch{continue}if(t&&i[n])return}throw this.logger.debug(`authzDynamic: token "${t.tenant}" denied ${n} on ${e}`),new i(`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{AuthzDynamic as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID as e}from"crypto";import o from"express-openid-connect";import
|
|
1
|
+
import{randomUUID as e}from"crypto";import o from"express-openid-connect";import t from"../../lib/auth/base.js";import{forbidden as s,serverError as i,unauthorized as n}from"../../lib/expressFormatedResponses.js";import{openidconnectOptions as r}from"./auth.options.js";const{auth:a,requiresAuth:c}=o,d=["/login","/logout","/callback","/backchannel-logout"],h=/end_session_endpoint/,u=/^\/logout\/?$/i,l="dmSession",sessionClaims=e=>{const{oidc:o,appSession:t}=e,s=o?.idTokenClaims;if(!s)return;const i=t?.[l];return"string"==typeof i?{...s,session:i}:s},subjectOf=e=>{try{const o=JSON.parse(Buffer.from(String(e).split(".")[1],"base64url").toString());return"string"==typeof o.sub?o.sub:void 0}catch{return}};class OpenIDConnect extends t{static optionDefinitions=r;name="openidconnect";configApiKey="openidconnect";roles=["auth","configurable"];router;warnedNoEndSession=!1;warnedNoRecord=!1;warnedUnnamed=!1;constructor(e){super(e);for(const e of["oidc_server","oidc_client_id","oidc_client_secret","base_url"])if(!this.config[e])throw new Error(`Missing config parameter ${e}`);for(const e of this.options(r).oidc_logout_origins)try{new URL(e)}catch{throw new Error(`${this.name}: --oidc-logout-origins holds "${e}", which is not a URL`)}}buildConfig(){const e=this.options(r);return{authRequired:!1,issuerBaseURL:e.oidc_server,clientID:e.oidc_client_id,secret:e.oidc_client_secret,baseURL:e.base_url,clientSecret:e.oidc_client_secret,authorizationParams:{response_type:"code",scope:"openid profile email"},idpLogout:!0,routes:{logout:!1},afterCallback:async(e,o,t)=>({...t,[l]:await this.sessionId(e,subjectOf(t.id_token))}),backchannelLogout:{onLogoutToken:async e=>{this.server.hookBus.has("oidclogouttoken")?await this.hookBus.notifyStrict("oidclogouttoken",e):this.logger.warn("openidconnect: a logout token arrived but no Back-Channel Logout plugin is loaded, so nothing records it")},onLogin:async e=>{if(!this.server.hookBus.has("oidclogin"))return;const o=e.oidc?.idTokenClaims;if(o)try{await this.hookBus.notifyStrict("oidclogin",o)}catch(o){const t=e.appSession;if(t)for(const e of Object.keys(t))delete t[e];throw o}},isLoggedOut:e=>this.loggedOut(e)}}}identitySource(){const e=this.options(r).oidc_username_claim;return`req.user: the OIDC sub, an opaque provider identifier; req.userName: the "${e}" claim`+("sub"===e?" — the same value, until --oidc-username-claim names another":"")}getConfigApiData(){return{enabled:!0,endpoints:{logout:"/logout"}}}get pathPrefixes(){const e=super.pathPrefixes;return 0===e.length?e:[...e,...d]}api(e){this.router=a(this.buildConfig()),super.api(e)}logoutOrigins(){const{base_url:e,oidc_logout_origins:o}=this.options(r),t=new Set;for(const s of[e,...o])try{t.add(new URL(s).origin)}catch{}return[...t]}crossSite(e){const o=e.headers["sec-fetch-site"];if("same-origin"===o||"none"===o)return!1;const t=this.logoutOrigins();let s=!1;for(const o of["origin","referer"]){const i=e.headers[o];if("string"==typeof i){s=!0;try{if(t.includes(new URL(i).origin))return!1}catch{return!0}}}return"string"==typeof o||s}authMethod(e,o,t){return("GET"===e.method||"HEAD"===e.method)&&u.test(e.path)&&this.crossSite(e)?s(o,"Cross-site logout refused"):this.router?void this.router(e,o,s=>{if(s&&u.test(e.path)&&this.noEndSession(s))return o.redirect(this.options(r).base_url);if(s)return i(o,s);if(("GET"===e.method||"HEAD"===e.method)&&u.test(e.path))return void this.endHere(e).then(()=>o.oidc.logout({logoutParams:{post_logout_redirect_uri:void 0}}));const guard=()=>{c()(e,o,s=>{if(s)return n(o);const i=e.oidc.user,a=this.options(r).oidc_username_claim,c=i[a];"sub"!==a&&"string"!=typeof c&&this.logger.warn(`${this.name}: no "${a}" claim on this session, so req.userName falls back to the sub. Check --oidc-username-claim against the scopes the provider is asked for`),this.publishIdentity(e,String(i.sub),"string"==typeof c?c:String(i.sub)),t()})},a=this.renewExpired(e);a?a.then(guard):guard()}):i(o,new Error(`${this.name}: api() has not built the router`))}renewExpired(e){const o=e.oidc,t=o.accessToken;if(!t?.isExpired())return;const end=()=>{e.appSession=void 0};if(o.refreshToken)return t.refresh().then(()=>{},e=>{this.logger.info(`${this.name}: the provider refused to renew an access token, so the session ends: ${String(e)}`),end()});end()}async loggedOut(e){if(!this.server.hookBus.has("oidcsessionvalid"))return!1;const o=sessionClaims(e),[,t]=await this.hookBus.chain("oidcsessionvalid",[o,!0]);return!t}async sessionId(o,t){const{oidc:s,appSession:i}=o,n=i?.[l];return"string"==typeof n&&void 0!==t&&s?.isAuthenticated()&&s.idTokenClaims?.sub===t&&!await this.loggedOut(o).catch(()=>!0)?n:e()}async endHere(e){if(!e.oidc?.isAuthenticated())return;const o=sessionClaims(e);if(!o)return;const{iss:t,sid:s,session:i}=o,n=i?{iss:t,session:i}:s?{iss:t,sid:s}:void 0;if(n)if(this.server.hookBus.has("oidclogouttoken"))try{await this.hookBus.notifyStrict("oidclogouttoken",n)}catch(e){this.logger.error(`${this.name}: /logout could not record the session it ends, so a copy of its cookie stays valid until its tokens expire: ${String(e)}`)}else this.warnedNoRecord||(this.warnedNoRecord=!0,this.logger.warn(`${this.name}: no Back-Channel Logout plugin is loaded, so /logout drops the browser's cookie and nothing else: a copy of it stays valid until its tokens expire. Load core/bcl to end the session here as well`));else this.warnedUnnamed||(this.warnedUnnamed=!0,this.logger.warn(`${this.name}: /logout ended a session nothing names — one established before this version, from a provider that sends no sid — so a copy of its cookie stays valid until its tokens expire`))}noEndSession(e){return!!(e instanceof Error&&h.test(e.message))&&(this.warnedNoEndSession||(this.warnedNoEndSession=!0,this.logger.warn(`${this.name}: the provider publishes no end_session_endpoint, so /logout ends the session here only, and the provider may log the caller back in`)),!0)}}export{OpenIDConnect as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import r from"../../abstract/plugin.js";import{trustedProxyOptions as
|
|
1
|
+
import{isIP as r}from"node:net";import e from"../../abstract/plugin.js";import{trustedProxyOptions as t}from"./auth.options.js";class TrustedProxy extends e{static optionDefinitions=t;name="trustedProxy";roles=["protect"];trustedNetworks=[];authHeader;constructor(...r){super(...r);const e=this.options(t),s=e.trusted_proxy;if(0===s.length)throw new Error("TrustedProxy plugin requires trusted_proxy configuration (array of IPs or CIDR ranges)");for(const r of s)try{this.trustedNetworks.push(this.parseCIDR(r))}catch(e){throw new Error(`Invalid trusted proxy address "${r}": ${e instanceof Error?e.message:String(e)}`)}this.authHeader=e.trusted_proxy_auth_header.toLowerCase(),this.logger.info(`Trusted proxy validation enabled for ${s.length} address(es): ${s.join(", ")} (auth header: ${this.authHeader})`)}api(r){r.use((r,e,t)=>{const s=r.socket.remoteAddress;if(!s)return this.logger.debug("No remote address, removing X-Forwarded-For header"),delete r.headers["x-forwarded-for"],t();const o=r.headers["x-forwarded-for"];if(this.isTrustedProxy(s)){r.trustedProxy=!0;const e=r.headers[this.authHeader];e?(r.proxyAuthUser=Array.isArray(e)?e[0]:e,this.logger.debug(`Request from trusted proxy ${s}, Auth-User: ${r.proxyAuthUser}, X-Forwarded-For: ${o?String(o):"(none)"}`)):this.logger.debug(`Request from trusted proxy ${s}, X-Forwarded-For: ${o?String(o):"(none)"}`)}else r.trustedProxy=!1,o&&(this.logger.warn(`Removing X-Forwarded-For header from untrusted source ${s} (was: ${String(o)})`),delete r.headers["x-forwarded-for"]);t()})}isTrustedProxy(r){try{const e=this.parseIP(r);for(const r of this.trustedNetworks)if(this.isInNetwork(e,r))return!0;return!1}catch(e){return this.logger.error(`Failed to parse IP address "${r}": ${e instanceof Error?e.message:String(e)}`),!1}}isInNetwork(r,e){const t=[];if(r.isIPv6===e.isIPv6&&t.push(r.ip),r.isIPv6&&!e.isIPv6){const e=this.mappedIPv4(r.ip);void 0!==e&&t.push(e)}return t.some(r=>(r&e.mask)===(e.ip&e.mask))}mappedIPv4(r){return r>>32n==0xffffn?0xffffffffn&r:void 0}parseIP(e){const t=e.indexOf("%"),s=e.includes(":")&&-1!==t?e.slice(0,t):e,o=r(s);if(6===o)return{ip:this.parseIPv6(s),isIPv6:!0};if(4===o)return{ip:this.parseIPv4(s),isIPv6:!1};throw new Error(`Invalid IP address: ${e}`)}parseCIDR(r){const e=r.indexOf("/"),t=-1===e?r:r.slice(0,e),s=-1===e?void 0:r.slice(e+1);if(t.includes("%"))throw new Error(`Invalid network address: ${t}`);const o=this.parseIP(t);let i,{isIPv6:n}=o,{ip:d}=o;if(void 0!==s){if(!/^\d{1,3}$/.test(s))throw new Error(`Invalid prefix length: ${s}`);if(i=Number(s),i>(n?128:32))throw new Error(`Invalid prefix length: ${s}`)}if(n&&(void 0===i||i>=96)){const r=this.mappedIPv4(d);void 0!==r&&(n=!1,d=r,void 0!==i&&(i-=96))}const a=n?128:32;let f;if(void 0===i)f=(1n<<BigInt(a))-1n;else if(0===i)f=0n;else{const r=BigInt(a);f=(1n<<r)-1n^(1n<<r-BigInt(i))-1n}return{ip:d,mask:f,isIPv6:n}}parseIPv4(e){if(4!==r(e))throw new Error(`Invalid IPv4 address: ${e}`);return e.split(".").reduce((r,e)=>r<<8n|BigInt(Number(e)),0n)}parseIPv6(e){if(6!==r(e))throw new Error(`Invalid IPv6 address: ${e}`);const t=/^(.*:)(\d+\.\d+\.\d+\.\d+)$/.exec(e);let s,o=e;if(t){const r=this.parseIPv4(t[2]);o=`${t[1]}${(r>>16n).toString(16)}:${(0xffffn&r).toString(16)}`}const i=o.split("::");if(i.length>2)throw new Error(`Invalid IPv6 address: ${e}`);if(2===i.length){const r=i[0]?i[0].split(":"):[],t=i[1]?i[1].split(":"):[],o=8-r.length-t.length;if(o<0)throw new Error(`Invalid IPv6 address: ${e}`);s=[...r,...Array(o).fill("0"),...t]}else s=o.split(":");if(8!==s.length)throw new Error(`Invalid IPv6 address: ${e}`);let n=0n;for(const r of s){if(!/^[0-9a-fA-F]{1,4}$/.test(r))throw new Error(`Invalid IPv6 address: ${e}`);n=n<<16n|BigInt(parseInt(r,16))}return n}}export{TrustedProxy as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../../abstract/plugin.js";import{roleAttribute as e}from"../../config/schema.js";import{isDnInBranch as r,
|
|
1
|
+
import t from"../../abstract/plugin.js";import{roleAttribute as e}from"../../config/schema.js";import{isDnInBranch as r,getParentDn as a,parseDn as n,normalizeDn as i,rdnValue as s,escapeLdapFilter as o,rdnTypes as h}from"../../lib/utils.js";import{organizationOptionsOf as c}from"./ldap.options.js";function isOrganizationDn(t){try{return h(t).includes("ou")}catch{return!1}}class LdapDepartmentSync extends t{name="ldapDepartmentSync";roles=["consistency"];settings;linkAttr;pathAttr;constructor(t){super(t),this.settings=c(t.config),this.linkAttr=this.settings.ldap_organization_link_attribute,this.pathAttr=this.settings.ldap_organization_path_attribute}organizationPathAttribute(){const t=this.server.loadedPlugins.ldapOrganizations;return e(t?.schema,"organizationPath")||this.pathAttr}linkedAttributes(){const t=[],r=this.server.loadedPlugins.ldapFlatGeneric;for(const e of r?.instances||[])t.push(e.schema);const a=this.server.loadedPlugins.ldapGroups;t.push(a?.schema);const n=[];for(const r of t){const t=e(r,"organizationLink");t&&n.push({link:t,path:e(r,"organizationPath")||this.pathAttr})}n.push({link:this.linkAttr,path:this.pathAttr});const i=new Set;return n.filter(({link:t,path:e})=>{const r=`${t.toLowerCase()} ${e.toLowerCase()}`;return!i.has(r)&&(i.add(r),!0)})}hooks={ldaprenamedone:async(t,e)=>{const n=this.settings.ldap_top_organization;if(n&&r(t,n)&&isOrganizationDn(t)){this.logger.info(`Organization renamed from ${t} to ${e}, synchronizing linked resources...`);try{const r=a(n);await this.updateOrganizationPaths(e);for(const a of this.linkedAttributes())await this.updateLinkedResources(t,e,r,a),await this.updateDescendantReferences(t,e,r,a)}catch(t){this.logger.error(`Error synchronizing department links after rename: ${t}`)}}}};async updateOrganizationPaths(t){const e=this.organizationPathAttribute(),o=this.settings.ldap_top_organization;if(!o||!r(t,o))return;const h=this.settings.ldap_organization_path_separator,c=await this.server.ldap.system.search({paged:!0,scope:"sub",filter:`(${e}=*)`,attributes:[e]},t),d=[];for await(const t of c)for(const r of t.searchEntries){const t=String(r.dn);if(!isOrganizationDn(t))continue;const a=r[e];d.push({dn:t,path:String(Array.isArray(a)?a[0]:a)})}d.sort((t,e)=>n(t.dn).length-n(e.dn).length);const l=i(o),g=new Map;let p=0;for(const{dn:t,path:r}of d){const n=a(t),o=i(n);let c;if(o!==l&&(c=g.get(o)??await this.readPath(n)??void 0,void 0===c)){this.logger.warn(`Organization ${n} has no ${e}: the path of ${t} is left as it is`);continue}const d=s(t),u=c?`${c}${h}${d}`:d;if(g.set(i(t),u),u!==r)try{await this.server.ldap.system.modify(t,{replace:{[e]:u}}),p++}catch(e){this.logger.error(`Failed to update the path of ${t} after organization rename: ${e}`)}}this.logger.info(`Updated the path of ${p} organizations below ${t}`)}async readPath(t){const e=this.organizationPathAttribute();try{const r=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[e]},t),a=r.searchEntries[0]?.[e];return void 0===a?null:String(Array.isArray(a)?a[0]:a)}catch{return null}}async updateLinkedResources(t,e,r,a){const n=`(${a.link}=${o(t)})`;this.logger.debug(`Searching for resources directly linked to ${t}: ${n}`);const i=await this.server.ldap.system.search({paged:!0,filter:n,attributes:[a.link,a.path]},r);let s=0;for await(const t of i)for(const r of t.searchEntries){const t=String(r.dn);try{const r=await this.getDepartmentPath(e);await this.server.ldap.system.modify(t,{replace:{[a.link]:e,[a.path]:r}}),s++,this.logger.debug(`Updated ${t}: ${a.link}=${e}, ${a.path}=${r}`)}catch(e){this.logger.error(`Failed to update ${t} after organization rename: ${e}`)}}this.logger.info(`Updated ${s} resources directly linked to the renamed organization`)}async updateDescendantReferences(t,e,a,n){const s=`(${n.link}=*)`;this.logger.debug(`Searching for resources linked to descendants of ${t}: ${s}`);const o=await this.server.ldap.system.search({paged:!0,filter:s,attributes:[n.link,n.path]},a);let h=0;for await(const a of o)for(const s of a.searchEntries){const a=String(s.dn),o=s[n.link],c=Array.isArray(o)?String(o[0]):String(o);if(i(c)!==i(t)&&r(c,t))try{const r=this.replaceDnTail(c,t,e),i=await this.getDepartmentPath(r);await this.server.ldap.system.modify(a,{replace:{[n.link]:r,[n.path]:i}}),h++,this.logger.debug(`Updated descendant link ${a}: ${n.link}=${r}, ${n.path}=${i}`)}catch(t){this.logger.error(`Failed to update descendant reference ${a}: ${t}`)}}this.logger.info(`Updated ${h} resources linked to descendants of the renamed organization`)}replaceDnTail(t,e,r){const a=n(t),i=n(e).length;return[...a.slice(0,a.length-i),r].join(",")}async getDepartmentPath(t){const e=this.organizationPathAttribute();try{const r=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[e,"ou","o"]},t);if(!r.searchEntries||0===r.searchEntries.length)throw new Error(`Organization ${t} not found`);const a=r.searchEntries[0];if(a[e]){const t=a[e];return Array.isArray(t)?String(t[0]):String(t)}const n=a.ou||a.o;if(n){return`/${Array.isArray(n)?String(n[0]):String(n)}`}return this.logger.warn(`Organization ${t} has no ${e} attribute, using DN`),t}catch(e){throw new Error(`Failed to fetch organization ${t}: ${String(e)}`,{cause:e})}}}export{LdapDepartmentSync as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Type as t}from"@sinclair/typebox";import i from"../../abstract/plugin.js";import{assertModifyRequest as a}from"../../lib/ldapActions.js";import{jsonBody as e,tryMethod as s,wantJson as n,tryMethodData as r,WriteResponse as o}from"../../lib/expressFormatedResponses.js";import{validateDnValue as h,escapeDnValue as d,getRdn as c,isDnInBranch as l,rdnValue as g,normalizeDn as p,getParentDn as u,escapeLdapFilter as m,assertRfc4514Escapes as w,isChildOf as f,substringSearchFilter as z}from"../../lib/utils.js";import{BadRequestError as y,NotFoundError as b,ConflictError as $,isAuthzRefusal as _,HttpError as v}from"../../lib/errors.js";import{extractLdapCode as O}from"../../lib/ldapCodes.js";import{loadSchemaFileAsync as E}from"../../lib/schemaFile.js";import{staticSchemaUrl as A}from"../../lib/staticFiles.js";import{assertClientMaySet as L,modifiedAttributeNames as k}from"../../config/schema.js";import{applyFixedDefaults as j,EntityValidator as C}from"../../lib/entitySchema/validator.js";import{organizationOptions as S}from"./ldap.options.js";import{defineRoute as q}from"../../lib/route.js";const x=t.Object({dn:t.String({minLength:1})}),D=t.Object({q:t.String({minLength:1})});class LdapOrganizations extends i{static optionDefinitions=S;name="ldapOrganizations";roles=["api","consistency","configurable"];settings;pathAttr;linkAttr;schema;constructor(t){if(super(t),this.settings=this.options(S),!this.settings.ldap_top_organization)throw new Error("Missing --ldap-top-organization");if(this.pathAttr=this.settings.ldap_organization_path_attribute,this.linkAttr=this.settings.ldap_organization_link_attribute,t.ldap.declareObjectClasses(this.settings.ldap_top_organization,this.settings.ldap_organization_class,!0),this.settings.organization_schema){const t=this.settings.organization_schema;E(t,{config:this.config}).then(t=>{this.schema=t,this.server.ldap.knowAttributeNames(Object.keys(t.attributes)),this.logger.debug("Organization schema loaded")}).catch(i=>this.logger.error(`Failed to load organization schema from ${t}: ${i}`))}}api(t){q(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/organizations/top`,handler:async({req:t,res:i})=>{await r(i,async()=>this.hideNeverReturn(await this.getOrganisationTop(t)))}}),q(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/organizations/:dn`,params:x,handler:async({req:t,params:i,res:a})=>{const e=i.dn;await r(a,async()=>this.hideNeverReturn(await this.getOrganisationByDn(e,t)))}}),q(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes`,params:x,handler:async({req:t,params:i,res:a})=>{const e=i.dn;await r(a,async()=>this.hideNeverReturn(await this.getOrganisationSubnodes(e,t)))}}),q(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes/search`,params:x,query:D,handler:async({req:t,params:i,query:a,res:e})=>{const s=i.dn;await r(e,async()=>this.hideNeverReturn(await this.searchOrganisationSubnodes(s,a.q,t)))}}),q(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/organizations/:dn/search`,params:x,query:D,handler:async({req:t,params:i,query:a,res:e})=>{const s=i.dn;await r(e,async()=>this.hideNeverReturn(await this.searchOrganisationTree(s,a.q,t)))}}),q(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/organizations`,responses:{200:o},handler:async({req:t,res:i})=>this.apiAdd(t,i)}),q(t,{method:"put",path:`${this.config.api_prefix}/v1/ldap/organizations/:dn`,responses:{200:o},params:x,handler:async({req:t,res:i})=>this.apiModify(t,i)}),q(t,{method:"delete",path:`${this.config.api_prefix}/v1/ldap/organizations/:dn`,responses:{200:o},params:x,handler:async({req:t,res:i})=>this.apiDelete(t,i)}),q(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/organizations/:dn/move`,responses:{200:o},params:x,handler:async({req:t,res:i})=>this.apiMove(t,i)})}async apiAdd(t,i){const a=e(t,i,"ou");if(!a)return;L(this.schema,Object.keys(a),"ou"),h(a.ou,"ou");const n=a.parentDn||this.settings.ldap_top_organization,r=`ou=${d(a.ou)},${n}`,o={ou:a.ou,...Object.fromEntries(Object.entries(a).filter(([t])=>"ou"!==t&&"parentDn"!==t)),objectClass:this.settings.ldap_organization_class};await s(i,this.addOrganization.bind(this),r,o,t)}async apiModify(t,i){const n=e(t,i);if(!n)return;a(n);const r=t.params.dn;if(!r)throw new y("dn is required");L(this.schema,k(n)),await s(i,this.modifyOrganization.bind(this),r,n,t)}async apiDelete(t,i){if(!n(t,i))return;const a=t.params.dn;if(!a)throw new y("dn is required");await s(i,this.deleteOrganization.bind(this),a,t)}async apiMove(t,i){if(!n(t,i))return;const a=e(t,i,"targetOrgDn");if(!a)return;const s=t.params.dn;if(!s)throw new y("dn is required");const{targetOrgDn:o}=a;if(!o||"string"!=typeof o)throw new y("Missing or invalid targetOrgDn in request body");await r(i,this.moveOrganization.bind(this),s,o,t)}hooks={ldapaddrequest:async([t,i,a])=>(this.isOu(i)||await this.checkDeptLink(i),this.isOu(i)&&await this.checkDeptPath(i,t),void 0!==a?[t,i,a]:[t,i]),ldapmodifyrequest:async([t,i,a,e])=>{let s,n={},r={};const checkIsOu=async()=>{if(void 0!==s)return s;if(i.replace?.objectClass)s=this.isOu({objectClass:i.replace.objectClass});else if(i.add?.objectClass)s=this.isOu({objectClass:i.add.objectClass});else{const i=await this.server.ldap.system.search({paged:!1,scope:"base"},t);s=i.searchEntries.length>0&&this.isOu(i.searchEntries[0])}return s};if(i.delete){const t=(Array.isArray(i.delete)?i.delete.map(String):Object.keys(i.delete)).map(t=>t.split(";")[0].toLowerCase()),a=t.includes(this.linkAttr.toLowerCase()),e=t.includes(this.pathAttr.toLowerCase());if(a||e){if(!await checkIsOu()&&a)throw new y("An organization link cannot be deleted");if(e)throw new y("An organization path cannot be deleted")}}if(i.replace&&(i.replace[this.linkAttr]&&(n={...i.replace}),i.replace[this.pathAttr]&&(r={...i.replace})),i.add&&(i.add[this.linkAttr]&&(n={...n,...i.add}),i.add[this.pathAttr]&&(r={...r,...i.add})),Object.keys(n).length>0){await checkIsOu()||await this.checkDeptLink(n)}if(Object.keys(r).length>0){await checkIsOu()&&await this.checkDeptPath(r,t)}return[t,i,a,e]},ldapdeleterequest:async([t,i])=>{const a=Array.isArray(t)?t:[t];for(const t of a)/^\s*ou\s*=/i.test(t)&&await this.isEmptyOrganization(t);return[t,i]},ldaprenamerequest:([t,i,a])=>[t,i,a]};async checkDeptLink(t){if(t[this.linkAttr]){const i=t[this.linkAttr],a=Array.isArray(i)?i[0]:i,e=await this.server.ldap.system.search({paged:!1,scope:"base"},a);if(0===e.searchEntries.length)throw new b(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.settings.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new y(`Entry ${a} isn't in top organization branch`)}}async checkDeptPath(t,i){if(t[this.pathAttr]){const a=t[this.pathAttr],e=Array.isArray(a)?a[0]:a,s=this.settings.ldap_organization_path_separator,n=this.settings.ldap_top_organization,r=i&&n&&/^ou=/i.test(c(i))&&l(i,n)?g(i):void 0,o=t.ou,h=(Array.isArray(o)?o[0]:o)||r;let d=e;if((this.isOu(t)||void 0!==r)&&h){const t=n?g(n):"";if(t&&e.startsWith(h+s)&&e.endsWith(s+t))return;if(e===h){const t=n?p(n):"";if(!i||!t||!l(i,n)||p(i)===t||p(u(i))===t)return;throw new y(`Organization path "${e}" names no parent, but ${i} is not directly under ${n}`)}if(!e.endsWith(s+h))throw new y(`Organization path must end with its own name, preceded by separator "${s}"`);d=e.slice(0,e.length-s.length-h.length)}const w=await this.server.ldap.system.search({paged:!1,scope:"sub",filter:`(${this.pathAttr}=${m(d)})`,attributes:["dn"]},this.settings.ldap_top_organization);if(!w.searchEntries||0===w.searchEntries.length)throw new y(`Invalid organization path ${e}: no organization holds "${d}"`)}}async isEmptyOrganization(t){if((await this.server.ldap.system.search({paged:!1,filter:`(${this.settings.ldap_organization_link_attribute}=${m(t)})`})).searchEntries.length>0)throw new $(`Organization ${t} is not empty`)}isOu(t){if(!t.objectClass)return!1;const i=[].concat(t.objectClass).map(t=>String(t).toLowerCase());return this.settings.ldap_organization_class.filter(t=>"top"!==t.toLowerCase()).some(t=>i.includes(t.toLowerCase()))}async getOrganisationTop(t){if(!this.settings.ldap_top_organization)throw new y("No top organization configured");const i=await this.server.ldap.search({paged:!1,scope:"base"},this.settings.ldap_top_organization,t);if(1!==i.searchEntries.length)throw new b("Top organization not found");const a=i.searchEntries[0],[,e]=await this.hookBus.chain("getOrganisationTop",[t,a]);return e}async getOrganisationByDn(t,i){let a;try{a=await this.server.ldap.search({paged:!1,scope:"base",filter:"(objectClass=organizationalUnit)"},t,i)}catch(i){const a=O(i);if(32===a)throw new b(`Organization ${t} not found`);if(34===a)throw new y(`${t} is not a valid DN`);throw i}if(1!==a.searchEntries.length)throw new b(`Organization ${t} not found`);return a.searchEntries[0]}async getOrganisationSubnodes(t,i){const a=[],e=this.settings.ldap_organization_max_subnodes,s=i?.query?.objectClass;s&&"organizationalUnit"!==s||a.push(...await this.childOrganizations(t,"(objectClass=organizationalUnit)",e,i));const n=this.settings.ldap_top_organization,r=u(n);let o=`(${this.settings.ldap_organization_link_attribute}=${m(t)})`;return s&&(o=`(&${o}(objectClass=${m(s)}))`),this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${r}`),a.push(...await this.linkedEntities(r,o,e,t,i)),a}sizeLimitWarned=new Set;static SIZE_LIMIT_WARNED_MAX=500;warnSizeLimit(t,i){this.sizeLimitWarned.has(t)?this.server.logger.debug(i):(this.sizeLimitWarned.size<LdapOrganizations.SIZE_LIMIT_WARNED_MAX&&this.sizeLimitWarned.add(t),this.server.logger.warn(i))}static moreIndicator(t,i,a,e){return{dn:t,cn:[i],objectClass:["moreIndicator"],_isMoreIndicator:"true",...void 0===e?{}:{_totalCount:e.toString()},_displayedCount:a.toString()}}async childOrganizations(t,i,a,e){try{const a=await this.server.ldap.search({paged:!0,scope:"one",filter:i},t,e),s=[];for await(const t of a)s.push(...t.searchEntries);return this.server.logger.debug(`Found ${s.length} sub-OUs for ${t}`),s}catch(i){const a=O(i);if(32===a)return this.server.logger.debug(`No sub-OUs for ${t}: the node holds none`),[];if(34===a)throw new y(`${t} is not a valid DN`);if(4!==a)throw i}const s=(await this.server.ldap.search({paged:!1,scope:"one",filter:i,sizeLimit:a+1},t,e)).searchEntries.slice(0,a);return this.warnSizeLimit(`children:${t}`,`Organization ${t} holds more child organizations than the directory will list in one answer, so ${s.length} are returned and the rest are hidden. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to list them all.`),[...s,LdapOrganizations.moreIndicator(`more-organizations-${t}`,"... more organizations than the directory will list",s.length)]}async linkedEntities(t,i,a,e,s){const n=[];let r=0;try{const e=await this.server.ldap.search({paged:!0,filter:i},t,s);for await(const t of e){r+=t.searchEntries.length;const i=a-n.length;i>0&&n.push(...t.searchEntries.slice(0,i))}}catch(n){if(t!==e&&(_(n)||403===n?.statusCode))return this.linkedEntities(e,i,a,e,s);if(4!==O(n))throw n;const r=await this.server.ldap.search({paged:!1,filter:i,sizeLimit:a+1},t,s);this.warnSizeLimit(`linked:${e}`,`More entries are attached to ${e} than the directory will list in one answer. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to count them all.`);const o=r.searchEntries.slice(0,a);return[...o,LdapOrganizations.moreIndicator(`more-${e}`,"... more elements than the directory will list",o.length)]}return this.server.logger.debug(`Found ${r} linked entities for ${e}, returning ${n.length}`),r<=a?n:[...n,LdapOrganizations.moreIndicator(`more-${e}`,`... ${r-a} more ${r-a===1?"element":"elements"}`,a,r)]}async addOrganization(t,i,a){return j(this.schema,i),await this.server.ldap.authorize("ldapaddrequest",[t,i,a]),i=await this.validator().validateNew(i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i,a){return await this.server.ldap.authorize("ldapmodifyrequest",[t,i,this.opNumber(),a]),await this.validator().validateChanges(i),await this.server.ldap.modify(t,i,a)}validator(){return new C(this.schema,{entryExists:t=>this.entryExists(t),loadedPlugins:this.server.loadedPlugins,singularName:"organization"})}async entryExists(t){try{return(await this.server.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}}async moveOrganization(t,i,a){w(i,"targetOrgDn");const e=`${c(t)},${i}`;let s,n;await this.server.ldap.authorize("ldaprenamerequest",[t,e,a]);try{s=await this.server.ldap.system.search({paged:!1,scope:"base"},i)}catch(t){throw new b(`Target organization ${i} not found: ${t instanceof Error?t.message:String(t)}`)}if(!s.searchEntries||0===s.searchEntries.length)throw new b(`Target organization ${i} not found`);if(!this.isOu(s.searchEntries[0]))throw new y(`Target ${i} is not an organizational unit`);try{n=await this.server.ldap.system.search({paged:!1,scope:"base"},t)}catch(t){throw new b(`Source organization not found: ${t}`)}if(!n.searchEntries||0===n.searchEntries.length)throw new b(`Source organization ${t} not found`);if(e===t)throw new y("Cannot move organization to its current location");if(i.toLowerCase()===t.toLowerCase()||f(i,t))throw new y("Cannot move organization into itself or its descendant");try{await this.server.ldap.rename(t,e,a),this.logger.info(`Moved organization from ${t} to ${e}`)}catch(t){if(t instanceof v||_(t))throw t;throw new Error(`Failed to move organization: ${String(t)}`,{cause:t})}return{newDn:e}}async deleteOrganization(t,i){try{return await this.server.ldap.delete(t,i)}catch(i){if(32===O(i))throw new b(`Organization ${t} not found`);throw i}}async searchOrganisationSubnodes(t,i,a){const e=[],s=this.settings.ldap_organization_max_subnodes;e.push(...await this.childOrganizations(t,`(&(objectClass=organizationalUnit)${z(i,"ou,description")})`,s,a));const n=this.settings.ldap_top_organization,r=u(n),o=m(t),h=this.config.display_name_attribute||"displayName",d=`(&(${this.settings.ldap_organization_link_attribute}=${o})${z(i,`uid,cn,mail,sn,givenName,${h}`)})`;return this.server.logger.debug(`Searching for linked entities with filter: ${d} in ${r}`),e.push(...await this.linkedEntities(r,d,s,t,a)),e}async searchOrganisationTree(t,i,a){const e=this.settings.ldap_organization_max_subnodes,s=this.pathAttr?`ou,description,${this.pathAttr}`:"ou,description",n=`(&(objectClass=organizationalUnit)${z(i,s)})`,r=[];let o=0;try{const i=await this.server.ldap.search({paged:!0,scope:"sub",filter:n},t,a);for await(const t of i){o+=t.searchEntries.length;const i=e-r.length;i>0&&r.push(...t.searchEntries.slice(0,i))}}catch(i){const s=O(i);if(32===s)return this.server.logger.debug(`No organizations under ${t}: no such node`),[];if(34===s)throw new y(`${t} is not a valid DN`);if(4!==s)throw i;const r=(await this.server.ldap.search({paged:!1,scope:"sub",filter:n,sizeLimit:e+1},t,a)).searchEntries.slice(0,e);return this.warnSizeLimit(`search:${t}`,`A search under ${t} matched more organizations than the directory will list in one answer, so ${r.length} are returned and the rest are hidden. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to search them all.`),[...r,LdapOrganizations.moreIndicator(`more-organizations-${t}`,"... more organizations than the directory will list",r.length)]}if(o<=e)return r;const h=o-e;return[...r,LdapOrganizations.moreIndicator(`more-organizations-${t}`,`... ${h} more ${1===h?"organization":"organizations"}, narrow the search`,e,o)]}getConfigApiData(){const t=this.config.api_prefix||"/api",i=this.settings.organization_schema?A(this.server,this.settings.organization_schema):void 0;return{enabled:!0,topOrganization:this.settings.ldap_top_organization,organizationClass:this.settings.ldap_organization_class,linkAttribute:this.linkAttr||"",pathAttribute:this.pathAttr||"",pathSeparator:this.settings.ldap_organization_path_separator,maxSubnodes:this.settings.ldap_organization_max_subnodes,schema:this.schema,schemaUrl:i,endpoints:{getTop:`${t}/v1/ldap/organizations/top`,get:`${t}/v1/ldap/organizations/:dn`,getSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes`,searchSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes/search`,search:`${t}/v1/ldap/organizations/:dn/search`,create:`${t}/v1/ldap/organizations`,update:`${t}/v1/ldap/organizations/:dn`,move:`${t}/v1/ldap/organizations/:dn/move`,delete:`${t}/v1/ldap/organizations/:dn`}}}}export{x as OrganizationParams,D as OrganizationSearchQuery,LdapOrganizations as default};
|
|
1
|
+
import{Type as t}from"@sinclair/typebox";import i from"../../abstract/plugin.js";import{assertModifyRequest as a}from"../../lib/ldapActions.js";import{jsonBody as e,tryMethod as s,wantJson as n,tryMethodData as r,WriteResponse as o}from"../../lib/expressFormatedResponses.js";import{validateDnValue as h,escapeDnValue as d,getRdn as c,isDnInBranch as l,rdnValue as g,normalizeDn as p,getParentDn as u,escapeLdapFilter as m,assertRfc4514Escapes as w,isChildOf as f,substringSearchFilter as z}from"../../lib/utils.js";import{BadRequestError as y,NotFoundError as b,ConflictError as $,isAuthzRefusal as _,HttpError as v}from"../../lib/errors.js";import{extractLdapCode as O}from"../../lib/ldapCodes.js";import{loadSchemaFileAsync as E}from"../../lib/schemaFile.js";import{staticSchemaUrl as A}from"../../lib/staticFiles.js";import{assertClientMaySet as L,modifiedAttributeNames as k}from"../../config/schema.js";import{applyFixedDefaults as j,EntityValidator as C}from"../../lib/entitySchema/validator.js";import{organizationOptions as S}from"./ldap.options.js";import{defineRoute as q}from"../../lib/route.js";const x=t.Object({dn:t.String({minLength:1})}),D=t.Object({q:t.String({minLength:1})});class LdapOrganizations extends i{static optionDefinitions=S;name="ldapOrganizations";roles=["api","consistency","configurable"];settings;pathAttr;linkAttr;schema;constructor(t){if(super(t),this.settings=this.options(S),!this.settings.ldap_top_organization)throw new Error("Missing --ldap-top-organization");if(this.pathAttr=this.settings.ldap_organization_path_attribute,this.linkAttr=this.settings.ldap_organization_link_attribute,t.ldap.declareObjectClasses(this.settings.ldap_top_organization,this.settings.ldap_organization_class,!0),this.settings.organization_schema){const t=this.settings.organization_schema;E(t,{config:this.config}).then(t=>{this.schema=t,this.server.ldap.knowAttributeNames(Object.keys(t.attributes)),this.logger.debug("Organization schema loaded")}).catch(i=>this.logger.error(`Failed to load organization schema from ${t}: ${i}`))}}api(t){q(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/organizations/top`,handler:async({req:t,res:i})=>{await r(i,async()=>this.hideNeverReturn(await this.getOrganisationTop(t)))}}),q(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/organizations/:dn`,params:x,handler:async({req:t,params:i,res:a})=>{const e=i.dn;await r(a,async()=>this.hideNeverReturn(await this.getOrganisationByDn(e,t)))}}),q(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes`,params:x,handler:async({req:t,params:i,res:a})=>{const e=i.dn;await r(a,async()=>this.hideNeverReturn(await this.getOrganisationSubnodes(e,t)))}}),q(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes/search`,params:x,query:D,handler:async({req:t,params:i,query:a,res:e})=>{const s=i.dn;await r(e,async()=>this.hideNeverReturn(await this.searchOrganisationSubnodes(s,a.q,t)))}}),q(t,{method:"get",path:`${this.config.api_prefix}/v1/ldap/organizations/:dn/search`,params:x,query:D,handler:async({req:t,params:i,query:a,res:e})=>{const s=i.dn;await r(e,async()=>this.hideNeverReturn(await this.searchOrganisationTree(s,a.q,t)))}}),q(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/organizations`,responses:{200:o},handler:async({req:t,res:i})=>this.apiAdd(t,i)}),q(t,{method:"put",path:`${this.config.api_prefix}/v1/ldap/organizations/:dn`,responses:{200:o},params:x,handler:async({req:t,res:i})=>this.apiModify(t,i)}),q(t,{method:"delete",path:`${this.config.api_prefix}/v1/ldap/organizations/:dn`,responses:{200:o},params:x,handler:async({req:t,res:i})=>this.apiDelete(t,i)}),q(t,{method:"post",path:`${this.config.api_prefix}/v1/ldap/organizations/:dn/move`,responses:{200:o},params:x,handler:async({req:t,res:i})=>this.apiMove(t,i)})}async apiAdd(t,i){const a=e(t,i,"ou");if(!a)return;L(this.schema,Object.keys(a),"ou"),h(a.ou,"ou");const n=a.parentDn||this.settings.ldap_top_organization,r=`ou=${d(a.ou)},${n}`,o={ou:a.ou,...Object.fromEntries(Object.entries(a).filter(([t])=>"ou"!==t&&"parentDn"!==t)),objectClass:this.settings.ldap_organization_class};await s(i,this.addOrganization.bind(this),r,o,t)}async apiModify(t,i){const n=e(t,i);if(!n)return;a(n);const r=t.params.dn;if(!r)throw new y("dn is required");L(this.schema,k(n)),await s(i,this.modifyOrganization.bind(this),r,n,t)}async apiDelete(t,i){if(!n(t,i))return;const a=t.params.dn;if(!a)throw new y("dn is required");await s(i,this.deleteOrganization.bind(this),a,t)}async apiMove(t,i){if(!n(t,i))return;const a=e(t,i,"targetOrgDn");if(!a)return;const s=t.params.dn;if(!s)throw new y("dn is required");const{targetOrgDn:o}=a;if(!o||"string"!=typeof o)throw new y("Missing or invalid targetOrgDn in request body");await r(i,this.moveOrganization.bind(this),s,o,t)}hooks={ldapaddrequest:async([t,i,a])=>(this.isOu(i)||await this.checkDeptLink(i),this.isOu(i)&&await this.checkDeptPath(i,t),void 0!==a?[t,i,a]:[t,i]),ldapmodifyrequest:async([t,i,a,e])=>{let s,n={},r={};const checkIsOu=async()=>{if(void 0!==s)return s;if(i.replace?.objectClass)s=this.isOu({objectClass:i.replace.objectClass});else if(i.add?.objectClass)s=this.isOu({objectClass:i.add.objectClass});else{const i=await this.server.ldap.system.search({paged:!1,scope:"base"},t);s=i.searchEntries.length>0&&this.isOu(i.searchEntries[0])}return s};if(i.delete){const t=(Array.isArray(i.delete)?i.delete.map(String):Object.keys(i.delete)).map(t=>t.split(";")[0].toLowerCase()),a=t.includes(this.linkAttr.toLowerCase()),e=t.includes(this.pathAttr.toLowerCase());if(a||e){if(!await checkIsOu()&&a)throw new y("An organization link cannot be deleted");if(e)throw new y("An organization path cannot be deleted")}}if(i.replace&&(i.replace[this.linkAttr]&&(n={...i.replace}),i.replace[this.pathAttr]&&(r={...i.replace})),i.add&&(i.add[this.linkAttr]&&(n={...n,...i.add}),i.add[this.pathAttr]&&(r={...r,...i.add})),Object.keys(n).length>0){await checkIsOu()||await this.checkDeptLink(n)}if(Object.keys(r).length>0){await checkIsOu()&&await this.checkDeptPath(r,t)}return[t,i,a,e]},ldapdeleterequest:async([t,i])=>{const a=Array.isArray(t)?t:[t];for(const t of a)/^\s*ou\s*=/i.test(t)&&await this.isEmptyOrganization(t);return[t,i]},ldaprenamerequest:([t,i,a])=>[t,i,a]};async checkDeptLink(t){if(t[this.linkAttr]){const i=t[this.linkAttr],a=Array.isArray(i)?i[0]:i,e=await this.server.ldap.system.search({paged:!1,scope:"base"},a);if(0===e.searchEntries.length)throw new b(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.settings.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new y(`Entry ${a} isn't in top organization branch`)}}async checkDeptPath(t,i){if(t[this.pathAttr]){const a=t[this.pathAttr],e=Array.isArray(a)?a[0]:a,s=this.settings.ldap_organization_path_separator,n=this.settings.ldap_top_organization,r=i&&n&&/^ou=/i.test(c(i))&&l(i,n)?g(i):void 0,o=t.ou,h=(Array.isArray(o)?o[0]:o)||r;let d=e;if((this.isOu(t)||void 0!==r)&&h){const t=n?g(n):"";if(t&&e.startsWith(h+s)&&e.endsWith(s+t))return;if(e===h){const t=n?p(n):"";if(!i||!t||!l(i,n)||p(i)===t||p(u(i))===t)return;throw new y(`Organization path "${e}" names no parent, but ${i} is not directly under ${n}`)}if(!e.endsWith(s+h))throw new y(`Organization path must end with its own name, preceded by separator "${s}"`);d=e.slice(0,e.length-s.length-h.length)}const w=await this.server.ldap.system.search({paged:!1,scope:"sub",filter:`(${this.pathAttr}=${m(d)})`,attributes:["dn"]},this.settings.ldap_top_organization);if(!w.searchEntries||0===w.searchEntries.length)throw new y(`Invalid organization path ${e}: no organization holds "${d}"`)}}async isEmptyOrganization(t){if((await this.server.ldap.system.search({paged:!1,filter:`(${this.settings.ldap_organization_link_attribute}=${m(t)})`})).searchEntries.length>0)throw new $(`Organization ${t} is not empty`)}isOu(t){if(!t.objectClass)return!1;const i=[].concat(t.objectClass).map(t=>String(t).toLowerCase());return this.settings.ldap_organization_class.filter(t=>"top"!==t.toLowerCase()).some(t=>i.includes(t.toLowerCase()))}async getOrganisationTop(t){if(!this.settings.ldap_top_organization)throw new y("No top organization configured");const i=await this.server.ldap.system.search({paged:!1,scope:"base"},this.settings.ldap_top_organization);if(1!==i.searchEntries.length)throw new b("Top organization not found");const a=i.searchEntries[0],[,e]=await this.hookBus.chain("getOrganisationTop",[t,a]);return e}async getOrganisationByDn(t,i){let a;try{a=await this.server.ldap.search({paged:!1,scope:"base",filter:"(objectClass=organizationalUnit)"},t,i)}catch(i){const a=O(i);if(32===a)throw new b(`Organization ${t} not found`);if(34===a)throw new y(`${t} is not a valid DN`);throw i}if(1!==a.searchEntries.length)throw new b(`Organization ${t} not found`);return a.searchEntries[0]}async getOrganisationSubnodes(t,i){const a=[],e=this.settings.ldap_organization_max_subnodes,s=i?.query?.objectClass;s&&"organizationalUnit"!==s||a.push(...await this.childOrganizations(t,"(objectClass=organizationalUnit)",e,i));const n=this.settings.ldap_top_organization,r=u(n);let o=`(${this.settings.ldap_organization_link_attribute}=${m(t)})`;return s&&(o=`(&${o}(objectClass=${m(s)}))`),this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${r}`),a.push(...await this.linkedEntities(r,o,e,t,i)),a}sizeLimitWarned=new Set;static SIZE_LIMIT_WARNED_MAX=500;warnSizeLimit(t,i){this.sizeLimitWarned.has(t)?this.server.logger.debug(i):(this.sizeLimitWarned.size<LdapOrganizations.SIZE_LIMIT_WARNED_MAX&&this.sizeLimitWarned.add(t),this.server.logger.warn(i))}static moreIndicator(t,i,a,e){return{dn:t,cn:[i],objectClass:["moreIndicator"],_isMoreIndicator:"true",...void 0===e?{}:{_totalCount:e.toString()},_displayedCount:a.toString()}}async childOrganizations(t,i,a,e){try{const a=await this.server.ldap.search({paged:!0,scope:"one",filter:i},t,e),s=[];for await(const t of a)s.push(...t.searchEntries);return this.server.logger.debug(`Found ${s.length} sub-OUs for ${t}`),s}catch(i){const a=O(i);if(32===a)return this.server.logger.debug(`No sub-OUs for ${t}: the node holds none`),[];if(34===a)throw new y(`${t} is not a valid DN`);if(4!==a)throw i}const s=(await this.server.ldap.search({paged:!1,scope:"one",filter:i,sizeLimit:a+1},t,e)).searchEntries.slice(0,a);return this.warnSizeLimit(`children:${t}`,`Organization ${t} holds more child organizations than the directory will list in one answer, so ${s.length} are returned and the rest are hidden. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to list them all.`),[...s,LdapOrganizations.moreIndicator(`more-organizations-${t}`,"... more organizations than the directory will list",s.length)]}async linkedEntities(t,i,a,e,s){const n=[];let r=0;try{const e=await this.server.ldap.search({paged:!0,filter:i},t,s);for await(const t of e){r+=t.searchEntries.length;const i=a-n.length;i>0&&n.push(...t.searchEntries.slice(0,i))}}catch(n){if(t!==e&&(_(n)||403===n?.statusCode))return this.linkedEntities(e,i,a,e,s);if(4!==O(n))throw n;const r=await this.server.ldap.search({paged:!1,filter:i,sizeLimit:a+1},t,s);this.warnSizeLimit(`linked:${e}`,`More entries are attached to ${e} than the directory will list in one answer. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to count them all.`);const o=r.searchEntries.slice(0,a);return[...o,LdapOrganizations.moreIndicator(`more-${e}`,"... more elements than the directory will list",o.length)]}return this.server.logger.debug(`Found ${r} linked entities for ${e}, returning ${n.length}`),r<=a?n:[...n,LdapOrganizations.moreIndicator(`more-${e}`,`... ${r-a} more ${r-a===1?"element":"elements"}`,a,r)]}async addOrganization(t,i,a){return j(this.schema,i),await this.server.ldap.authorize("ldapaddrequest",[t,i,a]),i=await this.validator().validateNew(i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i,a){return await this.server.ldap.authorize("ldapmodifyrequest",[t,i,this.opNumber(),a]),await this.validator().validateChanges(i),await this.server.ldap.modify(t,i,a)}validator(){return new C(this.schema,{entryExists:t=>this.entryExists(t),loadedPlugins:this.server.loadedPlugins,singularName:"organization"})}async entryExists(t){try{return(await this.server.ldap.system.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}}async moveOrganization(t,i,a){w(i,"targetOrgDn");const e=`${c(t)},${i}`;let s,n;await this.server.ldap.authorize("ldaprenamerequest",[t,e,a]);try{s=await this.server.ldap.system.search({paged:!1,scope:"base"},i)}catch(t){throw new b(`Target organization ${i} not found: ${t instanceof Error?t.message:String(t)}`)}if(!s.searchEntries||0===s.searchEntries.length)throw new b(`Target organization ${i} not found`);if(!this.isOu(s.searchEntries[0]))throw new y(`Target ${i} is not an organizational unit`);try{n=await this.server.ldap.system.search({paged:!1,scope:"base"},t)}catch(t){throw new b(`Source organization not found: ${t}`)}if(!n.searchEntries||0===n.searchEntries.length)throw new b(`Source organization ${t} not found`);if(e===t)throw new y("Cannot move organization to its current location");if(i.toLowerCase()===t.toLowerCase()||f(i,t))throw new y("Cannot move organization into itself or its descendant");try{await this.server.ldap.rename(t,e,a),this.logger.info(`Moved organization from ${t} to ${e}`)}catch(t){if(t instanceof v||_(t))throw t;throw new Error(`Failed to move organization: ${String(t)}`,{cause:t})}return{newDn:e}}async deleteOrganization(t,i){try{return await this.server.ldap.delete(t,i)}catch(i){if(32===O(i))throw new b(`Organization ${t} not found`);throw i}}async searchOrganisationSubnodes(t,i,a){const e=[],s=this.settings.ldap_organization_max_subnodes;e.push(...await this.childOrganizations(t,`(&(objectClass=organizationalUnit)${z(i,"ou,description")})`,s,a));const n=this.settings.ldap_top_organization,r=u(n),o=m(t),h=this.config.display_name_attribute||"displayName",d=`(&(${this.settings.ldap_organization_link_attribute}=${o})${z(i,`uid,cn,mail,sn,givenName,${h}`)})`;return this.server.logger.debug(`Searching for linked entities with filter: ${d} in ${r}`),e.push(...await this.linkedEntities(r,d,s,t,a)),e}async searchOrganisationTree(t,i,a){const e=this.settings.ldap_organization_max_subnodes,s=this.pathAttr?`ou,description,${this.pathAttr}`:"ou,description",n=`(&(objectClass=organizationalUnit)${z(i,s)})`,r=[];let o=0;try{const i=await this.server.ldap.search({paged:!0,scope:"sub",filter:n},t,a);for await(const t of i){o+=t.searchEntries.length;const i=e-r.length;i>0&&r.push(...t.searchEntries.slice(0,i))}}catch(i){const s=O(i);if(32===s)return this.server.logger.debug(`No organizations under ${t}: no such node`),[];if(34===s)throw new y(`${t} is not a valid DN`);if(4!==s)throw i;const r=(await this.server.ldap.search({paged:!1,scope:"sub",filter:n,sizeLimit:e+1},t,a)).searchEntries.slice(0,e);return this.warnSizeLimit(`search:${t}`,`A search under ${t} matched more organizations than the directory will list in one answer, so ${r.length} are returned and the rest are hidden. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to search them all.`),[...r,LdapOrganizations.moreIndicator(`more-organizations-${t}`,"... more organizations than the directory will list",r.length)]}if(o<=e)return r;const h=o-e;return[...r,LdapOrganizations.moreIndicator(`more-organizations-${t}`,`... ${h} more ${1===h?"organization":"organizations"}, narrow the search`,e,o)]}getConfigApiData(){const t=this.config.api_prefix||"/api",i=this.settings.organization_schema?A(this.server,this.settings.organization_schema):void 0;return{enabled:!0,topOrganization:this.settings.ldap_top_organization,organizationClass:this.settings.ldap_organization_class,linkAttribute:this.linkAttr||"",pathAttribute:this.pathAttr||"",pathSeparator:this.settings.ldap_organization_path_separator,maxSubnodes:this.settings.ldap_organization_max_subnodes,schema:this.schema,schemaUrl:i,endpoints:{getTop:`${t}/v1/ldap/organizations/top`,get:`${t}/v1/ldap/organizations/:dn`,getSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes`,searchSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes/search`,search:`${t}/v1/ldap/organizations/:dn/search`,create:`${t}/v1/ldap/organizations`,update:`${t}/v1/ldap/organizations/:dn`,move:`${t}/v1/ldap/organizations/:dn/move`,delete:`${t}/v1/ldap/organizations/:dn`}}}}export{x as OrganizationParams,D as OrganizationSearchQuery,LdapOrganizations as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"fs";import{readOptions as s}from"../../config/options.js";import{assertIdentityMode as r,identityFor as t}from"../../lib/auth/base.js";import{fillPattern as a,escapeDnValue as o,isChildOf as
|
|
1
|
+
import e from"fs";import{readOptions as s}from"../../config/options.js";import{assertIdentityMode as r,identityFor as t}from"../../lib/auth/base.js";import{fillPattern as a,escapeDnValue as o,isDnSyntax as i,isChildOf as u,normalizeDn as p}from"../../lib/utils.js";import{scimOptions as h}from"./scim.options.js";class BaseResolver{defaultUserBase;defaultGroupBase;userTemplate;groupTemplate;map;userBaseHeader;groupBaseHeader;headerRoot;config;constructor(t,a=s(h,t)){r(t,"scim base resolver"),this.config=t;const o=t.ldap_base||"";this.defaultUserBase=a.scim_user_base||o,this.defaultGroupBase=a.scim_group_base||o,this.userTemplate=a.scim_user_base_template,this.groupTemplate=a.scim_group_base_template,this.userBaseHeader=a.scim_user_base_header.toLowerCase(),this.groupBaseHeader=a.scim_group_base_header.toLowerCase(),this.headerRoot=a.scim_base_header_root;const i=a.scim_base_map;if(i)try{const s=e.readFileSync(i,"utf8");this.map=JSON.parse(s)}catch(e){throw new Error(`Failed to load SCIM base map from ${i}: ${e instanceof Error?e.message:String(e)}`)}}applyTemplate(e,s){return a(e,"{user}",s?o(s):"")}readHeader(e,s){if(!e||"object"!=typeof e)return;const r=e;if("function"==typeof r.get){const e=r.get(s);if("string"==typeof e&&e.length>0)return e}const t=r.headers?.[s];return"string"==typeof t&&t.length>0?t:Array.isArray(t)&&"string"==typeof t[0]&&t[0].length>0?t[0]:void 0}resolve(e,s){const r=s&&"object"==typeof s?t(s,this.config).value: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}const a="user"===e?this.userBaseHeader:this.groupBaseHeader;if(a&&this.headerRoot){const e=this.readHeader(s,a)?.trim();if(e&&i(e)&&!/[\u0000-\u001f\u007f]/.test(e)&&(e.toLowerCase()===this.headerRoot.toLowerCase()||u(e,this.headerRoot)))return p(e)}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 o="user"===e?this.userTemplate:this.groupTemplate;return o?this.applyTemplate(o,r):"user"===e?this.defaultUserBase:this.defaultGroupBase}userBase(e){return this.resolve("user",e)}groupBase(e){return this.resolve("group",e)}}export{BaseResolver};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Express, Request, Response } from 'express';
|
|
2
|
-
import type { DM } from '../bin';
|
|
3
|
-
import type ldapActions from '../lib/ldapActions';
|
|
4
|
-
import type { AttributesList, AttributeValue, LdapList, ModifyRequest } from '../lib/ldapActions';
|
|
5
|
-
import type { Schema } from '../config/schema';
|
|
6
|
-
import { EntityValidator } from '../lib/entitySchema/validator';
|
|
7
|
-
import DmPlugin from './plugin';
|
|
2
|
+
import type { DM } from '../bin/index.js';
|
|
3
|
+
import type ldapActions from '../lib/ldapActions.js';
|
|
4
|
+
import type { AttributesList, AttributeValue, LdapList, ModifyRequest } from '../lib/ldapActions.js';
|
|
5
|
+
import type { Schema } from '../config/schema.js';
|
|
6
|
+
import { EntityValidator } from '../lib/entitySchema/validator.js';
|
|
7
|
+
import DmPlugin from './plugin.js';
|
|
8
8
|
/** What {@link LdapFlat.listEntries} is asked for. */
|
|
9
9
|
export interface ListEntriesOptions {
|
|
10
10
|
/** LDAP filter, every entry of the branch when absent */
|
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { Express } from 'express';
|
|
6
6
|
import type winston from 'winston';
|
|
7
|
-
import type { Config, DM } from '../bin';
|
|
8
|
-
import type { Hooks, MaybePromise } from '../hooks';
|
|
9
|
-
import type HookBus from '../lib/hookBus';
|
|
10
|
-
import type { HealthStatus } from '../lib/health';
|
|
11
|
-
import { type ServiceProvider, type ServiceToken } from '../lib/services';
|
|
12
|
-
import { type OptionsOf, type OptionSet } from '../config/options';
|
|
13
|
-
export { asyncHandler, escapeDnValue, unescapeDnValue, escapeLdapFilter, validateDnValue, } from '../lib/utils';
|
|
14
|
-
export { defineRoute, registeredRoutes } from '../lib/route';
|
|
15
|
-
export { serviceToken } from '../lib/services';
|
|
16
|
-
export type { ServiceProvider, ServiceToken } from '../lib/services';
|
|
17
|
-
export { HttpError, BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, UriTooLongError, TooManyRequestsError, BadGatewayError, ServiceUnavailableError, GatewayTimeoutError, } from '../lib/errors';
|
|
7
|
+
import type { Config, DM } from '../bin/index.js';
|
|
8
|
+
import type { Hooks, MaybePromise } from '../hooks.js';
|
|
9
|
+
import type HookBus from '../lib/hookBus.js';
|
|
10
|
+
import type { HealthStatus } from '../lib/health.js';
|
|
11
|
+
import { type ServiceProvider, type ServiceToken } from '../lib/services/index.js';
|
|
12
|
+
import { type OptionsOf, type OptionSet } from '../config/options.js';
|
|
13
|
+
export { asyncHandler, escapeDnValue, unescapeDnValue, escapeLdapFilter, validateDnValue, } from '../lib/utils.js';
|
|
14
|
+
export { defineRoute, registeredRoutes } from '../lib/route.js';
|
|
15
|
+
export { serviceToken } from '../lib/services/index.js';
|
|
16
|
+
export type { ServiceProvider, ServiceToken } from '../lib/services/index.js';
|
|
17
|
+
export { HttpError, BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, UriTooLongError, TooManyRequestsError, BadGatewayError, ServiceUnavailableError, GatewayTimeoutError, } from '../lib/errors.js';
|
|
18
18
|
export type { HealthStatus };
|
|
19
19
|
export type Role = 'auth' | 'authz' | 'protect' | 'api' | 'logging' | 'demo' | 'consistency' | 'configurable';
|
|
20
20
|
export default abstract class DmPlugin {
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Request } from 'express';
|
|
7
7
|
import pLimit from 'p-limit';
|
|
8
|
-
import type { DM } from '../bin';
|
|
9
|
-
import { type HealthStatus } from '../lib/health';
|
|
10
|
-
import type { AttributesList } from '../lib/ldapActions';
|
|
11
|
-
import DmPlugin, { type Role } from './plugin';
|
|
8
|
+
import type { DM } from '../bin/index.js';
|
|
9
|
+
import { type HealthStatus } from '../lib/health.js';
|
|
10
|
+
import type { AttributesList } from '../lib/ldapActions.js';
|
|
11
|
+
import DmPlugin, { type Role } from './plugin.js';
|
|
12
12
|
/** What a Twake plugin needs to reach its WebAdmin API */
|
|
13
13
|
export interface TwakeConnection {
|
|
14
14
|
url: string;
|