ldap-rest 0.8.2 → 0.10.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/bin/index.mjs +1 -1
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/auth/base.js +1 -1
- package/dist/lib/authz/base.js +1 -1
- package/dist/lib/authz/composition.js +1 -0
- package/dist/lib/changeContext.js +1 -0
- package/dist/lib/errors.js +1 -1
- package/dist/lib/expressFormatedResponses.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/ldapCodes.js +1 -0
- package/dist/lib/parseConfig.js +1 -1
- package/dist/lib/storage/store.js +1 -0
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/auth/authzDynamic.js +1 -1
- package/dist/plugins/auth/authzLinid1.js +1 -1
- package/dist/plugins/auth/authzPerBranch.js +1 -1
- package/dist/plugins/auth/authzPerRoute.js +1 -1
- package/dist/plugins/auth/authzScope.js +1 -1
- package/dist/plugins/auth/hmac.js +1 -1
- package/dist/plugins/auth/llng.js +1 -1
- package/dist/plugins/auth/openidconnect.js +1 -1
- package/dist/plugins/auth/token.js +1 -1
- package/dist/plugins/auth/totp.js +1 -1
- package/dist/plugins/bcl.js +1 -0
- package/dist/plugins/ldap/departmentSync.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/onChange.js +1 -1
- package/dist/plugins/ldap/organizations.js +1 -1
- package/dist/plugins/ldap/trash.js +1 -1
- package/dist/plugins/priority.json.js +1 -1
- package/dist/plugins/scim/baseResolver.js +1 -1
- package/dist/plugins/scim/errors.js +1 -1
- package/dist/plugins/scim/groups.js +1 -1
- package/dist/plugins/scim/list.js +1 -1
- package/dist/plugins/scim/scim.js +1 -1
- package/dist/plugins/scim/users.js +1 -1
- package/dist/plugins/storage/file.js +1 -0
- package/dist/plugins/storage/ldap.js +1 -0
- package/dist/plugins/storage.js +1 -0
- package/dist/plugins/twake/appAccountsApi.js +1 -1
- package/dist/plugins/twake/appAccountsConsistency.js +1 -1
- package/dist/plugins/twake/clouderyProvision.js +1 -1
- package/dist/plugins/twake/cozyProvision.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +7 -0
- package/dist/src/abstract/plugin.d.ts +26 -0
- package/dist/src/bin/index.d.ts +50 -8
- package/dist/src/config/args.d.ts +36 -0
- package/dist/src/hooks.d.ts +70 -6
- package/dist/src/lib/auth/base.d.ts +175 -9
- package/dist/src/lib/authz/base.d.ts +159 -4
- package/dist/src/lib/authz/composition.d.ts +145 -0
- package/dist/src/lib/changeContext.d.ts +24 -0
- package/dist/src/lib/errors.d.ts +15 -0
- package/dist/src/lib/expressFormatedResponses.d.ts +9 -2
- package/dist/src/lib/ldapActions.d.ts +32 -1
- package/dist/src/lib/ldapCodes.d.ts +28 -0
- package/dist/src/lib/storage/store.d.ts +84 -0
- package/dist/src/lib/utils.d.ts +19 -0
- package/dist/src/plugins/auth/authzDynamic.d.ts +76 -0
- package/dist/src/plugins/auth/authzLinid1.d.ts +6 -0
- package/dist/src/plugins/auth/authzPerBranch.d.ts +46 -0
- package/dist/src/plugins/auth/authzPerRoute.d.ts +26 -0
- package/dist/src/plugins/auth/authzScope.d.ts +44 -3
- package/dist/src/plugins/auth/hmac.d.ts +2 -0
- package/dist/src/plugins/auth/llng.d.ts +39 -0
- package/dist/src/plugins/auth/openidconnect.d.ts +66 -3
- package/dist/src/plugins/auth/rateLimit.d.ts +3 -1
- package/dist/src/plugins/auth/token.d.ts +2 -0
- package/dist/src/plugins/auth/totp.d.ts +2 -0
- package/dist/src/plugins/bcl.d.ts +65 -0
- package/dist/src/plugins/ldap/groups.d.ts +7 -5
- package/dist/src/plugins/ldap/onChange.d.ts +17 -8
- package/dist/src/plugins/ldap/organizations.d.ts +92 -3
- package/dist/src/plugins/scim/baseResolver.d.ts +3 -1
- package/dist/src/plugins/scim/errors.d.ts +2 -10
- package/dist/src/plugins/storage/file.d.ts +19 -0
- package/dist/src/plugins/storage/ldap.d.ts +17 -0
- package/dist/src/plugins/storage.d.ts +44 -0
- package/docs/plugin-development/README.md +13 -19
- package/docs/plugin-development/dependencies.md +40 -15
- package/docs/plugin-development/hooks.md +6 -0
- package/docs/plugin-development/testing.md +1 -17
- package/docs/usage/configuration.md +90 -14
- package/docs/usage/plugins/README.md +17 -12
- package/docs/usage/plugins/auth/README.md +129 -0
- package/docs/usage/plugins/auth/authz-dynamic.md +42 -0
- package/docs/usage/plugins/auth/authz-linid1.md +33 -0
- package/docs/usage/plugins/auth/authz-per-branch.md +41 -11
- package/docs/usage/plugins/auth/authz-per-route.md +6 -0
- package/docs/usage/plugins/auth/authz-scope.md +74 -16
- package/docs/usage/plugins/auth/back-channel-logout.md +86 -0
- package/docs/usage/plugins/auth/llng.md +29 -0
- package/docs/usage/plugins/auth/oidc.md +52 -0
- package/docs/usage/plugins/ldap/on-change.md +51 -284
- package/docs/usage/plugins/ldap/organizations.md +37 -0
- package/docs/usage/plugins/utilities/storage.md +94 -0
- package/docs/usage/troubleshooting.md +1 -9
- package/docs/usage/upgrading.md +341 -0
- package/package.json +17 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHmac as e,createHash as t,timingSafeEqual as i}from"crypto";import{unauthorized as r}from"../../lib/expressFormatedResponses.js";import s from"../../lib/auth/base.js";class AuthHmac extends s{constructor(...e){super(...e),this.name="authHmac",this.roles=["auth"],this.services=new Map;const t=this.config.auth_hmac;this.timeWindow=this.config.auth_hmac_window??12e4,t&&Array.isArray(t)&&t.forEach((e,t)=>{const i=e.split(":");if(i.length>=3){const e=i[0].trim(),r=i[1].trim(),s=i.slice(2).join(":").trim();if(!e||!r||!s)return void this.logger.warn(`Invalid HMAC config at index ${t}: missing id, secret, or name`);r.length<32&&this.logger.warn(`HMAC secret for service "${e}" is too short (minimum 32 characters recommended)`),this.services.set(e,{id:e,secret:r,name:s})}else this.logger.warn(`Invalid HMAC config format at index ${t}: expected "service-id:secret:name"`)}),0===this.services.size?this.logger.warn("No valid HMAC services configured"):(this.logger.info(`HMAC authentication initialized with ${this.services.size} service(s): ${Array.from(this.services.keys()).join(", ")}`),this.logger.info(`Time window for replay protection: ${this.timeWindow}ms`))}authMethod(e,t,i){const s=e.headers.authorization;if(!s||!s.startsWith("HMAC-SHA256 "))return this.logger.warn("Missing or invalid Authorization header (expected HMAC-SHA256)"),r(t);const n=s.substring(12).split(":");if(3!==n.length)return this.logger.warn("Invalid HMAC authorization format (expected service-id:timestamp:signature)"),r(t);const[o,
|
|
1
|
+
import{createHmac as e,createHash as t,timingSafeEqual as i}from"crypto";import{unauthorized as r}from"../../lib/expressFormatedResponses.js";import s from"../../lib/auth/base.js";class AuthHmac extends s{knownIdentities(){return[...this.services.values()].map(e=>e.name)}identitySource(){return"req.user and req.userName: the service name given in --auth-hmac"}constructor(...e){super(...e),this.name="authHmac",this.roles=["auth"],this.services=new Map;const t=this.config.auth_hmac;this.timeWindow=this.config.auth_hmac_window??12e4,t&&Array.isArray(t)&&t.forEach((e,t)=>{const i=e.split(":");if(i.length>=3){const e=i[0].trim(),r=i[1].trim(),s=i.slice(2).join(":").trim();if(!e||!r||!s)return void this.logger.warn(`Invalid HMAC config at index ${t}: missing id, secret, or name`);r.length<32&&this.logger.warn(`HMAC secret for service "${e}" is too short (minimum 32 characters recommended)`),this.services.set(e,{id:e,secret:r,name:s})}else this.logger.warn(`Invalid HMAC config format at index ${t}: expected "service-id:secret:name"`)}),0===this.services.size?this.logger.warn("No valid HMAC services configured"):(this.logger.info(`HMAC authentication initialized with ${this.services.size} service(s): ${Array.from(this.services.keys()).join(", ")}`),this.logger.info(`Time window for replay protection: ${this.timeWindow}ms`))}authMethod(e,t,i){const s=e.headers.authorization;if(!s||!s.startsWith("HMAC-SHA256 "))return this.logger.warn("Missing or invalid Authorization header (expected HMAC-SHA256)"),r(t);const n=s.substring(12).split(":");if(3!==n.length)return this.logger.warn("Invalid HMAC authorization format (expected service-id:timestamp:signature)"),r(t);const[a,o,c]=n,h=this.services.get(a);if(!h)return this.logger.warn(`Unknown service ID: ${a}`),r(t);const u=parseInt(o,10);if(isNaN(u)||u<=0)return this.logger.warn(`Invalid timestamp: ${o}`),r(t);const m=Date.now(),g=Math.abs(m-u);if(g>this.timeWindow)return this.logger.warn(`Timestamp outside allowed window: ${g}ms (max: ${this.timeWindow}ms) for service ${a}`),r(t);const d=this.calculateBodyHash(e),l=`${e.method.toUpperCase()}|${e.originalUrl||e.url}|${u}|${d}`,f=this.calculateHmac(h.secret,l);if(!this.secureCompare(c,f))return this.logger.warn(`Invalid signature for service ${a} (${h.name}). Expected: ${f.substring(0,8)}..., Got: ${c.substring(0,8)}...`),this.logger.debug(`Signing string: ${l}`),r(t);this.logger.debug(`HMAC authentication successful for service: ${a} (${h.name})`),this.publishIdentity(e,h.name),i()}calculateHmac(t,i){const r=e("sha256",t);return r.update(i),r.digest("hex")}calculateBodyHash(e){const i=e.method.toUpperCase();if("GET"===i||"DELETE"===i||"HEAD"===i)return"";if(e.body){const i="string"==typeof e.body?e.body:JSON.stringify(e.body),r=t("sha256");return r.update(i),r.digest("hex")}return""}secureCompare(e,t){if(e.length!==t.length)return!1;try{const r=Buffer.from(e,"utf8"),s=Buffer.from(t,"utf8");return i(r,s)}catch{return!1}}}export{AuthHmac as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"../../lib/auth/base.js";const t=new WeakMap;class AuthLLNG extends e{constructor(){super(...arguments),this.name="authLemonldapNg",this.roles=["auth"]}async api(e){let n;try{n=await this.loadHandler()}catch(e){throw new Error(`${this.name}: requires the optional dependency "lemonldap-ng-handler", which is not installed. Install it to use this plugin, or remove it from the configuration. (${e instanceof Error?e.message:String(e)})`)}const
|
|
1
|
+
import e from"../../lib/auth/base.js";const t=new WeakMap;class AuthLLNG extends e{constructor(){super(...arguments),this.name="authLemonldapNg",this.roles=["auth"]}async api(e){let n;try{n=await this.loadHandler()}catch(e){throw new Error(`${this.name}: requires the optional dependency "lemonldap-ng-handler", which is not installed. Install it to use this plugin, or remove it from the configuration. (${e instanceof Error?e.message:String(e)})`)}const r=this.config.llng_ini??"",a=t.get(n);if(void 0!==a&&a!==r)throw new Error(`${this.name}: lemonldap-ng-handler is already initialized from "${a}"; it keeps its state at the module level, so a second instance of this plugin cannot also use "${r}". Load one instance of this plugin, or point every instance at the same lemonldap-ng.ini.`);try{await n.init({configStorage:{confFile:r}})}catch(e){throw new Error(`${this.name}: cannot initialize the LemonLDAP::NG handler from ${r}. Check that the file exists, that its [configuration] section is reachable and that [node-handler] lists this server in nodeVhosts. (${e instanceof Error?e.message:String(e)})`)}t.set(n,r),this.handler=n,super.api(e)}async loadHandler(){return import("lemonldap-ng-handler")}static dropForgedIdentity(e,t){const n=t??AuthLLNG.USER_HEADER;for(const t of Object.keys(e.headers))t.toLowerCase()===n&&delete e.headers[t]}static headerValue(e,t){for(const[n,r]of Object.entries(e.headers))if(n.toLowerCase()===t&&"string"==typeof r)return r}static vouchedIdentity(e){return AuthLLNG.headerValue(e,AuthLLNG.USER_HEADER)}identitySource(){const e=this.config.llng_username_header||"";return"req.user: LemonLDAP::NG's whatToTrace, via Lm-Remote-User; "+(e?`req.userName: the ${e} header LLNG exports`:"req.userName: the same value, until --llng-username-header names an exported header")}authMethod(e,t,n){if(!this.handler)throw new Error(`${this.name}: lemonldap-ng-handler is not loaded`);const r=(this.config.llng_username_header||"").toLowerCase();AuthLLNG.dropForgedIdentity(e),r&&AuthLLNG.dropForgedIdentity(e,r),this.handler.run(e,t,()=>{const a=AuthLLNG.vouchedIdentity(e);if(!a)return this.logger.warn(`${this.name}: the handler passed a request on without naming a user, which a \`skip\` rule does. Refusing rather than serving it with no identity`),void t.status(401).json({error:"Unauthorized"});const i=r?AuthLLNG.headerValue(e,r):void 0;this.publishIdentity(e,a,i),n()})}}AuthLLNG.USER_HEADER="lm-remote-user";export{AuthLLNG as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import e from"express-openid-connect";import o from"../../lib/auth/base.js";import{launchHooksChained as i}from"../../lib/utils.js";import{serverError as t}from"../../lib/expressFormatedResponses.js";const{auth:s,requiresAuth:r}=e,n=["/login","/logout","/callback","/backchannel-logout"],walkSubscribers=async(e,o)=>{let i;for(const t of e??[])if(t)try{await t(o)}catch(e){i??(i=e instanceof Error?e:new Error(String(e)))}if(i)throw i};class OpenIDConnect extends o{constructor(e){super(e),this.name="openidconnect",this.roles=["auth"];for(const e of["oidc_server","oidc_client_id","oidc_client_secret","base_url"])if(!this.config[e])throw new Error(`Missing config parameter ${e}`)}buildConfig(){return{authRequired:!1,issuerBaseURL:this.config.oidc_server,clientID:this.config.oidc_client_id,secret:this.config.oidc_client_secret,baseURL:this.config.base_url,clientSecret:this.config.oidc_client_secret,authorizationParams:{response_type:"code",scope:"openid profile email"},backchannelLogout:{onLogoutToken:async e=>{this.server.hooks.oidclogouttoken?await walkSubscribers(this.server.hooks.oidclogouttoken,e):this.logger.warn("openidconnect: a logout token arrived but no Back-Channel Logout plugin is loaded, so nothing records it")},onLogin:async e=>{if(!this.server.hooks.oidclogin)return;const o=e.oidc?.idTokenClaims;if(o)try{await walkSubscribers(this.server.hooks.oidclogin,o)}catch(o){const i=e.appSession;if(i)for(const e of Object.keys(i))delete i[e];throw o}},isLoggedOut:async e=>{if(!this.server.hooks.oidcsessionvalid)return!1;const o=e.oidc.idTokenClaims,[,t]=await i(this.server.hooks.oidcsessionvalid,[o,!0]);return!t}}}}identitySource(){const e=this.config.oidc_username_claim||"sub";return`req.user: the OIDC sub, an opaque provider identifier; req.userName: the "${e}" claim`+("sub"===e?" — the same value, until --oidc-username-claim names another":"")}get pathPrefixes(){const e=super.pathPrefixes;return 0===e.length?e:[...e,...n]}api(e){this.router=s(this.buildConfig()),super.api(e)}authMethod(e,o,i){if(!this.router)return t(o,new Error(`${this.name}: api() has not built the router`));this.router(e,o,s=>{if(s)return t(o,s);r()(e,o,()=>{const o=e.oidc.user,t=this.config.oidc_username_claim||"sub",s=o[t];"sub"!==t&&"string"!=typeof s&&this.logger.warn(`${this.name}: no "${t}" claim on this session, so req.userName falls back to the sub. Check --oidc-username-claim against the scopes the provider is asked for`),this.publishIdentity(e,String(o.sub),"string"==typeof s?s:String(o.sub)),i()})})}}export{OpenIDConnect as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{unauthorized as t}from"../../lib/expressFormatedResponses.js";import e from"../../lib/auth/base.js";class AuthToken extends e{constructor(...t){super(...t),this.name="authToken",this.roles=["auth"],this.tokenMap=new Map;const e=this.config.auth_token;e&&Array.isArray(e)&&e.forEach((t,e)=>{if(t.includes(":")){const[e,s]=t.split(":",2);this.tokenMap.set(e.trim(),s.trim())}else this.tokenMap.set(t.trim(),`token ${e}`)})}authMethod(e,s,
|
|
1
|
+
import{unauthorized as t}from"../../lib/expressFormatedResponses.js";import e from"../../lib/auth/base.js";class AuthToken extends e{knownIdentities(){return[...new Set(this.tokenMap.values())]}identitySource(){return"req.user and req.userName: the name given in --auth-token"}constructor(...t){super(...t),this.name="authToken",this.roles=["auth"],this.tokenMap=new Map;const e=this.config.auth_token;e&&Array.isArray(e)&&e.forEach((t,e)=>{if(t.includes(":")){const[e,s]=t.split(":",2);this.tokenMap.set(e.trim(),s.trim())}else this.tokenMap.set(t.trim(),`token ${e}`)})}authMethod(e,s,n){let r=e.headers.authorization;if(!r||!/^Bearer .+/.test(r))return this.logger.warn("Missing or invalid Authorization header"),t(s);r=r.split(" ")[1];const i=this.tokenMap.get(r);if(!i){const e=r.length>8?`${r.substring(0,8)}...`:"***";return this.logger.warn(`Unauthorized token: ${e}`),t(s)}this.publishIdentity(e,i),n()}}export{AuthToken as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHmac as t}from"crypto";import{unauthorized as e}from"../../lib/expressFormatedResponses.js";import s from"../../lib/auth/base.js";class AuthTotp extends s{constructor(...t){super(...t),this.name="authTotp",this.roles=["auth"],this.totpUsers=[];const e=this.config.auth_totp;this.window=this.config.auth_totp_window??1,this.step=this.config.auth_totp_step??30,e&&Array.isArray(e)&&e.forEach((t,e)=>{const s=t.split(":");if(s.length>=2){const t=s[0].trim(),r=s[1].trim(),i=s[2]?parseInt(s[2].trim(),10):6;if(!this.isValidBase32(t))return void this.logger.warn(`Invalid Base32 secret for TOTP user "${r}" (index ${e})`);if(i<6||i>10)return void this.logger.warn(`Invalid digits count for TOTP user "${r}": ${i} (must be 6-10)`);this.totpUsers.push({name:r,secret:t,digits:i})}else this.logger.warn(`Invalid TOTP config format at index ${e}: expected "secret:name[:digits]"`)}),0===this.totpUsers.length?this.logger.warn("No valid TOTP users configured"):this.logger.info(`TOTP authentication initialized with ${this.totpUsers.length} user(s)`)}authMethod(t,s,r){const i=t.headers.authorization;if(!i||!/^Bearer .+/.test(i))return this.logger.warn("Missing or invalid Authorization header"),e(s);const o=i.split(" ")[1];for(const e of this.totpUsers)if(this.verifyTotp(e.secret,o,e.digits))return this.logger.debug(`TOTP authentication successful for user: ${e.name}`),t
|
|
1
|
+
import{createHmac as t}from"crypto";import{unauthorized as e}from"../../lib/expressFormatedResponses.js";import s from"../../lib/auth/base.js";class AuthTotp extends s{knownIdentities(){return this.totpUsers.map(t=>t.name)}identitySource(){return"req.user and req.userName: the name given in --auth-totp"}constructor(...t){super(...t),this.name="authTotp",this.roles=["auth"],this.totpUsers=[];const e=this.config.auth_totp;this.window=this.config.auth_totp_window??1,this.step=this.config.auth_totp_step??30,e&&Array.isArray(e)&&e.forEach((t,e)=>{const s=t.split(":");if(s.length>=2){const t=s[0].trim(),r=s[1].trim(),i=s[2]?parseInt(s[2].trim(),10):6;if(!this.isValidBase32(t))return void this.logger.warn(`Invalid Base32 secret for TOTP user "${r}" (index ${e})`);if(i<6||i>10)return void this.logger.warn(`Invalid digits count for TOTP user "${r}": ${i} (must be 6-10)`);this.totpUsers.push({name:r,secret:t,digits:i})}else this.logger.warn(`Invalid TOTP config format at index ${e}: expected "secret:name[:digits]"`)}),0===this.totpUsers.length?this.logger.warn("No valid TOTP users configured"):this.logger.info(`TOTP authentication initialized with ${this.totpUsers.length} user(s)`)}authMethod(t,s,r){const i=t.headers.authorization;if(!i||!/^Bearer .+/.test(i))return this.logger.warn("Missing or invalid Authorization header"),e(s);const o=i.split(" ")[1];for(const e of this.totpUsers)if(this.verifyTotp(e.secret,o,e.digits))return this.logger.debug(`TOTP authentication successful for user: ${e.name}`),this.publishIdentity(t,e.name),r();return this.logger.warn(`Unauthorized TOTP token: ${o}`),e(s)}verifyTotp(t,e,s){const r=Math.floor(Date.now()/1e3);for(let i=-this.window;i<=this.window;i++){const o=Math.floor(r/this.step)+i;if(this.generateTotp(t,o,s)===e)return!0}return!1}generateTotp(e,s,r){const i=this.base32Decode(e),o=Buffer.alloc(8);o.writeBigUInt64BE(BigInt(s));const n=t("sha1",i);n.update(o);const a=n.digest(),h=15&a[a.length-1];return(((127&a[h])<<24|(255&a[h+1])<<16|(255&a[h+2])<<8|255&a[h+3])%Math.pow(10,r)).toString().padStart(r,"0")}base32Decode(t){let e=t.toUpperCase();for(;e.endsWith("=");)e=e.slice(0,-1);let s=0,r=0;const i=[];for(let t=0;t<e.length;t++){const o="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(e[t]);if(-1===o)throw new Error(`Invalid Base32 character: ${e[t]}`);r=r<<5|o,s+=5,s>=8&&(i.push(r>>>s-8&255),s-=8)}return Buffer.from(i)}isValidBase32(t){return/^[A-Z2-7]+=*$/.test(t.toUpperCase())}}export{AuthTotp as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import t from"../abstract/plugin.js";const e="bcl";class Bcl extends t{constructor(t){super(t),this.name="bcl",this.dependencies={storage:"core/storage"},this.hooks={oidclogouttoken:async t=>{const s=Bcl.keys(t);if(0===s.length)return void this.logger.warn(`${this.name}: a logout token carried no sid nor sub`);const o=Date.now()+this.retention;await Promise.all(s.map(t=>this.store.set(e,t,"1",o)))},oidclogin:async t=>{await Promise.all(Bcl.keys(t).map(t=>this.store.delete(e,t)))},oidcsessionvalid:async([t,s])=>{if(!s)return[t,s];for(const s of Bcl.keys(t))if(null!==await this.store.get(e,s))return[t,!1];return[t,!0]}},this.retention=1e3*(t.config.bcl_retention||604800)}api(){const t=this.requirePlugin("storage");if(!t)throw new Error("bcl: needs core/storage loaded, and --storage-backend set. Without somewhere to keep what a logout killed, every session would outlive the logout that closed it and nothing would say so.");this.storage=t}get store(){if(!this.storage)throw new Error("bcl: the store was asked for before api() took hold of it. A plugin built outside registerPlugin has to call api() as it does.");return this.storage.store}static keys(t){const e=[];return t.sid&&e.push(`${t.iss}|sid|${t.sid}`),t.sub&&e.push(`${t.iss}|sub|${t.sub}`),e}}export{Bcl as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../../abstract/plugin.js";import{roleAttribute as e}from"../../config/schema.js";import{isDnInBranch as a,parseDn as r,normalizeDn as n,getParentDn as i,rdnValue as s}from"../../lib/utils.js";class LdapDepartmentSync extends t{constructor(t){super(t),this.name="ldapDepartmentSync",this.roles=["consistency"],this.hooks={ldaprenamedone:async([t,e])=>{if(/^ou=/.test(t)){this.logger.info(`Organization renamed from ${t} to ${e}, synchronizing linked resources...`);try{const a=this.config.ldap_top_organization.replace(/^ou=[^,]+,/,"");await this.updateOrganizationPaths(e);for(const r of this.linkedAttributes())await this.updateLinkedResources(t,e,a,r),await this.updateDescendantReferences(t,e,a,r)}catch(t){this.logger.error(`Error synchronizing department links after rename: ${t}`)}}}},this.linkAttr=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",this.pathAttr=this.config.ldap_organization_path_attribute||"twakeDepartmentPath"}organizationPathAttribute(){const t=this.server.loadedPlugins.ldapOrganizations;return e(t?.schema,"organizationPath")||this.pathAttr}linkedAttributes(){const t=[],a=this.server.loadedPlugins.ldapFlatGeneric;for(const e of a?.instances||[])t.push(e.schema);const r=this.server.loadedPlugins.ldapGroups;t.push(r?.schema);const n=[];for(const a of t){const t=e(a,"organizationLink");t&&n.push({link:t,path:e(a,"organizationPath")||this.pathAttr})}n.push({link:this.linkAttr,path:this.pathAttr});const i=new Set;return n.filter(({link:t,path:e})=>{const a=`${t.toLowerCase()} ${e.toLowerCase()}`;return!i.has(a)&&(i.add(a),!0)})}async updateOrganizationPaths(t){const e=this.organizationPathAttribute(),o=this.config.ldap_top_organization;if(!o||!a(t,o))return;const h=this.config.ldap_organization_path_separator||" / ",c=await this.server.ldap.search({paged:!0,scope:"sub",filter:`(${e}=*)`,attributes:[e]},t),d=[];for await(const t of c)for(const a of t.searchEntries){const t=String(a.dn);if(!/^ou=/i.test(t))continue;const r=a[e];d.push({dn:t,path:String(Array.isArray(r)?r[0]:r)})}d.sort((t,e)=>r(t.dn).length-r(e.dn).length);const l=n(o),g=new Map;let p=0;for(const{dn:t,path:a}of d){const r=i(t),o=n(r);let c;if(o!==l&&(c=g.get(o)??await this.readPath(r)??void 0,void 0===c)){this.logger.warn(`Organization ${r} has no ${e}: the path of ${t} is left as it is`);continue}const d=s(t),u=c?`${c}${h}${d}`:d;if(g.set(n(t),u),u!==a)try{await this.server.ldap.modify(t,{replace:{[e]:u}}),p++}catch(e){this.logger.error(`Failed to update the path of ${t} after organization rename: ${e}`)}}this.logger.info(`Updated the path of ${p} organizations below ${t}`)}async readPath(t){const e=this.organizationPathAttribute();try{const a=await this.server.ldap.search({paged:!1,scope:"base",attributes:[e]},t),r=a.searchEntries[0]?.[e];return void 0===r?null:String(Array.isArray(r)?r[0]:r)}catch{return null}}async updateLinkedResources(t,e,a,r){const n=`(${r.link}=${t})`;this.logger.debug(`Searching for resources directly linked to ${t}: ${n}`);const i=await this.server.ldap.search({paged:!0,filter:n,attributes:[r.link,r.path]},a);let s=0;for await(const t of i)for(const a of t.searchEntries){const t=String(a.dn);try{const a=await this.getDepartmentPath(e);await this.server.ldap.modify(t,{replace:{[r.link]:e,[r.path]:a}}),s++,this.logger.debug(`Updated ${t}: ${r.link}=${e}, ${r.path}=${a}`)}catch(e){this.logger.error(`Failed to update ${t} after organization rename: ${e}`)}}this.logger.info(`Updated ${s} resources directly linked to the renamed organization`)}async updateDescendantReferences(t,e,a,r){const n=`(${r.link}=*)`;this.logger.debug(`Searching for resources linked to descendants of ${t}: ${n}`);const i=await this.server.ldap.search({paged:!0,filter:n,attributes:[r.link,r.path]},a);let s=0;for await(const a of i)for(const n of a.searchEntries){const a=String(n.dn),i=n[r.link],o=Array.isArray(i)?String(i[0]):String(i);if(o!==t&&o.endsWith(`,${t}`))try{const n=o.replace(`,${t}`,`,${e}`),i=await this.getDepartmentPath(n);await this.server.ldap.modify(a,{replace:{[r.link]:n,[r.path]:i}}),s++,this.logger.debug(`Updated descendant link ${a}: ${r.link}=${n}, ${r.path}=${i}`)}catch(t){this.logger.error(`Failed to update descendant reference ${a}: ${t}`)}}this.logger.info(`Updated ${s} resources linked to descendants of the renamed organization`)}async getDepartmentPath(t){const e=this.organizationPathAttribute();try{const a=await this.server.ldap.search({paged:!1,scope:"base",attributes:[e,"ou","o"]},t);if(!a.searchEntries||0===a.searchEntries.length)throw new Error(`Organization ${t} not found`);const r=a.searchEntries[0];if(r[e]){const t=r[e];return Array.isArray(t)?String(t[0]):String(t)}const n=r.ou||r.o;if(n){return`/${Array.isArray(n)?String(n[0]):String(n)}`}return this.logger.warn(`Organization ${t} has no ${e} attribute, using DN`),t}catch(e){throw new Error(`Failed to fetch organization ${t}: ${e}`)}}}export{LdapDepartmentSync as default};
|
|
1
|
+
import t from"../../abstract/plugin.js";import{roleAttribute as e}from"../../config/schema.js";import{isDnInBranch as a,parseDn as r,normalizeDn as n,getParentDn as i,rdnValue as s,escapeLdapFilter as o}from"../../lib/utils.js";class LdapDepartmentSync extends t{constructor(t){super(t),this.name="ldapDepartmentSync",this.roles=["consistency"],this.hooks={ldaprenamedone:async([t,e])=>{if(/^ou=/.test(t)){this.logger.info(`Organization renamed from ${t} to ${e}, synchronizing linked resources...`);try{const a=this.config.ldap_top_organization.replace(/^ou=[^,]+,/,"");await this.updateOrganizationPaths(e);for(const r of this.linkedAttributes())await this.updateLinkedResources(t,e,a,r),await this.updateDescendantReferences(t,e,a,r)}catch(t){this.logger.error(`Error synchronizing department links after rename: ${t}`)}}}},this.linkAttr=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",this.pathAttr=this.config.ldap_organization_path_attribute||"twakeDepartmentPath"}organizationPathAttribute(){const t=this.server.loadedPlugins.ldapOrganizations;return e(t?.schema,"organizationPath")||this.pathAttr}linkedAttributes(){const t=[],a=this.server.loadedPlugins.ldapFlatGeneric;for(const e of a?.instances||[])t.push(e.schema);const r=this.server.loadedPlugins.ldapGroups;t.push(r?.schema);const n=[];for(const a of t){const t=e(a,"organizationLink");t&&n.push({link:t,path:e(a,"organizationPath")||this.pathAttr})}n.push({link:this.linkAttr,path:this.pathAttr});const i=new Set;return n.filter(({link:t,path:e})=>{const a=`${t.toLowerCase()} ${e.toLowerCase()}`;return!i.has(a)&&(i.add(a),!0)})}async updateOrganizationPaths(t){const e=this.organizationPathAttribute(),o=this.config.ldap_top_organization;if(!o||!a(t,o))return;const h=this.config.ldap_organization_path_separator||" / ",c=await this.server.ldap.search({paged:!0,scope:"sub",filter:`(${e}=*)`,attributes:[e]},t),d=[];for await(const t of c)for(const a of t.searchEntries){const t=String(a.dn);if(!/^ou=/i.test(t))continue;const r=a[e];d.push({dn:t,path:String(Array.isArray(r)?r[0]:r)})}d.sort((t,e)=>r(t.dn).length-r(e.dn).length);const l=n(o),g=new Map;let p=0;for(const{dn:t,path:a}of d){const r=i(t),o=n(r);let c;if(o!==l&&(c=g.get(o)??await this.readPath(r)??void 0,void 0===c)){this.logger.warn(`Organization ${r} has no ${e}: the path of ${t} is left as it is`);continue}const d=s(t),u=c?`${c}${h}${d}`:d;if(g.set(n(t),u),u!==a)try{await this.server.ldap.modify(t,{replace:{[e]:u}}),p++}catch(e){this.logger.error(`Failed to update the path of ${t} after organization rename: ${e}`)}}this.logger.info(`Updated the path of ${p} organizations below ${t}`)}async readPath(t){const e=this.organizationPathAttribute();try{const a=await this.server.ldap.search({paged:!1,scope:"base",attributes:[e]},t),r=a.searchEntries[0]?.[e];return void 0===r?null:String(Array.isArray(r)?r[0]:r)}catch{return null}}async updateLinkedResources(t,e,a,r){const n=`(${r.link}=${o(t)})`;this.logger.debug(`Searching for resources directly linked to ${t}: ${n}`);const i=await this.server.ldap.search({paged:!0,filter:n,attributes:[r.link,r.path]},a);let s=0;for await(const t of i)for(const a of t.searchEntries){const t=String(a.dn);try{const a=await this.getDepartmentPath(e);await this.server.ldap.modify(t,{replace:{[r.link]:e,[r.path]:a}}),s++,this.logger.debug(`Updated ${t}: ${r.link}=${e}, ${r.path}=${a}`)}catch(e){this.logger.error(`Failed to update ${t} after organization rename: ${e}`)}}this.logger.info(`Updated ${s} resources directly linked to the renamed organization`)}async updateDescendantReferences(t,e,a,r){const n=`(${r.link}=*)`;this.logger.debug(`Searching for resources linked to descendants of ${t}: ${n}`);const i=await this.server.ldap.search({paged:!0,filter:n,attributes:[r.link,r.path]},a);let s=0;for await(const a of i)for(const n of a.searchEntries){const a=String(n.dn),i=n[r.link],o=Array.isArray(i)?String(i[0]):String(i);if(o!==t&&o.endsWith(`,${t}`))try{const n=o.replace(`,${t}`,`,${e}`),i=await this.getDepartmentPath(n);await this.server.ldap.modify(a,{replace:{[r.link]:n,[r.path]:i}}),s++,this.logger.debug(`Updated descendant link ${a}: ${r.link}=${n}, ${r.path}=${i}`)}catch(t){this.logger.error(`Failed to update descendant reference ${a}: ${t}`)}}this.logger.info(`Updated ${s} resources linked to descendants of the renamed organization`)}async getDepartmentPath(t){const e=this.organizationPathAttribute();try{const a=await this.server.ldap.search({paged:!1,scope:"base",attributes:[e,"ou","o"]},t);if(!a.searchEntries||0===a.searchEntries.length)throw new Error(`Organization ${t} not found`);const r=a.searchEntries[0];if(r[e]){const t=r[e];return Array.isArray(t)?String(t[0]):String(t)}const n=r.ou||r.o;if(n){return`/${Array.isArray(n)?String(n[0]):String(n)}`}return this.logger.warn(`Organization ${t} has no ${e} attribute, using DN`),t}catch(e){throw new Error(`Failed to fetch organization ${t}: ${e}`)}}}export{LdapDepartmentSync as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,jsonBody as i,tryMethod as s,ok as a}from"../../lib/expressFormatedResponses.js";import{transformSchemas as o,asyncHandler as n,isDummyMemberDn as h,escapeDnValue as c,validateDnValue as d,launchHooksChained as p,launchHooks as m,substringSearchFilter as l,escapeLdapFilter as u}from"../../lib/utils.js";import{NotFoundError as f,BadRequestError as g,HttpError as b}from"../../lib/errors.js";import{assertClientMaySet as w,modifiedAttributeNames as y,missingRequiredAttribute as $,checkDnValues as v,matchesPattern as _}from"../../config/schema.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeleterequest:async([e,t])=>{let r=e;return Array.isArray(r)||(r=[r]),this.logger.debug(`User deletion detected, removing from groups: ${r.join(", ")}`),await Promise.all(r.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)}),[e,t]}},this.ldap=t.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");this.config.group_schema&&e.readFile(this.config.group_schema,(e,t)=>{if(e)this.logger.error(`Failed to load group schema from ${this.config.group_schema}: ${e}`);else try{this.schema=JSON.parse(o(t.toString(),this.config)),this.logger.debug("Group schema loaded")}catch(e){this.logger.error(`Failed to parse ${this.config.schemas_path}/group.json: ${e}`)}})}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,n(async(e,t)=>{if(!r(e,t))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new g("Invalid match query");if("string"==typeof e.query.attribute&&e.query.attribute)i.filter=l(e.query.match,e.query.attribute);else if(/=/.test(e.query.match)){if(!/^[\w*=()&|, -]+$/.test(e.query.match))throw new g("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=u(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const s=await this.listGroups(i);return a(t,this.hideNeverReturn(s))})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,n(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,n(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,n(async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)throw new g("cn is required");const a=i(e,t,"member");if(!a)return;if((Array.isArray(a.member)?a.member:[a.member]).some(e=>"string"!=typeof e))throw new g("member must be a string or string array");await s(t,this.addMember.bind(this),r,a.member)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,n(async(e,t)=>{if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!i||!a)throw new g("cn and member are required");await s(t,this.deleteMember.bind(this),i,a)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,n(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new g("cn is required");const o=i(e,t,"targetOrgDn");if(o){if("string"!=typeof o.targetOrgDn)throw new g("targetOrgDn must be a string");await s(t,this.moveGroup.bind(this),a,o.targetOrgDn,e)}})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,n(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new g("cn is required");const o=i(e,t,"newCn");if(o){if("string"!=typeof o.newCn)throw new g("newCn must be a string");await s(t,this.renameGroup.bind(this),a,o.newCn)}}))}isDummyMember(e){return h(e,this.config.group_dummy_user)}withoutDummyMember(e){const t=e.member;if(void 0===t)return e;const r=Array.isArray(t)?t:[t];return{...e,member:r.filter(e=>!this.isDummyMember(e))}}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const e=/,/.test(i)?i:`${this.cn}=${c(i)},${this.base}`,r=await this.ldap.search({paged:!1,scope:"base"},e);if(0===r.searchEntries.length)throw new f("Group not found");t.json(this.hideNeverReturn(this.withoutDummyMember(r.searchEntries[0])))}catch(e){if(e.code&&32===e.code)throw new f("Group not found");throw e}}async apiAdd(e,t,...r){const a=i(e,t,...r);if(!a)return;w(this.schema,Object.keys(a),this.cn);const o=a[this.cn],n=a.member?a.member:[],h=Object.fromEntries(Object.entries(a).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.addGroup.bind(this),o,n,h)}async apiDelete(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);if(!i)throw new g("cn is required");await s(t,this.deleteGroup.bind(this),i)}async apiModify(e,t){const r=i(e,t);if(!r)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new g("cn is required");w(this.schema,y(r));const o=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.modifyGroup.bind(this),a,o)}async addGroup(e,t=[],r={}){let i;new RegExp(`^${this.cn}=`).test(e)?(i=e,e=e.replace(new RegExp(`^${this.cn}=((?:\\\\.|[^,])+)(?:,.*)?$`),"$1")):(d(e,this.cn),i=`${this.cn}=${c(e)},${this.base}`),await this.validateMembers(i,t),await this.validateNewGroup(i,{objectClass:this.config.group_class,cn:e,member:t,...r});const s=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||s.includes("twakeGroup")||s.includes("twakeStaticGroup")||s.push("twakeGroup");let a,o={objectClass:s,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&o.member.push(...t),[i,o]=await p(this.registeredHooks.ldapgroupadd,[i,o]);try{a=await this.ldap.add(i,o)}catch(e){if(e instanceof b)throw e;throw new Error(`Failed to add group ${i}: ${e}`)}return m(this.registeredHooks.ldapgroupadddone,[i,o]),a}async modifyGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;const i=this.opNumber();if([r,t]=await p(this.registeredHooks.ldapgroupmodify,[r,t,i]),t.add){if(t.add.member)throw new Error("Use dedicated API to add members");if(t.add[this.cn])throw new Error(`${this.cn} attribute iq unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new Error("Use dedicated API to delete members");if(t.delete[this.cn])throw new Error(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new Error("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new Error(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new Error("Use dedicated API to replace members");if(t.replace[this.cn])throw new Error(`Use dedicated API to change ${this.cn} attribute`)}await this.validateChanges(r,t);const s=await this.ldap.modify(r,t);return m(this.registeredHooks.ldapgroupmodifydone,[r,t,i]),s}async renameGroup(e,t){/,/.test(e)||d(e,this.cn),/,/.test(t)||d(t,this.cn);let r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`,i=/,/.test(t)?t:`${this.cn}=${c(t)},${this.base}`;[r,i]=await p(this.registeredHooks.ldapgrouprename,[r,i]);const s=await this.ldap.rename(r,i);return m(this.registeredHooks.ldapgrouprenamedone,[r,i]),s}async deleteGroup(e){let t=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;t=await p(this.registeredHooks.ldapgroupdelete,t);const r=await this.ldap.delete(t);return m(this.registeredHooks.ldapgroupdeletedone,t),r}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await p(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{if(e instanceof b)throw e;throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;if([e,t]=await p(this.registeredHooks.ldapgroupdeletemember,[e,t]),this.isDummyMember(t))throw new g("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{if(e instanceof b)throw e;throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${e}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.ldap.modify(t.dn,{delete:{member:e}}).catch(r=>this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r))))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0][i];if(!n)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(n===t)throw new Error("Group is already in the target organization");let h;try{h=await this.ldap.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===h.searchEntries.length)throw new Error(`Target organization ${t} not found`);const d=h.searchEntries[0][s];if(!d)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:d}},r),this.logger.info(`Group ${a} moved from ${n} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t}={}){const r=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let i={};for await(const e of r)for(const t of e.searchEntries){const e=t[this.cn];if(Array.isArray(e)&&0===e.length)continue;e&&(i[e]=this.withoutDummyMember(t))}return i=await p(this.registeredHooks._ldapgrouplist,i),i}async searchGroupsByName(e,t=!1,r=[this.cn,"member"]){const i=t?`(${this.cn}=*${e}*)`:`(${this.cn}=${e})`;return await this.listGroups({filter:i,attributes:r})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`)}async validateMembers(e,t){if([e,t]=await p(this.server.hooks.ldapgroupvalidatemembers,[e,t]),!this.config.groups_allow_unexistent_members&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{try{await this.ldap.search({paged:!1,scope:"base"},e)}catch(t){throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await p(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new g(`Invalid value for field ${e}`);const r=$(this.schema,t,this.server.loadedPlugins);if(r)throw new g(`Missing required field ${r}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await p(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))await this._validateOneChange(e,r);if(t.replace)for(const[e,r]of Object.entries(t.replace))await this._validateOneChange(e,r);if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=this.schema.attributes[e];if(!r){if(this.schema.strict)throw new g(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new g(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new g(`Field ${e} must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test&&"pointer"!==r.items.type)for(const i of t)if(typeof i!==r.items.type)throw new g(`Field ${e} must be of type ${r.items.type}`)}else if("pointer"!==r.type&&typeof t!==r.type)return!1;if(await v(e,r,t,async e=>(await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},e)).searchEntries.length>0),!_(r,t))throw new g(`Field ${e} has invalid value ${String(t)}`);return!0}getConfigApiData(){const e=this.config.api_prefix||"/api";let t;if(this.server.loadedPlugins.static&&this.config.group_schema){const e=this.config.static_name||"static",r=this.config.group_schema.indexOf("/schemas/");if(-1!==r){t=`/${e}${this.config.group_schema.substring(r)}`}}return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};
|
|
1
|
+
import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,jsonBody as i,tryMethod as s,ok as a}from"../../lib/expressFormatedResponses.js";import{extractLdapCode as o}from"../../lib/ldapCodes.js";import{transformSchemas as n,asyncHandler as h,isDummyMemberDn as d,escapeDnValue as c,validateDnValue as p,launchHooksChained as m,launchHooks as l,escapeLdapFilter as u,substringSearchFilter as f}from"../../lib/utils.js";import{NotFoundError as g,BadRequestError as b,HttpError as w}from"../../lib/errors.js";import{assertClientMaySet as y,modifiedAttributeNames as $,missingRequiredAttribute as v,checkDnValues as _,matchesPattern as E}from"../../config/schema.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeletedone:async e=>{const t=Array.isArray(e)?e:[e];this.logger.debug(`User deletion detected, removing from groups: ${t.join(", ")}`),await Promise.all(t.map(e=>this.deleteMemberFromAll(e))).catch(e=>{this.logger.error("Failed to process user deletion in groups:",e)})}},this.ldap=t.ldap,this.base=this.config.ldap_group_base,this.cn=this.config.ldap_groups_main_attribute,this.base||(this.base=this.config.ldap_base,this.logger.warn(`LDAP group base is not defined, using "${this.base}"`)),!this.base)throw new Error("LDAP base is not defined, please set --ldap-group-base");this.config.group_schema&&e.readFile(this.config.group_schema,(e,t)=>{if(e)this.logger.error(`Failed to load group schema from ${this.config.group_schema}: ${e}`);else try{this.schema=JSON.parse(n(t.toString(),this.config)),this.logger.debug("Group schema loaded")}catch(e){this.logger.error(`Failed to parse ${this.config.schemas_path}/group.json: ${e}`)}})}api(e){e.get(`${this.config.api_prefix}/v1/ldap/groups`,h(async(e,t)=>{if(!r(e,t))return;const i={};if(e.query.match){if("string"!=typeof e.query.match)throw new b("Invalid match query");if("string"==typeof e.query.attribute&&e.query.attribute)i.filter=f(e.query.match,e.query.attribute);else if(/=/.test(e.query.match)){if(!/^[\w*=()&|, -]+$/.test(e.query.match))throw new b("Invalid match query: contains forbidden characters");i.filter=e.query.match}else{const t=u(e.query.match);i.filter=`(${this.cn}=${t})`}}e.query.attributes&&"string"==typeof e.query.attributes&&(i.attributes=e.query.attributes.split(","));const s=await this.listGroups(i);return a(t,this.hideNeverReturn(s))})),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiGet(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups`,h(async(e,t)=>this.apiAdd(e,t,this.cn))),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiDelete(e,t))),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,h(async(e,t)=>this.apiModify(e,t))),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,h(async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)throw new b("cn is required");const a=i(e,t,"member");if(!a)return;if((Array.isArray(a.member)?a.member:[a.member]).some(e=>"string"!=typeof e))throw new b("member must be a string or string array");await s(t,this.addMember.bind(this),r,a.member)})),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,h(async(e,t)=>{if(!r(e,t))return;const i=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!i||!a)throw new b("cn and member are required");await s(t,this.deleteMember.bind(this),i,a)})),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,h(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new b("cn is required");const o=i(e,t,"targetOrgDn");if(o){if("string"!=typeof o.targetOrgDn)throw new b("targetOrgDn must be a string");await s(t,this.moveGroup.bind(this),a,o.targetOrgDn,e)}})),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,h(async(e,t)=>{if(!r(e,t))return;const a=decodeURIComponent(e.params.cn);if(!a)throw new b("cn is required");const o=i(e,t,"newCn");if(o){if("string"!=typeof o.newCn)throw new b("newCn must be a string");await s(t,this.renameGroup.bind(this),a,o.newCn,e)}}))}isDummyMember(e){return d(e,this.config.group_dummy_user)}withoutDummyMember(e){const t=e.member;if(void 0===t)return e;const r=Array.isArray(t)?t:[t];return{...e,member:r.filter(e=>!this.isDummyMember(e))}}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const e=/,/.test(i)?i:`${this.cn}=${c(i)},${this.base}`,r=await this.ldap.search({paged:!1,scope:"base"},e);if(0===r.searchEntries.length)throw new g("Group not found");t.json(this.hideNeverReturn(this.withoutDummyMember(r.searchEntries[0])))}catch(e){if(e.code&&32===e.code)throw new g("Group not found");throw e}}async apiAdd(e,t,...r){const a=i(e,t,...r);if(!a)return;y(this.schema,Object.keys(a),this.cn);const o=a[this.cn],n=a.member?a.member:[],h=Object.fromEntries(Object.entries(a).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.addGroup.bind(this),o,n,h,e)}async apiDelete(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);if(!i)throw new b("cn is required");await s(t,this.deleteGroup.bind(this),i,e)}async apiModify(e,t){const r=i(e,t);if(!r)return;const a=this.fixDn(decodeURIComponent(e.params.cn));if(!a)throw new b("cn is required");y(this.schema,$(r));const o=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await s(t,this.modifyGroup.bind(this),a,o,e)}async addGroup(e,t=[],r={},i){let s;new RegExp(`^${this.cn}=`).test(e)?(s=e,e=e.replace(new RegExp(`^${this.cn}=((?:\\\\.|[^,])+)(?:,.*)?$`),"$1")):(p(e,this.cn),s=`${this.cn}=${c(e)},${this.base}`),await this.validateMembers(s,t),await this.validateNewGroup(s,{objectClass:this.config.group_class,cn:e,member:t,...r});const a=[...this.config.group_class];!(r.mail||r.twakeMailboxType||r.twakeDepartmentLink||r.twakeDepartmentPath)||a.includes("twakeGroup")||a.includes("twakeStaticGroup")||a.push("twakeGroup");let o,n={objectClass:a,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&n.member.push(...t),[s,n]=await m(this.registeredHooks.ldapgroupadd,[s,n]);try{o=await this.ldap.add(s,n,i)}catch(e){if(e instanceof w)throw e;throw new Error(`Failed to add group ${s}: ${e}`)}return l(this.registeredHooks.ldapgroupadddone,[s,n]),o}async modifyGroup(e,t,r){let i=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;const s=this.opNumber();if([i,t]=await m(this.registeredHooks.ldapgroupmodify,[i,t,s]),t.add){if(t.add.member)throw new Error("Use dedicated API to add members");if(t.add[this.cn])throw new Error(`${this.cn} attribute iq unique, cannot add`)}if(t.delete){if(t.delete instanceof Object){if(t.delete.member)throw new Error("Use dedicated API to delete members");if(t.delete[this.cn])throw new Error(`Cannot delete ${this.cn} attribute`)}if(Array.isArray(t.delete)){if(t.delete.includes("member"))throw new Error("Use dedicated API to delete members");if(t.delete.includes(this.cn))throw new Error(`Cannot delete ${this.cn} attribute`)}}if(t.replace){if(t.replace.member)throw new Error("Use dedicated API to replace members");if(t.replace[this.cn])throw new Error(`Use dedicated API to change ${this.cn} attribute`)}await this.validateChanges(i,t);const a=await this.ldap.modify(i,t,r);return l(this.registeredHooks.ldapgroupmodifydone,[i,t,s]),a}async renameGroup(e,t,r){/,/.test(e)||p(e,this.cn),/,/.test(t)||p(t,this.cn);let i=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`,s=/,/.test(t)?t:`${this.cn}=${c(t)},${this.base}`;[i,s]=await m(this.registeredHooks.ldapgrouprename,[i,s]);const a=await this.ldap.rename(i,s,r);return l(this.registeredHooks.ldapgrouprenamedone,[i,s]),a}async deleteGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;r=await m(this.registeredHooks.ldapgroupdelete,r);const i=await this.ldap.delete(r,t);return l(this.registeredHooks.ldapgroupdeletedone,r),i}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await m(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{if(e instanceof w)throw e;throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`;if([e,t]=await m(this.registeredHooks.ldapgroupdeletemember,[e,t]),this.isDummyMember(t))throw new b("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(e=>{if(e instanceof w)throw e;throw new Error(`Failed to delete member ${t} from ${r}: ${e}`)})}async deleteMemberFromAll(e){const t=await this.ldap.search({filter:`member=${u(e)}`,paged:!1,attributes:[this.cn]},this.base).catch(e=>{throw new Error(`Failed to search groups from ${this.base}: ${e}`)});await Promise.all(t.searchEntries.map(t=>this.ldap.modify(t.dn,{delete:{member:e}}).catch(r=>{if(65===o(r)&&this.config.group_dummy_user)return this.ldap.modify(t.dn,{add:{member:this.config.group_dummy_user},delete:{member:e}});throw r}).catch(r=>{const i=o(r);16!==i&&32!==i&&this.logger.error(`Failed to remove ${e} from group ${t.dn}:`,r)})))}async moveGroup(e,t,r){const i=this.config.ldap_organization_link_attribute,s=this.config.ldap_organization_path_attribute,a=/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`,o=await this.ldap.search({paged:!1,scope:"base"},a);if(0===o.searchEntries.length)throw new Error(`Group ${a} not found`);const n=o.searchEntries[0][i];if(!n)throw new Error(`Group ${a} does not have ${i} attribute and cannot be moved`);if(n===t)throw new Error("Group is already in the target organization");let h;try{h=await this.ldap.search({paged:!1,scope:"base",attributes:[s,"ou","o"]},t)}catch(e){throw new Error(`Target organization ${t} not found: ${e instanceof Error&&e.message?e.message:String(e)}`)}if(0===h.searchEntries.length)throw new Error(`Target organization ${t} not found`);const d=h.searchEntries[0][s];if(!d)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:d}},r),this.logger.info(`Group ${a} moved from ${n} to ${t}`),{success:!0}}async listGroups({filter:e,attributes:t}={}){const r=await this.ldap.search({filter:e||"(objectClass=*)",attributes:t||[this.cn,"member"],paged:!0},this.base).catch(e=>{throw new Error(`Failed to list groups from ${this.base}: ${e}`)});let i={};for await(const e of r)for(const t of e.searchEntries){const e=t[this.cn];if(Array.isArray(e)&&0===e.length)continue;e&&(i[e]=this.withoutDummyMember(t))}return i=await m(this.registeredHooks._ldapgrouplist,i),i}async searchGroupsByName(e,t=!1,r=[this.cn,"member"]){const i=u(e),s=t?`(${this.cn}=*${i}*)`:`(${this.cn}=${i})`;return await this.listGroups({filter:s,attributes:r})}fixDn(e){return!!e&&(/,/.test(e)?e:`${this.cn}=${c(e)},${this.base}`)}async validateMembers(e,t){if([e,t]=await m(this.server.hooks.ldapgroupvalidatemembers,[e,t]),!this.config.groups_allow_unexistent_members&&t&&t.length)try{await Promise.all(t.map(e=>this.server.ldap.queryLimit(async()=>{try{await this.ldap.search({paged:!1,scope:"base"},e)}catch(t){throw new Error(`Member ${e} not found: ${t}`)}})))}catch(e){throw new Error(`Failed to find member(s): ${e}`)}}async validateNewGroup(e,t){if(!this.schema)return!0;[e,t]=await m(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new b(`Invalid value for field ${e}`);const r=v(this.schema,t,this.server.loadedPlugins);if(r)throw new b(`Missing required field ${r}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await m(this.server.hooks.ldapgroupvalidatechanges,[e,t]),t.add)for(const[e,r]of Object.entries(t.add))await this._validateOneChange(e,r);if(t.replace)for(const[e,r]of Object.entries(t.replace))await this._validateOneChange(e,r);if(t.delete&&t.delete instanceof Object)for(const e of Array.isArray(t.delete)?t.delete:Object.keys(t.delete))await this._validateOneChange(e,null);return!0}async _validateOneChange(e,t){if(!this.schema)return!0;const r=this.schema.attributes[e];if(!r){if(this.schema.strict)throw new b(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new b(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new b(`Field ${e} must be an array`);if(!r.items)throw new Error(`Schema error: no item for array ${e}`);if("array"===r.items.type)throw new Error(`Schema error: array of array not supported for ${e}`);if(r.items.test&&"pointer"!==r.items.type)for(const i of t)if(typeof i!==r.items.type)throw new b(`Field ${e} must be of type ${r.items.type}`)}else if("pointer"!==r.type&&typeof t!==r.type)return!1;if(await _(e,r,t,async e=>(await this.ldap.search({paged:!1,scope:"base",attributes:["dn"]},e)).searchEntries.length>0),!E(r,t))throw new b(`Field ${e} has invalid value ${String(t)}`);return!0}getConfigApiData(){const e=this.config.api_prefix||"/api";let t;if(this.server.loadedPlugins.static&&this.config.group_schema){const e=this.config.static_name||"static",r=this.config.group_schema.indexOf("/schemas/");if(-1!==r){t=`/${e}${this.config.group_schema.substring(r)}`}}return{enabled:!0,base:this.base||"",mainAttribute:this.cn||"cn",objectClass:this.config.group_class||["top","groupOfNames"],schema:this.schema,schemaUrl:t,endpoints:{list:`${e}/v1/ldap/groups`,get:`${e}/v1/ldap/groups/:id`,create:`${e}/v1/ldap/groups`,update:`${e}/v1/ldap/groups/:id`,delete:`${e}/v1/ldap/groups/:id`,addMember:`${e}/v1/ldap/groups/:id/members`,removeMember:`${e}/v1/ldap/groups/:id/members/:memberId`,rename:`${e}/v1/ldap/groups/:id/rename`,move:`${e}/v1/ldap/groups/:id/move`}}}}export{LdapGroups as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../../abstract/plugin.js";import{launchHooks as e}from"../../lib/utils.js";const n={mail_attribute:"onLdapMailChange",quota_attribute:"onLdapQuotaChange",alias_attribute:"onLdapAliasChange",forward_attribute:"onLdapForwardChange",display_name_attribute:"onLdapDisplayNameChange",drive_quota_attribute:"onLdapDriveQuotaChange"};class OnLdapChange extends t{constructor(){super(...arguments),this.name="onLdapChange",this.roles=["consistency"],this.stack={},this.pendingDeletions=new Map,this.hooks={ldapadddone:t
|
|
1
|
+
import t from"../../abstract/plugin.js";import{launchHooks as e}from"../../lib/utils.js";const normalize=t=>(void 0===t?[]:Array.isArray(t)?t:[t]).map(t=>Buffer.isBuffer(t)?t.toString("base64"):String(t)).sort();function diffEntries(t,e){const index=t=>new Map(Object.keys(t||{}).filter(t=>"dn"!==t).map(t=>[t.toLowerCase(),t])),n=index(t),a=index(e),i={};for(const r of new Set([...n.keys(),...a.keys()])){const s=n.get(r),o=a.get(r),h=s?t[s]:void 0,d=o?e[o]:void 0,l=normalize(h),u=normalize(d);l.length===u.length&&l.every((t,e)=>t===u[e])||(i[o||s]=[l.length?h:null,u.length?d:null])}return i}const n={mail_attribute:"onLdapMailChange",quota_attribute:"onLdapQuotaChange",alias_attribute:"onLdapAliasChange",forward_attribute:"onLdapForwardChange",display_name_attribute:"onLdapDisplayNameChange",drive_quota_attribute:"onLdapDriveQuotaChange"};class OnLdapChange extends t{constructor(){super(...arguments),this.name="onLdapChange",this.roles=["consistency"],this.stack={},this.pendingDeletions=new Map,this.pendingRenames=new Map,this.hooks={ldapadddone:async([t,e],n)=>{const a=await this.read(t)||{dn:t,...e};this.publish(t,null,a,n)},ldapmodifyrequest:async([t,e,n,a])=>{const i=await this.read(t);return i?this.stack[n]=i:this.logger.warn(`Could not read ${t} before modification`),[t,e,n,a]},ldapmodifydone:async([t,e,n],a)=>{const i=this.stack[n];if(delete this.stack[n],!i)return void this.logger.warn(`Received a ldapmodifydone for an unknown operation (${n})`);if(0===Object.keys(e).length)return;const r=await this.read(t);r?this.publish(t,i,r,a):this.logger.warn(`Could not read ${t} after modification`)},ldaprenamerequest:async([t,e,n])=>{const a=await this.read(t);return a&&this.pendingRenames.set(t,a),[t,e,n]},ldaprenamedone:async([t,e],n)=>{const a=this.pendingRenames.get(t);if(this.pendingRenames.delete(t),!a)return void this.logger.debug(`${t}: no snapshot: a move made outside a request`);const i=await this.read(e);i?this.publish(e,a,i,n):this.logger.warn(`Could not read ${e} after the rename of ${t}`)},ldapdeleterequest:async([t,e])=>{for(const e of Array.isArray(t)?t:[t]){const t=await this.read(e);t&&this.pendingDeletions.set(e,t)}return[t,e]},ldapdeletedone:(t,e)=>{for(const n of Array.isArray(t)?t:[t]){const t=this.pendingDeletions.get(n);t&&(this.pendingDeletions.delete(n),this.publish(n,t,null,e))}}}}async read(t){const e=new Set(Object.values(this.server.loadedPlugins).flatMap(t=>t.followedOperationalAttributes||[]));try{const n=(await this.server.ldap.search({paged:!1,scope:"base",...e.size?{attributes:["*",...e]}:{}},t)).searchEntries[0];if(!n)return;return Object.fromEntries(Object.entries(n).filter(([,t])=>!Array.isArray(t)||t.length>0))}catch(t){return}}publish(t,n,a,i={}){const r=diffEntries(n,a);0===Object.keys(r).length&&n?.dn===a?.dn||(e(this.server.hooks.onLdapEntryChange,t,n,a,i),this.notify(t,r,n,a))}notify(t,a,i,r){e(this.server.hooks.onLdapChange,t,a);for(const[i,s]of Object.entries(n))if(this.config[i]&&a[this.config[i]])if("onLdapQuotaChange"===s||"onLdapForwardChange"===s||"onLdapAliasChange"===s)this.notifyAttributeChangeWithMail(this.config[i],s,t,a,r);else if("onLdapMailChange"===s)this.notifyAttributeChange(this.config[i],s,t,a);else if("onLdapDriveQuotaChange"===s){const[n,r]=a[this.config[i]]||[],s=n?Number(n):null,o=r?Number(r):null;s!==o&&e(this.server.hooks.onLdapDriveQuotaChange,t,s,o)}const s=this.reconstructDisplayName(i),o=this.reconstructDisplayName(r);s!==o&&e(this.server.hooks.onLdapDisplayNameChange,t,s,o)}notifyAttributeChange(t,n,a,i,r=!1){const[s,o]=i[t]||[];void 0===s&&void 0===o||(r&&(Array.isArray(s)||Array.isArray(o))?this.logger.error(`Attribute ${t} change detected but one of the values is an array, cannot handle that`):s!==o&&e(this.server.hooks[n],a,s,o))}notifyAttributeChangeWithMail(t,n,a,i,r){const[s,o]=i[t]||[];if(void 0===s&&void 0===o)return;const h=this.config.mail_attribute||"mail",d=i[h];let l;if(d)l=Array.isArray(d[1])?String(d[1][0]):String(d[1]);else{if(!r?.[h])return void this.logger.warn(`Could not find mail for ${a}, skipping ${n} notification`);{const t=r[h];l=Array.isArray(t)?String(t[0]):String(t)}}if("onLdapQuotaChange"===n){const t=s?Number(s):0,i=o?Number(o):0;t!==i&&e(this.server.hooks[n],a,l,t,i)}else if("onLdapForwardChange"===n||"onLdapAliasChange"===n){const t=s?Array.isArray(s)?s:[s]:[],i=o?Array.isArray(o)?o:[o]:[];(t.length>0||i.length>0)&&e(this.server.hooks[n],a,l,t,i)}}reconstructDisplayName(t){const getValue=e=>{const n=t?.[e];return n?Array.isArray(n)?n.length>0?String(n[0]):null:String(n):null},e=getValue("cn");if(e)return e;const n=getValue("givenName"),a=getValue("sn");if(n||a){const t=[];return n&&t.push(n),a&&t.push(a),t.join(" ")}return null}}export{OnLdapChange as default,diffEntries};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"fs";import i from"../../abstract/plugin.js";import{tryMethodData as a,jsonBody as e,tryMethod as n,wantJson as s}from"../../lib/expressFormatedResponses.js";import{transformSchemas as r,asyncHandler as o,validateDnValue as h,escapeDnValue as c,getRdn as d,isDnInBranch as g,rdnValue as l,normalizeDn as p,getParentDn as f,escapeLdapFilter as u,launchHooksChained as w,isChildOf as m}from"../../lib/utils.js";import{BadRequestError as z,NotFoundError as $,ConflictError as b}from"../../lib/errors.js";import{assertClientMaySet as v,modifiedAttributeNames as y,missingRequiredAttribute as _,checkDnValues as O,matchesPattern as C}from"../../config/schema.js";class LdapOrganizations extends i{constructor(i){if(super(i),this.name="ldapOrganizations",this.roles=["api","consistency","configurable"],this.hooks={ldapaddrequest:async([t,i,a])=>(this.isOu(i)||await this.checkDeptLink(i),this.isOu(i)&&await this.checkDeptPath(i,t),void 0!==a?[t,i,a]:[t,i]),ldapmodifyrequest:async([t,i,a,e])=>{let n,s={},r={};const checkIsOu=async()=>{if(void 0!==n)return n;if(i.replace?.objectClass)n=this.isOu({objectClass:i.replace.objectClass});else if(i.add?.objectClass)n=this.isOu({objectClass:i.add.objectClass});else{const i=await this.server.ldap.search({paged:!1,scope:"base"},t);n=i.searchEntries.length>0&&this.isOu(i.searchEntries[0])}return n};if(i.delete){const t=(Array.isArray(i.delete)?i.delete.map(String):Object.keys(i.delete)).map(t=>t.split(";")[0].toLowerCase()),a=t.includes(this.linkAttr.toLowerCase()),e=t.includes(this.pathAttr.toLowerCase());if(a||e){if(!await checkIsOu()&&a)throw new z("An organization link cannot be deleted");if(e)throw new z("An organization path cannot be deleted")}}if(i.replace&&(i.replace[this.linkAttr]&&(s={...i.replace}),i.replace[this.pathAttr]&&(r={...i.replace})),i.add&&(i.add[this.linkAttr]&&(s={...s,...i.add}),i.add[this.pathAttr]&&(r={...r,...i.add})),Object.keys(s).length>0){await checkIsOu()||await this.checkDeptLink(s)}if(Object.keys(r).length>0){await checkIsOu()&&await this.checkDeptPath(r,t)}return[t,i,a,e]},ldapdeleterequest:async([t,i])=>{const a=Array.isArray(t)?t:[t];for(const t of a)/^ou=/.test(t)&&await this.isEmptyOrganization(t);return[t,i]},ldaprenamerequest:([t,i,a])=>[t,i,a]},!this.config.ldap_top_organization)throw new Error("Missing --ldap-top-organization");this.pathAttr=this.config.ldap_organization_path_attribute,this.linkAttr=this.config.ldap_organization_link_attribute,this.config.organization_schema&&t.readFile(this.config.organization_schema,(t,i)=>{if(t)this.logger.error(`Failed to load organization schema from ${this.config.organization_schema}: ${t}`);else try{this.schema=JSON.parse(r(i.toString(),this.config)),this.logger.debug("Organization schema loaded")}catch(t){this.logger.error(`Failed to parse organization schema: ${t}`)}})}api(t){t.get(`${this.config.api_prefix}/v1/ldap/organizations/top`,async(t,i)=>{await a(i,async()=>this.hideNeverReturn(await this.getOrganisationTop(t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,async()=>this.hideNeverReturn(await this.getOrganisationByDn(e,t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,async()=>this.hideNeverReturn(await this.getOrganisationSubnodes(e,t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes/search`,o(async(t,i)=>{const e=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)throw new z('query parameter "q" is required');await a(i,async()=>this.hideNeverReturn(await this.searchOrganisationSubnodes(e,n)))})),t.post(`${this.config.api_prefix}/v1/ldap/organizations`,o(async(t,i)=>this.apiAdd(t,i))),t.put(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,o(async(t,i)=>this.apiModify(t,i))),t.delete(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,o(async(t,i)=>this.apiDelete(t,i))),t.post(`${this.config.api_prefix}/v1/ldap/organizations/:dn/move`,o(async(t,i)=>this.apiMove(t,i)))}async apiAdd(t,i){const a=e(t,i,"ou");if(!a)return;v(this.schema,Object.keys(a),"ou"),h(a.ou,"ou");const s=a.parentDn||this.config.ldap_top_organization,r=`ou=${c(a.ou)},${s}`,o={objectClass:this.config.ldap_organization_class,ou:a.ou,...Object.fromEntries(Object.entries(a).filter(([t])=>"ou"!==t&&"parentDn"!==t))};await n(i,this.addOrganization.bind(this),r,o,t)}async apiModify(t,i){const a=e(t,i);if(!a)return;const s=decodeURIComponent(t.params.dn);if(!s)throw new z("dn is required");v(this.schema,y(a)),await n(i,this.modifyOrganization.bind(this),s,a)}async apiDelete(t,i){if(!s(t,i))return;const a=decodeURIComponent(t.params.dn);if(!a)throw new z("dn is required");await n(i,this.deleteOrganization.bind(this),a)}async apiMove(t,i){if(!s(t,i))return;const n=e(t,i,"targetOrgDn");if(!n)return;const r=decodeURIComponent(t.params.dn);if(!r)throw new z("dn is required");const{targetOrgDn:o}=n;if(!o||"string"!=typeof o)throw new z("Missing or invalid targetOrgDn in request body");await a(i,this.moveOrganization.bind(this),r,o,t)}async checkDeptLink(t){if(t[this.linkAttr]){const i=t[this.linkAttr],a=Array.isArray(i)?i[0]:i,e=await this.server.ldap.search({paged:!1,scope:"base"},a);if(0===e.searchEntries.length)throw new $(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new z(`Entry ${a} isn't in top organization branch`)}}async checkDeptPath(t,i){if(t[this.pathAttr]){const a=t[this.pathAttr],e=Array.isArray(a)?a[0]:a,n=this.config.ldap_organization_path_separator||" / ",s=this.config.ldap_top_organization||"",r=i&&s&&/^ou=/i.test(d(i))&&g(i,s)?l(i):void 0,o=t.ou,h=(Array.isArray(o)?o[0]:o)||r;let c=e;if((this.isOu(t)||void 0!==r)&&h){const t=s?l(s):"";if(t&&e.startsWith(h+n)&&e.endsWith(n+t))return;if(e===h){const t=s?p(s):"";if(!i||!t||!g(i,s)||p(i)===t||p(f(i))===t)return;throw new z(`Organization path "${e}" names no parent, but ${i} is not directly under ${s}`)}if(!e.endsWith(n+h))throw new z(`Organization path must end with its own name, preceded by separator "${n}"`);c=e.slice(0,e.length-n.length-h.length)}const w=await this.server.ldap.search({paged:!1,scope:"sub",filter:`(${this.pathAttr}=${u(c)})`,attributes:["dn"]},this.config.ldap_top_organization);if(!w.searchEntries||0===w.searchEntries.length)throw new z(`Invalid organization path ${e}: no organization holds "${c}"`)}}async isEmptyOrganization(t){if((await this.server.ldap.search({paged:!1,filter:`(${this.config.ldap_organization_link_attribute}=${t})`})).searchEntries.length>0)throw new b(`Organization ${t} is not empty`)}isOu(t){if(!t.objectClass)return!1;const i=t.objectClass.map(t=>t.toLowerCase());return this.config.ldap_organization_class.filter(t=>"top"!==t.toLowerCase()).some(t=>i.includes(t.toLowerCase()))}async getOrganisationTop(t){if(!this.config.ldap_top_organization)throw new z("No top organization configured");const i=await this.server.ldap.search({paged:!1,scope:"base"},this.config.ldap_top_organization,t);if(1!==i.searchEntries.length)throw new $("Top organization not found");const[,a]=await w(this.registeredHooks.getOrganisationTop,[t,i.searchEntries[0]]);return a}async getOrganisationByDn(t,i){const a=await this.server.ldap.search({paged:!1,scope:"base",filter:"(objectClass=organizationalUnit)"},t,i);if(1!==a.searchEntries.length)throw new $(`Organization ${t} not found`);return a.searchEntries[0]}async getOrganisationSubnodes(t,i){const a=[],e=this.config.ldap_organization_max_subnodes||50,n=i?.query?.objectClass;if(!n||"organizationalUnit"===n)try{const e=await this.server.ldap.search({paged:!1,scope:"one",filter:"(objectClass=organizationalUnit)"},t,i);this.server.logger.debug(`Found ${e.searchEntries.length} sub-OUs for ${t}`),a.push(...e.searchEntries)}catch(i){this.server.logger.debug(`No sub-OUs found for ${t}: ${i}`)}const s=this.config.ldap_top_organization,r=f(s);let o=`(${this.config.ldap_organization_link_attribute}=${u(t)})`;n&&(o=`(&${o}(objectClass=${u(n)}))`),this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${r}`);const h=await this.server.ldap.search({paged:!0,filter:o},r,i);let c=0,d=0;for await(const t of h){d+=t.searchEntries.length;const i=e-c;if(i>0){const e=t.searchEntries.slice(0,i);a.push(...e),c+=e.length}}return this.server.logger.debug(`Found ${d} linked entities for ${t}, returning ${c}`),d>e&&a.push({dn:`more-${t}`,cn:[`... ${d-e} more elements`],objectClass:["moreIndicator"],_isMoreIndicator:"true",_totalCount:d.toString(),_displayedCount:e.toString()}),a}async addOrganization(t,i,a){return await this.validateNewOrganization(t,i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i){return await this.validateChanges(t,i),await this.server.ldap.modify(t,i)}async validateNewOrganization(t,i){if(!this.schema)return!0;for(const[t,a]of Object.entries(i))if(!await this._validateOneChange(t,a))throw new z(this.invalidValueMessage(t));const a=_(this.schema,i,this.server.loadedPlugins);if(a)throw new z(`Missing required field ${a}`);return!0}invalidValueMessage(t){const i=this.schema?.attributes[t],a=i?.hint||i?.items?.hint;return a?`Invalid value for field ${t}: ${a}`:`Invalid value for field ${t}`}async validateChanges(t,i){if(!this.schema)return!0;if(i.add)for(const[t,a]of Object.entries(i.add))if(!await this._validateOneChange(t,a))throw new z(this.invalidValueMessage(t));if(i.replace)for(const[t,a]of Object.entries(i.replace))if(!await this._validateOneChange(t,a))throw new z(this.invalidValueMessage(t));return!0}async _validateOneChange(t,i){if(!this.schema)return!0;const a=this.schema.attributes[t];if(!a){if(this.schema.strict)throw new z(`Field ${t} is not allowed`);return!0}if(null==i){if(a.required)throw new z(`Field ${t} is required`);return!0}if("string"===a.type&&Array.isArray(i))throw new z(`Field ${t} must be a single value`);return await O(t,a,i,t=>this.entryExists(t)),C(a,i)}async entryExists(t){try{return(await this.server.ldap.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}}async moveOrganization(t,i,a){let e;try{e=await this.server.ldap.search({paged:!1,scope:"base"},i)}catch(t){throw new $(`Target organization ${i} not found: ${t instanceof Error?t.message:String(t)}`)}if(!e.searchEntries||0===e.searchEntries.length)throw new $(`Target organization ${i} not found`);if(!this.isOu(e.searchEntries[0]))throw new z(`Target ${i} is not an organizational unit`);const n=`${d(t)},${i}`;let s;try{s=await this.server.ldap.search({paged:!1,scope:"base"},t)}catch(t){throw new $(`Source organization not found: ${t}`)}if(!s.searchEntries||0===s.searchEntries.length)throw new $(`Source organization ${t} not found`);if(n===t)throw new z("Cannot move organization to its current location");if(i.toLowerCase()===t.toLowerCase()||m(i,t))throw new z("Cannot move organization into itself or its descendant");try{await this.server.ldap.rename(t,n,a),this.logger.info(`Moved organization from ${t} to ${n}`)}catch(t){throw new Error(`Failed to move organization: ${t}`)}return{newDn:n}}async deleteOrganization(t){return await this.server.ldap.delete(t)}async searchOrganisationSubnodes(t,i){const a=[],e=u(i);try{const n=await this.server.ldap.search({paged:!1,scope:"one",filter:`(&(objectClass=organizationalUnit)(|(ou=*${e}*)(description=*${e}*)))`},t);this.server.logger.debug(`Found ${n.searchEntries.length} sub-OUs matching "${i}" for ${t}`),a.push(...n.searchEntries)}catch(a){this.server.logger.debug(`No sub-OUs found matching "${i}" for ${t}: ${a}`)}const n=this.config.ldap_top_organization,s=f(n),r=u(t),o=`(&(${this.config.ldap_organization_link_attribute}=${r})(|(uid=*${e}*)(cn=*${e}*)(mail=*${e}*)(sn=*${e}*)(givenName=*${e}*)))`;this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${s}`);const h=await this.server.ldap.search({paged:!0,filter:o},s);let c=0;for await(const t of h)c+=t.searchEntries.length,a.push(...t.searchEntries);return this.server.logger.debug(`Found ${c} linked entities matching "${i}" for ${t}`),a}getConfigApiData(){const t=this.config.api_prefix||"/api";let i;if(this.server.loadedPlugins.static&&this.config.organization_schema){const t=this.config.static_name||"static",a=this.config.organization_schema.indexOf("/schemas/");if(-1!==a){i=`/${t}${this.config.organization_schema.substring(a)}`}}return{enabled:!0,topOrganization:this.config.ldap_top_organization||"",organizationClass:this.config.ldap_organization_class||["top","organizationalUnit"],linkAttribute:this.linkAttr||"",pathAttribute:this.pathAttr||"",pathSeparator:this.config.ldap_organization_path_separator||" / ",maxSubnodes:this.config.ldap_organization_max_subnodes||50,schema:this.schema,schemaUrl:i,endpoints:{getTop:`${t}/v1/ldap/organizations/top`,get:`${t}/v1/ldap/organizations/:dn`,getSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes`,searchSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes/search`,create:`${t}/v1/ldap/organizations`,update:`${t}/v1/ldap/organizations/:dn`,move:`${t}/v1/ldap/organizations/:dn/move`,delete:`${t}/v1/ldap/organizations/:dn`}}}}export{LdapOrganizations as default};
|
|
1
|
+
import t from"fs";import i from"../../abstract/plugin.js";import{tryMethodData as a,jsonBody as e,tryMethod as n,wantJson as s}from"../../lib/expressFormatedResponses.js";import{transformSchemas as r,asyncHandler as o,validateDnValue as h,escapeDnValue as c,getRdn as d,isDnInBranch as l,rdnValue as g,normalizeDn as p,getParentDn as f,escapeLdapFilter as u,launchHooksChained as w,isChildOf as m}from"../../lib/utils.js";import{BadRequestError as z,NotFoundError as y,ConflictError as b}from"../../lib/errors.js";import{extractLdapCode as _}from"../../lib/ldapCodes.js";import{assertClientMaySet as $,modifiedAttributeNames as v,missingRequiredAttribute as O,checkDnValues as C,matchesPattern as E}from"../../config/schema.js";class LdapOrganizations extends i{constructor(i){if(super(i),this.name="ldapOrganizations",this.roles=["api","consistency","configurable"],this.hooks={ldapaddrequest:async([t,i,a])=>(this.isOu(i)||await this.checkDeptLink(i),this.isOu(i)&&await this.checkDeptPath(i,t),void 0!==a?[t,i,a]:[t,i]),ldapmodifyrequest:async([t,i,a,e])=>{let n,s={},r={};const checkIsOu=async()=>{if(void 0!==n)return n;if(i.replace?.objectClass)n=this.isOu({objectClass:i.replace.objectClass});else if(i.add?.objectClass)n=this.isOu({objectClass:i.add.objectClass});else{const i=await this.server.ldap.search({paged:!1,scope:"base"},t);n=i.searchEntries.length>0&&this.isOu(i.searchEntries[0])}return n};if(i.delete){const t=(Array.isArray(i.delete)?i.delete.map(String):Object.keys(i.delete)).map(t=>t.split(";")[0].toLowerCase()),a=t.includes(this.linkAttr.toLowerCase()),e=t.includes(this.pathAttr.toLowerCase());if(a||e){if(!await checkIsOu()&&a)throw new z("An organization link cannot be deleted");if(e)throw new z("An organization path cannot be deleted")}}if(i.replace&&(i.replace[this.linkAttr]&&(s={...i.replace}),i.replace[this.pathAttr]&&(r={...i.replace})),i.add&&(i.add[this.linkAttr]&&(s={...s,...i.add}),i.add[this.pathAttr]&&(r={...r,...i.add})),Object.keys(s).length>0){await checkIsOu()||await this.checkDeptLink(s)}if(Object.keys(r).length>0){await checkIsOu()&&await this.checkDeptPath(r,t)}return[t,i,a,e]},ldapdeleterequest:async([t,i])=>{const a=Array.isArray(t)?t:[t];for(const t of a)/^ou=/.test(t)&&await this.isEmptyOrganization(t);return[t,i]},ldaprenamerequest:([t,i,a])=>[t,i,a]},this.sizeLimitWarned=new Set,!this.config.ldap_top_organization)throw new Error("Missing --ldap-top-organization");this.pathAttr=this.config.ldap_organization_path_attribute,this.linkAttr=this.config.ldap_organization_link_attribute,this.config.organization_schema&&t.readFile(this.config.organization_schema,(t,i)=>{if(t)this.logger.error(`Failed to load organization schema from ${this.config.organization_schema}: ${t}`);else try{this.schema=JSON.parse(r(i.toString(),this.config)),this.logger.debug("Organization schema loaded")}catch(t){this.logger.error(`Failed to parse organization schema: ${t}`)}})}api(t){t.get(`${this.config.api_prefix}/v1/ldap/organizations/top`,async(t,i)=>{await a(i,async()=>this.hideNeverReturn(await this.getOrganisationTop(t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,async()=>this.hideNeverReturn(await this.getOrganisationByDn(e,t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,async()=>this.hideNeverReturn(await this.getOrganisationSubnodes(e,t)))}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes/search`,o(async(t,i)=>{const e=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)throw new z('query parameter "q" is required');await a(i,async()=>this.hideNeverReturn(await this.searchOrganisationSubnodes(e,n,t)))})),t.post(`${this.config.api_prefix}/v1/ldap/organizations`,o(async(t,i)=>this.apiAdd(t,i))),t.put(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,o(async(t,i)=>this.apiModify(t,i))),t.delete(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,o(async(t,i)=>this.apiDelete(t,i))),t.post(`${this.config.api_prefix}/v1/ldap/organizations/:dn/move`,o(async(t,i)=>this.apiMove(t,i)))}async apiAdd(t,i){const a=e(t,i,"ou");if(!a)return;$(this.schema,Object.keys(a),"ou"),h(a.ou,"ou");const s=a.parentDn||this.config.ldap_top_organization,r=`ou=${c(a.ou)},${s}`,o={objectClass:this.config.ldap_organization_class,ou:a.ou,...Object.fromEntries(Object.entries(a).filter(([t])=>"ou"!==t&&"parentDn"!==t))};await n(i,this.addOrganization.bind(this),r,o,t)}async apiModify(t,i){const a=e(t,i);if(!a)return;const s=decodeURIComponent(t.params.dn);if(!s)throw new z("dn is required");$(this.schema,v(a)),await n(i,this.modifyOrganization.bind(this),s,a,t)}async apiDelete(t,i){if(!s(t,i))return;const a=decodeURIComponent(t.params.dn);if(!a)throw new z("dn is required");await n(i,this.deleteOrganization.bind(this),a,t)}async apiMove(t,i){if(!s(t,i))return;const n=e(t,i,"targetOrgDn");if(!n)return;const r=decodeURIComponent(t.params.dn);if(!r)throw new z("dn is required");const{targetOrgDn:o}=n;if(!o||"string"!=typeof o)throw new z("Missing or invalid targetOrgDn in request body");await a(i,this.moveOrganization.bind(this),r,o,t)}async checkDeptLink(t){if(t[this.linkAttr]){const i=t[this.linkAttr],a=Array.isArray(i)?i[0]:i,e=await this.server.ldap.search({paged:!1,scope:"base"},a);if(0===e.searchEntries.length)throw new y(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new z(`Entry ${a} isn't in top organization branch`)}}async checkDeptPath(t,i){if(t[this.pathAttr]){const a=t[this.pathAttr],e=Array.isArray(a)?a[0]:a,n=this.config.ldap_organization_path_separator||" / ",s=this.config.ldap_top_organization||"",r=i&&s&&/^ou=/i.test(d(i))&&l(i,s)?g(i):void 0,o=t.ou,h=(Array.isArray(o)?o[0]:o)||r;let c=e;if((this.isOu(t)||void 0!==r)&&h){const t=s?g(s):"";if(t&&e.startsWith(h+n)&&e.endsWith(n+t))return;if(e===h){const t=s?p(s):"";if(!i||!t||!l(i,s)||p(i)===t||p(f(i))===t)return;throw new z(`Organization path "${e}" names no parent, but ${i} is not directly under ${s}`)}if(!e.endsWith(n+h))throw new z(`Organization path must end with its own name, preceded by separator "${n}"`);c=e.slice(0,e.length-n.length-h.length)}const w=await this.server.ldap.search({paged:!1,scope:"sub",filter:`(${this.pathAttr}=${u(c)})`,attributes:["dn"]},this.config.ldap_top_organization);if(!w.searchEntries||0===w.searchEntries.length)throw new z(`Invalid organization path ${e}: no organization holds "${c}"`)}}async isEmptyOrganization(t){if((await this.server.ldap.search({paged:!1,filter:`(${this.config.ldap_organization_link_attribute}=${u(t)})`})).searchEntries.length>0)throw new b(`Organization ${t} is not empty`)}isOu(t){if(!t.objectClass)return!1;const i=t.objectClass.map(t=>t.toLowerCase());return this.config.ldap_organization_class.filter(t=>"top"!==t.toLowerCase()).some(t=>i.includes(t.toLowerCase()))}async getOrganisationTop(t){if(!this.config.ldap_top_organization)throw new z("No top organization configured");const i=await this.server.ldap.search({paged:!1,scope:"base"},this.config.ldap_top_organization,t);if(1!==i.searchEntries.length)throw new y("Top organization not found");const[,a]=await w(this.registeredHooks.getOrganisationTop,[t,i.searchEntries[0]]);return a}async getOrganisationByDn(t,i){const a=await this.server.ldap.search({paged:!1,scope:"base",filter:"(objectClass=organizationalUnit)"},t,i);if(1!==a.searchEntries.length)throw new y(`Organization ${t} not found`);return a.searchEntries[0]}async getOrganisationSubnodes(t,i){const a=[],e=this.config.ldap_organization_max_subnodes||50,n=i?.query?.objectClass;n&&"organizationalUnit"!==n||a.push(...await this.childOrganizations(t,"(objectClass=organizationalUnit)",e,i));const s=this.config.ldap_top_organization,r=f(s);let o=`(${this.config.ldap_organization_link_attribute}=${u(t)})`;return n&&(o=`(&${o}(objectClass=${u(n)}))`),this.server.logger.debug(`Searching for linked entities with filter: ${o} in ${r}`),a.push(...await this.linkedEntities(r,o,e,t,i)),a}warnSizeLimit(t,i){this.sizeLimitWarned.has(t)?this.server.logger.debug(i):(this.sizeLimitWarned.size<LdapOrganizations.SIZE_LIMIT_WARNED_MAX&&this.sizeLimitWarned.add(t),this.server.logger.warn(i))}async childOrganizations(t,i,a,e){try{const a=await this.server.ldap.search({paged:!0,scope:"one",filter:i},t,e),n=[];for await(const t of a)n.push(...t.searchEntries);return this.server.logger.debug(`Found ${n.length} sub-OUs for ${t}`),n}catch(i){const a=_(i);if(32===a)return this.server.logger.debug(`No sub-OUs for ${t}: the node holds none`),[];if(4!==a)throw i}const n=(await this.server.ldap.search({paged:!1,scope:"one",filter:i,sizeLimit:a+1},t,e)).searchEntries.slice(0,a);return this.warnSizeLimit(`children:${t}`,`Organization ${t} holds more child organizations than the directory will list in one answer, so ${n.length} are returned and the rest are hidden. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to list them all.`),[...n,{dn:`more-organizations-${t}`,cn:["... more organizations than the directory will list"],objectClass:["moreIndicator"],_isMoreIndicator:"true",_displayedCount:n.length.toString()}]}async linkedEntities(t,i,a,e,n){const s=[];let r=0;try{const e=await this.server.ldap.search({paged:!0,filter:i},t,n);for await(const t of e){r+=t.searchEntries.length;const i=a-s.length;i>0&&s.push(...t.searchEntries.slice(0,i))}}catch(s){if(4!==_(s))throw s;const r=await this.server.ldap.search({paged:!1,filter:i,sizeLimit:a+1},t,n);return this.warnSizeLimit(`linked:${e}`,`More entries are attached to ${e} than the directory will list in one answer. Raise the size limit for the account ldap-rest binds as (olcLimits, or olcSizeLimit) to count them all.`),[...r.searchEntries.slice(0,a),{dn:`more-${e}`,cn:["... more elements than the directory will list"],objectClass:["moreIndicator"],_isMoreIndicator:"true",_displayedCount:Math.min(r.searchEntries.length,a).toString()}]}return this.server.logger.debug(`Found ${r} linked entities for ${e}, returning ${s.length}`),r<=a?s:[...s,{dn:`more-${e}`,cn:[`... ${r-a} more elements`],objectClass:["moreIndicator"],_isMoreIndicator:"true",_totalCount:r.toString(),_displayedCount:a.toString()}]}async addOrganization(t,i,a){return await this.validateNewOrganization(t,i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i,a){return await this.validateChanges(t,i),await this.server.ldap.modify(t,i,a)}async validateNewOrganization(t,i){if(!this.schema)return!0;for(const[t,a]of Object.entries(i))if(!await this._validateOneChange(t,a))throw new z(this.invalidValueMessage(t));const a=O(this.schema,i,this.server.loadedPlugins);if(a)throw new z(`Missing required field ${a}`);return!0}invalidValueMessage(t){const i=this.schema?.attributes[t],a=i?.hint||i?.items?.hint;return a?`Invalid value for field ${t}: ${a}`:`Invalid value for field ${t}`}async validateChanges(t,i){if(!this.schema)return!0;if(i.add)for(const[t,a]of Object.entries(i.add))if(!await this._validateOneChange(t,a))throw new z(this.invalidValueMessage(t));if(i.replace)for(const[t,a]of Object.entries(i.replace))if(!await this._validateOneChange(t,a))throw new z(this.invalidValueMessage(t));return!0}async _validateOneChange(t,i){if(!this.schema)return!0;const a=this.schema.attributes[t];if(!a){if(this.schema.strict)throw new z(`Field ${t} is not allowed`);return!0}if(null==i){if(a.required)throw new z(`Field ${t} is required`);return!0}if("string"===a.type&&Array.isArray(i))throw new z(`Field ${t} must be a single value`);return await C(t,a,i,t=>this.entryExists(t)),E(a,i)}async entryExists(t){try{return(await this.server.ldap.search({paged:!1,scope:"base",attributes:["dn"]},t)).searchEntries.length>0}catch{return!1}}async moveOrganization(t,i,a){let e;try{e=await this.server.ldap.search({paged:!1,scope:"base"},i)}catch(t){throw new y(`Target organization ${i} not found: ${t instanceof Error?t.message:String(t)}`)}if(!e.searchEntries||0===e.searchEntries.length)throw new y(`Target organization ${i} not found`);if(!this.isOu(e.searchEntries[0]))throw new z(`Target ${i} is not an organizational unit`);const n=`${d(t)},${i}`;let s;try{s=await this.server.ldap.search({paged:!1,scope:"base"},t)}catch(t){throw new y(`Source organization not found: ${t}`)}if(!s.searchEntries||0===s.searchEntries.length)throw new y(`Source organization ${t} not found`);if(n===t)throw new z("Cannot move organization to its current location");if(i.toLowerCase()===t.toLowerCase()||m(i,t))throw new z("Cannot move organization into itself or its descendant");try{await this.server.ldap.rename(t,n,a),this.logger.info(`Moved organization from ${t} to ${n}`)}catch(t){throw new Error(`Failed to move organization: ${t}`)}return{newDn:n}}async deleteOrganization(t,i){return await this.server.ldap.delete(t,i)}async searchOrganisationSubnodes(t,i,a){const e=[],n=this.config.ldap_organization_max_subnodes||50,s=u(i);e.push(...await this.childOrganizations(t,`(&(objectClass=organizationalUnit)(|(ou=*${s}*)(description=*${s}*)))`,n,a));const r=this.config.ldap_top_organization,o=f(r),h=u(t),c=`(&(${this.config.ldap_organization_link_attribute}=${h})(|(uid=*${s}*)(cn=*${s}*)(mail=*${s}*)(sn=*${s}*)(givenName=*${s}*)))`;return this.server.logger.debug(`Searching for linked entities with filter: ${c} in ${o}`),e.push(...await this.linkedEntities(o,c,n,t,a)),e}getConfigApiData(){const t=this.config.api_prefix||"/api";let i;if(this.server.loadedPlugins.static&&this.config.organization_schema){const t=this.config.static_name||"static",a=this.config.organization_schema.indexOf("/schemas/");if(-1!==a){i=`/${t}${this.config.organization_schema.substring(a)}`}}return{enabled:!0,topOrganization:this.config.ldap_top_organization||"",organizationClass:this.config.ldap_organization_class||["top","organizationalUnit"],linkAttribute:this.linkAttr||"",pathAttribute:this.pathAttr||"",pathSeparator:this.config.ldap_organization_path_separator||" / ",maxSubnodes:this.config.ldap_organization_max_subnodes||50,schema:this.schema,schemaUrl:i,endpoints:{getTop:`${t}/v1/ldap/organizations/top`,get:`${t}/v1/ldap/organizations/:dn`,getSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes`,searchSubnodes:`${t}/v1/ldap/organizations/:dn/subnodes/search`,create:`${t}/v1/ldap/organizations`,update:`${t}/v1/ldap/organizations/:dn`,move:`${t}/v1/ldap/organizations/:dn/move`,delete:`${t}/v1/ldap/organizations/:dn`}}}}LdapOrganizations.SIZE_LIMIT_WARNED_MAX=500;export{LdapOrganizations as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"../../abstract/plugin.js";class TrashPlugin extends t{constructor(t){super(t),this.name="trash",this.roles=["consistency"],this.watchedBases=[],this.trashBase="",this.addMetadata=!0,this.autoCreateTrash=!0,this.trashInitialized=!1,this.hooks={ldapdeleterequest:async([t,s])=>{const a=Array.isArray(t)?t:[t],e=[];for(const t of a){if(!this.isWatched(t)){this.logger.debug(`Trash plugin: ${t} not in watched branches, allowing native delete`),e.push(t);continue}this.logger.info(`Trash plugin: Intercepting delete of ${t}`),await this.ensureTrashExists();const
|
|
1
|
+
import t from"../../abstract/plugin.js";class TrashPlugin extends t{constructor(t){super(t),this.name="trash",this.roles=["consistency"],this.watchedBases=[],this.trashBase="",this.addMetadata=!0,this.autoCreateTrash=!0,this.trashInitialized=!1,this.hooks={ldapdeleterequest:async([t,s])=>{const a=Array.isArray(t)?t:[t],e=[];for(const t of a){if(!this.isWatched(t)){this.logger.debug(`Trash plugin: ${t} not in watched branches, allowing native delete`),e.push(t);continue}this.logger.info(`Trash plugin: Intercepting delete of ${t}`),await this.ensureTrashExists();const a=this.buildTrashDN(t);await this.removeOldTrashEntry(a);try{this.logger.debug(`Moving ${t} to ${a} using LDAP move (atomic)`),await this.server.ldap.move(t,a,s),this.logger.info(`Entry ${t} moved to trash at ${a}`),await this.addMetadataToTrash(a,t)}catch(s){if(this.logger.error(`Trash plugin: Failed to move ${t} to trash: ${String(s)}`),s instanceof Error&&(s.message.includes("Insufficient access")||s.message.includes("permissions")||s.message.includes("access denied")))throw new Error(`Trash plugin: Insufficient LDAP permissions to move ${t} to trash. Please grant modifyDN permissions or disable trash plugin. Original error: ${s.message}`);throw new Error(`Trash plugin: Failed to move ${t} to trash: ${String(s)}`)}}return[Array.isArray(t)?e:e[0]??void 0,s]}};const s=this.config.trash_base;this.trashBase="string"==typeof s?s:"ou=trash,dc=example,dc=com";const a=this.config.trash_watched_bases;"string"==typeof a&&a&&(this.watchedBases=a.split(",").map(t=>t.trim()).filter(t=>t.length>0)),this.addMetadata="false"!==this.config.trash_add_metadata,this.autoCreateTrash="false"!==this.config.trash_auto_create,this.logger.info(`Trash plugin initialized: base=${this.trashBase}, watched=${this.watchedBases.join(",")}, metadata=${this.addMetadata}`)}isWatched(t){return t!==this.trashBase&&!t.endsWith(","+this.trashBase)&&(0===this.watchedBases.length||this.watchedBases.some(s=>t===s||t.endsWith(","+s)))}extractRDN(t){return t.split(",")[0]}buildTrashDN(t){return`${this.extractRDN(t)},${this.trashBase}`}async ensureTrashExists(){if(!this.trashInitialized)if(this.autoCreateTrash){try{if((await this.server.ldap.search({paged:!1},this.trashBase)).searchEntries.length>0)return this.logger.debug(`Trash branch ${this.trashBase} already exists`),void(this.trashInitialized=!0)}catch{this.logger.info(`Creating trash branch ${this.trashBase}`)}try{const t=this.extractRDN(this.trashBase).split("=")[1];await this.server.ldap.add(this.trashBase,{objectClass:["top","organizationalUnit"],ou:t,description:"LDAP Trash - Deleted entries are moved here"}),this.logger.info(`Trash branch ${this.trashBase} created successfully`),this.trashInitialized=!0}catch(t){throw this.logger.error(`Failed to create trash branch ${this.trashBase}: ${String(t)}`),new Error(`Trash plugin: Unable to create trash branch ${this.trashBase}. Please create it manually or set DM_TRASH_AUTO_CREATE=false`)}}else this.trashInitialized=!0}async removeOldTrashEntry(t){try{(await this.server.ldap.search({paged:!1},t)).searchEntries.length>0&&(this.logger.info(`Removing old trash entry ${t} before overwrite`),await this.server.ldap.delete(t),this.logger.debug(`Old trash entry ${t} deleted`))}catch(s){this.logger.debug(`No old trash entry to remove at ${t}: ${String(s)}`)}}async addMetadataToTrash(t,s){if(this.addMetadata)try{const a=`Deleted on ${(new Date).toISOString()} from ${s}`;await this.server.ldap.modify(t,{replace:{description:a}}),this.logger.debug(`Metadata added to ${t}`)}catch(s){this.logger.warn(`Failed to add metadata to ${t}: ${String(s)}. Entry moved to trash but without metadata.`)}}}export{TrashPlugin as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var e=["core/auth/trustedProxy","core/weblogs","core/auth/crowdsec","core/auth/rateLimit","core/auth/token","core/auth/llng","core/auth/openidconnect","core/auth/authzPerRoute"];export{e as default};
|
|
1
|
+
var e=["core/storage","core/auth/trustedProxy","core/weblogs","core/auth/crowdsec","core/auth/rateLimit","core/auth/token","core/auth/llng","core/auth/openidconnect","core/auth/authzPerRoute"];export{e as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"fs";import{
|
|
1
|
+
import e from"fs";import{assertIdentityMode as s,identityFor as r}from"../../lib/auth/base.js";import{escapeDnValue as t,isChildOf as a}from"../../lib/utils.js";class BaseResolver{constructor(r){s(r,"scim base resolver"),this.config=r;const t=r.ldap_base||"";this.defaultUserBase=r.scim_user_base||t,this.defaultGroupBase=r.scim_group_base||t,this.userTemplate=r.scim_user_base_template||"",this.groupTemplate=r.scim_group_base_template||"",this.userBaseHeader=(r.scim_user_base_header||"").toLowerCase(),this.groupBaseHeader=(r.scim_group_base_header||"").toLowerCase(),this.headerRoot=r.scim_base_header_root||"";const a=r.scim_base_map||"";if(a)try{const s=e.readFileSync(a,"utf8");this.map=JSON.parse(s)}catch(e){throw new Error(`Failed to load SCIM base map from ${a}: ${e instanceof Error?e.message:String(e)}`)}}applyTemplate(e,s){const r=s?t(s):"";return e.replace(/\{user\}/g,r)}readHeader(e,s){if(!e||"object"!=typeof e)return;const r=e;if("function"==typeof r.get){const e=r.get(s);if("string"==typeof e&&e.length>0)return e}const t=r.headers?.[s];return"string"==typeof t&&t.length>0?t:Array.isArray(t)&&"string"==typeof t[0]&&t[0].length>0?t[0]:void 0}resolve(e,s){const t=s&&"object"==typeof s?r(s,this.config).value:void 0;if(this.map&&t&&this.map[t]){const s=this.map[t];if("user"===e&&s.userBase)return s.userBase;if("group"===e&&s.groupBase)return s.groupBase}const o="user"===e?this.userBaseHeader:this.groupBaseHeader;if(o&&this.headerRoot){const e=this.readHeader(s,o)?.trim();if(e&&!/[\u0000-\u001f\u007f]/.test(e)&&(e.toLowerCase()===this.headerRoot.toLowerCase()||a(e,this.headerRoot)))return e}if(this.map&&this.map["*"]){const s=this.map["*"];if("user"===e&&s.userBase)return this.applyTemplate(s.userBase,t);if("group"===e&&s.groupBase)return this.applyTemplate(s.groupBase,t)}const i="user"===e?this.userTemplate:this.groupTemplate;return i?this.applyTemplate(i,t):"user"===e?this.defaultUserBase:this.defaultGroupBase}userBase(e){return this.resolve("user",e)}groupBase(e){return this.resolve("group",e)}}export{BaseResolver};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{HttpError as
|
|
1
|
+
import{HttpError as r}from"../../lib/errors.js";import{extractLdapCode as e}from"../../lib/ldapCodes.js";import{SCHEMA_ERROR as t}from"./types.js";const i="application/scim+json";class ScimError extends r{constructor(r,e,t){super(e,r),this.name="ScimError",this.scimType=t}}const scimInvalidFilter=(r="Invalid filter")=>new ScimError(400,r,"invalidFilter"),scimInvalidPath=(r="Invalid path")=>new ScimError(400,r,"invalidPath"),scimInvalidValue=(r="Invalid value")=>new ScimError(400,r,"invalidValue"),scimInvalidSyntax=(r="Invalid syntax")=>new ScimError(400,r,"invalidSyntax"),scimNoTarget=(r="No target")=>new ScimError(400,r,"noTarget"),scimMutability=(r="Immutable attribute")=>new ScimError(400,r,"mutability"),scimUniqueness=(r="Uniqueness violation")=>new ScimError(409,r,"uniqueness"),scimTooMany=(r="Too many results")=>new ScimError(400,r,"tooMany"),scimNotFound=(r="Resource not found")=>new ScimError(404,r);function writeScimError(r,e,o,n){const s={schemas:[t],status:String(e),...n?{scimType:n}:{},detail:o};r.status(e).type(i).json(s)}function scimErrorFromException(i,o=500){const sanitize=r=>/\[authz-forbidden\]/.test(r)?"Token does not have permission on this branch":r,body=(r,e,i)=>({schemas:[t],status:String(r),...i?{scimType:i}:{},detail:e});if(i instanceof ScimError)return body(i.statusCode,sanitize(i.message),i.scimType);if(i instanceof r)return body(i.statusCode,sanitize(i.message));const n=i instanceof Error?i.message:String(i);if(/\[authz-forbidden\]/.test(n))return body(403,"Token does not have permission on this branch");const s=e(i);return 32===s?body(404,"Resource not found"):68===s?body(409,sanitize(n),"uniqueness"):17===s||65===s?body(400,`The directory rejected this write against its schema: ${sanitize(n)}`,"invalidValue"):body(o,sanitize(n)||"Internal error")}function writeScimErrorFromException(r,e,t=500){const o=scimErrorFromException(e,t);r.status(Number(o.status)).type(i).json(o)}const scimAsyncHandler=r=>(e,t,i)=>{Promise.resolve().then(()=>r(e,t,i)).catch(r=>{t.headersSent?i(r):writeScimErrorFromException(t,r)})};export{i as SCIM_CONTENT_TYPE,ScimError,e as extractLdapCode,scimAsyncHandler,scimErrorFromException,scimInvalidFilter,scimInvalidPath,scimInvalidSyntax,scimInvalidValue,scimMutability,scimNoTarget,scimNotFound,scimTooMany,scimUniqueness,writeScimError,writeScimErrorFromException};
|
|
@@ -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 p,requiredLdapAttributes as m,ldapToScimGroup as l,scimGroupToLdap as d}from"./mapping.js";import{scimFilterToLdap as f}from"./filter.js";import{pagedSearch as g}from"./list.js";import{patchToModifyRequest as b}from"./patch.js";import{
|
|
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 p,requiredLdapAttributes as m,ldapToScimGroup as l,scimGroupToLdap as d}from"./mapping.js";import{scimFilterToLdap as f}from"./filter.js";import{pagedSearch as g}from"./list.js";import{patchToModifyRequest as b}from"./patch.js";import{scimNotFound as w,ScimError as y,scimInvalidValue as R,scimUniqueness as _}from"./errors.js";import{extractLdapCode as $}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(p,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"),p=t.toLowerCase();let m;s&&p.endsWith(s.toLowerCase())?m="User":r&&p.endsWith(r.toLowerCase())?m="Group":h[1]===a?m="User":h[1]===n&&(m="Group");const l={value:u};if(m&&(l.type=m,o)){const t="User"===m?"Users":"Groups";l.$ref=`${o.replace(/\/$/,"")}${i}/${t}/${encodeURIComponent(u)}`}return l}}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:[...m(this.mapping),"member"]},s)}catch(t){if(32===$(t))throw w(`Group ${e} not found`);throw t}if(!r.searchEntries||0===r.searchEntries.length)throw w(`Group ${e} not found`);return l(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 y&&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:[...m(this.mapping),"member"],startIndex:r,count:i,maxScanned:this.maxScanned}),u=this.buildMemberResolver(t),p=c.map(e=>l(e,this.mapping,this.ctx(t),u));return{schemas:[n],totalResults:h,startIndex:r,itemsPerPage:p.length,Resources:p}}async create(e,o){if(!o.displayName)throw R("displayName is required");const a=(await s(this.hooks.scimgroupcreate,[o,e]))[0],{rdn:n,attributes:c}=d(a,this.mapping,this.objectClass);if(!n)throw R("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),p=`${this.rdnAttribute}=${t(n)},${u}`;try{await this.ldap.forRequest(e).add(p,c)}catch(t){if(68===$(t))throw _(`Group ${n} already exists`);throw t}const m=await this.get(e,n);return i(this.hooks.scimgroupcreatedone,m),m}async replace(t,e,r){await this.get(t,e);const o=(await s(this.hooks.scimgroupupdate,[e,r,t]))[1],{attributes:a}=d(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),o=await this.currentEntry(t,e),a=await b(s,{mapping:this.mapping,memberAttribute:"member",resolveMemberRef:async e=>this.users.resolveRef(t,e),current:o});if(a.delete&&!Array.isArray(a.delete)&&"member"in a.delete){const t=this.config.group_dummy_user||"cn=fakeuser";delete a.delete.member,0===Object.keys(a.delete).length&&delete a.delete,a.replace||(a.replace={}),a.replace.member=[t]}await this.ldap.forRequest(t).modify(r,a);const n=await this.get(t,e);return i(this.hooks.scimgroupupdatedone,e,n),n}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:[...m(this.mapping),"member"]},s)}catch(t){if(32===$(t))throw w(`Group ${e} not found`);throw t}if(!r.searchEntries?.length)throw w(`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!==$(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!==$(t))throw t}}get groupMapping(){return this.mapping}}export{ScimGroups};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{scimTooMany as t,ScimError as e
|
|
1
|
+
import{scimTooMany as t,ScimError as e}from"./errors.js";import{extractLdapCode as r}from"../../lib/ldapCodes.js";async function pagedSearch(a){const i=Math.max(0,a.startIndex-1),s=[];let o=0;try{const e=await a.directory.search({filter:a.filter,scope:"sub",paged:{pageSize:(n=a.count,Math.min(500,Math.max(n,100)))},attributes:a.attributes,sizeLimit:0},a.base);for await(const r of e)for(const e of r.searchEntries||[])if(o>=i&&s.length<a.count&&s.push(e),o++,o>a.maxScanned)throw t(`Query matched more than ${a.maxScanned} resources; narrow it with a filter or raise --scim-max-scanned`)}catch(a){if(a instanceof e)throw a;const i=r(a);if(32===i)return{entries:[],totalResults:0};if(4===i)throw t("The directory refused the query with sizeLimitExceeded; narrow it with a filter or raise the directory size limit");throw a}var n;return{entries:s,totalResults:o}}export{pagedSearch};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fileURLToPath as s}from"url";import{join as e,dirname as i}from"path";import t from"body-parser";import o from"../../abstract/plugin.js";import{
|
|
1
|
+
import{fileURLToPath as s}from"url";import{join as e,dirname as i}from"path";import t from"body-parser";import o from"../../abstract/plugin.js";import{setChangeSource as r}from"../../lib/changeContext.js";import{BaseResolver as a}from"./baseResolver.js";import{ScimUsers as n}from"./users.js";import{ScimGroups as c}from"./groups.js";import{ScimBulk as p}from"./bulk.js";import{ScimDiscovery as m}from"./discovery.js";import{SCIM_CONTENT_TYPE as u,scimAsyncHandler as d,writeScimError as h}from"./errors.js";export{writeScimErrorFromException}from"./errors.js";import{projectResource as l,assertProjection as g,projectList as f}from"./projection.js";class Scim extends o{constructor(t){super(t),this.name="scim",this.roles=["api","configurable"],this.ldap=t.ldap,this.scimPrefix=this.config.scim_prefix||"/scim/v2",this.baseResolver=new a(this.config),this.users=new n({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,hooks:this.registeredHooks}),this.groups=new c({ldap:this.ldap,config:this.config,logger:this.logger,baseResolver:this.baseResolver,users:this.users,hooks:this.registeredHooks}),this.bulk=new p({config:this.config,logger:this.logger,users:this.users,groups:this.groups,scimPrefix:this.scimPrefix,hooks:this.registeredHooks});const o=this.config.schemas_path||e(i(s(import.meta.url)),"..","..","..","static","schemas");this.discovery=new m({config:this.config,schemaDir:e(o,"scim"),scimPrefix:this.scimPrefix,loadedPlugins:t.loadedPlugins})}scimJson(s,e,i){s.status(e).type(u).json(i)}scimResource(s,e,i,t,o={}){const r=i.id?`${this.scimPrefix}/${t}/${encodeURIComponent(i.id)}`:void 0,a=(this.config.scim_base_url||"")&&i.meta?.location?i.meta.location:r;a&&s.set("Location",a),this.scimJson(s,e,l(i,o))}parseProjection(s){const toCsv=s=>{if("string"!=typeof s)return;const e=s.split(",").map(s=>s.trim()).filter(Boolean);return e.length>0?e:void 0},e={attributes:toCsv(s.query.attributes),excludedAttributes:toCsv(s.query.excludedAttributes)};return g(e),e}readScimBody(s){const e=String(s.headers["content-type"]||"");if(e.startsWith("application/scim+json")||e.startsWith("application/json"))return s.body}api(s){const e=this.scimPrefix;s.use(e,t.json({type:["application/json","application/scim+json"],limit:`${this.config.scim_bulk_max_payload_size||1048576}b`})),s.use(e,(s,e,i)=>{r(s,"scim"),i()}),s.use(e,d((s,e,i)=>{this.parseProjection(s),i()})),s.get(`${e}/Users`,d(async(s,e)=>{const i=this.parseListQuery(s);g(i);const t=await this.users.list(s,i);this.scimJson(e,200,f(t,i))})),s.get(`${e}/Users/:id`,d(async(s,e)=>{const i=this.parseProjection(s),t=await this.users.get(s,decodeURIComponent(s.params.id));this.scimJson(e,200,l(t,i))})),s.post(`${e}/Users`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.users.create(s,i);this.scimResource(e,201,t,"Users",this.parseProjection(s))})),s.put(`${e}/Users/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.users.replace(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Users",this.parseProjection(s))})),s.patch(`${e}/Users/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.users.patch(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Users",this.parseProjection(s))})),s.delete(`${e}/Users/:id`,d(async(s,e)=>{await this.users.delete(s,decodeURIComponent(s.params.id)),e.status(204).end()})),s.get(`${e}/Groups`,d(async(s,e)=>{const i=this.parseListQuery(s);g(i);const t=await this.groups.list(s,i);this.scimJson(e,200,f(t,i))})),s.get(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.parseProjection(s),t=await this.groups.get(s,decodeURIComponent(s.params.id));this.scimJson(e,200,l(t,i))})),s.post(`${e}/Groups`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.groups.create(s,i);this.scimResource(e,201,t,"Groups",this.parseProjection(s))})),s.put(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.groups.replace(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Groups",this.parseProjection(s))})),s.patch(`${e}/Groups/:id`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=await this.groups.patch(s,decodeURIComponent(s.params.id),i);this.scimResource(e,200,t,"Groups",this.parseProjection(s))})),s.delete(`${e}/Groups/:id`,d(async(s,e)=>{await this.groups.delete(s,decodeURIComponent(s.params.id)),e.status(204).end()})),s.post(`${e}/Bulk`,d(async(s,e)=>{const i=this.readScimBody(s);if(!i||"object"!=typeof i)return h(e,400,"Missing body","invalidSyntax");const t=this.config.scim_bulk_max_payload_size||1048576,o=JSON.stringify(i).length;if(o>t)return h(e,413,`Bulk payload too large: ${o} > ${t}`,"invalidValue");const r=await this.bulk.execute(s,i);this.scimJson(e,200,r)})),s.get(`${e}/ServiceProviderConfig`,d((s,e)=>{this.scimJson(e,200,this.discovery.serviceProviderConfig(s))})),s.get(`${e}/ResourceTypes`,d((s,e)=>{const i=this.discovery.resourceTypes(s);this.scimJson(e,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:i.length,Resources:i,startIndex:1,itemsPerPage:i.length})})),s.get(`${e}/ResourceTypes/:name`,d((s,e)=>{const i=this.discovery.resourceType(s.params.name,s);if(!i)return h(e,404,"ResourceType not found");this.scimJson(e,200,i)})),s.get(`${e}/Schemas`,d((s,e)=>{const i=this.discovery.schemas(s);this.scimJson(e,200,{schemas:["urn:ietf:params:scim:api:messages:2.0:ListResponse"],totalResults:i.length,Resources:i,startIndex:1,itemsPerPage:i.length})})),s.get(`${e}/Schemas/:id`,d((s,e)=>{const i=this.discovery.schema(decodeURIComponent(s.params.id),s);if(!i)return h(e,404,"Schema not found");this.scimJson(e,200,i)}))}parseListQuery(s){const e=s.query,toInt=s=>{if("string"!=typeof s)return;const e=parseInt(s,10);return Number.isFinite(e)?e:void 0},toCsv=s=>{if("string"==typeof s)return s.split(",").map(s=>s.trim()).filter(Boolean)},i="string"==typeof e.sortOrder?e.sortOrder:void 0;return{filter:"string"==typeof e.filter?e.filter:void 0,startIndex:toInt(e.startIndex),count:toInt(e.count),attributes:toCsv(e.attributes),excludedAttributes:toCsv(e.excludedAttributes),sortBy:"string"==typeof e.sortBy?e.sortBy:void 0,sortOrder:"ascending"===i||"descending"===i?i:void 0}}getConfigApiData(){const s=this.scimPrefix;let e;if(this.server.loadedPlugins.static){const s=this.config.static_name||"static";e={user:`/${s}/schemas/scim/User.json`,group:`/${s}/schemas/scim/Group.json`,defaultMapping:`/${s}/schemas/scim/default-mapping.json`}}const i=this.users.userMapping,t=this.groups.groupMapping,o=i.entries.map(s=>s.scim).concat(["id","active"]),r=t.entries.map(s=>s.scim).concat(["id"]);return{enabled:!0,version:"2.0",prefix:s,endpoints:{users:`${s}/Users`,user:`${s}/Users/:id`,groups:`${s}/Groups`,group:`${s}/Groups/:id`,bulk:`${s}/Bulk`,serviceProviderConfig:`${s}/ServiceProviderConfig`,resourceTypes:`${s}/ResourceTypes`,schemas:`${s}/Schemas`},schemaUrls:e,capabilities:{patch:!0,bulk:!0,filter:!0,sort:!0,etag:Boolean(this.config.scim_etag),changePassword:!1,maxResults:this.config.scim_max_results||200,maxScanned:this.config.scim_max_scanned||1e4,bulkMaxOperations:this.config.scim_bulk_max_operations||100,bulkMaxPayloadSize:this.config.scim_bulk_max_payload_size||1048576},resourceTypes:[{id:"User",endpoint:`${s}/Users`,schema:"urn:ietf:params:scim:schemas:core:2.0:User",rdnAttribute:this.config.scim_user_rdn_attribute||"uid",objectClass:this.config.scim_user_object_class,filterableAttributes:o,mapping:i.entries},{id:"Group",endpoint:`${s}/Groups`,schema:"urn:ietf:params:scim:schemas:core:2.0:Group",rdnAttribute:this.config.scim_group_rdn_attribute||"cn",objectClass:this.config.scim_group_object_class,filterableAttributes:r,mapping:t.entries}],idStrategy:this.config.scim_id_attribute||"rdn",baseResolution:{userBaseTemplate:this.config.scim_user_base_template||void 0,groupBaseTemplate:this.config.scim_group_base_template||void 0,hasBaseMap:Boolean(this.config.scim_base_map)}}}}export{Scim as default};
|
|
@@ -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 b}from"./patch.js";import{
|
|
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 b}from"./patch.js";import{scimNotFound as g,ScimError as w,scimInvalidValue as k,scimUniqueness as A}from"./errors.js";import{extractLdapCode as y}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===y(t))throw g(`User ${e} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw g(`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 w&&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 k("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 k("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===y(t))throw A(`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 g(`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!==y(t))throw t;throw new w(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),a=await this.currentEntry(t,e),o=await b(s,{mapping:this.mapping,lockAttribute:this.lockAttribute,lockValue:this.lockValue,supportsActive:!0,current:a});try{await this.ldap.forRequest(t).modify(r,o)}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}(o,this.lockAttribute))throw this.lockSchemaError(t);throw t}const c=await this.get(t,e);return i(this.hooks.scimuserupdatedone,e,c),c}lockSchemaError(t){const e=y(t);if(17!==e&&65!==e)return t;const s=t instanceof Error?t.message:String(t);return k(`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===y(t))throw g(`User ${e} not found`);throw t}if(!i.searchEntries?.length)throw g(`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!==y(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!==y(t))throw t}}get userMapping(){return this.mapping}}export{ScimUsers};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHash as t}from"node:crypto";import{promises as e}from"node:fs";import{join as a}from"node:path";import r from"../../lib/storage/store.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t);class FileStore extends r{constructor(t,e,a){super(t),this.name="storage/file",this.dir=e,this.abandonedAfter=1e3*Math.max(a,60)}async ready(){await e.mkdir(this.dir,{recursive:!0,mode:448})}path(e){return a(this.dir,t("sha256").update(e).digest("hex"))}static encode(t,e){return`${e.deadline} ${FileStore.readable(t)}\n${e.value}\n`}static decode(t){const e=t.indexOf("\n");if(e<0)return null;const a=parseInt(t.slice(0,e).split(" ")[0],10);return Number.isFinite(a)?{deadline:a,value:t.slice(e+1).replace(/\n$/,"")}:null}async load(t){try{return FileStore.decode(await e.readFile(this.path(t),"utf8"))}catch(t){return"ENOENT"!==t?.code&&this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,a){await this.ready();const r=this.path(t),i=`${r}.${process.pid}.tmp`;try{await e.writeFile(i,FileStore.encode(t,a),{mode:384}),await e.rename(i,r)}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),await e.unlink(i).catch(()=>{}),t}}async drop(t){await e.unlink(this.path(t)).catch(t=>{"ENOENT"!==t?.code&&this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)})}async sweep(){const t=Date.now();let r,i=0;try{r=await e.readdir(this.dir)}catch{return 0}for(const n of r){const r=a(this.dir,n);if(n.endsWith(".tmp")){await e.stat(r).then(e=>t-e.mtimeMs).catch(()=>0)>this.abandonedAfter&&(await e.unlink(r).catch(()=>{}),i++);continue}let s=null;try{s=FileStore.decode(await e.readFile(r,"utf8"))}catch{continue}null!==s&&s.deadline>t||(await e.unlink(r).catch(()=>{}),i++)}return i}}export{FileStore as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHash as e}from"node:crypto";import t from"../../lib/storage/store.js";const errorText=e=>e instanceof Error?e.message:JSON.stringify(e);class LdapStore extends t{constructor(e,t,s){super(e.logger),this.name="storage/ldap",this.server=e,this.base=t,this.objectClass=s}dn(t){return`cn=${e("sha256").update(t).digest("hex")},${this.base}`}static encode(e,t){return`${t.deadline} ${LdapStore.readable(e)}\n${t.value}`}static decode(e){const t=String(e),s=t.indexOf("\n"),r=s<0?t:t.slice(0,s),a=parseInt(r.split(" ")[0],10);return Number.isFinite(a)?{deadline:a,value:s<0?"":t.slice(s+1)}:null}async load(e){try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:["description"]},this.dn(e)),s=t.searchEntries?.[0];return s?LdapStore.decode(s.description):null}catch(e){return 32!==e?.code&&this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(e)}`),null}}async save(e,t){const s=this.dn(e),r=LdapStore.encode(e,t);try{await this.server.ldap.add(s,{objectClass:["top",this.objectClass],cn:s.slice(3,s.indexOf(",")),description:r})}catch(e){if(68!==e?.code)throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(e)}`),e;await this.server.ldap.modify(s,{replace:{description:r}})}}async drop(e){await this.server.ldap.delete(this.dn(e)).catch(e=>{32!==e?.code&&this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(e)}`)})}async sweep(){const e=Date.now();let t=0;const s=await this.server.ldap.search({paged:!1,scope:"one",filter:`(objectClass=${this.objectClass})`,attributes:["description"]},this.base);for(const r of s.searchEntries||[]){const s=LdapStore.decode(r.description);null!==s&&s.deadline>e||(await this.server.ldap.delete(String(r.dn)).catch(()=>{}),t++)}return t}}export{LdapStore as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"../abstract/plugin.js";import r from"./storage/file.js";import t from"./storage/ldap.js";let s;class Storage extends e{constructor(e){if(super(e),this.name="storage",s)throw new Error(`storage: an instance is already loaded, keeping its records in ${s.store.name}. A second one would put the same record in two places and leave registration order to decide which answers. Load one, and let its consumers share it.`);const o=e.config.storage_backend||"";switch(o){case"ldap":{const r=e.config.storage_ldap_base;if(!r)throw new Error("storage: the ldap backend needs --storage-ldap-base, the branch it writes its records to. It holds records and nothing else, so it belongs outside the branches the directory serves.");this.store=new t(e,r,e.config.storage_ldap_object_class||"applicationProcess");break}case"file":{const t=e.config.storage_file_directory;if(!t)throw new Error("storage: the file backend needs --storage-file-directory, the directory it writes its records to. It writes one file per record, so nothing else should be writing there.");this.store=new r(e.logger,t,e.config.storage_sweep_interval||600);break}default:throw new Error(o?`storage: unknown backend "${o}". Known: ldap, file.`:"storage: --storage-backend says which sub-plugin keeps the records. Known: ldap, file.")}s=this,this.logger.info(`storage: records kept by ${this.store.name}`),this.store.startSweeping(e.config.storage_sweep_interval||0)}static release(e){s===e&&(s=void 0)}}export{Storage as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomInt as t}from"crypto";import r from"../../abstract/plugin.js";import{wantJson as s}from"../../lib/expressFormatedResponses.js";import{escapeLdapFilter as e,isDnInBranch as i,escapeDnValue as a}from"../../lib/utils.js";class AppAccountsApi extends r{constructor(t){if(super(t),this.name="appAccountsApi",this.roles=["api","configurable"],this.dependencies={appAccountsConsistency:"core/twake/appAccountsConsistency"},this.applicativeAccountBase=this.config.applicative_account_base,this.maxAppAccounts=this.config.max_app_accounts||5,this.mailAttr=this.config.mail_attribute||"mail",this.userAttr=this.config.app_accounts_user_attribute||this.mailAttr,!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}, max_app_accounts=${this.maxAppAccounts}, user_attribute=${this.userAttr}`)}api(t){const r=this.config.api_prefix||"/api";t.get(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.listAccounts(t,r)),t.post(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.createAccount(t,r)),t.delete(`${r}/v1/users/:user/app-accounts/:uid`,(t,r)=>this.deleteAccount(t,r)),this.logger.info(`AppAccounts API registered at ${r}/v1/users/:user/app-accounts`)}async resolvePrincipal(t,r,s){const a=((await this.server.ldap.search({scope:"sub",filter:`(${this.userAttr}=${e(t)})`,paged:!1,...s?{attributes:s}:{}},this.config.ldap_base||"")).searchEntries||[]).filter(t=>!i(t.dn,this.applicativeAccountBase));return 0===a.length?(r.status(404).json({error:`User ${t} not found`}),null):a.length>1?(this.logger.error(`${this.name}: Ambiguous principal lookup for ${t}: ${a.length} entries share ${this.userAttr}`),r.status(409).json({error:`Multiple users share ${this.userAttr} ${t}`}),null):a[0]}async listAccounts(t,r){if(!s(t,r))return;const i=t.params.user;try{const t=await this.resolvePrincipal(i,r,[this.mailAttr]);if(!t)return;const s=t[this.mailAttr];if(!s)return void r.status(400).json({error:`User ${i} has no mail attribute`});const a=Array.isArray(s)?String(s[0]):String(s),o=await this.server.ldap.search({scope:"sub",filter:`(${this.mailAttr}=${e(a)})`,paged:!1},this.applicativeAccountBase),n=(o.searchEntries||[]).filter(t=>{const r=t.uid;return(Array.isArray(r)?String(r[0]):String(r)).toLowerCase()!==a.toLowerCase()}).map(t=>{const r=t.uid,s=Array.isArray(r)?String(r[0]):String(r),e=t.description,i=e?Array.isArray(e)?String(e[0]):String(e):void 0;return{uid:s,...i?{name:i}:{}}}).sort((t,r)=>t.uid.localeCompare(r.uid));r.json(n)}catch(t){this.logger.error(`${this.name}: Failed to list accounts for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}async createAccount(t,r){if(!s(t,r))return;const i=t.params.user,{name:o}=t.body||{};try{const
|
|
1
|
+
import{randomInt as t}from"crypto";import r from"../../abstract/plugin.js";import{wantJson as s}from"../../lib/expressFormatedResponses.js";import{escapeLdapFilter as e,isDnInBranch as i,escapeDnValue as a}from"../../lib/utils.js";class AppAccountsApi extends r{constructor(t){if(super(t),this.name="appAccountsApi",this.roles=["api","configurable"],this.dependencies={appAccountsConsistency:"core/twake/appAccountsConsistency"},this.applicativeAccountBase=this.config.applicative_account_base,this.maxAppAccounts=this.config.max_app_accounts||5,this.mailAttr=this.config.mail_attribute||"mail",this.userAttr=this.config.app_accounts_user_attribute||this.mailAttr,!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}, max_app_accounts=${this.maxAppAccounts}, user_attribute=${this.userAttr}`)}api(t){const r=this.config.api_prefix||"/api";t.get(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.listAccounts(t,r)),t.post(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.createAccount(t,r)),t.delete(`${r}/v1/users/:user/app-accounts/:uid`,(t,r)=>this.deleteAccount(t,r)),this.logger.info(`AppAccounts API registered at ${r}/v1/users/:user/app-accounts`)}async resolvePrincipal(t,r,s){const a=((await this.server.ldap.search({scope:"sub",filter:`(${this.userAttr}=${e(t)})`,paged:!1,...s?{attributes:s}:{}},this.config.ldap_base||"")).searchEntries||[]).filter(t=>!i(t.dn,this.applicativeAccountBase));return 0===a.length?(r.status(404).json({error:`User ${t} not found`}),null):a.length>1?(this.logger.error(`${this.name}: Ambiguous principal lookup for ${t}: ${a.length} entries share ${this.userAttr}`),r.status(409).json({error:`Multiple users share ${this.userAttr} ${t}`}),null):a[0]}async listAccounts(t,r){if(!s(t,r))return;const i=t.params.user;try{const t=await this.resolvePrincipal(i,r,[this.mailAttr]);if(!t)return;const s=t[this.mailAttr];if(!s)return void r.status(400).json({error:`User ${i} has no mail attribute`});const a=Array.isArray(s)?String(s[0]):String(s),o=await this.server.ldap.search({scope:"sub",filter:`(${this.mailAttr}=${e(a)})`,paged:!1},this.applicativeAccountBase),n=(o.searchEntries||[]).filter(t=>{const r=t.uid;return(Array.isArray(r)?String(r[0]):String(r)).toLowerCase()!==a.toLowerCase()}).map(t=>{const r=t.uid,s=Array.isArray(r)?String(r[0]):String(r),e=t.description,i=e?Array.isArray(e)?String(e[0]):String(e):void 0;return{uid:s,...i?{name:i}:{}}}).sort((t,r)=>t.uid.localeCompare(r.uid));r.json(n)}catch(t){this.logger.error(`${this.name}: Failed to list accounts for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}async createAccount(t,r){if(!s(t,r))return;const i=t.params.user,{name:o}=t.body||{};try{const s=await this.resolvePrincipal(i,r);if(!s)return;const n=s[this.mailAttr];if(!n)return void r.status(400).json({error:`User ${i} has no mail attribute`});const c=Array.isArray(n)?String(n[0]):String(n),u=this.appUidPrefix(i),p=(await this.server.ldap.search({scope:"sub",filter:`(${this.mailAttr}=${e(c)})`,paged:!1},this.applicativeAccountBase)).searchEntries||[];if(p.filter(t=>{const r=t.uid;return(Array.isArray(r)?String(r[0]):String(r)).toLowerCase()!==c.toLowerCase()}).length>=this.maxAppAccounts)return void r.status(400).json({error:`Maximum number of accounts (${this.maxAppAccounts}) reached`});let l,h=0;do{if(l=`${u}_${this.generateAccountId()}`,h++,h>100)throw new Error("Failed to generate unique account ID after 100 attempts")}while(await this.uidExists(l));const d=this.generatePassword(),A={objectClass:["inetOrgPerson"],uid:l,userPassword:d},g=[...new Set([...this.config.applicative_account_attribute||[],this.mailAttr])].filter(t=>"objectClass"!==t&&"description"!==t);for(const t of g)if(s[t]){const r=s[t];Array.isArray(r)?A[t]=r.map(t=>Buffer.isBuffer(t)?t.toString():String(t)):A[t]=Buffer.isBuffer(r)?r.toString():String(r)}o&&(A.description=o);const f=`uid=${a(l)},${this.applicativeAccountBase}`;await this.server.ldap.add(f,A,t),this.logger.info(`${this.name}: Created applicative account ${f} for user ${i}`);const m=`uid=${a(c)},${this.applicativeAccountBase}`;try{await this.server.ldap.modify(m,{add:{userPassword:d}},t)}catch(s){this.logger.error(`${this.name}: Failed to add password to principal account ${m}:`,s);try{await this.server.ldap.delete(f,t)}catch(t){this.logger.error(`${this.name}: Failed to roll back applicative account ${f}:`,t)}return void r.status(500).json({error:"Failed to register the password on the principal account"})}r.json({uid:l,pwd:d,mail:c})}catch(t){this.logger.error(`${this.name}: Failed to create account for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}appUidPrefix(t){return t.replace(/[^A-Za-z0-9_-]/g,"_")}async uidExists(t){return((await this.server.ldap.search({scope:"sub",filter:`(uid=${e(t)})`,paged:!1,attributes:["uid"]},this.applicativeAccountBase)).searchEntries||[]).length>0}async deleteAccount(t,r){if(!s(t,r))return;const i=t.params.user,o=t.params.uid;try{const s=await this.server.ldap.search({scope:"sub",filter:`(uid=${e(o)})`,paged:!1,attributes:["uid",this.mailAttr,"userPassword"]},this.applicativeAccountBase),n=s.searchEntries?.[0];if(!n)return this.logger.warn(`${this.name}: Account ${o} already deleted or not found`),void r.json({uid:o});const c=n[this.mailAttr],u=c?Array.isArray(c)?String(c[0]):String(c):null;let p=i;if(this.userAttr!==this.mailAttr){const t=await this.resolvePrincipal(i,r,[this.mailAttr]);if(!t)return;const s=t[this.mailAttr],e=s?Array.isArray(s)?String(s[0]):String(s):null;if(!e)return void r.status(400).json({error:`User ${i} has no mail attribute`});p=e}if(!u||u.toLowerCase()!==p.toLowerCase())return void r.status(403).json({error:`Account ${o} does not belong to user ${i}`});const l=n.userPassword;if(!l)return this.logger.error(`${this.name}: Account ${o} has no readable userPassword, refusing to delete: the bind DN needs read access on userPassword in the applicative branch`),void r.status(500).json({error:`Cannot revoke account ${o}: its password is not readable`});const h=`uid=${a(u)},${this.applicativeAccountBase}`;try{const r=Array.isArray(l)?l[0]:l;await this.server.ldap.modify(h,{delete:{userPassword:r}},t)}catch(t){const s=String(t.message??"");if(16!==t.code&&!/no ?such ?attribute/i.test(s))return this.logger.error(`${this.name}: Failed to delete password from principal account ${h}:`,t),void r.status(500).json({error:`Failed to revoke the password of account ${o}`});this.logger.warn(`${this.name}: Password of ${o} was already absent from principal account ${h}, resuming deletion`)}const d=n.dn;await this.server.ldap.delete(d,t),this.logger.info(`${this.name}: Deleted applicative account ${d}`),r.json({uid:o})}catch(t){this.logger.error(`${this.name}: Failed to delete account ${o} for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}generateAccountId(){return`c${t(0,1e8).toString().padStart(8,"0")}`}generatePassword(){const r="ABCDEFGHJKMNPQRSTUVWXYZ",s="abcdefghjkmnpqrstuvwxyz",e="23456789",i="@!#$%",a=[];for(let o=0;o<6;o++){const o=t(0,23),n=t(0,23),c=t(0,8),u=t(0,5),p=[r[o],s[n],e[c],i[u]];for(let r=p.length-1;r>0;r--){const s=t(0,r+1);[p[r],p[s]]=[p[s],p[r]]}a.push(p.join(""))}return a.join("-")}getConfigApiData(){const t=this.config.api_prefix||"/api";return{enabled:!0,base:this.applicativeAccountBase,maxAccounts:this.maxAppAccounts,mailAttribute:this.mailAttr,endpoints:{list:`${t}/v1/users/:user/app-accounts`,create:`${t}/v1/users/:user/app-accounts`,delete:`${t}/v1/users/:user/app-accounts/:uid`}}}}export{AppAccountsApi as default};
|