ldap-rest 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/authz/composition.js +1 -1
- package/dist/lib/instanceProviders.js +1 -0
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/ldapSchema.js +1 -1
- package/dist/plugins/auth/openidconnect.js +1 -1
- package/dist/plugins/configApi.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/ldap/raw.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/scim/mapping.js +1 -1
- package/dist/plugins/scim/patch.js +1 -1
- package/dist/plugins/scim/users.js +1 -1
- package/dist/plugins/twake/instances.js +1 -0
- package/dist/plugins/twake/lifecycleAttributes.js +1 -0
- package/dist/plugins/twake/lifecycleEvents.js +1 -0
- package/dist/plugins/twake/tombstone.js +1 -0
- package/dist/src/abstract/plugin.d.ts +7 -4
- package/dist/src/config/args.d.ts +50 -0
- package/dist/src/hooks.d.ts +2 -0
- package/dist/src/lib/authz/composition.d.ts +1 -1
- package/dist/src/lib/instanceProviders.d.ts +86 -0
- package/dist/src/lib/ldapActions.d.ts +79 -2
- package/dist/src/lib/ldapSchema.d.ts +14 -0
- package/dist/src/plugins/auth/openidconnect.d.ts +24 -0
- package/dist/src/plugins/ldap/raw.d.ts +3 -6
- package/dist/src/plugins/scim/mapping.d.ts +4 -1
- package/dist/src/plugins/scim/patch.d.ts +7 -0
- package/dist/src/plugins/twake/instances.d.ts +88 -0
- package/dist/src/plugins/twake/lifecycleAttributes.d.ts +29 -0
- package/dist/src/plugins/twake/lifecycleEvents.d.ts +45 -0
- package/dist/src/plugins/twake/tombstone.d.ts +86 -0
- package/docs/client-development/api/rest-api.md +2 -0
- package/docs/plugin-development/hooks.md +24 -0
- package/docs/usage/configuration.md +86 -10
- package/docs/usage/plugins/README.md +9 -6
- package/docs/usage/plugins/auth/oidc.md +27 -0
- package/docs/usage/plugins/integrations/README.md +11 -4
- package/docs/usage/plugins/integrations/instances.md +122 -0
- package/docs/usage/plugins/integrations/lifecycle-events.md +200 -0
- package/docs/usage/plugins/integrations/scim.md +11 -0
- package/docs/usage/plugins/integrations/tombstone.md +115 -0
- package/docs/usage/plugins/ldap/flat-generic.md +26 -0
- package/docs/usage/plugins/ldap/raw.md +1 -1
- package/docs/usage/upgrading.md +58 -0
- package/package.json +17 -1
package/dist/plugins/ldap/raw.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{FilterParser as t}from"ldapts";import e from"../../abstract/plugin.js";import{tryMethodData as s}from"../../lib/expressFormatedResponses.js";import{isDnInBranch as r,getRdn as
|
|
1
|
+
import{FilterParser as t}from"ldapts";import e from"../../abstract/plugin.js";import{tryMethodData as s}from"../../lib/expressFormatedResponses.js";import{isDnInBranch as r,getRdn as i,getParentDn as a,asyncHandler as n}from"../../lib/utils.js";import{BadRequestError as o,ForbiddenError as h,NotFoundError as c}from"../../lib/errors.js";const d=["base","one","sub"],l=["userpassword","pwdhistory","sambantpassword","sambalmpassword","sambapasswordhistory","krbprincipalkey","krbextradata","krbpwdhistory","unicodepwd","dbcspwd","lmpwdhistory","ntpwdhistory","supplementalcredentials","userpkcs12","authpassword"];class LdapRaw extends e{constructor(t){super(t),this.name="ldapRaw",this.roles=["api"];const e=(this.config.ldap_raw_base||[]).filter(t=>t&&t.length>0);if(0===e.length){if(!this.config.ldap_base)throw new Error("Missing --ldap-base or --ldap-raw-base for plugin core/ldap/raw");e.push(this.config.ldap_base)}this.bases=e,this.showSecrets=!0===this.config.ldap_raw_show_secrets,this.hiddenAttributes=new Set([...this.showSecrets?[]:l,...(this.config.ldap_raw_hidden_attribute||[]).filter(t=>t&&t.length>0).map(t=>t.toLowerCase())]),this.maxResults=this.config.ldap_raw_max_results||200,this.logger.info(`LDAP raw API enabled on ${this.bases.join(", ")} (read-only)`),this.showSecrets&&this.logger.warn("LDAP raw API: --ldap-raw-show-secrets is set, password hashes and other credential attributes are served over the API")}checkDn(t){if(!t||!t.trim())throw new o("DN is required");if(!this.bases.some(e=>r(t,e)||t.toLowerCase()===e.toLowerCase()))throw new h(`DN ${t} is outside the exposed bases`)}checkFilter(e){try{t.parseString(e)}catch(t){const s=t instanceof Error?t.message:String(t);throw new o(`Invalid LDAP filter ${JSON.stringify(e)}: ${s}. A filter must be parenthesised, e.g. (cn=foo) or (|(cn=*foo*)(ou=*foo*))`)}}formatEntry(t){const e={};for(const[s,r]of Object.entries(t)){if("dn"===s)continue;if("*"===s||"+"===s||"1.1"===s)continue;if(this.hiddenAttributes.has(s.split(";")[0].toLowerCase()))continue;const t=Array.isArray(r)?r:[r],i=t.some(t=>Buffer.isBuffer(t));e[s]={values:t.map(t=>Buffer.isBuffer(t)?t.toString("base64"):i?Buffer.from(String(t),"utf8").toString("base64"):String(t)),binary:i}}return{dn:t.dn,attributes:e}}async searchDirectory(t,e,s){try{return await this.server.ldap.search({paged:!1,...e},t,s)}catch(e){if(32===e?.code)throw new c(`Entry ${t||"(root DSE)"} not found`);throw e}}async getRootDse(){const t=(await this.searchDirectory("",{scope:"base",filter:"(objectClass=*)",attributes:["*","+"]})).searchEntries[0];if(!t)throw new c("Root DSE is not readable");return this.formatEntry({...t,dn:""})}async getSubschemaDn(){return this.server.ldap.subschemaDn()}async getSchemaIndex(){return this.server.ldap.schemaIndex()}async getSchema(){return(await this.getSchemaIndex()).schema}async getEntry(t,e){this.checkDn(t);const read=async s=>this.searchDirectory(t,{scope:"base",filter:"(objectClass=*)",attributes:s},e);let s;try{s=await read(["*","+"])}catch(e){if(e instanceof Error&&("statusCode"in e||/\[authz-forbidden\]/.test(e.message)))throw e;this.logger.debug(`Reading ${t} with operational attributes failed (${e}), retrying without`),s=await read(["*"])}const r=s.searchEntries[0];if(!r)throw new c(`Entry ${t} not found`);return this.formatEntry(r)}async getChildren(t,e,s){this.checkDn(t);const r=await this.searchDirectory(t,{scope:"one",filter:"(objectClass=*)",attributes:["objectClass"],sizeLimit:this.maxResults+1},s),a=r.searchEntries.length>this.maxResults,n=[];for(const t of r.searchEntries.slice(0,this.maxResults)){const e=t.objectClass;n.push({dn:t.dn,rdn:i(t.dn),objectClass:Array.isArray(e)?e.map(String):e?[String(e)]:[],hasChildren:!1})}return n.sort((t,e)=>t.rdn.localeCompare(e.rdn)),e&&await Promise.all(n.map(async t=>{t.hasChildren=await this.hasChildren(t.dn,s)})),a&&this.logger.info(`Children of ${t} truncated to ${this.maxResults} entries`),{children:n,truncated:a}}async hasChildren(t,e){try{return(await this.searchDirectory(t,{scope:"one",filter:"(objectClass=*)",attributes:["1.1"],sizeLimit:1},e)).searchEntries.length>0}catch{return!1}}async search(t,e){this.checkDn(t.base),this.checkFilter(t.filter);const s=Math.min(t.limit||this.maxResults,this.maxResults),r=await this.searchDirectory(t.base,{scope:t.scope,filter:t.filter,attributes:t.attributes?.length?t.attributes:["*"],sizeLimit:s+1},e),i=r.searchEntries.length>s;return{entries:r.searchEntries.slice(0,s).map(t=>this.formatEntry(t)),truncated:i}}parentOf(t){if(this.bases.some(e=>t.toLowerCase()===e.toLowerCase()))return null;const e=a(t);return e&&e!==t?e:null}api(t){const e=`${this.config.api_prefix}/v1/ldap/raw`;t.get(`${e}/bases`,n(async(t,e)=>{await s(e,()=>({bases:this.bases}))})),t.get(`${e}/rootdse`,n(async(t,e)=>{await s(e,this.getRootDse.bind(this))})),t.get(`${e}/schema`,n(async(t,e)=>{await s(e,this.getSchema.bind(this))})),t.get(`${e}/search`,n(async(t,e)=>{const r=t.query.scope||"sub";if(!d.includes(r))throw new o(`Invalid scope "${r}", expected one of ${d.join(", ")}`);const i="string"==typeof t.query.attributes?t.query.attributes.split(",").map(t=>t.trim()).filter(Boolean):void 0,a="string"==typeof t.query.limit?parseInt(t.query.limit,10):void 0;if(void 0!==a&&(isNaN(a)||a<1))throw new o("limit must be a positive integer");await s(e,this.search.bind(this),{base:t.query.base||this.bases[0],scope:r,filter:t.query.filter||"(objectClass=*)",attributes:i,limit:a},t)})),t.get(`${e}/children/:dn`,n(async(t,e)=>{const r=decodeURIComponent(t.params.dn),i=void 0!==t.query.children&&"false"!==t.query.children;await s(e,this.getChildren.bind(this),r,i,t)})),t.get(`${e}/entry/:dn`,n(async(t,e)=>{const r=decodeURIComponent(t.params.dn);await s(e,this.getEntry.bind(this),r,t)}))}getConfigApiData(){return{bases:this.bases,readOnly:!0,maxResults:this.maxResults,hiddenAttributes:[...this.hiddenAttributes],showSecrets:this.showSecrets}}}export{LdapRaw as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{escapeDnValue as t,isDummyMemberDn as e,launchHooksChained as s,validateDnValue as r,launchHooks as i,isChildOf as o,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as n}from"./types.js";import{withExternalId as c,mergeMapping as h,loadMappingFile as u,DEFAULT_GROUP_MAPPING as
|
|
1
|
+
import{escapeDnValue as t,isDummyMemberDn as e,launchHooksChained as s,validateDnValue as r,launchHooks as i,isChildOf as o,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as n}from"./types.js";import{withExternalId as c,mergeMapping as h,loadMappingFile as u,DEFAULT_GROUP_MAPPING as m,requiredLdapAttributes as d,ldapToScimGroup as p,scimGroupToLdap as l}from"./mapping.js";import{scimFilterToLdap as f}from"./filter.js";import{pagedSearch as g}from"./list.js";import{writePatch as b,patchToModifyRequest as w}from"./patch.js";import{scimNotFound as y,ScimError as R,scimInvalidValue as _,scimUniqueness as $}from"./errors.js";import{extractLdapCode as v}from"../../lib/ldapCodes.js";class ScimGroups{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.users=t.users,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_group_rdn_attribute||"cn",this.objectClass=this.config.scim_group_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=this.config.scim_group_mapping||"";this.mapping=c(h(m,e?u(e):void 0),this.config.scim_group_external_id_attribute||"")}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"Group",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:"",lockValue:""}}dnForId(e,s){const r=this.baseResolver.groupBase(s);return`${this.rdnAttribute}=${t(e)},${r}`}buildMemberResolver(t){const s=this.baseResolver.userBase(t),r=this.baseResolver.groupBase(t),i=this.scimPrefix,o=this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),a=this.config.scim_user_rdn_attribute||"uid",n=this.rdnAttribute,c=this.config.group_dummy_user||"cn=fakeuser";return t=>{if(!t)return;if(e(t,c))return;const h=/^([^=]+)=((?:\\.|[^,])+)/.exec(t);if(!h)return;const u=h[2].replace(/\\(.)/g,"$1"),m=t.toLowerCase();let d;s&&m.endsWith(s.toLowerCase())?d="User":r&&m.endsWith(r.toLowerCase())?d="Group":h[1]===a?d="User":h[1]===n&&(d="Group");const p={value:u};if(d&&(p.type=d,o)){const t="User"===d?"Users":"Groups";p.$ref=`${o.replace(/\/$/,"")}${i}/${t}/${encodeURIComponent(u)}`}return p}}async get(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:[...d(this.mapping),"member"]},s)}catch(t){if(32===v(t))throw y(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw y(`Group ${e} not found`);return p(r.searchEntries[0],this.mapping,this.ctx(t),this.buildMemberResolver(t))}async list(t,e){const s=this.baseResolver.groupBase(t),r=Math.max(1,e.startIndex||1),i=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let o,a=`(objectClass=${this.objectClass.find(t=>"top"!==t)||"groupOfNames"})`;if(e.filter){const t=f(e.filter,this.mapping);t.idEquals?o=t.idEquals:a=`(&${a}${t.ldapFilter})`}if(o)try{const e=await this.get(t,o);return{schemas:[n],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof R&&404===t.statusCode)return{schemas:[n],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:c,totalResults:h}=await g({directory:this.ldap.forRequest(t),base:s,filter:a,attributes:[...d(this.mapping),"member"],startIndex:r,count:i,maxScanned:this.maxScanned}),u=this.buildMemberResolver(t),m=c.map(e=>p(e,this.mapping,this.ctx(t),u));return{schemas:[n],totalResults:h,startIndex:r,itemsPerPage:m.length,Resources:m}}async create(e,o){if(!o.displayName)throw _("displayName is required");const a=(await s(this.hooks.scimgroupcreate,[o,e]))[0],{rdn:n,attributes:c}=l(a,this.mapping,this.objectClass);if(!n)throw _("displayName is required");r(n,this.rdnAttribute),c[this.rdnAttribute]=n;const h=[];if(a.members&&Array.isArray(a.members))for(const t of a.members){if(!t||!t.value)continue;const s=await this.users.resolveRef(e,t.value);s&&h.push(s)}if(0===h.length){const t=this.config.group_dummy_user||"cn=fakeuser";h.push(t)}c.member=h;const u=this.baseResolver.groupBase(e),m=`${this.rdnAttribute}=${t(n)},${u}`;try{await this.ldap.forRequest(e).add(m,c)}catch(t){if(68===v(t))throw $(`Group ${n} already exists`);throw t}const d=await this.get(e,n);return i(this.hooks.scimgroupcreatedone,d),d}async replace(t,e,r){await this.get(t,e);const o=(await s(this.hooks.scimgroupupdate,[e,r,t]))[1],{attributes:a}=l(o,this.mapping,this.objectClass),n=[];if(o.members&&Array.isArray(o.members))for(const e of o.members){if(!e||!e.value)continue;const s=await this.users.resolveRef(t,e.value);s&&n.push(s)}if(0===n.length){const t=this.config.group_dummy_user||"cn=fakeuser";n.push(t)}const c=this.dnForId(e,t),h={replace:{}};for(const[t,e]of Object.entries(a))"objectClass"!==t&&t!==this.rdnAttribute&&(h.replace[t]=e);h.replace.member=n,await this.ldap.forRequest(t).modify(c,h);const u=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,u),u}async patch(t,e,s){const r=this.dnForId(e,t);await b(r,async()=>{const i=await this.currentEntry(t,e),o=await w(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e),current:i}),a=o.delete&&!Array.isArray(o.delete)?o.delete.member:void 0;if(void 0!==a){const t=this.config.group_dummy_user||"cn=fakeuser";if(""===a)delete o.delete.member,0===Object.keys(o.delete).length&&delete o.delete,o.replace||(o.replace={}),o.replace.member=[t];else{const e=[].concat(a);0===[].concat(i.member??[],o.add?.member??[]).map(String).filter(t=>!e.includes(t)).length&&(o.add||(o.add={}),o.add.member=t)}}await this.ldap.forRequest(t).modify(r,o,{permissive:!0})});const o=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,o),o}async currentEntry(t,e){const s=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:[...d(this.mapping),"member"]},s)}catch(t){if(32===v(t))throw y(`Group ${e} not found`);throw t}if(!r.searchEntries?.length)throw y(`Group ${e} not found`);return r.searchEntries[0]}async delete(t,e){await this.get(t,e);const r=(await s(this.hooks.scimgroupdelete,[e,t]))[0],o=this.dnForId(r,t);await this.ldap.forRequest(t).delete(o),i(this.hooks.scimgroupdeletedone,r)}async resolveRef(e,s){if(!s)return;const r=this.baseResolver.groupBase(e);if(s.includes("=")&&s.includes(","))return o(s,r)?s:void 0;const i=`${this.rdnAttribute}=${t(s)},${r}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==v(t))throw t}try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},r);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==v(t))throw t}}get groupMapping(){return this.mapping}}export{ScimGroups};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"fs";import{scimInvalidValue as r}from"./errors.js";import{SCHEMA_GROUP as t,SCHEMA_USER as i}from"./types.js";const a=["createTimestamp","modifyTimestamp","entryUUID"],n={resourceType:"User",schemas:[i],entries:[{scim:"userName",ldap:"uid"},{scim:"externalId",ldap:"employeeNumber"},{scim:"name",sub:{familyName:"sn",givenName:"givenName",formatted:"cn",middleName:"initials"}},{scim:"displayName",ldap:"displayName"},{scim:"nickName",ldap:"displayName"},{scim:"title",ldap:"title"},{scim:"preferredLanguage",ldap:"preferredLanguage"},{scim:"emails",ldapPrimary:"mail",ldapSecondary:"mailAlternateAddress",multi:"array"},{scim:"phoneNumbers",ldapPrimary:"telephoneNumber",ldapSecondary:"mobile",multi:"array"}]},o={resourceType:"Group",schemas:[t],entries:[{scim:"displayName",ldap:"cn"}]};function withExternalId(e,r){const t=(r||"").trim();return t?e.entries.some(e=>"externalId"===e.scim)?e:{...e,entries:[...e.entries,{scim:"externalId",ldap:t}]}:e}function asString(e){if(null!=e){if(Array.isArray(e)){const r=e[0];return null==r?void 0:String(r)}return Buffer.isBuffer(e)?e.toString():String(e)}}function ldapTimeToIso(e){if(!e)return;const r=/^(\d{4})(\d{2})(\d{2})(\d{2})(?:(\d{2})(?:(\d{2}))?)?(?:[.,](\d+))?(Z|[+-]\d{2}(?:\d{2})?)?$/.exec(e);if(!r)return e;const[,t,i,a,n,o,s,c,l]=r;let d=0;if(c){const e=s?1e3:o?6e4:36e5;d=Math.round(Number(`0.${c}`)*e)}let u=1e3*(60*(60*Number(n)+Number(o||0))+Number(s||0))+d;const m=Math.floor(u/36e5);u-=36e5*m;const p=Math.floor(u/6e4);u-=6e4*p;const f=Math.floor(u/1e3),y=u-1e3*f;let b="Z";l&&"Z"!==l&&(b=`${l[0]}${l.slice(1,3)}:${l.slice(3,5)||"00"}`);const pad=e=>String(e).padStart(2,"0"),g=`${t}-${i}-${a}T${pad(m)}:${pad(p)}:${pad(f)}${y?`.${String(y).padStart(3,"0")}`:""}${b}`;return Number.isNaN(Date.parse(g))?e:g}function asArray(e){return null==e?[]:Array.isArray(e)?e.map(e=>Buffer.isBuffer(e)?e.toString():String(e)):Buffer.isBuffer(e)?[e.toString()]:[String(e)]}function loadMappingFile(r){if(!r)return;const t=e.readFileSync(r,"utf8");return JSON.parse(t)}function mergeMapping(e,r){if(!r)return e;const t=new Map;for(const r of e.entries)t.set(r.scim,r);for(const e of r.entries)t.set(e.scim,e);return{...e,entries:Array.from(t.values())}}const s="pwdAccountLockedTime",c="000001010000Z",l=/^\d{10,14}(\.\d+)?(Z|[+-]\d{2,4})$/;let warn=e=>{console.warn(`WARNING: ${e}`)};function setLockConfigWarn(e){const r=warn;return warn=e,r}const d=/^([A-Za-z][A-Za-z0-9-]*|\d+(\.\d+)+)$/;function resolveLockConfig(e,r){const
|
|
1
|
+
import e from"fs";import{scimInvalidValue as r}from"./errors.js";import{SCHEMA_GROUP as t,SCHEMA_USER as i}from"./types.js";const a=["createTimestamp","modifyTimestamp","entryUUID"],n={resourceType:"User",schemas:[i],entries:[{scim:"userName",ldap:"uid"},{scim:"externalId",ldap:"employeeNumber"},{scim:"name",sub:{familyName:"sn",givenName:"givenName",formatted:"cn",middleName:"initials"}},{scim:"displayName",ldap:"displayName"},{scim:"nickName",ldap:"displayName"},{scim:"title",ldap:"title"},{scim:"preferredLanguage",ldap:"preferredLanguage"},{scim:"emails",ldapPrimary:"mail",ldapSecondary:"mailAlternateAddress",multi:"array"},{scim:"phoneNumbers",ldapPrimary:"telephoneNumber",ldapSecondary:"mobile",multi:"array"}]},o={resourceType:"Group",schemas:[t],entries:[{scim:"displayName",ldap:"cn"}]};function withExternalId(e,r){const t=(r||"").trim();return t?e.entries.some(e=>"externalId"===e.scim)?e:{...e,entries:[...e.entries,{scim:"externalId",ldap:t}]}:e}function asString(e){if(null!=e){if(Array.isArray(e)){const r=e[0];return null==r?void 0:String(r)}return Buffer.isBuffer(e)?e.toString():String(e)}}function ldapTimeToIso(e){if(!e)return;const r=/^(\d{4})(\d{2})(\d{2})(\d{2})(?:(\d{2})(?:(\d{2}))?)?(?:[.,](\d+))?(Z|[+-]\d{2}(?:\d{2})?)?$/.exec(e);if(!r)return e;const[,t,i,a,n,o,s,c,l]=r;let d=0;if(c){const e=s?1e3:o?6e4:36e5;d=Math.round(Number(`0.${c}`)*e)}let u=1e3*(60*(60*Number(n)+Number(o||0))+Number(s||0))+d;const m=Math.floor(u/36e5);u-=36e5*m;const p=Math.floor(u/6e4);u-=6e4*p;const f=Math.floor(u/1e3),y=u-1e3*f;let b="Z";l&&"Z"!==l&&(b=`${l[0]}${l.slice(1,3)}:${l.slice(3,5)||"00"}`);const pad=e=>String(e).padStart(2,"0"),g=`${t}-${i}-${a}T${pad(m)}:${pad(p)}:${pad(f)}${y?`.${String(y).padStart(3,"0")}`:""}${b}`;return Number.isNaN(Date.parse(g))?e:g}function asArray(e){return null==e?[]:Array.isArray(e)?e.map(e=>Buffer.isBuffer(e)?e.toString():String(e)):Buffer.isBuffer(e)?[e.toString()]:[String(e)]}function loadMappingFile(r){if(!r)return;const t=e.readFileSync(r,"utf8");return JSON.parse(t)}function mergeMapping(e,r){if(!r)return e;const t=new Map;for(const r of e.entries)t.set(r.scim,r);for(const e of r.entries)t.set(e.scim,e);return{...e,entries:Array.from(t.values())}}const s="pwdAccountLockedTime",c="000001010000Z",l=/^\d{10,14}(\.\d+)?(Z|[+-]\d{2,4})$/;let warn=e=>{console.warn(`WARNING: ${e}`)};function setLockConfigWarn(e){const r=warn;return warn=e,r}const d=/^([A-Za-z][A-Za-z0-9-]*|\d+(\.\d+)+)$/;function resolveLockConfig(e,r,t={attribute:"--scim-user-lock-attribute",value:"--scim-user-lock-value"}){const i=(e||"").trim()||s;if(!d.test(i))throw new Error(`${t.attribute} must be an LDAP attribute name, got '${i}'`);const a=i.toLowerCase()===s.toLowerCase(),n=(r||"").trim();if(!n){if(a)return{attribute:i,value:c};throw new Error(`${t.attribute} is '${i}', so ${t.value} must say what marks an account locked (for nsAccountLock on 389-ds, 'TRUE'). The default '${c}' only means anything to ${s}.`)}return a||n!==c||warn(`${t.value} is the ppolicy convention '${c}' but ${t.attribute} is '${i}', not ${s}. Unless '${i}' is a GeneralizedTime your directory reads as a lock, every deactivation will answer 200 and read back as active:false while the account keeps binding. Verify a deactivation actually prevents a bind.`),a&&!l.test(n)&&warn(`${t.value} '${n}' is not a GeneralizedTime, and ${s} holds one. The directory will refuse the write, or — where the attribute was redefined locally — store a value it does not read as a lock. The ppolicy convention is '${c}'.`),{attribute:i,value:n}}function readActive(e){if(void 0!==e){if("boolean"==typeof e)return e;if("string"==typeof e){const r=e.trim().toLowerCase();if("true"===r)return!0;if("false"===r)return!1}throw r(`Cannot read ${JSON.stringify(e)} as active`)}}function isLocked(e,r){const t=e[r.lockAttribute];return null!=t&&(Array.isArray(t)?t.length>0:String(t).length>0)}function resolveScimId(e,r){if("rdn"===r.idAttribute)return asString(e[r.rdnAttribute]);return asString(e[r.idAttribute])??asString(e[r.rdnAttribute])}function buildLocation(e,r){if(!r.baseUrl)return;const t="User"===r.resourceType?"Users":"Groups";return`${r.baseUrl.replace(/\/$/,"")}${r.scimPrefix}/${t}/${encodeURIComponent(e)}`}function ldapToScimUser(e,r,t){const a={schemas:[i]},n=resolveScimId(e,t);n&&(a.id=n);for(const t of r.entries){if(t.sub){const r={};for(const[i,a]of Object.entries(t.sub)){const t=asString(e[a]);null!=t&&(r[i]=t)}Object.keys(r).length>0&&(a[t.scim]=r);continue}if(t.ldapPrimary||t.ldapSecondary){const r=[];if(t.ldapPrimary){const i=asString(e[t.ldapPrimary]);null!=i&&r.push({value:i,primary:!0})}if(t.ldapSecondary)for(const i of asArray(e[t.ldapSecondary]))r.push({value:i});r.length>0&&(a[t.scim]=r);continue}if(t.ldap)if("array"===t.multi){const r=asArray(e[t.ldap]);r.length>0&&(a[t.scim]=r)}else{const r=asString(e[t.ldap]);null!=r&&(a[t.scim]=r)}}return a.active=!isLocked(e,t),n&&(a.meta={resourceType:"User",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(n,t)}),a}function scimUserToLdap(e,r,t,i){const a={objectClass:i},n=e.userName||e.id||"";for(const t of r.entries){if(t.readOnly||t.operational)continue;const r=e[t.scim];if(null!=r)if(t.sub&&"object"==typeof r)for(const[e,i]of Object.entries(t.sub)){const t=r[e];"string"==typeof t&&t.length>0&&(a[i]=t)}else{if(t.ldapPrimary||t.ldapSecondary){if(!Array.isArray(r))continue;const e=r,i=e.find(e=>!0===e.primary)||e[0],n=e.filter(e=>e!==i);t.ldapPrimary&&i&&i.value&&(a[t.ldapPrimary]=i.value),t.ldapSecondary&&n.length>0&&(a[t.ldapSecondary]=n.map(e=>e.value).filter(Boolean));continue}t.ldap&&(Array.isArray(r)?a[t.ldap]=r.map(e=>String(e)):"string"!=typeof r&&"number"!=typeof r||(a[t.ldap]=String(r)))}}const o=readActive(e.active);if(!1===o&&(a[t.lockAttribute]=t.lockValue||c),!a.cn&&n&&(a.cn=n),!a.sn){const r=e.name?.familyName||e.displayName||n;r&&(a.sn=r)}return{rdn:n,attributes:a,active:o}}function ldapToScimGroup(e,r,i,a){const n={schemas:[t]},o=resolveScimId(e,i);o&&(n.id=o);for(const t of r.entries){if(!t.ldap)continue;const r=e[t.ldap];if(null!=r)if("array"===t.multi){const e=asArray(r);e.length>0&&(n[t.scim]=e)}else{const e=asString(r);null!=e&&(n[t.scim]=e)}}const s=asArray(e.member),c=[];for(const e of s)if(e)if(a){const r=a(e);r&&c.push(r)}else c.push({value:e,type:"User"});return c.length>0&&(n.members=c),o&&(n.meta={resourceType:"Group",created:ldapTimeToIso(asString(e.createTimestamp)),lastModified:ldapTimeToIso(asString(e.modifyTimestamp)),location:buildLocation(o,i)}),n}function scimGroupToLdap(e,r,t){const i={objectClass:t},a=e.displayName||e.id||"";for(const t of r.entries){if(t.readOnly||t.operational)continue;if(!t.ldap)continue;const r=e[t.scim];null!=r&&(Array.isArray(r)?i[t.ldap]=r.map(e=>"string"==typeof e?e:JSON.stringify(e)):"string"!=typeof r&&"number"!=typeof r||(i[t.ldap]=String(r)))}return{rdn:a,attributes:i}}function requiredLdapAttributes(e,r=[]){const t=new Set(["objectClass",...a,...r]);for(const r of e.entries)if(r.ldap&&t.add(r.ldap),r.ldapPrimary&&t.add(r.ldapPrimary),r.ldapSecondary&&t.add(r.ldapSecondary),r.sub)for(const e of Object.values(r.sub))t.add(e);return Array.from(t)}function scimPathToLdapAttribute(e,r){const t=e.split(".")[0],i=e.includes(".")?e.split(".").slice(1).join("."):"";for(const e of r.entries)if(e.scim===t){if(e.sub&&i)return e.sub[i];if((e.ldapPrimary||e.ldapSecondary)&&("value"===i||!i))return e.ldapPrimary||e.ldapSecondary;if(e.ldap)return e.ldap}}export{o as DEFAULT_GROUP_MAPPING,s as DEFAULT_LOCK_ATTRIBUTE,c as DEFAULT_LOCK_VALUE,n as DEFAULT_USER_MAPPING,buildLocation,isLocked,ldapTimeToIso,ldapToScimGroup,ldapToScimUser,loadMappingFile,mergeMapping,readActive,requiredLdapAttributes,resolveLockConfig,resolveScimId,scimGroupToLdap,scimPathToLdapAttribute,scimUserToLdap,setLockConfigWarn,warn,withExternalId};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{scimInvalidValue as
|
|
1
|
+
import{extractLdapCode as e}from"../../lib/ldapCodes.js";import{scimInvalidValue as t,ScimError as r,scimInvalidPath as o,scimNoTarget as i}from"./errors.js";import{DEFAULT_LOCK_ATTRIBUTE as n,DEFAULT_LOCK_VALUE as a,scimPathToLdapAttribute as s,readActive as l}from"./mapping.js";function asValues(e){if(null==e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString():String(e)).filter(e=>e.length>0)}class WorkingEntry{constructor(e){this.current=e,this.values=new Map,this.touched=new Set,this.whole=new Set}get(e){return this.values.has(e)||this.values.set(e,asValues(this.current[e])),this.values.get(e)}set(e,t){this.whole.add(e),this.write(e,t)}append(e,t){const r=this.get(e);this.write(e,[...r,...t.filter(e=>!r.includes(e))])}drop(e,t){if(!t)return this.set(e,[]);this.write(e,this.get(e).filter(e=>!t.includes(e)))}write(e,t){this.touched.add(e),this.values.set(e,t)}diff(){const e={};for(const t of this.touched){const r=asValues(this.current[t]),o=this.get(t),i=o.filter(e=>!r.includes(e)),n=r.filter(e=>!o.includes(e));if(0!==i.length||0!==n.length)if(this.whole.has(t))if(0!==o.length)0===n.length&&r.length>0?(e.add||(e.add={}),e.add[t]=1===i.length?i[0]:i):(e.replace||(e.replace={}),e.replace[t]=1===o.length?o[0]:o);else{if(0===r.length)continue;e.delete||(e.delete={}),e.delete[t]=""}else i.length>0&&(e.add||(e.add={}),e.add[t]=1===i.length?i[0]:i),n.length>0&&(e.delete||(e.delete={}),e.delete[t]=1===n.length?n[0]:n)}return e}}function normalizeOp(e){const r=e.toLowerCase();if("add"===r||"remove"===r||"replace"===r)return r;throw t(`Unknown patch op '${e}'`)}const u=new Set(["__proto__","constructor","prototype"]),f=/^[A-Za-z_$:][\w$:]*$/;function assertSafeKey(e){if(u.has(e))throw o(`Forbidden attribute name '${e}'`)}function parsePath(e){if("string"!=typeof e||0===e.length||e.length>512)throw o(`Malformed path '${String(e)}'`);let t,r,i;const n=e.indexOf("[");if(n>=0){const a=e.lastIndexOf("]");if(a<=n)throw o(`Malformed path '${e}'`);t=e.slice(0,n),r=e.slice(n+1,a);const s=e.slice(a+1);if(s.length>0){if("."!==s[0])throw o(`Malformed path '${e}'`);i=s.slice(1)}}else{const r=e.indexOf(".");r>=0?(t=e.slice(0,r),i=e.slice(r+1)):t=e}if(!f.test(t))throw o(`Malformed path '${e}'`);if(assertSafeKey(t),null!=i){if(!f.test(i))throw o(`Malformed sub-attribute in '${e}'`);assertSafeKey(i)}return{top:t,filter:r,sub:i}}async function applyOperation(e,r,u){const f=normalizeOp(e.op),p=u.memberAttribute||"member";if(!e.path){if("remove"===f)throw i("PATCH 'remove' requires a path");if(null==e.value||"object"!=typeof e.value||Array.isArray(e.value))throw i("PATCH without path requires an object value");const t=e.value;for(const[o,i]of Object.entries(t))await applyOperation({op:e.op,path:o,value:i},r,u);return}const{top:c,sub:h,filter:d}=parsePath(e.path);if("members"===c){const t=u.resolveMemberRef;if(!t)throw i("Member operations require a member resolver");const asMemberValues=async e=>{const r=Array.isArray(e)?e:[e],o=[];for(const e of r){const r="string"==typeof e?e:"object"==typeof e&&null!=e&&"value"in e?String(e.value):"";if(!r)continue;const i=await t(r);i&&o.push(i)}return o};if("add"===f)return void r.append(p,await asMemberValues(e.value));if("replace"===f){const t=await asMemberValues(e.value);if(0===t.length)return;return void r.set(p,t)}if(!d&&null==e.value)return void r.drop(p);const o=d?/value\s+eq\s+"([^"]+)"/i.exec(d)?.[1]??"":"",n=[...o?await asMemberValues(o):[],...null!=e.value?await asMemberValues(e.value):[]];return void r.drop(p,n)}if("active"===c&&u.supportsActive){if(h||d)throw o(`'active' has no sub-attribute (got '${e.path}')`);const i=u.lockAttribute||n,s="remove"===f||function(e){const r=l(e);if(void 0===r)throw t("PATCH on 'active' requires a value");return r}(e.value);return void(s?r.drop(i):r.set(i,[u.lockValue||a]))}if(d)throw o(`Complex multi-valued filters are only supported on 'members' (got '${e.path}')`);const y=s(h?`${c}.${h}`:c,u.mapping);if(!y)throw o(`Unknown SCIM attribute path '${e.path}'`);if("remove"===f)return void r.drop(y);const v=function(e){if(null!=e)return"string"==typeof e?e:"number"==typeof e||"boolean"==typeof e?String(e):Array.isArray(e)?e.map(e=>{if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);if(e&&"object"==typeof e&&!Array.isArray(e)&&"value"in e&&("string"==typeof e.value||"number"==typeof e.value||"boolean"==typeof e.value))return String(e.value);throw t(`Unsupported array element in PATCH value: ${JSON.stringify(e)}`)}):void 0}(e.value);if(null==v)throw t(`PATCH ${e.op} ${e.path} missing value`);const w=Array.isArray(v)?v:[v];"add"===f?r.append(y,w):r.set(y,w)}async function patchToModifyRequest(e,r){if(!e.Operations||!Array.isArray(e.Operations))throw t("Missing Operations array");const o=new WorkingEntry(r.current);for(const t of e.Operations)await applyOperation(t,o,r);return o.diff()}async function writePatch(t,o){for(let i=1;;i++)try{return await o()}catch(o){if(16!==e(o))throw o;if(2===i)throw new r(409,`${t} changed while this PATCH was being applied; nothing was written. Retry the request.`)}}function applyPatchToResource(e,t){const r={...e};for(const e of t.Operations){const t=normalizeOp(e.op);if(!e.path){if(e.value&&"object"==typeof e.value&&!Array.isArray(e.value))for(const[t,o]of Object.entries(e.value))assertSafeKey(t),r[t]=o;continue}const{top:o,sub:i}=parsePath(e.path);if("remove"!==t)if(i){const t=r[o],n=t&&"object"==typeof t&&!Array.isArray(t)?{...t}:Object.create(null);n[i]=e.value,r[o]=n}else r[o]=e.value;else if(i){const e=r[o];e&&"object"==typeof e&&delete e[i]}else delete r[o]}return r}export{applyPatchToResource,patchToModifyRequest,writePatch};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{escapeDnValue as t,launchHooksChained as e,validateDnValue as s,launchHooks as i,isChildOf as r,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as o}from"./types.js";import{resolveLockConfig as c,mergeMapping as n,loadMappingFile as h,DEFAULT_USER_MAPPING as u,requiredLdapAttributes as l,ldapToScimUser as d,scimUserToLdap as p}from"./mapping.js";import{scimFilterToLdap as m}from"./filter.js";import{pagedSearch as f}from"./list.js";import{patchToModifyRequest as
|
|
1
|
+
import{escapeDnValue as t,launchHooksChained as e,validateDnValue as s,launchHooks as i,isChildOf as r,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as o}from"./types.js";import{resolveLockConfig as c,mergeMapping as n,loadMappingFile as h,DEFAULT_USER_MAPPING as u,requiredLdapAttributes as l,ldapToScimUser as d,scimUserToLdap as p}from"./mapping.js";import{scimFilterToLdap as m}from"./filter.js";import{pagedSearch as f}from"./list.js";import{writePatch as b,patchToModifyRequest as g}from"./patch.js";import{scimNotFound as w,ScimError as k,scimInvalidValue as A,scimUniqueness as y}from"./errors.js";import{extractLdapCode as R}from"../../lib/ldapCodes.js";class ScimUsers{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_user_rdn_attribute||"uid",this.objectClass=this.config.scim_user_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=c(this.config.scim_user_lock_attribute||"",this.config.scim_user_lock_value||"");this.lockAttribute=e.attribute,this.lockValue=e.value;const s=this.config.scim_user_mapping||"";this.mapping=n(u,s?h(s):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"User",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:this.lockAttribute,lockValue:this.lockValue}}ldapAttributes(){return l(this.mapping,[this.lockAttribute])}dnForId(e,s){const i=this.baseResolver.userBase(s);return`${this.rdnAttribute}=${t(e)},${i}`}async get(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===R(t))throw w(`User ${e} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw w(`User ${e} not found`);return d(i.searchEntries[0],this.mapping,this.ctx(t))}async list(t,e){const s=this.baseResolver.userBase(t),i=Math.max(1,e.startIndex||1),r=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let a,c=`(${this.objectClass.includes("inetOrgPerson")?"objectClass=inetOrgPerson":`objectClass=${this.objectClass[0]}`})`;if(e.filter){const t=m(e.filter,this.mapping,{lockAttribute:this.lockAttribute,supportsActive:!0});t.idEquals?a=t.idEquals:c=`(&${c}${t.ldapFilter})`}if(a)try{const e=await this.get(t,a);return{schemas:[o],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof k&&404===t.statusCode)return{schemas:[o],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:n,totalResults:h}=await f({directory:this.ldap.forRequest(t),base:s,filter:c,attributes:this.ldapAttributes(),startIndex:i,count:r,maxScanned:this.maxScanned});e.sortBy,e.sortOrder;const u=n.map(e=>d(e,this.mapping,this.ctx(t)));return{schemas:[o],totalResults:h,startIndex:i,itemsPerPage:u.length,Resources:u}}async create(r,a){if(!a.userName)throw A("userName is required");const o=(await e(this.hooks.scimusercreate,[a,r]))[0],{rdn:c,attributes:n}=p(o,this.mapping,this.ctx(r),this.objectClass);if(!c)throw A("userName is required");s(c,this.rdnAttribute),n[this.rdnAttribute]=c;const h=this.baseResolver.userBase(r),u=`${this.rdnAttribute}=${t(c)},${h}`;try{await this.ldap.forRequest(r).add(u,n)}catch(t){if(68===R(t))throw y(`User ${c} already exists`);if(void 0!==n[this.lockAttribute])throw this.lockSchemaError(t);throw t}const l=await this.get(r,c);return i(this.hooks.scimusercreatedone,l),l}async replace(t,s,r){const a=this.dnForId(s,t),o=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},a);if(!o.searchEntries?.length)throw w(`User ${s} not found`);const c=o.searchEntries[0],n=(await e(this.hooks.scimuserupdate,[s,r,t]))[1],{attributes:h,active:u}=p(n,this.mapping,this.ctx(t),this.objectClass),l={replace:{},delete:[]},d=new Set(["objectClass","entryUUID","createTimestamp","modifyTimestamp",this.rdnAttribute,this.lockAttribute]),hasAttrValue=t=>null!=t&&("string"==typeof t?t.length>0:!Array.isArray(t)||t.length>0&&t.some(t=>null!=t));for(const t of this.ldapAttributes())d.has(t)||(null!=h[t]?l.replace[t]=h[t]:hasAttrValue(c[t])&&l.delete.push(t));if(!1===u?l.replace[this.lockAttribute]=this.lockValue:!0===u&&hasAttrValue(c[this.lockAttribute])&&l.delete.push(this.lockAttribute),0===Object.keys(l.replace||{}).length&&delete l.replace,0===l.delete.length&&delete l.delete,l.replace||l.delete)try{await this.ldap.forRequest(t).modify(a,l)}catch(t){if(void 0!==u)throw this.lockSchemaError(t);if(16!==R(t))throw t;throw new k(409,`${a} changed while this PUT was being applied; nothing was written. Retry the request.`)}const m=await this.get(t,s);return i(this.hooks.scimuserupdatedone,s,m),m}async patch(t,e,s){const r=this.dnForId(e,t);await b(r,async()=>{const i=await this.currentEntry(t,e),a=await g(s,{mapping:this.mapping,lockAttribute:this.lockAttribute,lockValue:this.lockValue,supportsActive:!0,current:i});try{await this.ldap.forRequest(t).modify(r,a,{permissive:!0})}catch(t){if(function(t,e){if(t.replace&&e in t.replace)return!0;if(t.add&&e in t.add)return!0;const s=t.delete;return Array.isArray(s)?s.includes(e):!(!s||"object"!=typeof s)&&e in s}(a,this.lockAttribute))throw this.lockSchemaError(t);throw t}});const a=await this.get(t,e);return i(this.hooks.scimuserupdatedone,e,a),a}lockSchemaError(t){const e=R(t);if(17!==e&&65!==e)return t;const s=t instanceof Error?t.message:String(t);return A(`The directory rejected '${this.lockAttribute}', the attribute backing 'active': ${s}. Point --scim-user-lock-attribute and --scim-user-lock-value at an attribute this directory defines (nsAccountLock / TRUE on 389-ds); the default needs the ppolicy overlay.`)}async currentEntry(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===R(t))throw w(`User ${e} not found`);throw t}if(!i.searchEntries?.length)throw w(`User ${e} not found`);return i.searchEntries[0]}async delete(t,s){await this.get(t,s);const r=(await e(this.hooks.scimuserdelete,[s,t]))[0],a=this.dnForId(r,t);await this.ldap.forRequest(t).delete(a),i(this.hooks.scimuserdeletedone,r)}async resolveRef(e,s){if(!s)return;const i=this.baseResolver.userBase(e);if(s.includes("=")&&s.includes(","))return r(s,i)?s:void 0;const o=`${this.rdnAttribute}=${t(s)},${i}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},o);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==R(t))throw t}try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==R(t))throw t}}get userMapping(){return this.mapping}}export{ScimUsers};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import t from"../../abstract/plugin.js";import{NotFoundError as e,BadRequestError as i}from"../../lib/errors.js";import{jsonBody as a}from"../../lib/expressFormatedResponses.js";import{CozyStackProvider as n,ClouderyProvider as r}from"../../lib/instanceProviders.js";import{extractLdapCode as s}from"../../lib/ldapCodes.js";import{rdnValue as o,asyncHandler as c,escapeLdapFilter as d}from"../../lib/utils.js";import{lifecycleAttributes as h,isTombstone as u}from"./lifecycleAttributes.js";const first=t=>{const e=Array.isArray(t)?t[0]:t;return(null==e?"":e.toString())||void 0},l=/(^|\/)twake\/(cozy|cloudery)Provision(\.js)?$/;class TwakeInstances extends t{constructor(t){super(t),this.name="twakeInstances",this.roles=["consistency","api"],this.dependencies={rabbitmq:"core/rabbitmq"},this.queues=new Map,this.hooks={ldapaddrequest:([t,e,i])=>{if(this.patterns.some(e=>e.test(t))||this.isOrganization(t)){const t=[this.fqdnAttribute,this.sentAttribute,this.organizationFqdn].map(t=>t.toLowerCase());for(const i of Object.keys(e))t.includes(i.toLowerCase())&&delete e[i]}return[t,e,i]},ldapaddafter:async([t,e])=>{const i=this.account(t,e);i&&await this.serialized(t,async()=>{try{await this.provide(t,e,i)}catch(e){this.logger.error({plugin:this.name,event:"create",dn:t,error:String(e)})}})},ldapdeletedone:async t=>{const e=this.provider;if(e instanceof n)for(const i of Array.isArray(t)?t:[t]){const t=this.match(i),a=/^uid=/i.test(i)?o(i):"";t&&a&&await this.serialized(i,async()=>{try{await e.destroy(this.id(a,t),a)}catch(t){this.logger.error({plugin:this.name,event:"destroy",dn:i,error:String(t)})}})}}};const e=this.config,i=(e.plugin||[]).find(t=>l.test(t));if(i)throw new Error(`${this.name}: ${i} gives accounts their instance too; load one of them`);this.patterns=(e.twake_instance_dn||[]).map(t=>new RegExp(t,"i"));const a=new Set(["uid"]);for(const t of this.patterns)for(const[,e]of t.source.matchAll(/\(\?<(\w+)>/g))a.add(e);for(const[,t]of(e.twake_instance_id||"{uid}").matchAll(/\{(\w+)\}/g))if(!a.has(t))throw new Error(`${this.name}: --twake-instance-id names {${t}}, which no --twake-instance-dn group captures`);this.lifecycle=h(e),this.fqdnAttribute=e.twake_instance_fqdn_attribute||"twakeWorkspaceUrl",this.sentAttribute=e.twake_instance_sent_attribute||"twakeCreatedEventAt";const s=e.twake_instance_timeout||3e4;if("cozy-stack"===e.twake_instance_provider){if(!e.twake_instance_cozy_url||!e.twake_instance_cozy_domain)throw new Error(`${this.name}: --twake-instance-cozy-url and --twake-instance-cozy-domain are required`);this.provider=new n(e.twake_instance_cozy_url.replace(/\/$/,""),e.twake_instance_cozy_user||"admin",e.twake_instance_cozy_passphrase||"",e.twake_instance_cozy_domain,e.twake_instance_cozy_context||"",e.twake_instance_cozy_apps||"",s),this.locale=e.twake_instance_locale||"fr"}else{if("cloudery"!==e.twake_instance_provider)throw new Error(`${this.name}: --twake-instance-provider must be cloudery or cozy-stack`);if(!e.twake_instance_cloudery_url||!e.twake_instance_cloudery_domain)throw new Error(`${this.name}: --twake-instance-cloudery-url and --twake-instance-cloudery-domain are required`);this.provider=new r(e.twake_instance_cloudery_url.replace(/\/$/,""),e.twake_instance_cloudery_token||"",e.twake_instance_cloudery_domain,e.twake_instance_cloudery_offer||"",s),this.locale=e.twake_instance_locale||"en"}}async api(t){const n=this.requirePlugin("rabbitmq");if(!n||!await n.getRawClient())throw new Error(`${this.name}: RabbitMQ is required and unreachable`);const s=await this.server.ldap.schemaIndex(),o=[this.fqdnAttribute,this.sentAttribute,...this.config.twake_instance_organization_base?[this.organizationDomain,this.organizationFqdn]:[]].filter(t=>t&&!s.getAttributeType(t));if(o.length)throw new Error(`${this.name}: the directory schema defines no ${o.join(", ")}; load a schema that does, or name other attributes`);t.post(`${this.config.api_prefix}/v1/twake/instances/ensure`,c(async(t,n)=>{const r=a(t,n);if(!1===r)return;let s=r?.dn;if(void 0===s&&"string"==typeof r?.mail&&r.mail){const i=await this.findByMail(r.mail,t);if(!i)throw new e(`No account for ${r.mail}`);s=i.dn}if("string"!=typeof s||!s)throw new i("Give the account as dn or mail");n.json({state:await this.ensureInstance(s,t)})})),this.provider instanceof r&&await n.subscribe(this.config.twake_instance_auth_exchange||"auth",this.config.twake_instance_workplace_created_key||"workplace.created",this.config.twake_instance_queue||"workplace.created.ldap-rest",t=>this.onWorkplaceCreated(t))}async ensureInstance(t,a){return this.serialized(t,async()=>{const n=await this.read(t,a);if(!n)throw new e(`No entry at ${t}`);const r=this.account(t,n);if(!r)throw new i(`${t} gets no instance`);if(first(this.value(n,this.sentAttribute)))return"ready";a&&await this.server.ldap.modify(t,{replace:{[this.sentAttribute]:[]}},a);const s=first(this.value(n,this.fqdnAttribute));if(s){const e=s.toLowerCase()===this.provider.address(r)?{fqdn:s,ready:!0}:await this.provider.find(r,s);if(e?.ready)return await this.ready(t,n,r,e.fqdn,a),"ready"}return this.provide(t,n,r,a)})}async ensureOrganization(t){return this.serialized(`organization:${t.id}`,async()=>{const i=await this.findOrganization(t.id);if(!i)throw new e(`No organization ${t.id}`);if(first(this.value(i,this.sentAttribute)))return"ready";const a=this.organizationFqdn&&first(this.value(i,this.organizationFqdn)),n={id:t.id,email:this.organizationMail(t.id,t.domain),publicName:t.name,locale:this.locale,orgId:t.id,orgDomain:t.domain,offer:this.config.twake_instance_cloudery_organization_offer},s=a?{fqdn:a,ready:!0}:await this.provider.find(n);let o=s?.ready&&s.fqdn;return s||(this.provider instanceof r&&await this.provider.createOrganization(t),o=await this.provider.create(n)),o?(await this.organizationReady(i,o),"ready"):"pending"})}async onWorkplaceCreated(t){const{internalEmail:e,workplaceFqdn:i}=t;if(!e||!i)return;const a=t.twakeId&&await this.findOrganization(t.twakeId);if(a){const n=t.twakeId.toLowerCase(),r=first(this.value(a,this.organizationDomain)),s=e.toLowerCase();if(r?this.organizationMail(n,r)===s:s.startsWith(`${n}@`))return this.serialized(`organization:${t.twakeId}`,async()=>{const t=await this.read(a.dn);t&&await this.organizationReady(t,i)})}const n=await this.findByMail(e);if(!n)return void this.logger.debug(`${this.name}: no account here for ${e}`);const r=n.dn;await this.serialized(r,async()=>{const t=await this.read(r),e=t&&this.account(r,t);t&&e&&(first(this.value(t,this.sentAttribute))||await this.ready(r,t,e,i))})}async provide(t,e,i,a){const n=await this.provider.find(i),r=n?n.ready&&n.fqdn:await this.provider.create(i);return r?(await this.ready(t,e,i,r,a),"ready"):"pending"}async ready(t,e,i,a,n){first(this.value(e,this.fqdnAttribute))!==a&&await this.server.ldap.modify(t,{replace:{[this.fqdnAttribute]:a}},n),first(this.value(e,this.sentAttribute))||(await this.announce(this.config.twake_instance_auth_exchange||"auth",this.config.twake_instance_user_created_key||"user.created",{twakeId:i.twakeId,internalEmail:i.email,workplaceFqdn:a,...i.orgId?{organizationId:i.orgId}:{},...i.orgDomain?{domain:i.orgDomain,organizationDomain:i.orgDomain}:{},...i.phone?{mobile:i.phone}:{}}),await this.server.ldap.modify(t,{replace:{[this.sentAttribute]:(new Date).toISOString()}},n))}async organizationReady(t,e){const i=t.dn;if(first(this.value(t,this.sentAttribute)))return;this.organizationFqdn&&!first(this.value(t,this.organizationFqdn))&&await this.server.ldap.modify(i,{replace:{[this.organizationFqdn]:e}});const a=first(this.value(t,this.organizationId)),n=first(this.value(t,this.organizationDomain));await this.announce(this.config.twake_instance_b2b_exchange||"b2b",this.config.twake_instance_organization_created_key||"organization.created",{organizationId:a,workplaceFqdn:e,...n?{domain:n}:{}}),await this.server.ldap.modify(i,{replace:{[this.sentAttribute]:(new Date).toISOString()}})}async serialized(t,e){const i=t.toLowerCase(),a=(this.queues.get(i)??Promise.resolve()).catch(()=>{}).then(e),n=a.catch(()=>{});return this.queues.set(i,n),n.then(()=>{this.queues.get(i)===n&&this.queues.delete(i)}),a}get organizationId(){return this.config.twake_instance_organization_id_attribute||"ou"}get organizationDomain(){return this.config.twake_instance_organization_domain_attribute||"twakeDomain"}get organizationFqdn(){return this.config.twake_instance_organization_fqdn_attribute||""}organizationMail(t,e){return`${t}@${e}`.toLowerCase()}isOrganization(t){const e=this.config.twake_instance_organization_base;if(!e)return!1;return t.slice(t.indexOf(",")+1).toLowerCase()===e.toLowerCase()}async findOrganization(t){const e=this.config.twake_instance_organization_base;if(!e)return;return(await this.server.ldap.search({paged:!1,scope:"one",filter:`(${this.organizationId}=${d(t)})`},e)).searchEntries[0]}async read(t,e){try{return(await this.server.ldap.search({paged:!1,scope:"base"},t,e)).searchEntries[0]}catch(e){const a=s(e);if(32===a)return;if(34===a)throw new i(`Invalid DN: ${t}`);throw e}}async announce(t,e,i){const a=this.requirePlugin("rabbitmq");if(!a||!await a.getRawClient())throw new Error(`${this.name}: RabbitMQ unreachable, ${e} not sent`);await a.publish(t,e,i),this.logger.info({plugin:this.name,event:e,exchange:t})}account(t,e){const i=this.match(t);if(!i)return;if(u(e,this.lifecycle))return;const a=this.config.twake_instance_skip_attribute;if(a&&first(this.value(e,a))?.toLowerCase()===(this.config.twake_instance_skip_value||"").toLowerCase())return;const r=first(this.value(e,"mail")),s=first(this.value(e,"uid"));if(!r||!s)return void this.logger.warn(`${this.name}: ${t} has no mail or uid, no instance`);const o=this.provider instanceof n,c=i.groups?.org||(o?this.config.twake_instance_cozy_org_id:"")||void 0,d=i.groups?.org?r.split("@")[1]:o&&this.config.twake_instance_cozy_org_domain||void 0;return{id:this.id(s,i),...o?{oidc:s}:{},twakeId:s,email:r,publicName:first(this.value(e,"displayName"))||first(this.value(e,"cn"))||s,locale:first(this.value(e,"preferredLanguage"))||this.locale,phone:first(this.value(e,"mobile")),...c?{orgId:c}:{},...d?{orgDomain:d}:{}}}match(t){return this.patterns.map(e=>e.exec(t)).find(t=>null!==t)}id(t,e){const i={uid:t,...e.groups};return(this.config.twake_instance_id||"{uid}").replace(/\{(\w+)\}/g,(t,e)=>i[e]??"").replace(/\./g,"").toLowerCase()}value(t,e){const i=e.toLowerCase(),a=Object.keys(t).find(t=>t.toLowerCase()===i);return a?t[a]:void 0}async findByMail(t,e){return(await this.server.ldap.search({paged:!1,scope:"sub",filter:`(mail=${d(t)})`},this.config.ldap_base,e)).searchEntries.find(t=>this.patterns.some(e=>e.test(t.dn)))}}export{TwakeInstances as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{DEFAULT_LOCK_ATTRIBUTE as e,resolveLockConfig as t}from"../scim/mapping.js";function lifecycleAttributes(r){const l=r.twake_lifecycle_deleted_at_format||"iso8601";if("iso8601"!==l&&"generalizedTime"!==l)throw new Error(`--twake-lifecycle-deleted-at-format must be iso8601 or generalizedTime, got '${l}'`);const o=r.twake_lifecycle_lock_attribute?.trim(),i=r.scim_user_lock_attribute?.trim()||e,a=t(o||i,r.twake_lifecycle_lock_value?.trim()||(o&&o.toLowerCase()!==i.toLowerCase()?"":r.scim_user_lock_value||""),{attribute:"--twake-lifecycle-lock-attribute",value:"--twake-lifecycle-lock-value"});return{role:r.twake_lifecycle_role_attribute||"",lock:a.attribute,lockValue:a.value,deleted:r.twake_lifecycle_deleted_attribute||"",deletedValue:r.twake_lifecycle_deleted_value||"TRUE",deletedAt:r.twake_lifecycle_deleted_at_attribute||"",deletedAtFormat:l,reason:r.twake_lifecycle_reason_attribute||""}}function valueOf(e,t){if(!t)return;if(t in e)return e[t];const r=t.toLowerCase();for(const t of Object.keys(e))if(t.toLowerCase()===r)return e[t]}function first(e){const t=Array.isArray(e)?e[0]:e;if(null==t)return;const r=Buffer.isBuffer(t)?t.toString():String(t);return""===r?void 0:r}function holds(e,t,r){const l=valueOf(e,t);if(void 0===l)return!1;const o=r.toLowerCase();return(Array.isArray(l)?l:[l]).some(e=>String(e).toLowerCase()===o)}const isTombstone=(e,t)=>holds(e,t.deleted,t.deletedValue),isLocked=(e,t)=>holds(e,t.lock,t.lockValue);function formatDeletedAt(e,t){const r=e.toISOString();return"iso8601"===t?r:r.replace(/[-:T]/g,"").replace(/\.\d+Z$/,"Z")}function parseDeletedAt(e,t){if("iso8601"===t){const t=new Date(e);return isNaN(t.getTime())?void 0:t}const r=/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})?(?:[.,](\d+))?Z$/.exec(e);if(r)return new Date(Date.UTC(+r[1],+r[2]-1,+r[3],+r[4],+r[5],+(r[6]||0),Math.round(1e3*+`0.${r[7]||0}`)))}export{first,formatDeletedAt,isLocked,isTombstone,lifecycleAttributes,parseDeletedAt,valueOf};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUID as e}from"node:crypto";import t from"node:fs";import r from"../../abstract/plugin.js";import{unescapeDnValue as s,parseDn as i}from"../../lib/utils.js";import{DEFAULT_LOCK_ATTRIBUTE as n,DEFAULT_LOCK_VALUE as o}from"../scim/mapping.js";import{lifecycleAttributes as a,isTombstone as l,isLocked as c,valueOf as h,first as u,parseDeletedAt as f}from"./lifecycleAttributes.js";const d=["created","roleChanged","disabled","enabled","deleted"];const isObject=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function optionalString(e,t,r){if(void 0!==e&&"string"!=typeof e)throw new Error(`${t}: "${r}" must be a string`)}function optionalPayload(e,t,r){if(void 0!==e){if(!isObject(e)||Object.values(e).some(e=>"string"!=typeof e))throw new Error(`${t}: "${r}" must be an object of strings`);return e}}function parseRules(e){if(!e.trim())return[];const r=/^\s*[[{]/.test(e)?e:t.readFileSync(e,"utf8"),s=JSON.parse(r);if(!Array.isArray(s))throw new Error("--twake-lifecycle-rules must be a JSON array of rules");return s.map(e=>{if(!isObject(e))throw new Error("Every lifecycle rule must be an object");if("string"!=typeof e.dn)throw new Error('Every lifecycle rule needs a "dn" pattern');const{dn:t}=e,r=`Lifecycle rule ${t}`;optionalString(e.exchange,r,"exchange");const s=optionalPayload(e.payload,r,"payload");if(void 0!==e.events&&!isObject(e.events))throw new Error(`${r}: "events" must be an object`);const i={};for(const[r,n]of Object.entries(e.events||{})){if(!d.includes(r))throw new Error(`Unknown lifecycle event "${r}"; known: ${d.join(", ")}`);const o=`Lifecycle event "${r}" of ${t}`;i[r]=(Array.isArray(n)?n:[n]).map(t=>{if("string"!=typeof t&&!isObject(t))throw new Error(`${o}: a target is a routing key or an object`);const r="string"==typeof t?{routingKey:t}:t;optionalString(r.routingKey,o,"routingKey"),optionalString(r.exchange,o,"exchange");const i=r.exchange||e.exchange,n=r.routingKey;if(!i||!n)throw new Error(`${o} needs an exchange and a routing key`);return{exchange:i,routingKey:n,payload:optionalPayload(r.payload,o,"payload")||s||{},when:optionalPayload(r.when,o,"when")||{}}})}return{dn:new RegExp(t,"i"),targets:i}})}class TwakeLifecycleEvents extends r{constructor(e){if(super(e),this.name="twakeLifecycleEvents",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange",rabbitmq:"core/rabbitmq"},this.hooks={onLdapEntryChange:(e,t,r,s)=>this.onEntryChange(e,t,r,s),twakedeletionreplay:(e,t,r)=>this.replay(e,t,r)},this.attrs=a(this.config),this.followedOperationalAttributes=[this.attrs.lock],this.rules=parseRules(this.config.twake_lifecycle_rules||""),0===this.rules.length)this.logger.warn(`${this.name}: --twake-lifecycle-rules is empty, nothing will be published`);else if(!this.config.rabbitmq_url)throw new Error(`${this.name}: --twake-lifecycle-rules needs --rabbitmq-url`)}async assertComposition(){if(0!==this.rules.length&&!await(this.requirePlugin("rabbitmq")?.getRawClient()))throw new Error(`${this.name}: RabbitMQ at --rabbitmq-url cannot be reached`)}afterLoad(){const e=this.server.loadedPlugins.scim?.config;if(!e)return;const t=e.scim_user_lock_attribute?.trim()||n,r=e.scim_user_lock_value?.trim()||o,{lock:s,lockValue:i}=this.attrs;t.toLowerCase()===s.toLowerCase()&&r.toLowerCase()===i.toLowerCase()||this.logger.warn(`${this.name}: SCIM locks an account with ${t}: ${r}, these events read ${s}: ${i}, so a SCIM deactivation publishes no disabled`)}async replay(e,t,r){const s=this.match(e);s&&l(t,this.attrs)&&await this.publish(s.rule,"deleted",{before:t,after:t,dn:s.groups,change:r})}match(e){const t=function(e){return i(e).map(e=>{const t=e.indexOf("=");if(-1===t)return e;const r=e.slice(t+1).trim();return`${e.slice(0,t).trim()}=${r.replace(/\\,/g,"\\2C")}`}).join(",")}(e);for(const e of this.rules){const r=e.dn.exec(t);if(!r)continue;const i={};for(const[e,t]of Object.entries(r.groups||{}))void 0!==t&&(i[e]=s(t));return{rule:e,groups:i}}}async onEntryChange(e,t,r,s){const i=this.match(e);if(!i||t&&l(t,this.attrs))return;const{rule:n}=i,o={before:t||{},after:r||t||{},dn:i.groups,change:s};if(!r)return this.publish(n,"deleted",o);if(l(r,this.attrs))return t?this.publish(n,"deleted",o):void 0;if(!t)return await this.publish(n,"created",o),void(c(r,this.attrs)&&await this.publish(n,"disabled",o));this.attrs.role&&!function(e,t){const set=e=>new Set((void 0===e?[]:Array.isArray(e)?e:[e]).map(e=>e.toString().toLowerCase())),r=set(e),s=set(t);return r.size===s.size&&[...r].every(e=>s.has(e))}(h(t,this.attrs.role),h(r,this.attrs.role))&&await this.publish(n,"roleChanged",o);const a=c(r,this.attrs);a!==c(t,this.attrs)&&await this.publish(n,a?"disabled":"enabled",o)}resolve(e,t){if(!e.startsWith("$"))return e;if("$now"===e)return(new Date).toISOString();let r=e.slice(1);const s=r.endsWith("|domain");let i;s&&(r=r.slice(0,-7));let n=r;return r.startsWith("dn.")?(i=t.dn[r.slice(3)],n=""):r.startsWith("context.")?(i=t.change[r.slice(8)],n=""):r.startsWith("previous.")?(n=r.slice(9),i=u(h(t.before,n))):i=u(h(t.after,n)),void 0!==i&&this.attrs.deletedAt&&n.toLowerCase()===this.attrs.deletedAt.toLowerCase()&&(i=f(i,this.attrs.deletedAtFormat)?.toISOString()),s&&(i=i?.split("@")[1]),i}async publish(t,r,s){const i=t.targets[r];if(!i?.length)return;const n=this.requirePlugin("rabbitmq");if(n)for(const t of i){if(!Object.entries(t.when).every(([e,t])=>{const r=this.resolve(e,s);return t.startsWith("!")?r!==t.slice(1):r===t}))continue;const i={};for(const[e,r]of Object.entries(t.payload)){const t=this.resolve(r,s);void 0!==t&&(i[e]=t)}const o=e(),a={plugin:this.name,event:r,exchange:t.exchange,routingKey:t.routingKey,messageId:o};if(await n.getRawClient())try{await n.publish(t.exchange,t.routingKey,i,{messageId:o}),this.logger.info({...a,result:"published"})}catch(e){this.logger.error({...a,result:"error",error:String(e)})}else this.logger.error({...a,result:"no broker"})}}}export{d as LIFECYCLE_EVENTS,TwakeLifecycleEvents as default,parseRules};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"../../abstract/plugin.js";import{BadRequestError as t,NotFoundError as s,ConflictError as r}from"../../lib/errors.js";import{jsonBody as a}from"../../lib/expressFormatedResponses.js";import{changeContext as o}from"../../lib/changeContext.js";import{extractLdapCode as i}from"../../lib/ldapCodes.js";import{escapeLdapFilter as n,asyncHandler as h,launchHooks as l}from"../../lib/utils.js";import{isTombstone as d,lifecycleAttributes as c,first as f,valueOf as m,parseDeletedAt as p,formatDeletedAt as u}from"./lifecycleAttributes.js";const w=/^[A-Za-z0-9_.-]{1,64}$/;class TwakeTombstone extends e{constructor(e){super(e),this.name="twakeTombstone",this.roles=["consistency","api"],this.reasonFor=new Map,this.erasing=new Set,this.hooks={ldapdeleterequest:async([e,t])=>{const s=[];for(const r of Array.isArray(e)?e:[e]){if(!this.erasing.has(r)&&this.matches(r)){const e=await this.read(r);if(e){await this.writeTombstone(r,e,this.reasonOf(r,t),t);continue}}s.push(r)}return[s,t]},ldapaddrequest:async([e,t,s])=>{if(this.isScim(s)&&this.matches(e)){const t=await this.read(e);t&&d(t,this.attrs)&&await this.replace(e,t,s)}return[e,t,s]},ldapsearchrequest:([e,t,s])=>{if(!this.isScim(s))return[e,t,s];const r=t.filter||"(objectClass=*)",a=`(!(${this.attrs.deleted}=${n(this.attrs.deletedValue)}))`;return[e,{...t,filter:`(&${String(r).startsWith("(")?String(r):`(${String(r)})`}${a})`},s]}};const t=this.config;if(this.attrs=c(t),!this.attrs.deleted)throw new Error(`${this.name}: --twake-lifecycle-deleted-attribute is required`);this.patterns=(t.twake_tombstone_dn||[]).map(e=>new RegExp(e,"i")),this.reasons=t.twake_tombstone_reasons||[],this.defaultReason=this.checkReason(t.twake_tombstone_default_reason||"deleted"),this.reasonHeader=(t.twake_tombstone_reason_header||"x-deletion-reason").toLowerCase(),this.clearAttributes=t.twake_tombstone_clear_attributes||[],this.eraseMinAge=t.twake_tombstone_erase_min_age??2592e3,this.groupBases=t.twake_tombstone_group_bases?.length?t.twake_tombstone_group_bases:[t.ldap_group_base||t.ldap_base||""],this.scimPrefix=t.scim_prefix||"/scim/v2",0===this.patterns.length&&this.logger.warn(`${this.name}: --twake-tombstone-dn is empty, every delete stays a delete`)}afterLoad(){for(const e of["ldapdeleterequest","ldapaddrequest"]){const t=this.server.hooks[e],s=this.hooks[e],r=t&&s?t.indexOf(s):-1;r<0||(t.splice(r,1),t.push(s))}}api(e){e.post(`${this.config.api_prefix}/v1/twake/tombstones/erase`,h(async(e,s)=>{const r=a(e,s,"dn");if(r){if("string"!=typeof r.dn)throw new t("Field dn must be a string");await this.erase(r.dn,{force:!0===r.force,req:e}),s.json({success:!0})}}))}async tombstone(e,s,r){if(!this.matches(e))throw new t(`${e} is not an entry that becomes a tombstone`);this.reasonFor.set(e,this.checkReason(s));try{await this.server.ldap.delete(e,r)}finally{this.reasonFor.delete(e)}}async erase(e,{force:t=!1,req:a}={}){const o=await this.read(e);if(!o||!d(o,this.attrs))throw new s(`No tombstone at ${e}`);if(!t){const t=f(m(o,this.attrs.deletedAt)),s=t&&p(t,this.attrs.deletedAtFormat);if(!s)throw new r(`${e} holds no readable deletion date (--twake-lifecycle-deleted-at-attribute); force it to erase now`);if(Date.now()-s.getTime()<1e3*this.eraseMinAge)throw new r(`${e} was deleted less than ${this.eraseMinAge} seconds ago; force it to erase now`)}this.erasing.add(e);try{await this.server.ldap.delete(e,a)}finally{this.erasing.delete(e)}await this.leaveGroups(e),this.logger.info({plugin:this.name,event:"erase",dn:e,force:t})}assertComposition(){if(!this.server.loadedPlugins.trash)return;const e=String(this.config.trash_watched_bases||"").split(",").map(e=>e.trim().toLowerCase()).filter(Boolean),t=this.patterns.filter(t=>{const s=t.source.toLowerCase().replace(/\$$/,"");return 0===e.length||e.some(e=>s===e||s.endsWith(`,${e}`))});if(t.length)throw new Error(`${this.name}: core/ldap/trash watches the branch of ${t.map(e=>e.source).join(", ")}, and would move those entries instead of leaving a tombstone. Leave that branch out of --trash-watched-bases, or load one of the two plugins only`)}async replace(e,t,s){const r=await this.groupsOf(e),a=this.config.group_dummy_user,o=new Set(a?await this.groupsOf(a):[]);await this.erase(e,{force:!0,req:s});const i=s.res;if(!i)return;const n=i.end.bind(i);i.end=(...s)=>{i.end=n;const a=n(...s);return this.restore(e,t,r,o).catch(t=>this.logger.error({plugin:this.name,event:"restore",dn:e,error:String(t)})),a}}async restore(e,t,s,r){if(await this.read(e))return;const a=Object.fromEntries(Object.entries(t).filter(([e,t])=>"dn"!==e&&!(Array.isArray(t)&&0===t.length)));await this.server.ldap.add(e,a);const o=this.config.group_dummy_user;for(const t of s)await this.server.ldap.modify(t,{add:{member:e}}),o&&!r.has(t)&&await this.server.ldap.modify(t,{delete:{member:o}}).catch(e=>{if(16!==i(e))throw e});this.logger.warn({plugin:this.name,event:"restore",dn:e})}matches(e){return this.patterns.some(t=>t.test(e))}isScim(e){const t=e?.originalUrl??e?.url;return"string"==typeof t&&t.startsWith(this.scimPrefix)}checkReason(e){if(!(this.reasons.length?this.reasons.includes(e):w.test(e)))throw new t(`Unknown deletion reason "${e}"${this.reasons.length?`; known: ${this.reasons.join(", ")}`:""}`);return e}reasonOf(e,t){const s=this.reasonFor.get(e);if(s)return s;const r=t?.headers?.[this.reasonHeader],a=Array.isArray(r)?r[0]:r;return a?this.checkReason(a):this.defaultReason}async read(e){try{return(await this.server.ldap.search({paged:!1,scope:"base",attributes:["*",this.attrs.lock]},e)).searchEntries[0]}catch(e){if(32===i(e))return;throw e}}async writeTombstone(e,t,s,r){const{attrs:a}=this;if(d(t,a))return l(this.server.hooks.twakedeletionreplay,e,t,o(r)),void this.logger.info({plugin:this.name,event:"replay",dn:e});const i={[a.deleted]:a.deletedValue,[a.lock]:a.lockValue};a.deletedAt&&(i[a.deletedAt]=u(new Date,a.deletedAtFormat)),a.reason&&(i[a.reason]=s);const n=this.clearAttributes.filter(e=>void 0!==m(t,e));await this.server.ldap.modify(e,n.length?{replace:i,delete:n}:{replace:i}),this.logger.info({plugin:this.name,event:"tombstone",dn:e,reason:s})}async groupsOf(e){const t=[];for(const s of this.groupBases.filter(Boolean))try{const r=await this.server.ldap.search({paged:!1,scope:"sub",filter:`(member=${n(e)})`,attributes:["dn"]},s);t.push(...r.searchEntries.map(e=>e.dn))}catch(e){if(32!==i(e))throw e}return t}async leaveGroups(e){for(const t of await this.groupsOf(e))await this.leaveGroup(t,e)}async leaveGroup(e,t){const s=this.config.group_dummy_user;let r=!1;for(let a=0;;a++)try{return void await this.server.ldap.modify(e,r?{add:{member:s},delete:{member:t}}:{delete:{member:t}})}catch(e){const t=i(e);if(16===t)return;if(a>=2)throw e;if(65===t&&s)r=!0;else{if(20!==t)throw e;r=!1}}}}export{TwakeTombstone as default};
|
|
@@ -33,6 +33,8 @@ export default abstract class DmPlugin {
|
|
|
33
33
|
followedOperationalAttributes?: string[];
|
|
34
34
|
api?(app: Express): MaybePromise<void>;
|
|
35
35
|
getConfigApiData?(): Record<string, unknown> | undefined;
|
|
36
|
+
/** Key of that configuration under `features`; the instance name otherwise */
|
|
37
|
+
configApiKey?: string;
|
|
36
38
|
/**
|
|
37
39
|
* Called once every plugin is loaded, before the first request.
|
|
38
40
|
*
|
|
@@ -48,11 +50,12 @@ export default abstract class DmPlugin {
|
|
|
48
50
|
* Called once every plugin is loaded, before `afterLoad`: refuse a
|
|
49
51
|
* configuration this plugin cannot serve.
|
|
50
52
|
*
|
|
51
|
-
* `afterLoad` is an opinion, logged and served anyway. Throwing here
|
|
52
|
-
* the server from starting — for a setting that would
|
|
53
|
-
* plugin answer on behalf of something that is not
|
|
53
|
+
* `afterLoad` is an opinion, logged and served anyway. Throwing here, or
|
|
54
|
+
* rejecting, stops the server from starting — for a setting that would
|
|
55
|
+
* otherwise make the plugin answer on behalf of something that is not
|
|
56
|
+
* there, or a service it cannot work without.
|
|
54
57
|
*/
|
|
55
|
-
assertComposition?(): void
|
|
58
|
+
assertComposition?(): MaybePromise<void>;
|
|
56
59
|
abstract name: string;
|
|
57
60
|
/**
|
|
58
61
|
* Constructor
|
|
@@ -73,6 +73,8 @@ export interface Config {
|
|
|
73
73
|
ldap_organization_max_subnodes?: number;
|
|
74
74
|
organization_schema?: string;
|
|
75
75
|
ldap_flat_schema?: string[];
|
|
76
|
+
/** Add the auxiliary classes a modify needs, see ldapActions.classRepair */
|
|
77
|
+
ldap_flat_auto_repair?: boolean;
|
|
76
78
|
enterprise_domain_name_attribute?: string;
|
|
77
79
|
enterprise_domain_link_attribute?: string;
|
|
78
80
|
ldap_raw_base?: string[];
|
|
@@ -208,6 +210,54 @@ export interface Config {
|
|
|
208
210
|
cloudery_default_locale?: string;
|
|
209
211
|
cloudery_workflow_poll_interval_ms?: number;
|
|
210
212
|
cloudery_workflow_max_attempts?: number;
|
|
213
|
+
twake_lifecycle_role_attribute?: string;
|
|
214
|
+
twake_lifecycle_lock_attribute?: string;
|
|
215
|
+
twake_lifecycle_lock_value?: string;
|
|
216
|
+
twake_lifecycle_deleted_attribute?: string;
|
|
217
|
+
twake_lifecycle_deleted_value?: string;
|
|
218
|
+
twake_lifecycle_deleted_at_attribute?: string;
|
|
219
|
+
twake_lifecycle_deleted_at_format?: string;
|
|
220
|
+
twake_lifecycle_rules?: string;
|
|
221
|
+
twake_instance_dn?: string[];
|
|
222
|
+
twake_instance_skip_attribute?: string;
|
|
223
|
+
twake_instance_skip_value?: string;
|
|
224
|
+
twake_instance_provider?: string;
|
|
225
|
+
twake_instance_id?: string;
|
|
226
|
+
twake_instance_fqdn_attribute?: string;
|
|
227
|
+
twake_instance_sent_attribute?: string;
|
|
228
|
+
twake_instance_locale?: string;
|
|
229
|
+
twake_instance_timeout?: number;
|
|
230
|
+
twake_instance_cloudery_url?: string;
|
|
231
|
+
twake_instance_cloudery_token?: string;
|
|
232
|
+
twake_instance_cloudery_offer?: string;
|
|
233
|
+
twake_instance_cloudery_domain?: string;
|
|
234
|
+
twake_instance_cloudery_organization_offer?: string;
|
|
235
|
+
twake_instance_cozy_url?: string;
|
|
236
|
+
twake_instance_cozy_user?: string;
|
|
237
|
+
twake_instance_cozy_passphrase?: string;
|
|
238
|
+
twake_instance_cozy_domain?: string;
|
|
239
|
+
twake_instance_cozy_context?: string;
|
|
240
|
+
twake_instance_cozy_apps?: string;
|
|
241
|
+
twake_instance_cozy_org_id?: string;
|
|
242
|
+
twake_instance_cozy_org_domain?: string;
|
|
243
|
+
twake_instance_auth_exchange?: string;
|
|
244
|
+
twake_instance_b2b_exchange?: string;
|
|
245
|
+
twake_instance_user_created_key?: string;
|
|
246
|
+
twake_instance_organization_created_key?: string;
|
|
247
|
+
twake_instance_workplace_created_key?: string;
|
|
248
|
+
twake_instance_queue?: string;
|
|
249
|
+
twake_instance_organization_base?: string;
|
|
250
|
+
twake_instance_organization_id_attribute?: string;
|
|
251
|
+
twake_instance_organization_domain_attribute?: string;
|
|
252
|
+
twake_instance_organization_fqdn_attribute?: string;
|
|
253
|
+
twake_lifecycle_reason_attribute?: string;
|
|
254
|
+
twake_tombstone_dn?: string[];
|
|
255
|
+
twake_tombstone_default_reason?: string;
|
|
256
|
+
twake_tombstone_reason_header?: string;
|
|
257
|
+
twake_tombstone_reasons?: string[];
|
|
258
|
+
twake_tombstone_clear_attributes?: string[];
|
|
259
|
+
twake_tombstone_erase_min_age?: number;
|
|
260
|
+
twake_tombstone_group_bases?: string[];
|
|
211
261
|
rabbitmq_url?: string;
|
|
212
262
|
applicative_account_base?: string;
|
|
213
263
|
max_app_accounts?: number;
|
package/dist/src/hooks.d.ts
CHANGED
|
@@ -51,6 +51,8 @@ export interface Hooks {
|
|
|
51
51
|
ldapsearchfilter?: ChainedHook<[SearchResult, Request?, SearchOptions?]>;
|
|
52
52
|
ldapaddrequest?: ChainedHook<[string, AttributesList, Request?]>;
|
|
53
53
|
ldapadddone?: (args: [string, AttributesList], context?: ChangeContext) => MaybePromise<void>;
|
|
54
|
+
/** See docs/plugin-development/hooks.md#ldapaddafter */
|
|
55
|
+
ldapaddafter?: (args: [string, AttributesList], context?: ChangeContext) => MaybePromise<void>;
|
|
54
56
|
ldapmodifyrequest?: ChainedHook<[string, ModifyRequest, number, Request?]>;
|
|
55
57
|
ldapmodifydone?: (args: [string, ModifyRequest, number], context?: ChangeContext) => MaybePromise<void>;
|
|
56
58
|
ldapdeleterequest?: ChainedHook<[string | string[], Request?]>;
|
|
@@ -141,5 +141,5 @@ export declare function describePopulation(population: Population, authenticator
|
|
|
141
141
|
* `server.loadedPlugins`; only what concerns it is checked
|
|
142
142
|
* @throws Error naming the plugins when the composition is ambiguous
|
|
143
143
|
*/
|
|
144
|
-
export declare function assertAuthzComposition(server: DM, candidate?: DmPlugin): void
|
|
144
|
+
export declare function assertAuthzComposition(server: DM, candidate?: DmPlugin): Promise<void>;
|
|
145
145
|
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module lib/instanceProviders
|
|
3
|
+
*
|
|
4
|
+
* The two places a workplace instance can be created: the Cloudery, which
|
|
5
|
+
* answers at once and builds the instance afterwards, and the cozy-stack
|
|
6
|
+
* admin API, which builds it before answering.
|
|
7
|
+
*/
|
|
8
|
+
import { type RequestInit, type Response } from 'node-fetch';
|
|
9
|
+
export interface InstanceRequest {
|
|
10
|
+
/** Slug of the instance, and its OIDC id unless `oidc` says otherwise */
|
|
11
|
+
id: string;
|
|
12
|
+
oidc?: string;
|
|
13
|
+
email: string;
|
|
14
|
+
publicName: string;
|
|
15
|
+
locale: string;
|
|
16
|
+
phone?: string;
|
|
17
|
+
orgId?: string;
|
|
18
|
+
orgDomain?: string;
|
|
19
|
+
offer?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface FoundInstance {
|
|
22
|
+
fqdn: string;
|
|
23
|
+
/** Built and usable, not only accepted */
|
|
24
|
+
ready: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface InstanceProvider {
|
|
27
|
+
/**
|
|
28
|
+
* Ask for an instance. Answers the address when the instance already
|
|
29
|
+
* exists by then; otherwise it comes later, with `workplace.created`.
|
|
30
|
+
*/
|
|
31
|
+
create(request: InstanceRequest): Promise<string | undefined>;
|
|
32
|
+
/**
|
|
33
|
+
* The instance at this request's address, if any. One that belongs to
|
|
34
|
+
* another account is a conflict, not an answer.
|
|
35
|
+
*/
|
|
36
|
+
find(request: InstanceRequest, at?: string): Promise<FoundInstance | undefined>;
|
|
37
|
+
/** The address this request's instance gets. */
|
|
38
|
+
address(request: InstanceRequest): string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* HTTP with a deadline per call: these calls sit inside the add request.
|
|
42
|
+
* The deadline covers the answer's body too.
|
|
43
|
+
*/
|
|
44
|
+
declare abstract class HttpProvider {
|
|
45
|
+
protected readonly domain: string;
|
|
46
|
+
protected readonly timeout: number;
|
|
47
|
+
constructor(domain: string, timeout: number);
|
|
48
|
+
address(request: InstanceRequest): string;
|
|
49
|
+
protected call(url: string, init?: RequestInit): Promise<Response>;
|
|
50
|
+
protected json<T>(res: Response): Promise<T>;
|
|
51
|
+
private timedOut;
|
|
52
|
+
}
|
|
53
|
+
export declare class ClouderyProvider extends HttpProvider implements InstanceProvider {
|
|
54
|
+
private readonly url;
|
|
55
|
+
private readonly token;
|
|
56
|
+
private readonly offer;
|
|
57
|
+
constructor(url: string, token: string, domain: string, offer: string, timeout: number);
|
|
58
|
+
private headers;
|
|
59
|
+
create(request: InstanceRequest): Promise<undefined>;
|
|
60
|
+
find(request: InstanceRequest, at?: string): Promise<FoundInstance | undefined>;
|
|
61
|
+
createOrganization(org: {
|
|
62
|
+
id: string;
|
|
63
|
+
name: string;
|
|
64
|
+
domain: string;
|
|
65
|
+
}): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
export declare class CozyStackProvider extends HttpProvider implements InstanceProvider {
|
|
68
|
+
private readonly url;
|
|
69
|
+
private readonly context;
|
|
70
|
+
private readonly apps;
|
|
71
|
+
private readonly auth;
|
|
72
|
+
constructor(url: string, user: string, passphrase: string, domain: string, context: string, apps: string, timeout: number);
|
|
73
|
+
private headers;
|
|
74
|
+
create(request: InstanceRequest): Promise<string>;
|
|
75
|
+
/** Also finishes the onboarding of an instance left unfinished. */
|
|
76
|
+
find(request: InstanceRequest, at?: string): Promise<FoundInstance | undefined>;
|
|
77
|
+
/**
|
|
78
|
+
* Destroy the instance of a deleted account, the entry being gone: one
|
|
79
|
+
* another OIDC id owns is left alone.
|
|
80
|
+
*/
|
|
81
|
+
destroy(id: string, oidc: string): Promise<void>;
|
|
82
|
+
private read;
|
|
83
|
+
/** Without it, the first login lands on the setup wizard. */
|
|
84
|
+
private finishOnboarding;
|
|
85
|
+
}
|
|
86
|
+
export {};
|
|
@@ -4,6 +4,7 @@ import type winston from 'winston';
|
|
|
4
4
|
import pLimit from 'p-limit';
|
|
5
5
|
import { type Config } from '../config/args';
|
|
6
6
|
import { type DM } from '../bin';
|
|
7
|
+
import { SchemaIndex } from './ldapSchema';
|
|
7
8
|
export type AttributeValue = Buffer | Buffer[] | string[] | string;
|
|
8
9
|
export type AttributesList = Record<string, AttributeValue>;
|
|
9
10
|
export type LdapList = Record<string, AttributesList>;
|
|
@@ -25,7 +26,7 @@ export type { SearchOptions, SearchResult };
|
|
|
25
26
|
export interface RequestBoundLdap {
|
|
26
27
|
search(options: SearchOptions, base?: string): Promise<SearchResult | AsyncGenerator<SearchResult>>;
|
|
27
28
|
add(dn: string, entry: AttributesList): Promise<boolean>;
|
|
28
|
-
modify(dn: string, changes: ModifyRequest): Promise<boolean>;
|
|
29
|
+
modify(dn: string, changes: ModifyRequest, options?: ModifyOptions): Promise<boolean>;
|
|
29
30
|
rename(dn: string, newRdn: string): Promise<boolean>;
|
|
30
31
|
delete(dn: string | string[]): Promise<boolean>;
|
|
31
32
|
}
|
|
@@ -34,6 +35,14 @@ export interface ModifyRequest {
|
|
|
34
35
|
replace?: AttributesList;
|
|
35
36
|
delete?: string[] | AttributesList;
|
|
36
37
|
}
|
|
38
|
+
export interface ModifyOptions {
|
|
39
|
+
/**
|
|
40
|
+
* Send the permissive modify control, not critical: a directory that
|
|
41
|
+
* honours it accepts an add of a value already there (OpenLDAP 2.4 still
|
|
42
|
+
* refuses a delete of an absent one); one that does not ignores it.
|
|
43
|
+
*/
|
|
44
|
+
permissive?: boolean;
|
|
45
|
+
}
|
|
37
46
|
declare class ldapActions {
|
|
38
47
|
config: Config;
|
|
39
48
|
options: ClientOptions;
|
|
@@ -69,6 +78,14 @@ declare class ldapActions {
|
|
|
69
78
|
private waitingResolvers;
|
|
70
79
|
private availableConnections;
|
|
71
80
|
private isCleaningUp;
|
|
81
|
+
/** See {@link declareObjectClasses}. */
|
|
82
|
+
private declaredClasses;
|
|
83
|
+
private schemaCache?;
|
|
84
|
+
private schemaLoading?;
|
|
85
|
+
/** Whether the last schema read failed, so a failure is logged once. */
|
|
86
|
+
private schemaUnavailable;
|
|
87
|
+
/** Class problems already reported, per entry. See {@link warnOnce}. */
|
|
88
|
+
private classWarnings;
|
|
72
89
|
constructor(server: DM);
|
|
73
90
|
/**
|
|
74
91
|
* Create a new LDAP connection with failover support
|
|
@@ -142,6 +159,66 @@ declare class ldapActions {
|
|
|
142
159
|
* hooks always see it. See {@link RequestBoundLdap}.
|
|
143
160
|
*/
|
|
144
161
|
forRequest(req: Request): RequestBoundLdap;
|
|
162
|
+
/**
|
|
163
|
+
* Declare the object classes an entity gives the entries of its branch.
|
|
164
|
+
*
|
|
165
|
+
* An entry created by another tool may lack some of them, and a class it
|
|
166
|
+
* lacks can forbid an attribute written here: the organization link of an
|
|
167
|
+
* account made as a bare `inetOrgPerson` is refused by the directory. A
|
|
168
|
+
* modify writing such an attribute adds the auxiliary class that allows
|
|
169
|
+
* it, see {@link classRepair}.
|
|
170
|
+
*
|
|
171
|
+
* @param base branch holding the entity's entries
|
|
172
|
+
* @param classes object classes the entity's schema declares; a schema
|
|
173
|
+
* file may give a single one as a string
|
|
174
|
+
* @param includeBase whether the base is itself one of the entries, as the
|
|
175
|
+
* top organization is
|
|
176
|
+
*/
|
|
177
|
+
declareObjectClasses(base: string, classes: string[] | string, includeBase?: boolean): void;
|
|
178
|
+
/**
|
|
179
|
+
* The classes declared for the branch holding a DN. Several entities may
|
|
180
|
+
* share a branch — the shipped `users`, `posixAccounts` and
|
|
181
|
+
* `sshPublicKeys` schemas all live in `ou=users` — so every declaration at
|
|
182
|
+
* the deepest matching base counts.
|
|
183
|
+
*/
|
|
184
|
+
private declaredFor;
|
|
185
|
+
/**
|
|
186
|
+
* Log a class problem of an entry once, not on every write to it: nothing
|
|
187
|
+
* an administrator can do stops a sync job from touching it again.
|
|
188
|
+
*/
|
|
189
|
+
private warnOnce;
|
|
190
|
+
/**
|
|
191
|
+
* The auxiliary classes a modify needs added: for each attribute it writes
|
|
192
|
+
* that the entry's classes do not allow, a class declared for its branch
|
|
193
|
+
* that allows it, provided the entry holds or receives that class's
|
|
194
|
+
* mandatory attributes once the change is applied.
|
|
195
|
+
*
|
|
196
|
+
* Only what the write needs is added. Entities sharing a branch declare
|
|
197
|
+
* classes that are not all meant for every entry: `shadowAccount` is not
|
|
198
|
+
* something every account should be given because it sits in `ou=users`.
|
|
199
|
+
* And a write that the directory would have accepted is never changed.
|
|
200
|
+
*
|
|
201
|
+
* Nothing is added when the change names `objectClass` itself, and a
|
|
202
|
+
* schema or entry that cannot be read skips the repair.
|
|
203
|
+
*
|
|
204
|
+
* @param dn entry being modified
|
|
205
|
+
* @param changes the modification, after the hooks
|
|
206
|
+
* @returns names of the classes to add
|
|
207
|
+
*/
|
|
208
|
+
private classRepair;
|
|
209
|
+
/**
|
|
210
|
+
* DN of the subschema entry the root DSE advertises, or the conventional
|
|
211
|
+
* `cn=Subschema` when it advertises none.
|
|
212
|
+
*/
|
|
213
|
+
subschemaDn(): Promise<string>;
|
|
214
|
+
/**
|
|
215
|
+
* The directory schema, kept for `--ldap-raw-schema-cache-ttl`: it is
|
|
216
|
+
* large and changes rarely. The raw browser and the object class repair
|
|
217
|
+
* share it, and concurrent callers share a single read.
|
|
218
|
+
*
|
|
219
|
+
* @returns indexed schema
|
|
220
|
+
*/
|
|
221
|
+
schemaIndex(): Promise<SchemaIndex>;
|
|
145
222
|
/**
|
|
146
223
|
* Hand a search result to `ldapsearchfilter`, which may drop entries the
|
|
147
224
|
* caller is not allowed to see.
|
|
@@ -158,7 +235,7 @@ declare class ldapActions {
|
|
|
158
235
|
*/
|
|
159
236
|
private wrapPaginatedSearch;
|
|
160
237
|
add(dn: string, entry: AttributesList, req?: Request): Promise<boolean>;
|
|
161
|
-
modify(dn: string, changes: ModifyRequest, req?: Request): Promise<boolean>;
|
|
238
|
+
modify(dn: string, changes: ModifyRequest, req?: Request, options?: ModifyOptions): Promise<boolean>;
|
|
162
239
|
rename(dn: string, newRdn: string, req?: Request): Promise<boolean>;
|
|
163
240
|
/**
|
|
164
241
|
* Move an entry to a new location (different parent)
|
|
@@ -156,4 +156,18 @@ export declare class SchemaIndex {
|
|
|
156
156
|
must: string[];
|
|
157
157
|
may: string[];
|
|
158
158
|
};
|
|
159
|
+
/**
|
|
160
|
+
* The classes a schema declares for an entry and the entry lacks, split by
|
|
161
|
+
* what can be done about them: an auxiliary class can be added to an
|
|
162
|
+
* existing entry, a structural one cannot. Classes the directory does not
|
|
163
|
+
* know, and abstract ones, are left out.
|
|
164
|
+
*
|
|
165
|
+
* @param declared classes the entity's schema gives its entries
|
|
166
|
+
* @param present classes the entry carries
|
|
167
|
+
* @returns the missing auxiliary definitions and structural names
|
|
168
|
+
*/
|
|
169
|
+
missingClasses(declared: string[], present: string[]): {
|
|
170
|
+
auxiliary: ObjectClassDefinition[];
|
|
171
|
+
structural: string[];
|
|
172
|
+
};
|
|
159
173
|
}
|