ldap-rest 0.22.0 → 0.23.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/lib/authz/base.js +1 -1
- package/dist/plugins/auth/authzLinid1.js +1 -1
- package/dist/plugins/auth/authzScope.js +1 -1
- package/dist/src/config/schema.d.ts +9 -0
- package/dist/src/lib/authz/base.d.ts +13 -4
- package/dist/src/plugins/auth/authzLinid1.d.ts +6 -0
- package/docs/usage/plugins/auth/authz-linid1.md +19 -3
- package/docs/usage/plugins/ldap/flat-generic.md +25 -0
- package/package.json +1 -1
- package/static/schemas/ad/users.json +4 -2
- package/static/schemas/example/users.json +2 -0
- package/static/schemas/obm/users.json +4 -2
- package/static/schemas/standard/users.json +4 -2
- package/static/schemas/twake/users.json +21 -12
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{AmbiguousIdentityError as i,ForbiddenError as n}from"../errors.js";import{isDnInBranch as a,organizationLink as o,getParentDn as h,normalizeDn as c,escapeLdapFilter as u}from"../utils.js";import{authzFor as l,servesRequest as d}from"./composition.js";class AuthzBase extends t{constructor(t){super(t),this.roles=["authz"],this.resolutionCache=new Map,this.resolutionCacheFullWarned=!1,this.fallbackWarned=!1,this.hooks={ldaprenamedone:()=>{this.resolutionCache.clear()},ldapdeletedone:()=>{this.resolutionCache.clear()},ldapmodifyrequest:async([t,e,r,s])=>{if(this.shouldSkipAuthorization(s))return[t,e,r,s];const i=await this.resolveCaller(s);if(!i)return[t,e,r,s];const n=this.config.ldap_organization_link_attribute;if(n&&e.replace?.[n]){let r,a=this.extractBranchDn(t),h=!1;try{const e=(await this.server.ldap.search({paged:!1,scope:"base",attributes:[n]},t)).searchEntries[0];e&&(h=!0,r=o(e[n]),r&&(a=r))}catch{}const c=e.replace[n],u=Array.isArray(c)?String(c[0]):String(c),l=this.filtersAttachedEntries(),d=l&&h&&this.inTransit(r),f=l&&this.inTransit(u);if(!d){const t=f?"write":this.sourcePermission(a);if(!(await this.getUserPermissions(i,a))[t])throw new Error(`[authz-forbidden] User ${s.user} does not have ${t} permission for source branch ${a}`)}if(f){if(d){if(0===(await this.getAuthorizedBranches(i)).length)throw new Error(`[authz-forbidden] User ${s.user} administers no branch`)}}else{if(!(await this.getUserPermissions(i,u)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for destination branch ${u}`)}}else{const e=await this.effectiveBranch(t);if(!(await this.getUserPermissions(i,e)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for branch ${e}`)}const a=this.domainLinkAttribute();return a&&await this.guardDomainLinks(i,s,t,[...AuthzBase.valuesOf(e.add,a),...AuthzBase.valuesOf(e.replace,a)],a,!1),[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;if(n&&e[n]){const t=e[n];i=Array.isArray(t)?String(t[0]):String(t)}else i=this.extractBranchDn(t);if(!(await this.getUserPermissions(s,i)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for branch ${i}`);const a=this.domainLinkAttribute();return a&&await this.guardDomainLinks(s,r,t,AuthzBase.valuesOf(e,a),a,!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,r];if(this.filtersAttachedEntries()){if(0===(await this.getAuthorizedBranches(s)).length)throw new Error(`[authz-forbidden] User ${r.user} administers no branch`);if(this.globalBranches().some(e=>a(t,e))&&!(await this.getUserPermissions(s,t)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for branch ${t}`);const i=this.config.ldap_organization_link_attribute;if(i&&!AuthzBase.wantsAttribute(e,i)){const t=e.attributes,r=Array.isArray(t)?t.map(String):[String(t)];e={...e,attributes:[...r,i]},AuthzBase.widenedForLink.add(e)}return[t,e,r]}if(!(await this.getUserPermissions(s,t)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for branch ${t}`);return[t,e,r]},ldapsearchfilter:async([t,e,r])=>{const s=[t,e,r];if(!this.filtersAttachedEntries())return s;if(this.shouldSkipAuthorization(e))return s;if(!t?.searchEntries?.length)return s;const i=await this.resolveCaller(e);if(!i)return s;const n=this.globalBranches();if(n.length){const r=new Map,s=[];for(const o of t.searchEntries){const t=n.find(t=>a(o.dn,t));void 0===t||(r.has(t)||r.set(t,!0===(await this.getUserPermissions(i,t)).read),r.get(t))?s.push(o):this.logger.debug(`${this.name}: hiding ${String(o.dn)} from ${e.user}, in the global branch ${t}`)}t.searchEntries=s}const o=this.config.ldap_organization_link_attribute;if(!o)return s;const h=await this.getAuthorizedBranches(i);if(0===h.length)return s;const within=t=>h.some(e=>a(t,e)),c=this.transitKey();if(t.searchEntries=t.searchEntries.filter(t=>{const r=t[o];if(null==r)return!0;const s=(Array.isArray(r)?r:[r]).map(String);return 0===s.length||(!!s.some(within)||(!!s.some(t=>this.inTransit(t,c))||(this.logger.debug(`${this.name}: hiding ${String(t.dn)} from ${e.user}, attached to ${s[0]}`),!1)))}),r&&AuthzBase.widenedForLink.has(r))for(const e of t.searchEntries)delete e[o];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=this.extractBranchDn(t),n=this.extractBranchDn(e),a=await this.getUserPermissions(s,i),o=this.sourcePermission(i);if(!a[o])throw new Error(`[authz-forbidden] User ${r.user} does not have ${o} permission for source branch ${i}`);if(!(await this.getUserPermissions(s,n)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for destination branch ${n}`);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.effectiveBranch(t);if(!(await this.getUserPermissions(r,s)).delete)throw new Error(`[authz-forbidden] User ${e.user} does not have delete permission for branch ${s}`)}return[t,e]}},r(this.config,this.constructor.name),l(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)}globalBranches(){return[]}sourcePermission(t){return"read"}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.search({paged:!1,scope:"base",attributes:[e]},t),s=o(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&&c(t)===e}transitKey(){const t=this.transitBranch();return void 0===t?void 0:c(t)}async findUserDn(t){const e=`(${this.config.ldap_user_main_attribute||"uid"}=${u(t)})`,r=(await this.server.ldap.search({paged:!1,filter:e,attributes:["dn"],scope:"sub"},this.server.ldap.base)).searchEntries??[];if(r.length>1)throw new i(t,r.length);if(0===r.length)return null;const s=r[0].dn;return"string"==typeof s?s:String(s)}shouldSkipAuthorization(t){return!t?.user||!d(t,this.config)}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,a=Date.now(),o=this.resolutionCache.get(i);let h;if(o&&a-o.at<this.cacheTTL)h=o.user;else{if(h=await this.resolveUser(i),this.resolutionCache.size>=AuthzBase.RESOLUTION_CACHE_MAX)for(const[t,e]of this.resolutionCache)a-e.at>=this.cacheTTL&&this.resolutionCache.delete(t);this.resolutionCache.size<AuthzBase.RESOLUTION_CACHE_MAX?this.resolutionCache.set(i,{user:h,at:a}):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 n(`[authz-forbidden] 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 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.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 o=new Set((n?[]:await this.storedValues(r,i)).map(c)),u=s.filter(t=>!o.has(c(t)));if(0===u.length)return;const l=this.config.ldap_top_organization,d=c(l||this.server.ldap.base),f=new Set;let g=h(r);const w=new Set;for(;g&&!w.has(c(g));){const t=c(g);if(w.add(t),t!==d&&!a(g,d))break;for(const t of await this.storedValues(g,i))f.add(c(t));if(t===d)break;const e=h(g);if(e===g)break;g=e}const p=u.filter(t=>!f.has(c(t)));if(!(0===p.length||l&&(await this.getUserPermissions(t,l)).write))throw new Error(`[authz-forbidden] User ${e.user} may not link ${r} to domain ${p[0]}: no organization above it is linked to it`)}clearResolutionCache(){this.resolutionCache.clear()}}AuthzBase.widenedForLink=new WeakSet,AuthzBase.UNRESOLVED_POLICIES=["deny","allow"],AuthzBase.RESOLUTION_CACHE_MAX=1e4;export{AuthzBase as default};
|
|
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{AmbiguousIdentityError as i,ForbiddenError as n}from"../errors.js";import{isDnInBranch as a,organizationLink as o,getParentDn as h,normalizeDn as c,escapeLdapFilter as u}from"../utils.js";import{authzFor as l,servesRequest as d}from"./composition.js";class AuthzBase extends t{constructor(t){super(t),this.roles=["authz"],this.resolutionCache=new Map,this.resolutionCacheFullWarned=!1,this.fallbackWarned=!1,this.hooks={ldaprenamedone:()=>{this.resolutionCache.clear()},ldapdeletedone:()=>{this.resolutionCache.clear()},ldapmodifyrequest:async([t,e,r,s])=>{if(this.shouldSkipAuthorization(s))return[t,e,r,s];const i=await this.resolveCaller(s);if(!i)return[t,e,r,s];const n=this.config.ldap_organization_link_attribute;if(n&&e.replace?.[n]){let r,a=this.extractBranchDn(t),h=!1;try{const e=(await this.server.ldap.search({paged:!1,scope:"base",attributes:[n]},t)).searchEntries[0];e&&(h=!0,r=o(e[n]),r&&(a=r))}catch{}const c=e.replace[n],u=Array.isArray(c)?String(c[0]):String(c),l=this.filtersAttachedEntries(),d=l&&h&&this.inTransit(r),f=l&&this.inTransit(u);if(!d){const t=f?"write":this.sourcePermission(a);if(!(await this.getUserPermissions(i,a))[t])throw new Error(`[authz-forbidden] User ${s.user} does not have ${t} permission for source branch ${a}`)}if(f){if(d){if(0===(await this.getAuthorizedBranches(i)).length)throw new Error(`[authz-forbidden] User ${s.user} administers no branch`)}}else{if(!(await this.getUserPermissions(i,u)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for destination branch ${u}`)}}else{const e=await this.effectiveBranch(t);if(!(await this.getUserPermissions(i,e)).write)throw new Error(`[authz-forbidden] User ${s.user} does not have write permission for branch ${e}`)}const a=this.domainLinkAttribute();return a&&await this.guardDomainLinks(i,s,t,[...AuthzBase.valuesOf(e.add,a),...AuthzBase.valuesOf(e.replace,a)],a,!1),[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;if(n&&e[n]){const t=e[n];i=Array.isArray(t)?String(t[0]):String(t)}else i=this.extractBranchDn(t);if(!(await this.getUserPermissions(s,i)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for branch ${i}`);const a=this.domainLinkAttribute();return a&&await this.guardDomainLinks(s,r,t,AuthzBase.valuesOf(e,a),a,!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,r];if(this.filtersAttachedEntries()){if(0===(await this.getAuthorizedBranches(s)).length)throw new Error(`[authz-forbidden] User ${r.user} administers no branch`);if(this.globalBranches().some(e=>a(t,e))&&!(await this.getUserPermissions(s,t)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for branch ${t}`);const i=this.config.ldap_organization_link_attribute;if(i&&!AuthzBase.wantsAttribute(e,i)){const t=e.attributes,r=Array.isArray(t)?t.map(String):[String(t)];e={...e,attributes:[...r,i]},AuthzBase.widenedForLink.add(e)}return[t,e,r]}if(!(await this.getUserPermissions(s,t)).read)throw new Error(`[authz-forbidden] User ${r.user} does not have read permission for branch ${t}`);return[t,e,r]},ldapsearchfilter:async([t,e,r])=>{const s=[t,e,r];if(!this.filtersAttachedEntries())return s;if(this.shouldSkipAuthorization(e))return s;if(!t?.searchEntries?.length)return s;const i=await this.resolveCaller(e);if(!i)return s;const n=this.globalBranches();if(n.length){const r=new Map,s=[];for(const o of t.searchEntries){const t=n.find(t=>a(o.dn,t));void 0===t||(r.has(t)||r.set(t,!0===(await this.getUserPermissions(i,t)).read),r.get(t))?s.push(o):this.logger.debug(`${this.name}: hiding ${String(o.dn)} from ${e.user}, in the global branch ${t}`)}t.searchEntries=s}const o=this.config.ldap_organization_link_attribute;if(!o)return s;const h=await this.getAuthorizedBranches(i);if(0===h.length)return s;const within=t=>h.some(e=>a(t,e)),c=this.transitKey();if(t.searchEntries=t.searchEntries.filter(t=>{const r=t[o];if(null==r)return!0;const s=(Array.isArray(r)?r:[r]).map(String);return 0===s.length||(!!s.some(within)||(!!s.some(t=>this.inTransit(t,c))||(this.logger.debug(`${this.name}: hiding ${String(t.dn)} from ${e.user}, attached to ${s[0]}`),!1)))}),r&&AuthzBase.widenedForLink.has(r))for(const e of t.searchEntries)delete e[o];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=this.extractBranchDn(t),n=this.extractBranchDn(e),a=await this.getUserPermissions(s,i),o=this.sourcePermission(i);if(!a[o])throw new Error(`[authz-forbidden] User ${r.user} does not have ${o} permission for source branch ${i}`);if(!(await this.getUserPermissions(s,n)).write)throw new Error(`[authz-forbidden] User ${r.user} does not have write permission for destination branch ${n}`);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.effectiveBranch(t);if(!(await this.getUserPermissions(r,s)).delete)throw new Error(`[authz-forbidden] User ${e.user} does not have delete permission for branch ${s}`)}return[t,e]}},r(this.config,this.constructor.name),l(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)}globalBranches(){return[]}sourcePermission(t){return"read"}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.search({paged:!1,scope:"base",attributes:[e]},t),s=o(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&&c(t)===e}transitKey(){const t=this.transitBranch();return void 0===t?void 0:c(t)}async findUserDn(t){const e=`(${this.config.ldap_user_main_attribute||"uid"}=${u(t)})`,r=(await this.server.ldap.search({paged:!1,filter:e,attributes:["dn"],scope:"sub"},this.server.ldap.base)).searchEntries??[];if(r.length>1)throw new i(t,r.length);if(0===r.length)return null;const s=r[0].dn;return"string"==typeof s?s:String(s)}shouldSkipAuthorization(t){return!t?.user||!d(t,this.config)}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,a=Date.now(),o=this.resolutionCache.get(i);let h;if(o&&a-o.at<this.cacheTTL)h=o.user;else{if(h=await this.resolveUser(i),this.resolutionCache.size>=AuthzBase.RESOLUTION_CACHE_MAX)for(const[t,e]of this.resolutionCache)a-e.at>=this.cacheTTL&&this.resolutionCache.delete(t);this.resolutionCache.size<AuthzBase.RESOLUTION_CACHE_MAX?this.resolutionCache.set(i,{user:h,at:a}):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 n(`[authz-forbidden] 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 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.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 o=new Set((n?[]:await this.storedValues(r,i)).map(c)),u=s.filter(t=>!o.has(c(t)));if(0===u.length)return;const l=this.config.ldap_top_organization,d=c(l||this.server.ldap.base),f=new Set;let g=h(r);const w=new Set;for(;g&&!w.has(c(g));){const t=c(g);if(w.add(t),t!==d&&!a(g,d))break;for(const t of await this.storedValues(g,i))f.add(c(t));if(t===d)break;const e=h(g);if(e===g)break;g=e}const m=u.filter(t=>!f.has(c(t)));if(0!==m.length&&!await this.mayLinkAnyDomain(t))throw new Error(`[authz-forbidden] 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()}}AuthzBase.widenedForLink=new WeakSet,AuthzBase.UNRESOLVED_POLICIES=["deny","allow"],AuthzBase.RESOLUTION_CACHE_MAX=1e4;export{AuthzBase as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"../../lib/authz/base.js";import{isDnInBranch as t,escapeLdapFilter as i}from"../../lib/utils.js";class AuthzLinid1 extends e{constructor(e){super(e),this.name="authzLinid1",this.permissionsCache=new Map,this.cacheTTL=3e5;const i=this.config.authz_local_admin_attribute||"twakeLocalAdminLink";this.logger.info(`AuthzLinid1: Authorization based on ${i} attribute enabled`);const s=this.globalOrganization(),r=s&&this.globalBranches().find(e=>t(s,e));if(r)throw new Error(`AuthzLinid1: --authz-global-organization ${s} lies in the global branch ${r}`);const n=s&&this.globalWriteBranches().find(e=>t(s,e));if(n)throw new Error(`AuthzLinid1: --authz-global-organization ${s} lies in the global write branch ${n}`);for(const e of this.globalWriteBranches()){const i=this.globalBranches().find(i=>t(e,i)||t(i,e));if(i)throw new Error(`AuthzLinid1: the global branch ${i} and the global write branch ${e} overlap`)}}globalOrganization(){return this.config.authz_global_organization||this.config.ldap_top_organization||void 0}filtersAttachedEntries(){return!0}globalBranches(){return this.config.authz_global_branch??[]}globalWriteBranches(){return this.config.authz_global_write_branch??[]}isGlobalWrite(e){return this.globalWriteBranches().some(i=>t(e,i))}sourcePermission(e){return this.isGlobalWrite(e)?"delete":"read"}async resolveUser(e){return this.getUserDn(e)}async getUserDn(e){return this.findUserDn(e)}async getUserPermissions(e,i){const s=this.globalOrganization();if(this.globalBranches().some(e=>t(i,e)))return s&&!t(s,i)?this.getUserPermissions(e,s):{read:!1,write:!1,delete:!1};if(this.isGlobalWrite(i)){const r=s&&!t(s,i)?await this.getUserPermissions(e,s):{read:!1,write:!1,delete:!1};return{read:r.read||(await this.getAuthorizedBranches(e)).length>0,write:r.write,delete:r.delete}}return this.ownPermissions(e,i)}async ownPermissions(e,t){const i=Date.now(),s=this.permissionsCache.get(e);if(s&&i-s.timestamp<this.cacheTTL){const e=this.findBranchPermissions(s.branches,t);if(e)return e}await this.refreshUserPermissions(e);const r=this.permissionsCache.get(e);if(r){const e=this.findBranchPermissions(r.branches,t);if(e)return e}return{read:!1,write:!1,delete:!1}}async refreshUserPermissions(e){const t=new Map;try{const s=`(${this.config.authz_local_admin_attribute||"twakeLocalAdminLink"}=${i(e)})`,r=this.config.ldap_top_organization||this.config.ldap_base||"",n=await this.server.ldap.search({paged:!1,filter:s,attributes:["dn"],scope:"sub"},r);if(n.searchEntries)for(const e of n.searchEntries){const i="string"==typeof e.dn?e.dn:String(e.dn);t.set(i,{read:!0,write:!0,delete:!0})}}catch(t){this.logger.error(`Failed to refresh permissions for user ${e}: ${t}`)}this.permissionsCache.set(e,{branches:t,timestamp:Date.now()})}async getAuthorizedBranches(e){const t=Date.now(),i=this.permissionsCache.get(e);(!i||t-i.timestamp>=this.cacheTTL)&&await this.refreshUserPermissions(e);const s=this.permissionsCache.get(e);return s?Array.from(s.branches.keys()):[]}findBranchPermissions(e,t){if(e.has(t))return e.get(t)||null;for(const[i,s]of e.entries())if(t.toLowerCase().endsWith(`,${i.toLowerCase()}`))return s;return null}}export{AuthzLinid1 as default};
|
|
1
|
+
import e from"../../lib/authz/base.js";import{isDnInBranch as t,escapeLdapFilter as i}from"../../lib/utils.js";class AuthzLinid1 extends e{constructor(e){super(e),this.name="authzLinid1",this.permissionsCache=new Map,this.cacheTTL=3e5;const i=this.config.authz_local_admin_attribute||"twakeLocalAdminLink";this.logger.info(`AuthzLinid1: Authorization based on ${i} attribute enabled`);const s=this.globalOrganization(),r=s&&this.globalBranches().find(e=>t(s,e));if(r)throw new Error(`AuthzLinid1: --authz-global-organization ${s} lies in the global branch ${r}`);const n=s&&this.globalWriteBranches().find(e=>t(s,e));if(n)throw new Error(`AuthzLinid1: --authz-global-organization ${s} lies in the global write branch ${n}`);for(const e of this.globalWriteBranches()){const i=this.globalBranches().find(i=>t(e,i)||t(i,e));if(i)throw new Error(`AuthzLinid1: the global branch ${i} and the global write branch ${e} overlap`)}}async mayLinkAnyDomain(e){if(await super.mayLinkAnyDomain(e))return!0;const t=this.globalOrganization();return!!t&&!0===(await this.getUserPermissions(e,t)).write}globalOrganization(){return this.config.authz_global_organization||this.config.ldap_top_organization||void 0}filtersAttachedEntries(){return!0}globalBranches(){return this.config.authz_global_branch??[]}globalWriteBranches(){return this.config.authz_global_write_branch??[]}isGlobalWrite(e){return this.globalWriteBranches().some(i=>t(e,i))}sourcePermission(e){return this.isGlobalWrite(e)?"delete":"read"}async resolveUser(e){return this.getUserDn(e)}async getUserDn(e){return this.findUserDn(e)}async getUserPermissions(e,i){const s=this.globalOrganization();if(this.globalBranches().some(e=>t(i,e)))return s&&!t(s,i)?this.getUserPermissions(e,s):{read:!1,write:!1,delete:!1};if(this.isGlobalWrite(i)){const r=s&&!t(s,i)?await this.getUserPermissions(e,s):{read:!1,write:!1,delete:!1};return{read:r.read||(await this.getAuthorizedBranches(e)).length>0,write:r.write,delete:r.delete}}return this.ownPermissions(e,i)}async ownPermissions(e,t){const i=Date.now(),s=this.permissionsCache.get(e);if(s&&i-s.timestamp<this.cacheTTL){const e=this.findBranchPermissions(s.branches,t);if(e)return e}await this.refreshUserPermissions(e);const r=this.permissionsCache.get(e);if(r){const e=this.findBranchPermissions(r.branches,t);if(e)return e}return{read:!1,write:!1,delete:!1}}async refreshUserPermissions(e){const t=new Map;try{const s=`(${this.config.authz_local_admin_attribute||"twakeLocalAdminLink"}=${i(e)})`,r=this.config.ldap_top_organization||this.config.ldap_base||"",n=await this.server.ldap.search({paged:!1,filter:s,attributes:["dn"],scope:"sub"},r);if(n.searchEntries)for(const e of n.searchEntries){const i="string"==typeof e.dn?e.dn:String(e.dn);t.set(i,{read:!0,write:!0,delete:!0})}}catch(t){this.logger.error(`Failed to refresh permissions for user ${e}: ${t}`)}this.permissionsCache.set(e,{branches:t,timestamp:Date.now()})}async getAuthorizedBranches(e){const t=Date.now(),i=this.permissionsCache.get(e);(!i||t-i.timestamp>=this.cacheTTL)&&await this.refreshUserPermissions(e);const s=this.permissionsCache.get(e);return s?Array.from(s.branches.keys()):[]}findBranchPermissions(e,t){if(e.has(t))return e.get(t)||null;for(const[i,s]of e.entries())if(t.toLowerCase().endsWith(`,${i.toLowerCase()}`))return s;return null}}export{AuthzLinid1 as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"../../abstract/plugin.js";import{identityFor as s}from"../../lib/auth/base.js";import{servesRequest as t,overlap as
|
|
1
|
+
import e from"../../abstract/plugin.js";import{identityFor as s}from"../../lib/auth/base.js";import{servesRequest as t,overlap as n,EVERYONE as i,authzFor as a}from"../../lib/authz/composition.js";import{asyncHandler as r}from"../../lib/utils.js";import{UnauthorizedError as o}from"../../lib/errors.js";import{roleAttribute as c}from"../../config/schema.js";class AuthzScope extends e{constructor(){super(...arguments),this.name="authzScope",this.roles=["api"]}api(e){e.get(`${this.config.api_prefix}/v1/authz/scope`,r(async(e,s)=>this.scope(e,s)))}static canDescribe(e){if(!e.roles?.includes("authz"))return!1;const s=e;return"function"==typeof s.resolveUser&&"function"==typeof s.getAuthorizedBranches&&"function"==typeof s.getUserPermissions}describers(){return Object.values(this.server.loadedPlugins).filter(e=>AuthzScope.canDescribe(e))}static judges(e,s){return e.roles?.includes("auth")?Boolean(s.authenticators?.includes(e.name)):Boolean(s.user)&&t(s,e.config)}assertComposition(){const e=this.config.authz_scope_source;if(!e)return;const s=this.describers().map(e=>e.name);if(!s.includes(e))throw new Error(`${this.name}: --authz-scope-source names ${e}, which is not a loaded plugin able to describe a scope`+(s.length>0?` (loaded: ${s.join(", ")})`:""))}afterLoad(){if(this.config.authz_scope_source)return;const e=this.describers(),s=this.server.authenticators,population=e=>a(e.config,e.name)??i;for(let t=0;t<e.length;t++)for(let i=t+1;i<e.length;i++){const a=e[t],r=e[i];if(n(population(a),population(r),s))return void this.logger.warn(`${this.name}: ${a.name} and ${r.name} can both describe the scope of one caller, and ${a.name} answers because it loaded first. Set --authz-scope-source to choose; the answer lists both under \`sources\``)}}entities(){const e=(this.config.ldap_organization_link_attribute||"").toLowerCase(),linkable=s=>Boolean(e)&&(!s?.attributes||Object.keys(s.attributes).some(s=>s.toLowerCase()===e)),s=[],t=this.server.loadedPlugins.ldapFlatGeneric;for(const e of t?.instances||[])s.push({name:e.pluralName,base:e.base,linkable:linkable(e.schema)});const n=this.server.loadedPlugins.ldapGroups;return n?.base&&s.push({name:"groups",base:n.base,linkable:linkable(n.schema)}),this.server.loadedPlugins.ldapOrganizations&&this.config.ldap_top_organization&&s.push({name:"organizations",base:this.config.ldap_top_organization,linkable:!1}),s}async scope(e,t){const n=this.describers();if(Object.values(this.server.loadedPlugins).some(e=>e.roles?.includes("authz"))&&!e.user)throw new o("No authenticated user");const i=Object.values(this.server.loadedPlugins).filter(s=>s.roles?.includes("authz")&&AuthzScope.judges(s,e)),a=n.filter(e=>i.includes(e)),r=this.config.authz_scope_source,c=a.find(e=>e.name===r)??a[0],u=i.map(e=>e.name);if(!c)return 0===i.length?void t.json({user:e.user??null,unrestricted:!0,described:!0,source:null,sources:[],branches:[],linksAnyDomain:!0,entities:this.entities().map(({name:e,base:s})=>({name:e,base:s,create:!0}))}):void t.json({user:e.user??null,unrestricted:!1,described:!1,source:null,sources:u,branches:[],linksAnyDomain:!1,entities:[]});const l=s(e,c.config).value,h=await c.resolveUser(l);if(!h)throw new o(`Unknown user ${l}`);const d=await c.getAuthorizedBranches(h),p=[],f=new Map,permissionsOn=async e=>{let s=f.get(e);return s||(s=await c.getUserPermissions(h,e),f.set(e,s)),s};for(const e of d){const s=await permissionsOn(e);p.push({dn:e,...s,...await this.branchLabel(e)})}const m=d.some(e=>!0===f.get(e)?.write),b=this.config.ldap_top_organization,g=[];for(const{name:e,base:s,linkable:t}of this.entities()){const n=b&&s===b?!0===(await permissionsOn(b)).write:t?m:!0===(await permissionsOn(s)).write;g.push({name:e,base:s,create:n})}t.json({user:h,unrestricted:!1,described:!0,source:c.name,sources:u,branches:p,transit:c.transitBranch?.()??null,linksAnyDomain:!0===await(c.mayLinkAnyDomain?.(h)),entities:g})}async branchLabel(e){const s=this.server.loadedPlugins.ldapOrganizations,t=c(s?.schema,"organizationPath")||this.config.ldap_organization_path_attribute;try{const s=await this.server.ldap.search({paged:!1,scope:"base",attributes:["ou","o","cn",...t?[t]:[]]},e),n=s?.searchEntries?.[0];if(!n)return{};const first=e=>{const s=Array.isArray(e)?e[0]:e;return"string"==typeof s?s||void 0:Buffer.isBuffer(s)&&s.toString("utf8")||void 0};return{name:first(n.ou)||first(n.o)||first(n.cn),path:t?first(n[t]):void 0}}catch(e){return{}}}}export{AuthzScope as default};
|
|
@@ -32,6 +32,9 @@ export type SchemaAttributeType = 'string' | 'number' | 'integer' | 'boolean' |
|
|
|
32
32
|
* | `accountExpiry` | Date after which the account is to be removed |
|
|
33
33
|
* | `domainLink` | DNs of the mail domains an organization may use |
|
|
34
34
|
* | `domainName` | Domain name carried by a domain entry |
|
|
35
|
+
*
|
|
36
|
+
* Clients look for more: `givenName` and `surname` name the parts of a
|
|
37
|
+
* person's name, which a form composes the `displayName` from.
|
|
35
38
|
*/
|
|
36
39
|
export type SchemaRole = string;
|
|
37
40
|
/**
|
|
@@ -150,6 +153,12 @@ export interface SchemaAttribute {
|
|
|
150
153
|
* client never has to hardcode either.
|
|
151
154
|
*/
|
|
152
155
|
hint?: LocalizedText;
|
|
156
|
+
/**
|
|
157
|
+
* What the attribute is, for a client to show on demand — a tooltip next to
|
|
158
|
+
* the label — where its label alone does not say: "Employee ID" names a
|
|
159
|
+
* number, not whose records hold it.
|
|
160
|
+
*/
|
|
161
|
+
help?: LocalizedText;
|
|
153
162
|
branch?: string[];
|
|
154
163
|
fixed?: boolean;
|
|
155
164
|
/** Semantic role(s) — see {@link SchemaRole} */
|
|
@@ -238,10 +238,9 @@ export default abstract class AuthzBase extends DmPlugin {
|
|
|
238
238
|
* the directory — another customer's — and create addresses in it (#325).
|
|
239
239
|
*
|
|
240
240
|
* A domain newly linked to an organization must therefore be one an
|
|
241
|
-
* organization above it already links. Only a caller
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* branch and is judged as any other write.
|
|
241
|
+
* organization above it already links. Only a caller accepted by
|
|
242
|
+
* {@link mayLinkAnyDomain} may link another. Removing a link narrows
|
|
243
|
+
* nothing above the caller's own branch and is judged as any other write.
|
|
245
244
|
*
|
|
246
245
|
* @param user the resolved caller
|
|
247
246
|
* @param req the request, for the refusal's wording
|
|
@@ -251,6 +250,16 @@ export default abstract class AuthzBase extends DmPlugin {
|
|
|
251
250
|
* @param creating whether the organization is being created, so holds none
|
|
252
251
|
*/
|
|
253
252
|
private guardDomainLinks;
|
|
253
|
+
/**
|
|
254
|
+
* Whether a caller links an organization to any mail domain, not only to
|
|
255
|
+
* those an organization above it links: one holding write on the top
|
|
256
|
+
* organization, who creates the organizations domains are given to. A
|
|
257
|
+
* plugin knowing another such caller says so here; `GET /authz/scope`
|
|
258
|
+
* reports the answer.
|
|
259
|
+
*
|
|
260
|
+
* @param user the resolved caller
|
|
261
|
+
*/
|
|
262
|
+
mayLinkAnyDomain(user: string): Promise<boolean>;
|
|
254
263
|
/** Forget what was resolved, for a test that changes the directory. */
|
|
255
264
|
protected clearResolutionCache(): void;
|
|
256
265
|
/**
|
|
@@ -17,6 +17,12 @@ export default class AuthzLinid1 extends AuthzBase {
|
|
|
17
17
|
name: string;
|
|
18
18
|
permissionsCache: Map<string, CachedPermissions>;
|
|
19
19
|
constructor(server: DM);
|
|
20
|
+
/**
|
|
21
|
+
* The administrators of the global organization link any domain too: they
|
|
22
|
+
* create and delete the domains (`--authz-global-write-branches`), and the
|
|
23
|
+
* top organization may be a bare container nobody administers.
|
|
24
|
+
*/
|
|
25
|
+
mayLinkAnyDomain(user: string): Promise<boolean>;
|
|
20
26
|
/** The organization holding the global branches */
|
|
21
27
|
private globalOrganization;
|
|
22
28
|
/**
|
|
@@ -105,8 +105,11 @@ write on its parent, so the link itself is held to more:
|
|
|
105
105
|
|
|
106
106
|
- a domain newly linked to an organization must already be linked by an
|
|
107
107
|
organization above it, up to the top organization;
|
|
108
|
-
- a caller holding write on the top organization
|
|
109
|
-
|
|
108
|
+
- a caller holding write on the top organization, or on
|
|
109
|
+
`--authz-global-organization`, links any domain: they are the one giving
|
|
110
|
+
each organization its own. The second matters where the top organization
|
|
111
|
+
is a bare container nobody administers, the administrators of the whole
|
|
112
|
+
directory holding an organization under it;
|
|
110
113
|
- a domain the organization already links can be kept, and removing one is
|
|
111
114
|
judged as any other change.
|
|
112
115
|
|
|
@@ -115,7 +118,14 @@ Sales' domain, but cannot give it another customer's. The attribute is the
|
|
|
115
118
|
organization schema's `domainLink` role, or
|
|
116
119
|
`--enterprise-domain-link-attribute`; with neither, nothing is checked. The
|
|
117
120
|
rule belongs to the base of the authorization plugins, so `authzPerBranch`
|
|
118
|
-
applies it too.
|
|
121
|
+
applies it too, the top organization alone granting there.
|
|
122
|
+
|
|
123
|
+
`GET /authz/scope` answers `linksAnyDomain: true` to a caller linking any
|
|
124
|
+
domain, so that a form offers them every domain and the others only those
|
|
125
|
+
the organizations above inherit. Like the rest of the scope, it is the
|
|
126
|
+
answer of the plugin describing it (`source`, see `--authz-scope-source`):
|
|
127
|
+
where `authzPerBranch` judges the same caller too, it may still refuse the
|
|
128
|
+
link, granting it on the top organization only.
|
|
119
129
|
|
|
120
130
|
### Global branches
|
|
121
131
|
|
|
@@ -159,6 +169,12 @@ the read every administrator holds: a local administrator cannot move a
|
|
|
159
169
|
domain into their own organization and delete it from there. Several
|
|
160
170
|
branches are separated by `;`.
|
|
161
171
|
|
|
172
|
+
The administrators of `--authz-global-organization` also give the
|
|
173
|
+
organizations their domains: they link any domain, as those of the top
|
|
174
|
+
organization do ([Mail domains](#mail-domains)), so that the ones who create
|
|
175
|
+
a domain can hand it to an organization, the top organization being a bare
|
|
176
|
+
container or not.
|
|
177
|
+
|
|
162
178
|
`--authz-global-organization` must lie outside the global branches and the
|
|
163
179
|
global write branches: nobody would administer it otherwise. A global
|
|
164
180
|
branch and a global write branch may not be the same, nor lie one in the
|
|
@@ -222,6 +222,27 @@ cannot drift apart, and neither is hardcoded in the interface. A translated
|
|
|
222
222
|
hint is picked like a label; the server's messages are in English, so it
|
|
223
223
|
quotes the English text.
|
|
224
224
|
|
|
225
|
+
### Explaining an attribute: `help`
|
|
226
|
+
|
|
227
|
+
A label names an attribute; it does not always say what it is. `help` does,
|
|
228
|
+
for a client to show on demand — a tooltip next to the label — rather than
|
|
229
|
+
under every field:
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{
|
|
233
|
+
"employeeNumber": {
|
|
234
|
+
"type": "string",
|
|
235
|
+
"label": { "en": "Employee ID", "fr": "Matricule" },
|
|
236
|
+
"help": {
|
|
237
|
+
"en": "The person's registration number, as the personnel records hold it.",
|
|
238
|
+
"fr": "Le matricule de la personne, tel que le tient la gestion du personnel."
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
It is picked like a label. The server never shows it.
|
|
245
|
+
|
|
225
246
|
### Attributes a client may not send
|
|
226
247
|
|
|
227
248
|
| Marker | Meaning |
|
|
@@ -303,6 +324,10 @@ chose in its own. The roles the core looks for:
|
|
|
303
324
|
An attribute may carry several roles (`"role": ["identifier", "displayName"]`),
|
|
304
325
|
and a deployment is free to add its own for its plugins.
|
|
305
326
|
|
|
327
|
+
Clients look for more: `givenName` and `surname` name the parts of a person's
|
|
328
|
+
name, which a form composes the `displayName` from. The shipped user schemas
|
|
329
|
+
mark `givenName` and `sn` with them.
|
|
330
|
+
|
|
306
331
|
### Which attributes a search covers: `searchable`
|
|
307
332
|
|
|
308
333
|
`GET /{pluralName}?match=…&attribute=…` takes one attribute name or several
|
package/package.json
CHANGED
|
@@ -37,11 +37,13 @@
|
|
|
37
37
|
},
|
|
38
38
|
"sn": {
|
|
39
39
|
"type": "string",
|
|
40
|
-
"required": true
|
|
40
|
+
"required": true,
|
|
41
|
+
"role": "surname"
|
|
41
42
|
},
|
|
42
43
|
"givenName": {
|
|
43
44
|
"type": "string",
|
|
44
|
-
"required": false
|
|
45
|
+
"required": false,
|
|
46
|
+
"role": "givenName"
|
|
45
47
|
},
|
|
46
48
|
"displayName": {
|
|
47
49
|
"type": "string",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"test": "^[A-ZÀ-ÖØ-Þ](?!.*([\\s’'-]{2}|[.’'-]{2}|\\s\\.|[\\s’'-]$))[A-Za-zÀ-ÖØ-öø-ÿ\\s’'.-]{1,50}$",
|
|
58
58
|
"hint": "Starts with a capital, up to 51 letters, no repeated space, apostrophe, hyphen or dot",
|
|
59
59
|
"required": true,
|
|
60
|
+
"role": "surname",
|
|
60
61
|
"label": "Last name"
|
|
61
62
|
},
|
|
62
63
|
"givenName": {
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"test": "^[A-ZÀ-ÖØ-Þ](?!.*([\\s’'-]{2}|[.’'-]{2}|\\s\\.|[\\s’'-]$))[A-Za-zÀ-ÖØ-öø-ÿ\\s’'.-]{1,50}$",
|
|
66
67
|
"hint": "Starts with a capital, up to 51 letters, no repeated space, apostrophe, hyphen or dot",
|
|
67
68
|
"required": true,
|
|
69
|
+
"role": "givenName",
|
|
68
70
|
"label": "First name"
|
|
69
71
|
},
|
|
70
72
|
"displayName": {
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
},
|
|
54
54
|
"sn": {
|
|
55
55
|
"type": "string",
|
|
56
|
-
"required": true
|
|
56
|
+
"required": true,
|
|
57
|
+
"role": "surname"
|
|
57
58
|
},
|
|
58
59
|
"homeDirectory": {
|
|
59
60
|
"type": "string",
|
|
@@ -76,7 +77,8 @@
|
|
|
76
77
|
},
|
|
77
78
|
"givenName": {
|
|
78
79
|
"type": "string",
|
|
79
|
-
"required": true
|
|
80
|
+
"required": true,
|
|
81
|
+
"role": "givenName"
|
|
80
82
|
},
|
|
81
83
|
"displayName": {
|
|
82
84
|
"type": "string",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"sn": {
|
|
33
33
|
"type": "string",
|
|
34
|
-
"required": true
|
|
34
|
+
"required": true,
|
|
35
|
+
"role": "surname"
|
|
35
36
|
},
|
|
36
37
|
"mail": {
|
|
37
38
|
"type": "string",
|
|
@@ -55,7 +56,8 @@
|
|
|
55
56
|
},
|
|
56
57
|
"givenName": {
|
|
57
58
|
"type": "string",
|
|
58
|
-
"required": false
|
|
59
|
+
"required": false,
|
|
60
|
+
"role": "givenName"
|
|
59
61
|
},
|
|
60
62
|
"displayName": {
|
|
61
63
|
"type": "string",
|
|
@@ -53,12 +53,30 @@
|
|
|
53
53
|
"type": "string",
|
|
54
54
|
"searchable": true,
|
|
55
55
|
"required": true,
|
|
56
|
-
"unique": {
|
|
56
|
+
"unique": {
|
|
57
|
+
"sentinel": "UNIT"
|
|
58
|
+
},
|
|
57
59
|
"role": "employeeId",
|
|
58
60
|
"label": {
|
|
59
61
|
"en": "Employee ID",
|
|
60
62
|
"fr": "Matricule",
|
|
61
63
|
"mn": "Ажилтны дугаар"
|
|
64
|
+
},
|
|
65
|
+
"help": {
|
|
66
|
+
"en": "The person's registration number, as the personnel records hold it. Unique in the directory, except UNIT, which the accounts of no one in particular (shared, technical) take.",
|
|
67
|
+
"fr": "Le matricule de la personne, tel que le tient la gestion du personnel. Unique dans l'annuaire, sauf UNIT, que prennent les comptes qui ne sont à personne en particulier (partagés, techniques).",
|
|
68
|
+
"mn": "Хүний нөөцийн бүртгэлд байгаа ажилтны дугаар. Лавлахад давхцахгүй байх ёстой; хэн нэгэн хүнд хамааралгүй (дундын, техникийн) бүртгэлүүд UNIT утгыг авна."
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"twakeDepartmentLink": {
|
|
72
|
+
"type": "pointer",
|
|
73
|
+
"branch": ["__LDAP_BASE__"],
|
|
74
|
+
"required": true,
|
|
75
|
+
"role": "organizationLink",
|
|
76
|
+
"label": {
|
|
77
|
+
"en": "Department",
|
|
78
|
+
"fr": "Organisation",
|
|
79
|
+
"mn": "Байгууллага"
|
|
62
80
|
}
|
|
63
81
|
},
|
|
64
82
|
"cn": {
|
|
@@ -75,6 +93,7 @@
|
|
|
75
93
|
"type": "string",
|
|
76
94
|
"searchable": true,
|
|
77
95
|
"required": true,
|
|
96
|
+
"role": "surname",
|
|
78
97
|
"label": {
|
|
79
98
|
"en": "Last name",
|
|
80
99
|
"fr": "Nom de famille",
|
|
@@ -85,6 +104,7 @@
|
|
|
85
104
|
"type": "string",
|
|
86
105
|
"searchable": true,
|
|
87
106
|
"required": true,
|
|
107
|
+
"role": "givenName",
|
|
88
108
|
"label": {
|
|
89
109
|
"en": "First name",
|
|
90
110
|
"fr": "Prénom",
|
|
@@ -289,17 +309,6 @@
|
|
|
289
309
|
"mn": "Нууц үгээ солих шаардлагатай"
|
|
290
310
|
}
|
|
291
311
|
},
|
|
292
|
-
"twakeDepartmentLink": {
|
|
293
|
-
"type": "pointer",
|
|
294
|
-
"branch": ["__LDAP_BASE__"],
|
|
295
|
-
"required": true,
|
|
296
|
-
"role": "organizationLink",
|
|
297
|
-
"label": {
|
|
298
|
-
"en": "Department",
|
|
299
|
-
"fr": "Organisation",
|
|
300
|
-
"mn": "Байгууллага"
|
|
301
|
-
}
|
|
302
|
-
},
|
|
303
312
|
"twakeDepartmentPath": {
|
|
304
313
|
"type": "string",
|
|
305
314
|
"test": "^[^\\r\\n]{1,4096}$",
|