ldap-rest 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abstract/plugin.js +1 -1
- package/dist/abstract/twakePlugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/ldap/onChange.js +1 -1
- package/dist/plugins/twake/drive.js +1 -0
- package/dist/src/abstract/plugin.d.ts +1 -1
- package/dist/src/config/args.d.ts +6 -0
- package/dist/src/hooks.d.ts +1 -0
- package/dist/src/lib/utils.d.ts +17 -0
- package/dist/src/plugins/twake/drive.d.ts +76 -0
- package/docs/usage/plugins/integrations/drive.md +646 -0
- package/package.json +7 -2
- package/static/browser/shared/utils/form.js +1 -1
package/dist/abstract/plugin.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{asyncHandler,escapeDnValue,escapeLdapFilter,validateDnValue}from"../lib/utils.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DmPlugin{constructor(r){this.registeredHooks={},this.server=r,this.config=r.config,this.logger=r.logger,this.registeredHooks=r.hooks}opNumber(){return this.server.operationSequence++}}export{DmPlugin as default};
|
|
1
|
+
export{asyncHandler,escapeDnValue,escapeLdapFilter,unescapeDnValue,validateDnValue}from"../lib/utils.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DmPlugin{constructor(r){this.registeredHooks={},this.server=r,this.config=r.config,this.logger=r.logger,this.registeredHooks=r.hooks}opNumber(){return this.server.operationSequence++}}export{DmPlugin as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node-fetch";import
|
|
1
|
+
import t from"node-fetch";import e from"p-limit";import r from"./plugin.js";class TwakePlugin extends r{constructor(t,r,s,i){if(super(t),this.webadminUrl=this.config[r]||"",this.webadminToken=this.config[s]||"",!this.webadminUrl)throw new Error(`Twake plugin: ${r} is required`);this.webadminToken||this.logger.warn(`Twake plugin: No authentication token configured (${s}) - requests will be unauthenticated`);const n="number"==typeof this.config[i]?this.config[i]:10;this.requestLimit=e(n),this.logger.info(`Twake plugin HTTP request concurrency limit set to ${n}`),this.mailAttr=this.config.mail_attribute||"mail",this.displayNameAttr=this.config.display_name_attribute||"displayName"}createHeaders(t){const e={};return this.webadminToken&&(e.Authorization=`Bearer ${this.webadminToken}`),t&&(e["Content-Type"]=t),e}async callWebAdminApi(e,r,s,i,n,a){return this.requestLimit(async()=>{const o={plugin:this.name,event:e,result:"error",dn:i,...a};try{const i={method:s,headers:this.createHeaders(n?"application/json":void 0)};n&&(i.body=n);const a=await t(r,i);a.ok?this.logger.info({...o,result:"success",http_status:a.status,url:r}):this.shouldIgnoreError(a.status,e)?this.logger.debug({...o,result:"ignored",http_status:a.status,http_status_text:a.statusText,url:r}):this.logger.error({...o,http_status:a.status,http_status_text:a.statusText,url:r})}catch(t){this.logger.error({...o,error:t,url:r})}})}shouldIgnoreError(t,e){return 409===t&&e.includes("Alias")}attributeToString(t){return t?Array.isArray(t)?t.length>0?String(t[0]):null:String(t):null}async ldapGetAttributes(t,e){try{const r=e&&e.length>0?e:void 0,s=await this.server.ldap.search({paged:!1,scope:"base",attributes:r},t);return s.searchEntries&&s.searchEntries.length>0?s.searchEntries[0]:null}catch(e){return this.logger.debug(`Could not fetch attributes from DN ${t}: ${e}`),null}}getAliases(t){if(!t)return[];return(Array.isArray(t)?t:[t]).map(t=>Buffer.isBuffer(t)?t.toString("utf-8"):String(t)).map(t=>this.normalizeAlias(t))}normalizeAlias(t){return t.toLowerCase().startsWith("smtp:")?t.substring(5):t}extractMailDomain(t){const e=t.split("@");return 2===e.length?e[1]:null}extractDomainFromDn(t){const e=t.match(/dc=([^,]+)/gi);return e?e.map(t=>t.substring(3)).join("."):"example.com"}}export{TwakePlugin,TwakePlugin as default};
|
package/dist/bin/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fileURLToPath as e}from"url";import{join as r,dirname as i}from"path";import s from"express";import o from"body-parser";import{parseConfig as t}from"../lib/parseConfig.js";import n from"../config/args.js";import a from"../lib/ldapActions.js";import{buildLogger as l}from"../logger/winston.js";import{setLogger as d}from"../lib/expressFormatedResponses.js";import g from"../plugins/priority.json.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,launchHooks,launchHooksChained,parseDn,transformSchemas,validateDnValue}from"../lib/utils.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this._errorMiddlewareSetup=!1,this.config=t(n),this.app=s(),this.app.use(o.json()),this.app.use(o.urlencoded({extended:!0})),this.logger=l(this.config),this.ldap=new a(this),d(this.logger),this.operationSequence=0;const e=[];if(this.config.plugin){const r=this.config.plugin.find(e=>e.includes("configApi"));let i=this.config.plugin.filter(e=>!e.includes("configApi"));const s=(async()=>{for(const e of g)i.includes(e)&&(i=i.filter(r=>r!==e),await this.loadPlugin(e))})();e.push(s),e.push(s.then(async()=>{const e=i.map(e=>this.loadPlugin(e));await Promise.all(e)})),r&&e.push(Promise.all(e).then(()=>this.loadPlugin(r)))}this.ready=new Promise((r,i)=>{e.length>0?Promise.all(e).then(()=>{this.setupErrorMiddleware(),r()}).catch(e=>i(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),r())})}setupErrorMiddleware(){if(this._errorMiddlewareSetup&&this._errorMiddleware){const e=this.app._router?.stack;if(e){const r=e.findIndex(e=>e.handle===this._errorMiddleware);-1!==r&&e.splice(r,1)}}this._errorMiddleware=(e,r,i,s)=>{const o="statusCode"in e?e.statusCode:500;if(o>=400&&o<500)return this.logger.warn(`Client error ${o} in request ${r.method} ${r.path}: ${e.message}`),i.headersSent?void 0:i.status(o).json({error:e.message});this.logger.error(`Server error ${o} in request ${r.method} ${r.path}: ${e.message}`,{stack:e.stack,url:r.url,method:r.method}),i.headersSent||i.status(o).json({error:"Internal Server Error",message:this.config.debug?e.message:"An error occurred"})},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}run(){return process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack})}),process.on("unhandledRejection",(e,r)=>{this.logger.error(`Unhandled promise rejection: ${e}`,{reason:e,promise:r})}),new Promise((e,r)=>{this.server=this.app.listen(this.config.port,i=>{i?(this.logger.error(`Error starting server: ${i}`),r(i)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}loadPlugin(s){let o,t;if(/:/.test(s)){let e=s.substring(s.indexOf(":")+1);if(s=s.substring(0,s.indexOf(":")),/:/.test(e)){o=e.substring(0,e.indexOf(":")),o||(o=void 0),e=e.substring(e.indexOf(":")+1);try{t=JSON.parse(e),"object"!=typeof t?(this.logger.error(`Overrides for plugin ${s} are not valid: ${e}`),t=void 0):this.logger.debug(`Overrides for plugin ${o||s}: ${e}`)}catch(r){this.logger.error(`Failed to parse overrides for plugin ${s}: ${r}, using ${e}`),t=void 0}}else o=e,o||(o=void 0)}else o=void 0;return this.logger.debug(`Loading plugin ${s}`),s.startsWith("core/")&&(s=s.replace("core/",r(i(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js")),new Promise((e,r)=>{import(s).then(async i=>{let n;if(i&&i.default&&(i=i.default),t){const e={...this.config,...t};n=new i({...this,config:e})}else n=new i(this);if(!n)return r(new Error(`Unable to load ${s}`));e(await this.registerPlugin(s,n,o)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(e=>r(new Error(`Failed to load plugin ${s}: ${e}`)))})}async registerPlugin(e,r,i){if(r.name||(r.name=e),i&&(r.name=i),this.loadedPlugins[r.name])return this.logger.info(`Plugin ${e} already loaded as ${r.name}`),!1;if(this.logger.debug(`Registering plugin ${e} as ${r.name}`),r.dependencies)for(const e in r.dependencies)this.loadedPlugins[e]||(this.logger.debug(`Plugin ${r.name} depends on ${e}, loading it first`),await this.loadPlugin(r.dependencies[e]));if(r.api&&(this.logger.debug(`Plugin ${r.name} has API, registering it`),await r.api(this.app),this._errorMiddlewareSetup&&this.setupErrorMiddleware()),r.hooks)for(const e in r.hooks){this.logger.debug(`Plugin ${r.name} has hook ${e}, registering it`);const i=r.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!i||"function"!=typeof i)throw new Error(`Plugin ${r.name}: hook ${e} is invalid`);this.hooks[e].push(i)}return this.loadedPlugins[r.name]=r,!0}}export{DM};
|
|
1
|
+
import{fileURLToPath as e}from"url";import{join as r,dirname as i}from"path";import s from"express";import o from"body-parser";import{parseConfig as t}from"../lib/parseConfig.js";import n from"../config/args.js";import a from"../lib/ldapActions.js";import{buildLogger as l}from"../logger/winston.js";import{setLogger as d}from"../lib/expressFormatedResponses.js";import g from"../plugins/priority.json.js";export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,launchHooks,launchHooksChained,parseDn,transformSchemas,unescapeDnValue,validateDnValue}from"../lib/utils.js";export{BadGatewayError,BadRequestError,ConflictError,ForbiddenError,GatewayTimeoutError,HttpError,NotFoundError,ServiceUnavailableError,TooManyRequestsError,UnauthorizedError,UriTooLongError}from"../lib/errors.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this._errorMiddlewareSetup=!1,this.config=t(n),this.app=s(),this.app.use(o.json()),this.app.use(o.urlencoded({extended:!0})),this.logger=l(this.config),this.ldap=new a(this),d(this.logger),this.operationSequence=0;const e=[];if(this.config.plugin){const r=this.config.plugin.find(e=>e.includes("configApi"));let i=this.config.plugin.filter(e=>!e.includes("configApi"));const s=(async()=>{for(const e of g)i.includes(e)&&(i=i.filter(r=>r!==e),await this.loadPlugin(e))})();e.push(s),e.push(s.then(async()=>{const e=i.map(e=>this.loadPlugin(e));await Promise.all(e)})),r&&e.push(Promise.all(e).then(()=>this.loadPlugin(r)))}this.ready=new Promise((r,i)=>{e.length>0?Promise.all(e).then(()=>{this.setupErrorMiddleware(),r()}).catch(e=>i(new Error("Error loading plugins: "+e))):(this.setupErrorMiddleware(),r())})}setupErrorMiddleware(){if(this._errorMiddlewareSetup&&this._errorMiddleware){const e=this.app._router?.stack;if(e){const r=e.findIndex(e=>e.handle===this._errorMiddleware);-1!==r&&e.splice(r,1)}}this._errorMiddleware=(e,r,i,s)=>{const o="statusCode"in e?e.statusCode:500;if(o>=400&&o<500)return this.logger.warn(`Client error ${o} in request ${r.method} ${r.path}: ${e.message}`),i.headersSent?void 0:i.status(o).json({error:e.message});this.logger.error(`Server error ${o} in request ${r.method} ${r.path}: ${e.message}`,{stack:e.stack,url:r.url,method:r.method}),i.headersSent||i.status(o).json({error:"Internal Server Error",message:this.config.debug?e.message:"An error occurred"})},this.app.use(this._errorMiddleware),this._errorMiddlewareSetup=!0}run(){return process.on("uncaughtException",e=>{this.logger.error(`Uncaught exception: ${e.message}`,{stack:e.stack})}),process.on("unhandledRejection",(e,r)=>{this.logger.error(`Unhandled promise rejection: ${e}`,{reason:e,promise:r})}),new Promise((e,r)=>{this.server=this.app.listen(this.config.port,i=>{i?(this.logger.error(`Error starting server: ${i}`),r(i)):(this.logger.debug(`Server started on port ${this.config.port}`),e())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}loadPlugin(s){let o,t;if(/:/.test(s)){let e=s.substring(s.indexOf(":")+1);if(s=s.substring(0,s.indexOf(":")),/:/.test(e)){o=e.substring(0,e.indexOf(":")),o||(o=void 0),e=e.substring(e.indexOf(":")+1);try{t=JSON.parse(e),"object"!=typeof t?(this.logger.error(`Overrides for plugin ${s} are not valid: ${e}`),t=void 0):this.logger.debug(`Overrides for plugin ${o||s}: ${e}`)}catch(r){this.logger.error(`Failed to parse overrides for plugin ${s}: ${r}, using ${e}`),t=void 0}}else o=e,o||(o=void 0)}else o=void 0;return this.logger.debug(`Loading plugin ${s}`),s.startsWith("core/")&&(s=s.replace("core/",r(i(e(import.meta.url)),"..","plugins")+"/").replace(/$/,".js")),new Promise((e,r)=>{import(s).then(async i=>{let n;if(i&&i.default&&(i=i.default),t){const e={...this.config,...t};n=new i({...this,config:e})}else n=new i(this);if(!n)return r(new Error(`Unable to load ${s}`));e(await this.registerPlugin(s,n,o)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(e=>r(new Error(`Failed to load plugin ${s}: ${e}`)))})}async registerPlugin(e,r,i){if(r.name||(r.name=e),i&&(r.name=i),this.loadedPlugins[r.name])return this.logger.info(`Plugin ${e} already loaded as ${r.name}`),!1;if(this.logger.debug(`Registering plugin ${e} as ${r.name}`),r.dependencies)for(const e in r.dependencies)this.loadedPlugins[e]||(this.logger.debug(`Plugin ${r.name} depends on ${e}, loading it first`),await this.loadPlugin(r.dependencies[e]));if(r.api&&(this.logger.debug(`Plugin ${r.name} has API, registering it`),await r.api(this.app),this._errorMiddlewareSetup&&this.setupErrorMiddleware()),r.hooks)for(const e in r.hooks){this.logger.debug(`Plugin ${r.name} has hook ${e}, registering it`);const i=r.hooks[e];if(this.hooks[e]||(this.hooks[e]=[]),!i||"function"!=typeof i)throw new Error(`Plugin ${r.name}: hook ${e} is invalid`);this.hooks[e].push(i)}return this.loadedPlugins[r.name]=r,!0}}export{DM};
|
package/dist/config/args.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fileURLToPath as a}from"url";import{join as _,dirname as e}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",300,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",_(e(a(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",_(e(a(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",_(e(a(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""]];export{t as default};
|
|
1
|
+
import{fileURLToPath as a}from"url";import{join as _,dirname as e}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","notice"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL",["ldap://localhost"],"array","--ldap-urls"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",300,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",_(e(a(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--drive-quota-attribute","DM_DRIVE_QUOTA_ATTRIBUTE","twakeDriveQuota"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",_(e(a(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",_(e(a(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--twake-drive-webadmin-url","DM_TWAKE_DRIVE_WEBADMIN_URL",""],["--twake-drive-webadmin-token","DM_TWAKE_DRIVE_WEBADMIN_TOKEN",""],["--twake-drive-concurrency","DM_TWAKE_DRIVE_CONCURRENCY",10,"number"],["--twake-drive-domain-attribute","DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE","twakeCozyDomain"],["--twake-drive-default-domain-template","DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",""],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--applicative-account-base","DM_APPLICATIVE_ACCOUNT_BASE",""],["--max-app-accounts","DM_MAX_APP_ACCOUNTS",5,"number"],["--ldap-operational-attribute","DM_LDAP_OPERATIONAL_ATTRIBUTES",["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],"array","--ldap-operational-attributes"],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--auth-totp","DM_AUTH_TOTP",[],"array","--auth-totps"],["--auth-totp-window","DM_AUTH_TOTP_WINDOW",1,"number"],["--auth-totp-step","DM_AUTH_TOTP_STEP",30,"number"],["--auth-hmac","DM_AUTH_HMAC",[],"array","--auth-hmacs"],["--auth-hmac-window","DM_AUTH_HMAC_WINDOW",12e4,"number"],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"],["--trusted-proxy","DM_TRUSTED_PROXIES",[],"array","--trusted-proxies"],["--trusted-proxy-auth-header","DM_TRUSTED_PROXY_AUTH_HEADER","Auth-User"],["--ppolicy-default-dn","DM_PPOLICY_DEFAULT_DN",""],["--ppolicy-warn-days","DM_PPOLICY_WARN_DAYS",14,"number"],["--ppolicy-validate-complexity","DM_PPOLICY_VALIDATE_COMPLEXITY",!1,"boolean"],["--ppolicy-min-length","DM_PPOLICY_MIN_LENGTH",12,"number"],["--ppolicy-require-uppercase","DM_PPOLICY_REQUIRE_UPPERCASE",!0,"boolean"],["--ppolicy-require-lowercase","DM_PPOLICY_REQUIRE_LOWERCASE",!0,"boolean"],["--ppolicy-require-digit","DM_PPOLICY_REQUIRE_DIGIT",!0,"boolean"],["--ppolicy-require-special","DM_PPOLICY_REQUIRE_SPECIAL",!0,"boolean"],["--ldap-users-base","DM_LDAP_USERS_BASE",""]];export{t as default};
|
package/dist/lib/utils.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getLogger as e}from"./expressFormatedResponses.js";const
|
|
1
|
+
import{getLogger as e}from"./expressFormatedResponses.js";const t=e(),r=new Map;function getCompiledRegex(e,t){const n=t?`${e}:${t}`:e;let a=r.get(n);return a||(a=new RegExp(e,t),r.set(n,a)),a}function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const launchHooks=async(e,...r)=>{if(e)for(const n of e)if(n)try{await n(...r)}catch(e){t.error("Hook error",e)}},launchHooksChained=async(e,t)=>{if(e)for(const r of e)r&&(t=await r(t));return t},transformSchemas=(e,t)=>e.toString().replace(/__(\S+)__/g,(e,r)=>{if(!r||"string"!=typeof r)return e;const n=r.trim().toLowerCase();return t[n]?"object"!=typeof t[n]?t[n]:JSON.stringify(t[n]):e});function escapeLdapFilter(e){return e.replace(/\\/g,"\\5c").replace(/\*/g,"\\2a").replace(/\(/g,"\\28").replace(/\)/g,"\\29").replace(/\0/g,"\\00")}function escapeDnValue(e){return e.replace(/\\/g,"\\\\").replace(/,/g,"\\,").replace(/\+/g,"\\+").replace(/"/g,'\\"').replace(/</g,"\\<").replace(/>/g,"\\>").replace(/;/g,"\\;").replace(/=/g,"\\=").replace(/\0/g,"\\00").replace(/^\s/,"\\ ").replace(/\s$/,"\\ ").replace(/^#/,"\\#")}function unescapeDnValue(e){let t="",r=0;for(;r<e.length;)if("\\"===e[r]&&r+1<e.length){const n=e[r+1];if(/[0-9a-fA-F]/.test(n)&&r+2<e.length){const n=e.substring(r+1,r+3);if(/^[0-9a-fA-F]{2}$/.test(n)){t+=String.fromCharCode(parseInt(n,16)),r+=3;continue}}t+=n,r+=2}else t+=e[r],r++;return t}function validateDnValue(e,t){if(null==e||"string"!=typeof e)throw new Error(`${t} must be a string`);if(0===e.trim().length)throw new Error(`${t} must be a non-empty string`);if(/[\x00-\x1F\x7F]/.test(e))throw new Error(`${t} contains invalid control characters`);if(/[\u200B-\u200F\uFEFF]/.test(e))throw new Error(`${t} contains invalid invisible characters`)}function parseDn(e){const t=[];let r="",n=!1;for(let a=0;a<e.length;a++){const o=e[a];n?(r+=o,n=!1):"\\"===o?(r+=o,n=!0):","===o?(t.push(r.trim()),r=""):r+=o}return r&&t.push(r.trim()),t}function getParentDn(e){const t=parseDn(e);return t.length<=1?e:t.slice(1).join(",")}function getRdn(e){return parseDn(e)[0]||""}function isChildOf(e,t){const r=e.toLowerCase(),n=t.toLowerCase();if(!r.endsWith(n))return!1;if(r.length===n.length)return!1;return r.substring(0,r.length-n.length).endsWith(",")}const asyncHandler=e=>(t,r,n)=>{Promise.resolve(e(t,r,n)).catch(n)};export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,isChildOf,launchHooks,launchHooksChained,parseDn,transformSchemas,unescapeDnValue,validateDnValue};
|
|
@@ -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"};class OnLdapChange extends t{constructor(){super(...arguments),this.name="onLdapChange",this.roles=["consistency"],this.stack={},this.pendingDeletions=new Map,this.hooks={ldapadddone:t=>{const[e,n]=t,r={};for(const[t,e]of Object.entries(n))r[t]=[null,e];this.notify(e,r)},ldapmodifyrequest:async([t,e,n])=>{const r=await this.server.ldap.search({paged:!1},t);return 1==r.searchEntries.length?this.stack[n]=r.searchEntries[0]:this.logger.warn(`Could not find unique entry ${t} before modification, got ${r.searchEntries.length} entries`),[t,e,n]},ldapmodifydone:([t,e,n])=>{const r=this.stack[n];if(!r)return delete this.stack[n],void this.logger.warn(`Received a ldapmodifydone for an unknown operation (${n})`);const i={};if(e.add)for(const[t,n]of Object.entries(e.add))i[t]=[null,n];if(e.delete)if(Array.isArray(e.delete))for(const t of e.delete)i[t]=[r[t],null];else for(const[t,n]of Object.entries(e.delete))i[t]=[n,null];if(e.replace)for(const[t,n]of Object.entries(e.replace))i[t]=[r[t],n];this.notify(t,i)},ldapdeleterequest:async t=>{const e=Array.isArray(t)?t:[t];for(const t of e)try{const e=await this.server.ldap.search({scope:"base",paged:!1},t),n=e.searchEntries?.[0];n&&this.pendingDeletions.set(t,n)}catch(t){}return t},ldapdeletedone:t=>{const e=Array.isArray(t)?t:[t];for(const t of e){const e=this.pendingDeletions.get(t);if(e){this.pendingDeletions.delete(t);const n={};for(const[t,r]of Object.entries(e))"dn"!==t&&(n[t]=[r,null]);this.notify(t,n)}}}}}notify(t,r){e(this.server.hooks.onLdapChange,t,r);for(const[
|
|
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=>{const[e,n]=t,r={};for(const[t,e]of Object.entries(n))r[t]=[null,e];this.notify(e,r)},ldapmodifyrequest:async([t,e,n])=>{const r=await this.server.ldap.search({paged:!1},t);return 1==r.searchEntries.length?this.stack[n]=r.searchEntries[0]:this.logger.warn(`Could not find unique entry ${t} before modification, got ${r.searchEntries.length} entries`),[t,e,n]},ldapmodifydone:([t,e,n])=>{const r=this.stack[n];if(!r)return delete this.stack[n],void this.logger.warn(`Received a ldapmodifydone for an unknown operation (${n})`);const i={};if(e.add)for(const[t,n]of Object.entries(e.add))i[t]=[null,n];if(e.delete)if(Array.isArray(e.delete))for(const t of e.delete)i[t]=[r[t],null];else for(const[t,n]of Object.entries(e.delete))i[t]=[n,null];if(e.replace)for(const[t,n]of Object.entries(e.replace))i[t]=[r[t],n];this.notify(t,i)},ldapdeleterequest:async t=>{const e=Array.isArray(t)?t:[t];for(const t of e)try{const e=await this.server.ldap.search({scope:"base",paged:!1},t),n=e.searchEntries?.[0];n&&this.pendingDeletions.set(t,n)}catch(t){}return t},ldapdeletedone:t=>{const e=Array.isArray(t)?t:[t];for(const t of e){const e=this.pendingDeletions.get(t);if(e){this.pendingDeletions.delete(t);const n={};for(const[t,r]of Object.entries(e))"dn"!==t&&(n[t]=[r,null]);this.notify(t,n)}}}}}notify(t,r){e(this.server.hooks.onLdapChange,t,r);for(const[i,a]of Object.entries(n))if(this.config[i]&&r[this.config[i]])if("onLdapQuotaChange"===a||"onLdapForwardChange"===a||"onLdapAliasChange"===a)this.notifyAttributeChangeWithMail(this.config[i],a,t,r);else if("onLdapMailChange"===a)this.notifyAttributeChange(this.config[i],a,t,r);else if("onLdapDriveQuotaChange"===a){const[n,a]=r[this.config[i]]||[],s=n?Number(n):null,o=a?Number(a):null;s!==o&&e(this.server.hooks.onLdapDriveQuotaChange,t,s,o)}if(r.cn||r.givenName||r.sn){const n=this.reconstructDisplayName(r,0),i=this.reconstructDisplayName(r,1);e(this.server.hooks.onLdapDisplayNameChange,t,n,i)}}notifyAttributeChange(t,n,r,i,a=!1){const[s,o]=i[t]||[];void 0===s&&void 0===o||(a&&(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],r,s,o))}async notifyAttributeChangeWithMail(t,n,r,i){const[a,s]=i[t]||[];if(void 0===a&&void 0===s)return;const o=this.config.mail_attribute||"mail",h=i[o];let l;if(h)l=Array.isArray(h[1])?String(h[1][0]):String(h[1]);else try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[o]},r);if(1!==t.searchEntries.length)return void this.logger.warn(`Could not find mail for ${r}, skipping ${n} notification`);{const e=t.searchEntries[0][o];l=Array.isArray(e)?String(e[0]):String(e)}}catch(t){return void this.logger.error(`Error fetching mail for ${r}:`,t)}if("onLdapQuotaChange"===n){const t=a?Number(a):0,i=s?Number(s):0;t!==i&&e(this.server.hooks[n],r,l,t,i)}else if("onLdapForwardChange"===n||"onLdapAliasChange"===n){const t=a?Array.isArray(a)?a:[a]:[],i=s?Array.isArray(s)?s:[s]:[];(t.length>0||i.length>0)&&e(this.server.hooks[n],r,l,t,i)}}reconstructDisplayName(t,e){const getValue=n=>{if(!t[n])return null;const r=t[n][e];return r?Array.isArray(r)?r.length>0?String(r[0]):null:String(r):null},n=getValue("cn");if(n)return n;const r=getValue("givenName"),i=getValue("sn");if(r||i){const t=[];return r&&t.push(r),i&&t.push(i),t.join(" ")}return null}}export{OnLdapChange as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import t from"node-fetch";import{TwakePlugin as i}from"../../abstract/twakePlugin.js";class Drive extends i{constructor(t){super(t,"twake_drive_webadmin_url","twake_drive_webadmin_token","twake_drive_concurrency"),this.name="drive",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange"},this.hooks={onLdapMailChange:async(t,i,e)=>{const a=i?Array.isArray(i)?String(i[0]):String(i):null,n=e?Array.isArray(e)?String(e[0]):String(e):null;if(!a)return void this.logger.debug(`Skipping mail change for ${t}: oldmail is empty (mail attribute was added, not changed)`);if(!n)return void this.logger.debug(`Skipping mail change for ${t}: newmail is empty (mail attribute was deleted)`);const r=await this.getCozyDomain(t);r?await this.updateCozyInstance("onLdapMailChange",r,t,{Email:n}):this.logger.debug(`Skipping mail change for ${t}: no Cozy domain attribute (${this.cozyDomainAttr})`)},onLdapDisplayNameChange:async(t,i,e)=>{const a=await this.ldapGetAttributes(t,[this.cozyDomainAttr,this.displayNameAttr,"cn","givenName","sn"]);if(!a)return void this.logger.warn(`Cannot update Cozy display name: entry not found for ${t}`);const n=this.attributeToString(a[this.cozyDomainAttr]);if(!n)return void this.logger.debug(`Skipping display name change for ${t}: no Cozy domain attribute (${this.cozyDomainAttr})`);const r=this.getDisplayNameFromAttributes(a);r?await this.updateCozyInstance("onLdapDisplayNameChange",n,t,{PublicName:r}):this.logger.warn(`Cannot update Cozy display name: no display name found for ${t}`)},onLdapDriveQuotaChange:async(t,i,e)=>{if(null===e)return void this.logger.debug(`Skipping drive quota change for ${t}: quota was deleted`);const a=await this.getCozyDomain(t);a?await this.updateCozyInstance("onLdapDriveQuotaChange",a,t,{DiskQuota:String(e)}):this.logger.debug(`Skipping drive quota change for ${t}: no Twake Drive domain attribute (${this.cozyDomainAttr})`)}},this.cozyDomainAttr=this.config.twake_drive_domain_attribute||"twakeCozyDomain",this.defaultDomainTemplate=this.config.twake_drive_default_domain_template||"",this.templateAttrs=this.defaultDomainTemplate?[...this.defaultDomainTemplate.matchAll(/\{(\w+)\}/g)].map(t=>t[1]):[],this.driveQuotaAttr=this.config.drive_quota_attribute||"twakeDriveQuota"}isValidCozyDomain(t){if(!t||t.length>253)return!1;if(t.includes("/")||t.includes("\\")||t.includes("?")||t.includes("#"))return!1;return/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(t)}async getCozyDomain(t){const i=[this.cozyDomainAttr,...this.templateAttrs],e=await this.ldapGetAttributes(t,i),a=e?this.attributeToString(e[this.cozyDomainAttr]):null;if(a)return a;if(this.defaultDomainTemplate&&e){let t=this.defaultDomainTemplate;for(const i of this.templateAttrs){const a=this.attributeToString(e[i]);if(!a)return null;t=t.replace(`{${i}}`,a)}return t}return null}async updateCozyInstance(i,e,a,n){if(!this.isValidCozyDomain(e))return void this.logger.error({plugin:this.name,event:i,result:"error",dn:a,cozyDomain:e,error:"Invalid Cozy domain format (potential URL injection attempt)"});const r=new URL(`${this.webadminUrl}/instances/${e}`);r.searchParams.set("FromCloudery","true");for(const[t,i]of Object.entries(n))r.searchParams.set(t,i);return this.requestLimit(async()=>{const s={plugin:this.name,event:i,result:"error",dn:a,cozyDomain:e,...n};try{const i=await t(r.toString(),{method:"PATCH",headers:this.createHeaders("application/json")});i.ok?this.logger.info({...s,result:"success",http_status:i.status}):404===i.status?this.logger.debug({...s,result:"ignored",http_status:i.status,http_status_text:i.statusText,message:"Cozy instance not found (may not be provisioned yet)"}):this.logger.error({...s,http_status:i.status,http_status_text:i.statusText,url:r.toString()})}catch(t){this.logger.error({...s,error:t,url:r.toString()})}})}getDisplayNameFromAttributes(t){const i=this.attributeToString(t[this.displayNameAttr]);if(i)return i;const e=this.attributeToString(t.cn);if(e)return e;const a=this.attributeToString(t.givenName),n=this.attributeToString(t.sn);if(a||n){const t=[];return a&&t.push(a),n&&t.push(n),t.join(" ")}return null}async getDisplayNameFromDN(t){const i=[this.displayNameAttr,"cn","givenName","sn"],e=await this.ldapGetAttributes(t,i);return e?this.getDisplayNameFromAttributes(e):null}async getMailFromDN(t){const i=await this.ldapGetAttributes(t,[this.mailAttr]);return i?this.attributeToString(i[this.mailAttr]):null}async getDriveQuotaFromDN(t){const i=await this.ldapGetAttributes(t,[this.driveQuotaAttr]);if(!i)return null;const e=this.attributeToString(i[this.driveQuotaAttr]);return e?Number(e):null}async syncUserToCozy(t){const i=await this.ldapGetAttributes(t,[this.cozyDomainAttr,this.mailAttr,this.displayNameAttr,this.driveQuotaAttr,"cn","givenName","sn"]);if(!i)return this.logger.error(`Cannot sync user: entry not found for ${t}`),!1;const e=this.attributeToString(i[this.cozyDomainAttr]);if(!e)return this.logger.error(`Cannot sync user: no Twake Drive domain attribute for ${t}`),!1;const a=this.attributeToString(i[this.mailAttr]),n=this.getDisplayNameFromAttributes(i),r=this.attributeToString(i[this.driveQuotaAttr]),s={};return a&&(s.Email=a),n&&(s.PublicName=n),r&&(s.DiskQuota=r),0===Object.keys(s).length?(this.logger.warn(`No attributes to sync for ${t}`),!1):(await this.updateCozyInstance("syncUserToCozy",e,t,s),!0)}async blockInstance(t,i){const e=await this.getCozyDomain(t);if(!e)return this.logger.error(`Cannot block instance: no Cozy domain for ${t}`),!1;const a={Blocked:"true"};return i&&(a.BlockingReason=i),await this.updateCozyInstance("blockInstance",e,t,a),!0}async unblockInstance(t){const i=await this.getCozyDomain(t);return i?(await this.updateCozyInstance("unblockInstance",i,t,{Blocked:"false"}),!0):(this.logger.error(`Cannot unblock instance: no Cozy domain for ${t}`),!1)}}export{Drive as default};
|
|
@@ -6,7 +6,7 @@ import type { Express } from 'express';
|
|
|
6
6
|
import type winston from 'winston';
|
|
7
7
|
import type { Config, DM } from '../bin';
|
|
8
8
|
import type { Hooks, MaybePromise } from '../hooks';
|
|
9
|
-
export { asyncHandler, escapeDnValue, escapeLdapFilter, validateDnValue, } from '../lib/utils';
|
|
9
|
+
export { asyncHandler, escapeDnValue, unescapeDnValue, escapeLdapFilter, validateDnValue, } from '../lib/utils';
|
|
10
10
|
export { HttpError, BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, ConflictError, UriTooLongError, TooManyRequestsError, BadGatewayError, ServiceUnavailableError, GatewayTimeoutError, } from '../lib/errors';
|
|
11
11
|
export type Role = 'auth' | 'authz' | 'protect' | 'api' | 'logging' | 'demo' | 'consistency' | 'configurable';
|
|
12
12
|
export default abstract class DmPlugin {
|
|
@@ -92,6 +92,7 @@ export interface Config {
|
|
|
92
92
|
alias_attribute?: string;
|
|
93
93
|
forward_attribute?: string;
|
|
94
94
|
display_name_attribute?: string;
|
|
95
|
+
drive_quota_attribute?: string;
|
|
95
96
|
james_webadmin_url?: string;
|
|
96
97
|
james_webadmin_token?: string;
|
|
97
98
|
james_signature_template?: string;
|
|
@@ -100,6 +101,11 @@ export interface Config {
|
|
|
100
101
|
james_init_delay?: number;
|
|
101
102
|
james_mailing_list_branch?: string[];
|
|
102
103
|
james_mailbox_type_attribute?: string;
|
|
104
|
+
twake_drive_webadmin_url?: string;
|
|
105
|
+
twake_drive_webadmin_token?: string;
|
|
106
|
+
twake_drive_concurrency?: number;
|
|
107
|
+
twake_drive_domain_attribute?: string;
|
|
108
|
+
twake_drive_default_domain_template?: string;
|
|
103
109
|
calendar_webadmin_url?: string;
|
|
104
110
|
calendar_webadmin_token?: string;
|
|
105
111
|
calendar_concurrency?: number;
|
package/dist/src/hooks.d.ts
CHANGED
|
@@ -53,6 +53,7 @@ export interface Hooks {
|
|
|
53
53
|
onLdapQuotaChange?: (dn: string, mail: string, oldQuota: number, newQuota: number) => MaybePromise<void>;
|
|
54
54
|
onLdapForwardChange?: (dn: string, mail: string, oldForwards: string[], newForwards: string[]) => MaybePromise<void>;
|
|
55
55
|
onLdapDisplayNameChange?: (dn: string, oldDisplayName: string | null, newDisplayName: string | null) => MaybePromise<void>;
|
|
56
|
+
onLdapDriveQuotaChange?: (dn: string, oldDriveQuota: number | null, newDriveQuota: number | null) => MaybePromise<void>;
|
|
56
57
|
/** externalUsersInGroup */
|
|
57
58
|
externaluserentry?: ChainedHook<[string, AttributesList]>;
|
|
58
59
|
externaluseradded?: (dn: string, mail: string) => MaybePromise<void>;
|
package/dist/src/lib/utils.d.ts
CHANGED
|
@@ -58,6 +58,23 @@ export declare function escapeLdapFilter(value: string): string;
|
|
|
58
58
|
* ```
|
|
59
59
|
*/
|
|
60
60
|
export declare function escapeDnValue(value: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* Unescape special characters in LDAP DN attribute values according to RFC 4514
|
|
63
|
+
* Reverses the escaping done by escapeDnValue
|
|
64
|
+
*
|
|
65
|
+
* @param value - The escaped value to unescape
|
|
66
|
+
* @returns The unescaped original value
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* unescapeDnValue('Smith\\, John')
|
|
71
|
+
* // => 'Smith, John'
|
|
72
|
+
*
|
|
73
|
+
* unescapeDnValue('user\\+admin')
|
|
74
|
+
* // => 'user+admin'
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export declare function unescapeDnValue(value: string): string;
|
|
61
78
|
/**
|
|
62
79
|
* Validate a value intended for use in LDAP DN attribute values
|
|
63
80
|
* Rejects control characters and other problematic inputs
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import TwakePlugin from '../../abstract/twakePlugin';
|
|
2
|
+
import { type Role } from '../../abstract/plugin';
|
|
3
|
+
import type { DM } from '../../bin';
|
|
4
|
+
import { Hooks } from '../../hooks';
|
|
5
|
+
export default class Drive extends TwakePlugin {
|
|
6
|
+
name: string;
|
|
7
|
+
roles: Role[];
|
|
8
|
+
dependencies: {
|
|
9
|
+
onLdapChange: string;
|
|
10
|
+
};
|
|
11
|
+
private cozyDomainAttr;
|
|
12
|
+
private defaultDomainTemplate;
|
|
13
|
+
private templateAttrs;
|
|
14
|
+
private driveQuotaAttr;
|
|
15
|
+
constructor(server: DM);
|
|
16
|
+
/**
|
|
17
|
+
* Validate Cozy domain format to prevent URL injection attacks
|
|
18
|
+
* @param domain Cozy domain to validate
|
|
19
|
+
* @returns true if domain format is valid
|
|
20
|
+
*/
|
|
21
|
+
private isValidCozyDomain;
|
|
22
|
+
/**
|
|
23
|
+
* Get Cozy domain for a user from LDAP
|
|
24
|
+
* Falls back to template-based domain if attribute not found
|
|
25
|
+
* @param dn User's LDAP DN
|
|
26
|
+
* @returns Cozy domain or null if not found
|
|
27
|
+
*/
|
|
28
|
+
getCozyDomain(dn: string): Promise<string | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Update Cozy instance via Admin API
|
|
31
|
+
* @param hookname Hook name for logging
|
|
32
|
+
* @param cozyDomain Cozy instance domain
|
|
33
|
+
* @param dn User's LDAP DN
|
|
34
|
+
* @param params Query parameters to update
|
|
35
|
+
*/
|
|
36
|
+
private updateCozyInstance;
|
|
37
|
+
hooks: Hooks;
|
|
38
|
+
/**
|
|
39
|
+
* Extract display name from LDAP attributes
|
|
40
|
+
* Fallback logic: displayName → cn → givenName+sn
|
|
41
|
+
*/
|
|
42
|
+
private getDisplayNameFromAttributes;
|
|
43
|
+
/**
|
|
44
|
+
* Public method to get display name from DN
|
|
45
|
+
* Useful for external integrations
|
|
46
|
+
*/
|
|
47
|
+
getDisplayNameFromDN(dn: string): Promise<string | null>;
|
|
48
|
+
/**
|
|
49
|
+
* Public method to get mail from DN
|
|
50
|
+
* Useful for external integrations
|
|
51
|
+
*/
|
|
52
|
+
getMailFromDN(dn: string): Promise<string | null>;
|
|
53
|
+
/**
|
|
54
|
+
* Public method to get drive quota from DN (in bytes)
|
|
55
|
+
* Useful for external integrations
|
|
56
|
+
*/
|
|
57
|
+
getDriveQuotaFromDN(dn: string): Promise<number | null>;
|
|
58
|
+
/**
|
|
59
|
+
* Public method to manually sync a user to Twake Drive
|
|
60
|
+
* Useful for initial sync or manual refresh
|
|
61
|
+
*/
|
|
62
|
+
syncUserToCozy(dn: string): Promise<boolean>;
|
|
63
|
+
/**
|
|
64
|
+
* Block a Cozy instance
|
|
65
|
+
* @param dn User's LDAP DN
|
|
66
|
+
* @param reason Optional blocking reason (e.g., 'PAYMENT_FAILED', 'LOGIN_FAILED', 'SUSPENDED')
|
|
67
|
+
* @returns true if successful, false otherwise
|
|
68
|
+
*/
|
|
69
|
+
blockInstance(dn: string, reason?: string): Promise<boolean>;
|
|
70
|
+
/**
|
|
71
|
+
* Unblock a Cozy instance
|
|
72
|
+
* @param dn User's LDAP DN
|
|
73
|
+
* @returns true if successful, false otherwise
|
|
74
|
+
*/
|
|
75
|
+
unblockInstance(dn: string): Promise<boolean>;
|
|
76
|
+
}
|
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
# Twake Drive Plugin
|
|
2
|
+
|
|
3
|
+
Synchronize LDAP user changes with Twake Drive via Admin API.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `twake/drive` plugin automatically synchronizes email address, display name, and disk quota changes from LDAP to [Twake Drive](https://twake.app/).
|
|
8
|
+
It listens to `onChange` hooks and updates Twake Drive instances via the Admin API.
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
1. **Twake Drive / Cozy** instance with Admin API enabled
|
|
13
|
+
2. **onChange plugin** loaded to detect LDAP changes
|
|
14
|
+
3. **Cozy domain** - either:
|
|
15
|
+
- Per-user attribute in LDAP schema (e.g., `twakeCozyDomain`), or
|
|
16
|
+
- Default domain template configured (e.g., `{uid}.mycompany.cloud`)
|
|
17
|
+
|
|
18
|
+
## Configuration
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx ldap-rest \
|
|
22
|
+
--plugin core/ldap/onChange \
|
|
23
|
+
--plugin core/ldap/flatGeneric \
|
|
24
|
+
--plugin twake/drive \
|
|
25
|
+
--ldap-flat-schema ./static/schemas/twake/users.json \
|
|
26
|
+
--mail-attribute mail \
|
|
27
|
+
--twake-drive-domain-attribute twakeCozyDomain \
|
|
28
|
+
--twake-drive-webadmin-url http://cozy-admin:6060 \
|
|
29
|
+
--twake-drive-webadmin-token "your-admin-token" \
|
|
30
|
+
...
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Environment Variables:**
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
DM_TWAKE_DRIVE_WEBADMIN_URL="http://cozy-admin:6060"
|
|
37
|
+
DM_TWAKE_DRIVE_WEBADMIN_TOKEN="your-admin-token"
|
|
38
|
+
DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE="twakeCozyDomain"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Parameters
|
|
42
|
+
|
|
43
|
+
| Parameter | Description | Default |
|
|
44
|
+
| --------------------------------------- | ------------------------------------------------------------- | ----------------- |
|
|
45
|
+
| `--twake-drive-webadmin-url` | Twake Drive Admin API base URL (required) | - |
|
|
46
|
+
| `--twake-drive-webadmin-token` | Bearer token for Admin authentication | - |
|
|
47
|
+
| `--mail-attribute` | LDAP attribute for email | `mail` |
|
|
48
|
+
| `--display-name-attribute` | LDAP attribute for display name | `displayName` |
|
|
49
|
+
| `--drive-quota-attribute` | LDAP attribute for disk quota (in bytes) | `twakeDriveQuota` |
|
|
50
|
+
| `--twake-drive-domain-attribute` | LDAP attribute storing user's Twake Drive domain | `twakeCozyDomain` |
|
|
51
|
+
| `--twake-drive-default-domain-template` | Template for Twake Drive domain (e.g., `{uid}.company.cloud`) | - |
|
|
52
|
+
| `--twake-drive-concurrency` | Maximum concurrent API requests | `10` |
|
|
53
|
+
|
|
54
|
+
## Cozy Domain Resolution
|
|
55
|
+
|
|
56
|
+
The plugin determines each user's Cozy domain using the following logic:
|
|
57
|
+
|
|
58
|
+
1. **Per-user attribute** (if present): Use the `twakeCozyDomain` LDAP attribute value
|
|
59
|
+
2. **Default template** (fallback): Build domain from template with LDAP attribute placeholders
|
|
60
|
+
|
|
61
|
+
### Per-user Cozy domain attribute
|
|
62
|
+
|
|
63
|
+
Users can have an explicit `twakeCozyDomain` attribute for custom domains:
|
|
64
|
+
|
|
65
|
+
```ldif
|
|
66
|
+
dn: uid=jdoe,ou=users,dc=example,dc=com
|
|
67
|
+
uid: jdoe
|
|
68
|
+
mail: jdoe@example.com
|
|
69
|
+
twakeCozyDomain: john-doe.partner.cloud
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Default domain template
|
|
73
|
+
|
|
74
|
+
Configure a template for users without an explicit attribute. Use `{attribute}` placeholders:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
--twake-drive-default-domain-template "{uid}.mycompany.cloud"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Users without `twakeCozyDomain` will have their domain built from the template:
|
|
81
|
+
|
|
82
|
+
- User `uid=jdoe` → Cozy domain `jdoe.mycompany.cloud`
|
|
83
|
+
|
|
84
|
+
Other template examples:
|
|
85
|
+
|
|
86
|
+
- `"{mail}"` → use mail attribute directly as domain
|
|
87
|
+
- `"{cn}.cloud.example.com"` → use common name
|
|
88
|
+
|
|
89
|
+
### Combined usage
|
|
90
|
+
|
|
91
|
+
Both can be used together. The per-user attribute takes precedence:
|
|
92
|
+
|
|
93
|
+
```ldif
|
|
94
|
+
# User with custom domain (uses twakeCozyDomain)
|
|
95
|
+
dn: uid=partner1,ou=users,dc=example,dc=com
|
|
96
|
+
uid: partner1
|
|
97
|
+
twakeCozyDomain: partner1.external.cloud
|
|
98
|
+
|
|
99
|
+
# Regular user (uses default suffix → jdoe.mycompany.cloud)
|
|
100
|
+
dn: uid=jdoe,ou=users,dc=example,dc=com
|
|
101
|
+
uid: jdoe
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## How It Works
|
|
105
|
+
|
|
106
|
+
### Mail Address Changes
|
|
107
|
+
|
|
108
|
+
When a user's mail attribute changes:
|
|
109
|
+
|
|
110
|
+
1. **LDAP modify operation**
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
PUT /api/v1/ldap/users/jdoe
|
|
114
|
+
{
|
|
115
|
+
"replace": {
|
|
116
|
+
"mail": "john.doe@company.com"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
2. **onChange plugin** detects mail change and triggers `onLdapMailChange` hook
|
|
122
|
+
|
|
123
|
+
3. **Drive plugin** receives hook and calls Cozy Admin API:
|
|
124
|
+
|
|
125
|
+
```http
|
|
126
|
+
PATCH /instances/jdoe.mycompany.cloud?Email=john.doe@company.com&FromCloudery=true
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
4. **Cozy** updates the instance owner's email address
|
|
130
|
+
|
|
131
|
+
### Display Name Changes
|
|
132
|
+
|
|
133
|
+
When a user's display name changes:
|
|
134
|
+
|
|
135
|
+
1. **LDAP modify operation**
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
PUT /api/v1/ldap/users/jdoe
|
|
139
|
+
{
|
|
140
|
+
"replace": {
|
|
141
|
+
"displayName": "John M. Doe"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
2. **onChange plugin** triggers `onLdapDisplayNameChange` hook
|
|
147
|
+
|
|
148
|
+
3. **Drive plugin** updates Cozy instance:
|
|
149
|
+
|
|
150
|
+
```http
|
|
151
|
+
PATCH /instances/jdoe.mycompany.cloud?PublicName=John%20M.%20Doe&FromCloudery=true
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Display Name Fallback Logic
|
|
155
|
+
|
|
156
|
+
If `displayName` is not set, the plugin uses fallback logic:
|
|
157
|
+
|
|
158
|
+
1. `displayName` attribute (first choice)
|
|
159
|
+
2. `cn` (Common Name)
|
|
160
|
+
3. `givenName` + `sn` (First Name + Last Name)
|
|
161
|
+
|
|
162
|
+
### Disk Quota Changes
|
|
163
|
+
|
|
164
|
+
When a user's drive quota changes:
|
|
165
|
+
|
|
166
|
+
1. **LDAP modify operation**
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
PUT /api/v1/ldap/users/jdoe
|
|
170
|
+
{
|
|
171
|
+
"replace": {
|
|
172
|
+
"twakeDriveQuota": "5368709120"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
2. **onChange plugin** triggers `onLdapDriveQuotaChange` hook
|
|
178
|
+
|
|
179
|
+
3. **Drive plugin** updates Twake Drive instance:
|
|
180
|
+
|
|
181
|
+
```http
|
|
182
|
+
PATCH /instances/jdoe.company.cloud?DiskQuota=5368709120&FromCloudery=true
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
4. **Twake Drive** updates the instance disk quota
|
|
186
|
+
|
|
187
|
+
**Note:** The quota value is in bytes. Common values:
|
|
188
|
+
|
|
189
|
+
- 1 GB = `1073741824`
|
|
190
|
+
- 5 GB = `5368709120`
|
|
191
|
+
- 10 GB = `10737418240`
|
|
192
|
+
|
|
193
|
+
## Public Methods
|
|
194
|
+
|
|
195
|
+
Public methods available on the Drive plugin instance:
|
|
196
|
+
|
|
197
|
+
### Usage
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import type { DM } from 'ldap-rest';
|
|
201
|
+
import type Drive from 'ldap-rest/plugin-twake-drive';
|
|
202
|
+
|
|
203
|
+
// Get the Drive plugin instance
|
|
204
|
+
const drive = server.getPlugin('drive') as Drive;
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### getCozyDomain(dn)
|
|
208
|
+
|
|
209
|
+
Retrieve the Cozy domain for a user given their LDAP DN.
|
|
210
|
+
|
|
211
|
+
**Signature:**
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
async getCozyDomain(dn: string): Promise<string | null>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Example:**
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
const domain = await drive.getCozyDomain('uid=jdoe,ou=users,dc=example,dc=com');
|
|
221
|
+
// Returns: "jdoe.mycompany.cloud"
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### getDisplayNameFromDN(dn)
|
|
227
|
+
|
|
228
|
+
Retrieve the display name for a user using fallback logic.
|
|
229
|
+
|
|
230
|
+
**Signature:**
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
async getDisplayNameFromDN(dn: string): Promise<string | null>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Example:**
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
const name = await drive.getDisplayNameFromDN(
|
|
240
|
+
'uid=jdoe,ou=users,dc=example,dc=com'
|
|
241
|
+
);
|
|
242
|
+
// Returns: "John Doe"
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
### getMailFromDN(dn)
|
|
248
|
+
|
|
249
|
+
Retrieve the email address for a user.
|
|
250
|
+
|
|
251
|
+
**Signature:**
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
async getMailFromDN(dn: string): Promise<string | null>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Example:**
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
const mail = await drive.getMailFromDN('uid=jdoe,ou=users,dc=example,dc=com');
|
|
261
|
+
// Returns: "jdoe@example.com"
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
### getDriveQuotaFromDN(dn)
|
|
267
|
+
|
|
268
|
+
Retrieve the drive quota for a user (in bytes).
|
|
269
|
+
|
|
270
|
+
**Signature:**
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
async getDriveQuotaFromDN(dn: string): Promise<number | null>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Example:**
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
const quota = await drive.getDriveQuotaFromDN(
|
|
280
|
+
'uid=jdoe,ou=users,dc=example,dc=com'
|
|
281
|
+
);
|
|
282
|
+
// Returns: 5368709120 (5 GB)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
### syncUserToCozy(dn)
|
|
288
|
+
|
|
289
|
+
Manually synchronize a user's attributes (email, display name, disk quota) to their Twake Drive instance. Useful for initial sync or manual refresh.
|
|
290
|
+
|
|
291
|
+
**Signature:**
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
async syncUserToCozy(dn: string): Promise<boolean>
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Example:**
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
const success = await drive.syncUserToCozy(
|
|
301
|
+
'uid=jdoe,ou=users,dc=example,dc=com'
|
|
302
|
+
);
|
|
303
|
+
if (success) {
|
|
304
|
+
console.log('User synced successfully');
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
### blockInstance(dn, reason?)
|
|
311
|
+
|
|
312
|
+
Block a user's Cozy instance with an optional reason.
|
|
313
|
+
|
|
314
|
+
**Signature:**
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
async blockInstance(dn: string, reason?: string): Promise<boolean>
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Parameters:**
|
|
321
|
+
|
|
322
|
+
- `dn` - User's LDAP DN
|
|
323
|
+
- `reason` - Optional blocking reason. Common values:
|
|
324
|
+
- `PAYMENT_FAILED` - Payment issue
|
|
325
|
+
- `LOGIN_FAILED` - Too many failed login attempts
|
|
326
|
+
- `SUSPENDED` - Account suspended by admin
|
|
327
|
+
- Any custom string
|
|
328
|
+
|
|
329
|
+
**Example:**
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
// Block with reason
|
|
333
|
+
await drive.blockInstance(
|
|
334
|
+
'uid=jdoe,ou=users,dc=example,dc=com',
|
|
335
|
+
'PAYMENT_FAILED'
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
// Block without reason
|
|
339
|
+
await drive.blockInstance('uid=jdoe,ou=users,dc=example,dc=com');
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
### unblockInstance(dn)
|
|
345
|
+
|
|
346
|
+
Unblock a user's Cozy instance.
|
|
347
|
+
|
|
348
|
+
**Signature:**
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
async unblockInstance(dn: string): Promise<boolean>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Example:**
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
await drive.unblockInstance('uid=jdoe,ou=users,dc=example,dc=com');
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Cozy Admin API
|
|
361
|
+
|
|
362
|
+
### Endpoints Used
|
|
363
|
+
|
|
364
|
+
| Operation | Endpoint | Method |
|
|
365
|
+
| --------------- | ----------------------------------------------------------------------------------------- | ------ |
|
|
366
|
+
| Update email | `/instances/{domain}?Email={email}&FromCloudery=true` | PATCH |
|
|
367
|
+
| Update name | `/instances/{domain}?PublicName={name}&FromCloudery=true` | PATCH |
|
|
368
|
+
| Update quota | `/instances/{domain}?DiskQuota={bytes}&FromCloudery=true` | PATCH |
|
|
369
|
+
| Combined update | `/instances/{domain}?Email={email}&PublicName={name}&DiskQuota={bytes}&FromCloudery=true` | PATCH |
|
|
370
|
+
|
|
371
|
+
### Available Cozy Parameters
|
|
372
|
+
|
|
373
|
+
The Cozy Admin API supports the following parameters (query string):
|
|
374
|
+
|
|
375
|
+
| Parameter | Type | Description |
|
|
376
|
+
| ---------------- | ------- | ---------------------------------------------------- |
|
|
377
|
+
| `Email` | string | Owner email address (supported by this plugin) |
|
|
378
|
+
| `PublicName` | string | Display name (supported by this plugin) |
|
|
379
|
+
| `Locale` | string | Locale (e.g. `fr`, `en`) |
|
|
380
|
+
| `Timezone` | string | Timezone (e.g. `Europe/Paris`) |
|
|
381
|
+
| `Phone` | string | Phone number |
|
|
382
|
+
| `DiskQuota` | integer | Disk quota in bytes |
|
|
383
|
+
| `Blocked` | bool | Block or unblock the instance |
|
|
384
|
+
| `BlockingReason` | string | Reason code (e.g. `PAYMENT_FAILED`, `LOGIN_FAILED`) |
|
|
385
|
+
| `FromCloudery` | bool | Skip Cloudery callback (always true for this plugin) |
|
|
386
|
+
|
|
387
|
+
See the [Cozy Admin Documentation](https://docs.cozy.io/en/cozy-stack/admin/#patch-instancesdomain) for the complete list.
|
|
388
|
+
|
|
389
|
+
### FromCloudery Parameter
|
|
390
|
+
|
|
391
|
+
The `FromCloudery=true` query parameter is always included to:
|
|
392
|
+
|
|
393
|
+
- Prevent the Cozy stack from sending callback notifications
|
|
394
|
+
- Avoid notification loops when changes originate from the LDAP directory
|
|
395
|
+
- Indicate the change is administrative (from the cloudery/provisioning system)
|
|
396
|
+
|
|
397
|
+
### Authentication
|
|
398
|
+
|
|
399
|
+
The Cozy Admin API uses bearer token authentication:
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
--twake-drive-webadmin-token "your-admin-token"
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
The plugin automatically adds the `Authorization: Bearer {token}` header to all requests.
|
|
406
|
+
|
|
407
|
+
**Note:** The Cozy Admin API typically runs on port 6060 and should be protected at the network level.
|
|
408
|
+
|
|
409
|
+
## Examples
|
|
410
|
+
|
|
411
|
+
### Example 1: Basic Setup
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
--plugin core/ldap/onChange \
|
|
415
|
+
--plugin core/ldap/flatGeneric \
|
|
416
|
+
--plugin twake/drive \
|
|
417
|
+
--ldap-flat-schema ./schemas/twake/users.json \
|
|
418
|
+
--mail-attribute mail \
|
|
419
|
+
--twake-drive-webadmin-url http://cozy-admin:6060
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Example 2: With Authentication
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
--plugin core/ldap/onChange \
|
|
426
|
+
--plugin core/ldap/flatGeneric \
|
|
427
|
+
--plugin twake/drive \
|
|
428
|
+
--ldap-flat-schema ./schemas/twake/users.json \
|
|
429
|
+
--mail-attribute mail \
|
|
430
|
+
--twake-drive-webadmin-url http://cozy-admin:6060 \
|
|
431
|
+
--twake-drive-webadmin-token "cozy-admin-secret"
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Example 3: Complete Twake Setup (James + Drive)
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
--plugin core/auth/token \
|
|
438
|
+
--plugin core/ldap/onChange \
|
|
439
|
+
--plugin core/ldap/flatGeneric \
|
|
440
|
+
--plugin core/ldap/groups \
|
|
441
|
+
--plugin twake/james \
|
|
442
|
+
--plugin twake/drive \
|
|
443
|
+
--ldap-flat-schema ./schemas/twake/users.json \
|
|
444
|
+
--mail-attribute mail \
|
|
445
|
+
--quota-attribute mailQuotaSize \
|
|
446
|
+
--james-webadmin-url http://james:8000 \
|
|
447
|
+
--james-webadmin-token "james-admin-token" \
|
|
448
|
+
--twake-drive-webadmin-url http://cozy-admin:6060 \
|
|
449
|
+
--twake-drive-webadmin-token "cozy-admin-token" \
|
|
450
|
+
--twake-drive-domain-attribute twakeCozyDomain \
|
|
451
|
+
--auth-token "api-admin-token"
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## Logging
|
|
455
|
+
|
|
456
|
+
The plugin logs all operations:
|
|
457
|
+
|
|
458
|
+
### Successful Operations
|
|
459
|
+
|
|
460
|
+
```json
|
|
461
|
+
{
|
|
462
|
+
"level": "info",
|
|
463
|
+
"plugin": "drive",
|
|
464
|
+
"event": "onLdapMailChange",
|
|
465
|
+
"result": "success",
|
|
466
|
+
"http_status": 200,
|
|
467
|
+
"dn": "uid=jdoe,ou=users,dc=example,dc=com",
|
|
468
|
+
"cozyDomain": "jdoe.mycompany.cloud",
|
|
469
|
+
"Email": "john.doe@company.com"
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Skipped Operations
|
|
474
|
+
|
|
475
|
+
```json
|
|
476
|
+
{
|
|
477
|
+
"level": "debug",
|
|
478
|
+
"message": "Skipping mail change for uid=jdoe,...: no Cozy domain attribute (twakeCozyDomain)"
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Failed Operations
|
|
483
|
+
|
|
484
|
+
```json
|
|
485
|
+
{
|
|
486
|
+
"level": "error",
|
|
487
|
+
"plugin": "drive",
|
|
488
|
+
"event": "onLdapMailChange",
|
|
489
|
+
"result": "error",
|
|
490
|
+
"http_status": 500,
|
|
491
|
+
"http_status_text": "Internal Server Error",
|
|
492
|
+
"dn": "uid=jdoe,ou=users,dc=example,dc=com",
|
|
493
|
+
"cozyDomain": "jdoe.mycompany.cloud"
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Instance Not Found
|
|
498
|
+
|
|
499
|
+
```json
|
|
500
|
+
{
|
|
501
|
+
"level": "debug",
|
|
502
|
+
"plugin": "drive",
|
|
503
|
+
"event": "onLdapMailChange",
|
|
504
|
+
"result": "ignored",
|
|
505
|
+
"http_status": 404,
|
|
506
|
+
"message": "Cozy instance not found (may not be provisioned yet)"
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Error Handling
|
|
511
|
+
|
|
512
|
+
### Non-Existent Instance (404)
|
|
513
|
+
|
|
514
|
+
If the Cozy instance doesn't exist yet:
|
|
515
|
+
|
|
516
|
+
- Error is logged at DEBUG level (not ERROR)
|
|
517
|
+
- LDAP operation succeeds
|
|
518
|
+
- This is normal during user provisioning flow
|
|
519
|
+
|
|
520
|
+
### Cozy Server Down
|
|
521
|
+
|
|
522
|
+
If the Cozy Admin API is unreachable:
|
|
523
|
+
|
|
524
|
+
- Error is logged
|
|
525
|
+
- LDAP operation succeeds
|
|
526
|
+
- Manual sync may be required later using `syncUserToCozy()`
|
|
527
|
+
|
|
528
|
+
### Invalid Token
|
|
529
|
+
|
|
530
|
+
If the admin token is invalid:
|
|
531
|
+
|
|
532
|
+
- 401 or 403 error is logged
|
|
533
|
+
- LDAP operation succeeds
|
|
534
|
+
- Check token configuration
|
|
535
|
+
|
|
536
|
+
## Synchronization Scenarios
|
|
537
|
+
|
|
538
|
+
### Scenario 1: User Creation
|
|
539
|
+
|
|
540
|
+
1. Create user in LDAP with `twakeCozyDomain` attribute
|
|
541
|
+
2. Provision Cozy instance separately (via cloudery or API)
|
|
542
|
+
3. Future mail/name changes are synced automatically
|
|
543
|
+
4. Use `syncUserToCozy()` for initial sync if needed
|
|
544
|
+
|
|
545
|
+
### Scenario 2: Email Change
|
|
546
|
+
|
|
547
|
+
1. User changes email in LDAP: `old@company.com` → `new@company.com`
|
|
548
|
+
2. Drive plugin updates Cozy instance email
|
|
549
|
+
3. Cozy updates instance owner's email
|
|
550
|
+
4. User receives notifications at new email
|
|
551
|
+
|
|
552
|
+
### Scenario 3: Name Change
|
|
553
|
+
|
|
554
|
+
1. Admin updates user's display name in LDAP
|
|
555
|
+
2. Drive plugin updates Cozy instance PublicName
|
|
556
|
+
3. User's name is updated across Cozy apps
|
|
557
|
+
|
|
558
|
+
### Scenario 4: Manual Sync
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
// Sync all attributes for a user
|
|
562
|
+
const drive = server.getPlugin('drive') as Drive;
|
|
563
|
+
await drive.syncUserToCozy('uid=jdoe,ou=users,dc=example,dc=com');
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
## Limitations
|
|
567
|
+
|
|
568
|
+
1. **One-way sync**: LDAP → Cozy only. Cozy changes don't sync back to LDAP.
|
|
569
|
+
2. **No instance creation**: Plugin doesn't create Cozy instances, only updates existing ones.
|
|
570
|
+
3. **No user deletion**: Plugin doesn't delete Cozy instances when LDAP users are deleted.
|
|
571
|
+
4. **Domain required**: Users must have the `twakeCozyDomain` attribute to be synced.
|
|
572
|
+
|
|
573
|
+
## Troubleshooting
|
|
574
|
+
|
|
575
|
+
### Problem: Changes Not Syncing
|
|
576
|
+
|
|
577
|
+
**Solutions:**
|
|
578
|
+
|
|
579
|
+
1. Verify onChange plugin is loaded:
|
|
580
|
+
|
|
581
|
+
```bash
|
|
582
|
+
--plugin core/ldap/onChange
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
2. Check Cozy domain attribute is set for the user:
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
ldapsearch -b "uid=jdoe,ou=users,dc=example,dc=com" twakeCozyDomain
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
3. Enable debug logging:
|
|
592
|
+
|
|
593
|
+
```bash
|
|
594
|
+
--log-level debug
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
4. Verify Cozy Admin API is reachable:
|
|
598
|
+
|
|
599
|
+
```bash
|
|
600
|
+
curl http://cozy-admin:6060/instances
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Problem: 404 Errors
|
|
604
|
+
|
|
605
|
+
**Symptoms:**
|
|
606
|
+
|
|
607
|
+
```json
|
|
608
|
+
{ "level": "debug", "message": "Cozy instance not found" }
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
**Solutions:**
|
|
612
|
+
|
|
613
|
+
1. Provision Cozy instance for the user first
|
|
614
|
+
2. Verify `twakeCozyDomain` attribute matches the actual Cozy domain
|
|
615
|
+
3. Check Cozy logs for instance existence
|
|
616
|
+
|
|
617
|
+
### Problem: Authentication Errors
|
|
618
|
+
|
|
619
|
+
**Symptoms:**
|
|
620
|
+
|
|
621
|
+
```json
|
|
622
|
+
{ "http_status": 401, "http_status_text": "Unauthorized" }
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
**Solutions:**
|
|
626
|
+
|
|
627
|
+
1. Verify admin token is correct
|
|
628
|
+
2. Check token hasn't expired
|
|
629
|
+
3. Ensure Admin API is configured to accept bearer tokens
|
|
630
|
+
|
|
631
|
+
## Integration with James
|
|
632
|
+
|
|
633
|
+
When using both James and Drive plugins, email changes are propagated to both services:
|
|
634
|
+
|
|
635
|
+
```bash
|
|
636
|
+
--plugin twake/james \
|
|
637
|
+
--plugin twake/drive
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
Both plugins listen to `onLdapMailChange` and update their respective services in parallel.
|
|
641
|
+
|
|
642
|
+
## See Also
|
|
643
|
+
|
|
644
|
+
- [onChange.md](onChange.md) - LDAP change detection
|
|
645
|
+
- [james-mail.md](james-mail.md) - James mail server integration
|
|
646
|
+
- [Cozy Admin Documentation](https://docs.cozy.io/en/cozy-stack/admin/) - Admin API reference
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ldap-rest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Light directory manager",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Identity-Manager"
|
|
@@ -136,6 +136,10 @@
|
|
|
136
136
|
"types": "./dist/src/plugins/twake/calendarResources.d.ts",
|
|
137
137
|
"import": "./dist/plugins/twake/calendarResources.js"
|
|
138
138
|
},
|
|
139
|
+
"./plugin-twake-drive": {
|
|
140
|
+
"types": "./dist/src/plugins/twake/drive.d.ts",
|
|
141
|
+
"import": "./dist/plugins/twake/drive.js"
|
|
142
|
+
},
|
|
139
143
|
"./plugin-twake-james": {
|
|
140
144
|
"types": "./dist/src/plugins/twake/james.d.ts",
|
|
141
145
|
"import": "./dist/plugins/twake/james.js"
|
|
@@ -507,7 +511,8 @@
|
|
|
507
511
|
"diff": ">=8.0.3",
|
|
508
512
|
"fast-xml-parser": ">=5.3.6",
|
|
509
513
|
"glob": ">=11.0.4",
|
|
510
|
-
"minimatch": "^10.2.1"
|
|
514
|
+
"minimatch": "^10.2.1",
|
|
515
|
+
"serialize-javascript": ">=7.0.3"
|
|
511
516
|
},
|
|
512
517
|
"dependencies": {
|
|
513
518
|
"body-parser": "^2.2.0",
|