ldap-rest 0.0.1

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.
Files changed (296) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +48 -0
  3. package/bin/cleanup-external-users.mjs +219 -0
  4. package/bin/index.mjs +54 -0
  5. package/bin/sync-james.mjs +190 -0
  6. package/dist/abstract/ldapFlat.js +1 -0
  7. package/dist/abstract/plugin.js +1 -0
  8. package/dist/abstract/twakePlugin.js +1 -0
  9. package/dist/bin/index.js +1 -0
  10. package/dist/config/args.js +1 -0
  11. package/dist/lib/auth/base.js +1 -0
  12. package/dist/lib/authz/base.js +1 -0
  13. package/dist/lib/expressFormatedResponses.js +1 -0
  14. package/dist/lib/ldapActions.js +1 -0
  15. package/dist/lib/parseConfig.js +1 -0
  16. package/dist/lib/utils.js +1 -0
  17. package/dist/logger/winston.js +1 -0
  18. package/dist/plugins/auth/authzLinid1.js +1 -0
  19. package/dist/plugins/auth/authzPerBranch.js +1 -0
  20. package/dist/plugins/auth/crowdsec.js +1 -0
  21. package/dist/plugins/auth/llng.js +1 -0
  22. package/dist/plugins/auth/openidconnect.js +1 -0
  23. package/dist/plugins/auth/rateLimit.js +1 -0
  24. package/dist/plugins/auth/token.js +1 -0
  25. package/dist/plugins/configApi.js +1 -0
  26. package/dist/plugins/demo/helloworld.js +1 -0
  27. package/dist/plugins/ldap/bulkImport.js +1 -0
  28. package/dist/plugins/ldap/departmentSync.js +1 -0
  29. package/dist/plugins/ldap/externalUsersInGroups.js +1 -0
  30. package/dist/plugins/ldap/flatGeneric.js +1 -0
  31. package/dist/plugins/ldap/groups.js +1 -0
  32. package/dist/plugins/ldap/onChange.js +1 -0
  33. package/dist/plugins/ldap/organization.js +1 -0
  34. package/dist/plugins/ldap/trash.js +1 -0
  35. package/dist/plugins/priority.json.js +1 -0
  36. package/dist/plugins/static.js +1 -0
  37. package/dist/plugins/twake/calendarResources.js +1 -0
  38. package/dist/plugins/twake/james.js +1 -0
  39. package/dist/plugins/weblogs.js +1 -0
  40. package/dist/src/abstract/ldapFlat.d.ts +94 -0
  41. package/dist/src/abstract/plugin.d.ts +43 -0
  42. package/dist/src/abstract/twakePlugin.d.ts +90 -0
  43. package/dist/src/bin/index.d.ts +43 -0
  44. package/dist/src/browser/ldap-group-editor/LdapGroupEditor.d.ts +31 -0
  45. package/dist/src/browser/ldap-group-editor/api/GroupApiClient.d.ts +23 -0
  46. package/dist/src/browser/ldap-group-editor/components/CreateGroupModal.d.ts +19 -0
  47. package/dist/src/browser/ldap-group-editor/components/GroupPropertyEditor.d.ts +30 -0
  48. package/dist/src/browser/ldap-group-editor/components/GroupTree.d.ts +17 -0
  49. package/dist/src/browser/ldap-group-editor/components/MoveGroupModal.d.ts +20 -0
  50. package/dist/src/browser/ldap-group-editor/index.d.ts +6 -0
  51. package/dist/src/browser/ldap-group-editor/types.d.ts +58 -0
  52. package/dist/src/browser/ldap-resource-editor/api/ResourceApiClient.d.ts +53 -0
  53. package/dist/src/browser/ldap-resource-editor/cache/CacheManager.d.ts +65 -0
  54. package/dist/src/browser/ldap-resource-editor/index.d.ts +6 -0
  55. package/dist/src/browser/ldap-resource-editor/types.d.ts +78 -0
  56. package/dist/src/browser/ldap-tree-viewer/LdapTreeViewer.d.ts +26 -0
  57. package/dist/src/browser/ldap-tree-viewer/api/LdapApiClient.d.ts +30 -0
  58. package/dist/src/browser/ldap-tree-viewer/components/TreeNode.d.ts +17 -0
  59. package/dist/src/browser/ldap-tree-viewer/components/TreeRoot.d.ts +14 -0
  60. package/dist/src/browser/ldap-tree-viewer/index.d.ts +7 -0
  61. package/dist/src/browser/ldap-tree-viewer/store/Store.d.ts +18 -0
  62. package/dist/src/browser/ldap-tree-viewer/store/actions.d.ts +28 -0
  63. package/dist/src/browser/ldap-tree-viewer/store/reducers.d.ts +7 -0
  64. package/dist/src/browser/ldap-tree-viewer/types.d.ts +34 -0
  65. package/dist/src/browser/ldap-unit-editor/LdapUnitEditor.d.ts +27 -0
  66. package/dist/src/browser/ldap-unit-editor/api/UnitApiClient.d.ts +24 -0
  67. package/dist/src/browser/ldap-unit-editor/components/MoveUnitModal.d.ts +28 -0
  68. package/dist/src/browser/ldap-unit-editor/components/UnitPropertyEditor.d.ts +28 -0
  69. package/dist/src/browser/ldap-unit-editor/components/UnitTree.d.ts +18 -0
  70. package/dist/src/browser/ldap-unit-editor/index.d.ts +6 -0
  71. package/dist/src/browser/ldap-unit-editor/types.d.ts +55 -0
  72. package/dist/src/browser/ldap-user-editor/LdapUserEditor.d.ts +32 -0
  73. package/dist/src/browser/ldap-user-editor/api/UserApiClient.d.ts +55 -0
  74. package/dist/src/browser/ldap-user-editor/cache/CacheManager.d.ts +65 -0
  75. package/dist/src/browser/ldap-user-editor/components/MoveUserModal.d.ts +28 -0
  76. package/dist/src/browser/ldap-user-editor/components/PointerField.d.ts +22 -0
  77. package/dist/src/browser/ldap-user-editor/components/UserEditor.d.ts +46 -0
  78. package/dist/src/browser/ldap-user-editor/components/UserList.d.ts +24 -0
  79. package/dist/src/browser/ldap-user-editor/components/UserTree.d.ts +24 -0
  80. package/dist/src/browser/ldap-user-editor/index.d.ts +7 -0
  81. package/dist/src/browser/ldap-user-editor/types.d.ts +67 -0
  82. package/dist/src/browser/shared/components/Modal.d.ts +38 -0
  83. package/dist/src/browser/shared/components/StatusMessage.d.ts +48 -0
  84. package/dist/src/browser/shared/index.d.ts +10 -0
  85. package/dist/src/browser/shared/types/index.d.ts +65 -0
  86. package/dist/src/browser/shared/utils/dom.d.ts +33 -0
  87. package/dist/src/browser/shared/utils/form.d.ts +28 -0
  88. package/dist/src/browser/shared/utils/schema.d.ts +29 -0
  89. package/dist/src/config/args.d.ts +128 -0
  90. package/dist/src/config/schema.d.ts +18 -0
  91. package/dist/src/hooks.d.ts +76 -0
  92. package/dist/src/lib/auth/base.d.ts +9 -0
  93. package/dist/src/lib/authz/base.d.ts +63 -0
  94. package/dist/src/lib/expressFormatedResponses.d.ts +26 -0
  95. package/dist/src/lib/ldapActions.d.ts +91 -0
  96. package/dist/src/lib/parseConfig.d.ts +23 -0
  97. package/dist/src/lib/utils.d.ts +73 -0
  98. package/dist/src/logger/winston.d.ts +3 -0
  99. package/dist/src/plugins/auth/authzLinid1.d.ts +46 -0
  100. package/dist/src/plugins/auth/authzPerBranch.d.ts +59 -0
  101. package/dist/src/plugins/auth/crowdsec.d.ts +29 -0
  102. package/dist/src/plugins/auth/llng.d.ts +8 -0
  103. package/dist/src/plugins/auth/openidconnect.d.ts +11 -0
  104. package/dist/src/plugins/auth/rateLimit.d.ts +24 -0
  105. package/dist/src/plugins/auth/token.d.ts +17 -0
  106. package/dist/src/plugins/configApi.d.ts +24 -0
  107. package/dist/src/plugins/demo/helloworld.d.ts +12 -0
  108. package/dist/src/plugins/ldap/bulkImport.d.ts +48 -0
  109. package/dist/src/plugins/ldap/departmentSync.d.ts +35 -0
  110. package/dist/src/plugins/ldap/externalUsersInGroups.d.ts +21 -0
  111. package/dist/src/plugins/ldap/flatGeneric.d.ts +31 -0
  112. package/dist/src/plugins/ldap/groups.d.ts +95 -0
  113. package/dist/src/plugins/ldap/onChange.d.ts +32 -0
  114. package/dist/src/plugins/ldap/organization.d.ts +65 -0
  115. package/dist/src/plugins/ldap/trash.d.ts +52 -0
  116. package/dist/src/plugins/static.d.ts +7 -0
  117. package/dist/src/plugins/twake/calendarResources.d.ts +31 -0
  118. package/dist/src/plugins/twake/james.d.ts +83 -0
  119. package/dist/src/plugins/weblogs.d.ts +8 -0
  120. package/docs/DEVELOPER_GUIDE.md +298 -0
  121. package/docs/OPENAPI.md +440 -0
  122. package/docs/PLUGIN_DEPENDENCIES.md +612 -0
  123. package/docs/README.md +466 -0
  124. package/docs/api/REFERENCE.md +983 -0
  125. package/docs/api/REST_API.md +1237 -0
  126. package/docs/authentication.md +469 -0
  127. package/docs/authzLinid1.md +521 -0
  128. package/docs/authzPerBranch.md +596 -0
  129. package/docs/browser/LIBRARIES.md +1196 -0
  130. package/docs/examples/EXAMPLES.md +1659 -0
  131. package/docs/examples/twake-mailbox-type-nomenclature.ldif +37 -0
  132. package/docs/examples/twake-mailbox-type-schema-amendment.ldif +30 -0
  133. package/docs/jamesTeamMailboxes.md +343 -0
  134. package/docs/ldapBulkImport.md +722 -0
  135. package/docs/ldapExternalUsersInGroups.md +325 -0
  136. package/docs/ldapFlatGeneric.md +444 -0
  137. package/docs/ldapGroups.md +589 -0
  138. package/docs/ldapOrganizations.md +659 -0
  139. package/docs/ldapTrash.md +260 -0
  140. package/docs/linagora.png +0 -0
  141. package/docs/onChange.md +318 -0
  142. package/docs/plugins/DEVELOPMENT.md +1219 -0
  143. package/docs/presenterm-fr.md +721 -0
  144. package/docs/presenterm.md +721 -0
  145. package/docs/schemas/SCHEMAS.md +1212 -0
  146. package/docs/static.md +410 -0
  147. package/docs/twakeCalendarResources.md +148 -0
  148. package/docs/twakeJames.md +661 -0
  149. package/docs/weblogs.md +399 -0
  150. package/examples/web/ldap-group-editor.html +113 -0
  151. package/examples/web/ldap-tree-viewer.html +368 -0
  152. package/examples/web/ldap-unit-editor.html +108 -0
  153. package/examples/web/ldap-user-editor.html +412 -0
  154. package/package.json +458 -0
  155. package/static/browser/common-demo.css +450 -0
  156. package/static/browser/ldap-group-editor/LdapGroupEditor.js +207 -0
  157. package/static/browser/ldap-group-editor/LdapGroupEditor.js.map +1 -0
  158. package/static/browser/ldap-group-editor/api/GroupApiClient.js +112 -0
  159. package/static/browser/ldap-group-editor/api/GroupApiClient.js.map +1 -0
  160. package/static/browser/ldap-group-editor/components/CreateGroupModal.js +212 -0
  161. package/static/browser/ldap-group-editor/components/CreateGroupModal.js.map +1 -0
  162. package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js +497 -0
  163. package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js.map +1 -0
  164. package/static/browser/ldap-group-editor/components/GroupTree.js +153 -0
  165. package/static/browser/ldap-group-editor/components/GroupTree.js.map +1 -0
  166. package/static/browser/ldap-group-editor/components/MoveGroupModal.js +235 -0
  167. package/static/browser/ldap-group-editor/components/MoveGroupModal.js.map +1 -0
  168. package/static/browser/ldap-group-editor/index.js +2 -0
  169. package/static/browser/ldap-group-editor/index.js.map +1 -0
  170. package/static/browser/ldap-group-editor/types.js +2 -0
  171. package/static/browser/ldap-group-editor/types.js.map +1 -0
  172. package/static/browser/ldap-group-editor.esm.js +1399 -0
  173. package/static/browser/ldap-group-editor.esm.js.map +1 -0
  174. package/static/browser/ldap-group-editor.js +1407 -0
  175. package/static/browser/ldap-group-editor.js.map +1 -0
  176. package/static/browser/ldap-group-editor.min.js +2 -0
  177. package/static/browser/ldap-group-editor.min.js.map +1 -0
  178. package/static/browser/ldap-resource-editor/api/ResourceApiClient.js +226 -0
  179. package/static/browser/ldap-resource-editor/api/ResourceApiClient.js.map +1 -0
  180. package/static/browser/ldap-resource-editor/cache/CacheManager.js +123 -0
  181. package/static/browser/ldap-resource-editor/cache/CacheManager.js.map +1 -0
  182. package/static/browser/ldap-resource-editor/index.js +2 -0
  183. package/static/browser/ldap-resource-editor/index.js.map +1 -0
  184. package/static/browser/ldap-resource-editor/types.js +2 -0
  185. package/static/browser/ldap-resource-editor/types.js.map +1 -0
  186. package/static/browser/ldap-tree-viewer/LdapTreeViewer.js +187 -0
  187. package/static/browser/ldap-tree-viewer/LdapTreeViewer.js.map +1 -0
  188. package/static/browser/ldap-tree-viewer/api/LdapApiClient.js +50 -0
  189. package/static/browser/ldap-tree-viewer/api/LdapApiClient.js.map +1 -0
  190. package/static/browser/ldap-tree-viewer/components/TreeNode.js +140 -0
  191. package/static/browser/ldap-tree-viewer/components/TreeNode.js.map +1 -0
  192. package/static/browser/ldap-tree-viewer/components/TreeRoot.js +98 -0
  193. package/static/browser/ldap-tree-viewer/components/TreeRoot.js.map +1 -0
  194. package/static/browser/ldap-tree-viewer/index.js +2 -0
  195. package/static/browser/ldap-tree-viewer/index.js.map +1 -0
  196. package/static/browser/ldap-tree-viewer/store/Store.js +29 -0
  197. package/static/browser/ldap-tree-viewer/store/Store.js.map +1 -0
  198. package/static/browser/ldap-tree-viewer/store/actions.js +53 -0
  199. package/static/browser/ldap-tree-viewer/store/actions.js.map +1 -0
  200. package/static/browser/ldap-tree-viewer/store/reducers.js +79 -0
  201. package/static/browser/ldap-tree-viewer/store/reducers.js.map +1 -0
  202. package/static/browser/ldap-tree-viewer/types.js +2 -0
  203. package/static/browser/ldap-tree-viewer/types.js.map +1 -0
  204. package/static/browser/ldap-tree-viewer.css +2 -0
  205. package/static/browser/ldap-tree-viewer.css.map +1 -0
  206. package/static/browser/ldap-tree-viewer.esm.js +606 -0
  207. package/static/browser/ldap-tree-viewer.esm.js.map +1 -0
  208. package/static/browser/ldap-tree-viewer.js +614 -0
  209. package/static/browser/ldap-tree-viewer.js.map +1 -0
  210. package/static/browser/ldap-tree-viewer.min.js +2 -0
  211. package/static/browser/ldap-tree-viewer.min.js.map +1 -0
  212. package/static/browser/ldap-unit-editor/LdapUnitEditor.js +119 -0
  213. package/static/browser/ldap-unit-editor/LdapUnitEditor.js.map +1 -0
  214. package/static/browser/ldap-unit-editor/api/UnitApiClient.js +95 -0
  215. package/static/browser/ldap-unit-editor/api/UnitApiClient.js.map +1 -0
  216. package/static/browser/ldap-unit-editor/components/MoveUnitModal.js +295 -0
  217. package/static/browser/ldap-unit-editor/components/MoveUnitModal.js.map +1 -0
  218. package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js +321 -0
  219. package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js.map +1 -0
  220. package/static/browser/ldap-unit-editor/components/UnitTree.js +197 -0
  221. package/static/browser/ldap-unit-editor/components/UnitTree.js.map +1 -0
  222. package/static/browser/ldap-unit-editor/index.js +2 -0
  223. package/static/browser/ldap-unit-editor/index.js.map +1 -0
  224. package/static/browser/ldap-unit-editor/types.js +2 -0
  225. package/static/browser/ldap-unit-editor/types.js.map +1 -0
  226. package/static/browser/ldap-unit-editor.esm.js +1013 -0
  227. package/static/browser/ldap-unit-editor.esm.js.map +1 -0
  228. package/static/browser/ldap-unit-editor.js +1021 -0
  229. package/static/browser/ldap-unit-editor.js.map +1 -0
  230. package/static/browser/ldap-unit-editor.min.js +2 -0
  231. package/static/browser/ldap-unit-editor.min.js.map +1 -0
  232. package/static/browser/ldap-user-editor/LdapUserEditor.js +184 -0
  233. package/static/browser/ldap-user-editor/LdapUserEditor.js.map +1 -0
  234. package/static/browser/ldap-user-editor/api/UserApiClient.js +209 -0
  235. package/static/browser/ldap-user-editor/api/UserApiClient.js.map +1 -0
  236. package/static/browser/ldap-user-editor/cache/CacheManager.js +123 -0
  237. package/static/browser/ldap-user-editor/cache/CacheManager.js.map +1 -0
  238. package/static/browser/ldap-user-editor/components/MoveUserModal.js +302 -0
  239. package/static/browser/ldap-user-editor/components/MoveUserModal.js.map +1 -0
  240. package/static/browser/ldap-user-editor/components/PointerField.js +152 -0
  241. package/static/browser/ldap-user-editor/components/PointerField.js.map +1 -0
  242. package/static/browser/ldap-user-editor/components/UserEditor.js +608 -0
  243. package/static/browser/ldap-user-editor/components/UserEditor.js.map +1 -0
  244. package/static/browser/ldap-user-editor/components/UserList.js +181 -0
  245. package/static/browser/ldap-user-editor/components/UserList.js.map +1 -0
  246. package/static/browser/ldap-user-editor/components/UserTree.js +164 -0
  247. package/static/browser/ldap-user-editor/components/UserTree.js.map +1 -0
  248. package/static/browser/ldap-user-editor/index.js +2 -0
  249. package/static/browser/ldap-user-editor/index.js.map +1 -0
  250. package/static/browser/ldap-user-editor/types.js +2 -0
  251. package/static/browser/ldap-user-editor/types.js.map +1 -0
  252. package/static/browser/ldap-user-editor.css +2 -0
  253. package/static/browser/ldap-user-editor.css.map +1 -0
  254. package/static/browser/ldap-user-editor.esm.js +1897 -0
  255. package/static/browser/ldap-user-editor.esm.js.map +1 -0
  256. package/static/browser/ldap-user-editor.js +1905 -0
  257. package/static/browser/ldap-user-editor.js.map +1 -0
  258. package/static/browser/ldap-user-editor.min.js +2 -0
  259. package/static/browser/ldap-user-editor.min.js.map +1 -0
  260. package/static/browser/shared/components/Modal.js +121 -0
  261. package/static/browser/shared/components/Modal.js.map +1 -0
  262. package/static/browser/shared/components/StatusMessage.js +119 -0
  263. package/static/browser/shared/components/StatusMessage.js.map +1 -0
  264. package/static/browser/shared/index.js +7 -0
  265. package/static/browser/shared/index.js.map +1 -0
  266. package/static/browser/shared/types/index.js +37 -0
  267. package/static/browser/shared/types/index.js.map +1 -0
  268. package/static/browser/shared/utils/dom.js +78 -0
  269. package/static/browser/shared/utils/dom.js.map +1 -0
  270. package/static/browser/shared/utils/form.js +234 -0
  271. package/static/browser/shared/utils/form.js.map +1 -0
  272. package/static/browser/shared/utils/schema.js +121 -0
  273. package/static/browser/shared/utils/schema.js.map +1 -0
  274. package/static/browser/shared.esm.js +694 -0
  275. package/static/browser/shared.esm.js.map +1 -0
  276. package/static/browser/shared.js +723 -0
  277. package/static/browser/shared.js.map +1 -0
  278. package/static/browser/shared.min.js +2 -0
  279. package/static/browser/shared.min.js.map +1 -0
  280. package/static/schemas/ad/computers.json +92 -0
  281. package/static/schemas/ad/groups.json +55 -0
  282. package/static/schemas/ad/organizations.json +66 -0
  283. package/static/schemas/ad/users.json +116 -0
  284. package/static/schemas/standard/devices.json +72 -0
  285. package/static/schemas/standard/groups.json +54 -0
  286. package/static/schemas/standard/organizations.json +49 -0
  287. package/static/schemas/standard/users.json +86 -0
  288. package/static/schemas/twake/groups.json +75 -0
  289. package/static/schemas/twake/nomenclature/twakeAccountStatus.json +31 -0
  290. package/static/schemas/twake/nomenclature/twakeDeliveryMode.json +31 -0
  291. package/static/schemas/twake/nomenclature/twakeListType.json +31 -0
  292. package/static/schemas/twake/nomenclature/twakeMailboxType.json +31 -0
  293. package/static/schemas/twake/nomenclature/twakeTitle.json +31 -0
  294. package/static/schemas/twake/organizations.json +52 -0
  295. package/static/schemas/twake/positions.json +32 -0
  296. package/static/schemas/twake/users.json +112 -0
@@ -0,0 +1 @@
1
+ import{auth as e}from"express-openid-connect";import s from"../../abstract/plugin.js";import{launchHooksChained as o}from"../../lib/utils.js";import{serverError as t}from"../../lib/expressFormatedResponses.js";class OpenIDConnect extends s{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}`)}api(s){const i={authRequired:!0,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"}};s.use(async(e,s,i)=>{try{[e,s]=await o(this.server.hooks.beforeAuth,[e,s]),i()}catch(e){return t(s,e)}}),s.use(e(i)),s.use(async(e,s,i)=>{try{this.hooks?.onAuth&&([e,s]=await o(this.server.hooks.afterAuth,[e,s])),e.user=e.oidc.user.sub,i()}catch(e){t(s,e)}})}authMethod(s,o,t){e({issuerBaseURL:process.env.ISSUER_BASE_URL,clientID:process.env.CLIENT_ID,clientSecret:process.env.CLIENT_SECRET,baseURL:process.env.BASE_URL,authorizationParams:{response_type:"code",scope:"openid email profile"}})(s,o,t)}}export{OpenIDConnect as default};
@@ -0,0 +1 @@
1
+ import t from"../../abstract/plugin.js";class RateLimit extends t{constructor(...t){super(...t),this.name="rateLimit",this.roles=["protect"],this.store=new Map,this.windowMs=this.config.rate_limit_window_ms||9e5,this.maxRequests=this.config.rate_limit_max||100,this.logger.info(`Auth rate limiting enabled: ${this.maxRequests} failed attempts per ${this.windowMs/1e3} seconds`)}api(t){t.use((t,e,s)=>{const i=this.getClientIp(t),r=Date.now(),o=this.store.get(i);o&&r>o.resetTime&&this.store.delete(i);const a=this.store.get(i);if(a&&a.count>=this.maxRequests&&r<=a.resetTime)return this.logger.warn(`Rate limiting ${i}: ${a.count}/${this.maxRequests} failed attempts`),e.status(429).json({error:"Too many authentication attempts, please try again later",retryAfter:Math.ceil((a.resetTime-r)/1e3)});e.on("finish",()=>{if(401===e.statusCode||403===e.statusCode){const t=this.store.get(i),e=Date.now();!t||e>t.resetTime?this.store.set(i,{count:1,resetTime:e+this.windowMs}):t.count++,this.logger.debug(`Failed auth from ${i}: ${this.store.get(i)?.count}/${this.maxRequests}`)}}),s()})}getClientIp(t){const e=t.headers["x-forwarded-for"];if(e){return(Array.isArray(e)?e[0]:e).split(",")[0].trim()}return t.socket.remoteAddress||"unknown"}}export{RateLimit as default};
@@ -0,0 +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,r){let o=e.headers.authorization;if(!o||!/^Bearer .+/.test(o))return this.logger.warn("Missing or invalid Authorization header"),t(s);o=o.split(" ")[1];const i=this.tokenMap.get(o);if(!i)return this.logger.warn(`Unauthorized token: ${o}`),t(s);e.user=i,r()}}export{AuthToken as default};
@@ -0,0 +1 @@
1
+ import i from"../abstract/plugin.js";import{wantJson as o}from"../lib/expressFormatedResponses.js";class ConfigApi extends i{constructor(){super(...arguments),this.name="configApi",this.roles=["api"]}api(i){const t=this.config.api_prefix||"/api";i.get(`${t}/v1/config`,(i,e)=>{if(!o(i,e))return;const s={apiPrefix:t,ldapBase:this.config.ldap_base||"",features:this.collectPluginConfigs()};e.json(s)}),this.logger.info(`Configuration API registered at ${t}/v1/config`)}collectPluginConfigs(){const i={};for(const[o,t]of Object.entries(this.server.loadedPlugins))if("configApi"!==o&&t.roles?.includes("configurable")&&t.getConfigApiData)try{const e=t.getConfigApiData();e&&(i[o]=e)}catch(i){this.logger.warn(`Failed to get config from plugin ${o}: ${String(i)}`)}return i}}export{ConfigApi as default};
@@ -0,0 +1 @@
1
+ import o from"../../abstract/plugin.js";class HelloWorld extends o{constructor(){super(...arguments),this.name="hello",this.roles=["demo"]}api(o){o.get(`${this.config.api_prefix}/hello`,async(o,s)=>{const e={message:"Hello",hookResults:[]};if(this.server.hooks&&this.server.hooks.hello)for(const o of this.server.hooks.hello)e.hookResults.push(await o());s.json(e)})}}export{HelloWorld as default};
@@ -0,0 +1 @@
1
+ import t from"fs";import e from"multer";import{parse as i}from"csv-parse/sync";import r from"../../abstract/plugin.js";import{badRequest as s,serverError as o}from"../../lib/expressFormatedResponses.js";class LdapBulkImport extends r{constructor(i){super(i),this.name="ldapBulkImport",this.roles=["configurable"],this.resources=new Map;const r=parseInt(this.config.bulk_import_max_file_size,10)||10485760;this.upload=e({storage:e.memoryStorage(),limits:{fileSize:r},fileFilter:(t,e,i)=>{"text/csv"===e.mimetype||e.originalname.endsWith(".csv")?i(null,!0):i(new Error("Only CSV files are allowed"))}});const s=this.config.bulk_import_schemas;if(!s)return void this.logger.warn("No bulk import schemas configured");const o=s.split(",").map(t=>t.trim());for(const e of o){const[i,r]=e.split(":").map(t=>t.trim());if(i&&r)try{const e=t.readFileSync(r,"utf-8"),s=JSON.parse(e);this.resources.set(i,{name:i,schema:s,base:s.base||this.config.ldap_base,mainAttribute:s.mainAttribute||"cn"}),this.logger.info(`Loaded bulk import resource: ${i} from ${r}`)}catch(t){this.logger.error(`Failed to load schema for ${i}: ${t.message}`)}else this.logger.warn(`Invalid schema entry: ${e}`)}}api(t){for(const[e,i]of this.resources)t.get(`${this.config.api_prefix}/v1/ldap/bulk-import/${e}/template.csv`,(t,e)=>this.getTemplate(t,e,i)),t.post(`${this.config.api_prefix}/v1/ldap/bulk-import/${e}`,this.upload.single("file"),async(t,e)=>this.bulkImport(t,e,i))}getTemplate(t,e,i){const r=this.getEditableAttributes(i.schema);r.push("organizationDn"),e.setHeader("Content-Type","text/csv; charset=utf-8"),e.setHeader("Content-Disposition",`attachment; filename="${i.name}-template.csv"`),e.send(r.join(",")+"\n")}async bulkImport(t,e,r){if(!t.file)return s(e,"No file uploaded");const n="true"===t.body.dryRun||!0===t.body.dryRun,a="true"===t.body.updateExisting||!0===t.body.updateExisting,c="false"!==t.body.continueOnError&&!1!==t.body.continueOnError,l=Date.now(),p={success:!0,total:0,created:0,updated:0,skipped:0,failed:0,errors:[],details:{duration:"",linesProcessed:0}};try{const s=t.file.buffer.toString("utf-8"),o=i(s,{columns:!0,skip_empty_lines:!0,trim:!0});p.total=o.length;for(let e=0;e<o.length;e++){const i=e+2,s=o[e];try{const{dn:e,entry:i}=await this.processRecord(s,r),o=await this.entryExists(e);if(o&&!a){p.skipped++;continue}if(n){p.created++;continue}o&&a?(await this.server.ldap.modify(e,{replace:i},t),p.updated++):(await this.server.ldap.add(e,i,t),p.created++)}catch(t){if(p.failed++,p.errors.push({line:i,identifier:s[r.mainAttribute],error:t.message}),!c)throw t}}p.details.linesProcessed=o.length,p.details.duration=`${((Date.now()-l)/1e3).toFixed(1)}s`,e.json(p)}catch(t){return o(e,t)}}async processRecord(t,e){const i={},r=e.schema,s=t.organizationDn;delete t.organizationDn;for(const[t,e]of Object.entries(r.properties||{}))if(!0===e.fixed){const r=e.default||e.const;void 0!==r&&(i[t]=r)}for(const[e,r]of Object.entries(t))r&&""!==r.trim()&&(i[e]=r.includes(";")?r.split(";").map(t=>t.trim()):r);if(s){const t=this.findAttributeByRole(r,"organizationLink");t&&(i[t]=s);const e=this.findAttributeByRole(r,"organizationPath");if(e){const t=(await this.fetchOrganization(s))[e];i[e]=Array.isArray(t)?t[0]:t}}for(const[t,e]of Object.entries(r.properties||{}))if(!0===e.required&&!i[t])throw new Error(`Missing required attribute: ${t}`);const o=e.mainAttribute,n=i[o];if(!n)throw new Error(`Missing main attribute: ${o}`);return{dn:`${o}=${n},${e.base}`,entry:i}}getEditableAttributes(t){const e=[];for(const[i,r]of Object.entries(t.properties||{}))!0!==r.fixed&&(r.role?.includes("organizationLink")||r.role?.includes("organizationPath")||e.push(i));return e}findAttributeByRole(t,e){for(const[i,r]of Object.entries(t.properties||{}))if(r.role?.includes(e))return i;return null}async fetchOrganization(t){const e=await this.server.ldap.search({paged:!1,scope:"base"},t);if(0===e.searchEntries.length)throw new Error(`Organization not found: ${t}`);return e.searchEntries[0]}async entryExists(t){try{return(await this.server.ldap.search({paged:!1,scope:"base"},t)).searchEntries.length>0}catch(t){return!1}}getConfigApiData(){const t=this.config.api_prefix||"/api",e=[];return this.resources.forEach((i,r)=>{e.push({name:r,mainAttribute:i.mainAttribute,base:i.base,maxFileSize:parseInt(this.config.bulk_import_max_file_size,10)||10485760,batchSize:parseInt(this.config.bulk_import_batch_size,10)||100,endpoints:{template:`${t}/v1/ldap/bulk-import/${r}/template.csv`,import:`${t}/v1/ldap/bulk-import/${r}`}})}),{enabled:!0,resources:e}}}export{LdapBulkImport as default};
@@ -0,0 +1 @@
1
+ import t from"../../abstract/plugin.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 r=this.config.ldap_top_organization.replace(/^ou=[^,]+,/,"");await this.updateLinkedResources(t,e,r),await this.updateDescendantReferences(t,e,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"}async updateLinkedResources(t,e,r){const a=`(${this.linkAttr}=${t})`;this.logger.debug(`Searching for resources directly linked to ${t}: ${a}`);const i=await this.server.ldap.search({paged:!0,filter:a,attributes:[this.linkAttr,this.pathAttr]},r);let n=0;for await(const t of i)for(const r of t.searchEntries){const t=String(r.dn);try{const r=await this.getDepartmentPath(e);await this.server.ldap.modify(t,{replace:{[this.linkAttr]:e,[this.pathAttr]:r}}),n++,this.logger.debug(`Updated ${t}: ${this.linkAttr}=${e}, ${this.pathAttr}=${r}`)}catch(e){this.logger.error(`Failed to update ${t} after organization rename: ${e}`)}}this.logger.info(`Updated ${n} resources directly linked to the renamed organization`)}async updateDescendantReferences(t,e,r){const a=`(${this.linkAttr}=*)`;this.logger.debug(`Searching for resources linked to descendants of ${t}: ${a}`);const i=await this.server.ldap.search({paged:!0,filter:a,attributes:[this.linkAttr,this.pathAttr]},r);let n=0;for await(const r of i)for(const a of r.searchEntries){const r=String(a.dn),i=a[this.linkAttr],s=Array.isArray(i)?String(i[0]):String(i);if(s!==t&&s.endsWith(`,${t}`))try{const a=s.replace(`,${t}`,`,${e}`),i=await this.getDepartmentPath(a);await this.server.ldap.modify(r,{replace:{[this.linkAttr]:a,[this.pathAttr]:i}}),n++,this.logger.debug(`Updated descendant link ${r}: ${this.linkAttr}=${a}, ${this.pathAttr}=${i}`)}catch(t){this.logger.error(`Failed to update descendant reference ${r}: ${t}`)}}this.logger.info(`Updated ${n} resources linked to descendants of the renamed organization`)}async getDepartmentPath(t){try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.pathAttr,"ou","o"]},t);if(!e.searchEntries||0===e.searchEntries.length)throw new Error(`Organization ${t} not found`);const r=e.searchEntries[0];if(r[this.pathAttr]){const t=r[this.pathAttr];return Array.isArray(t)?String(t[0]):String(t)}const a=r.ou||r.o;if(a){return`/${Array.isArray(a)?String(a[0]):String(a)}`}return this.logger.warn(`Organization ${t} has no ${this.pathAttr} attribute, using DN`),t}catch(e){throw new Error(`Failed to fetch organization ${t}: ${e}`)}}}export{LdapDepartmentSync as default};
@@ -0,0 +1 @@
1
+ import e from"../../abstract/plugin.js";import{launchHooksChained as r,launchHooks as s}from"../../lib/utils.js";class ExternalUsersInGroups extends e{constructor(e){super(e),this.name="externalUsersInGroups",this.roles=["consistency"],this.dependencies={ldapGroups:"core/ldap/ldapGroups"},this.hooks={ldapgroupvalidatemembers:async([e,a])=>(await Promise.all(a.map(e=>e.replace(/\s/g,"")).map(e=>this.server.ldap.queryLimit(async()=>{if(!new RegExp(`mail=([^,]+),${this.config.external_members_branch}$`).test(e))return!1;try{return await this.ldap.search({paged:!1,scope:"base"},e),!0}catch{const a=e.replace(/^mail=([^,]+),.*$/,"$1");if(!a)throw new Error(`Malformed member ${e}`);if(this.config.mail_domain&&Array.isArray(this.config.mail_domain)){const e=a.split("@")[1];if(e&&this.config.mail_domain.includes(e))throw new Error(`Cannot create external user with managed domain: ${a}`)}let t={objectClass:this.config.external_branch_class||this.config.user_class,mail:a,[this.config.ldap_groups_main_attribute]:a,sn:a};[e,t]=await r(this.registeredHooks.externaluserentry,[e,t]);try{return await this.ldap.add(e,t),s(this.registeredHooks.externaluseradded,e,t),!0}catch(r){throw new Error(`Unable to insert ${e}: ${r instanceof Error?r.message:String(r)}`)}}}))),[e,a])},this.ldap=e.ldap}}export{ExternalUsersInGroups as default};
@@ -0,0 +1 @@
1
+ import t from"fs";import a from"../../abstract/plugin.js";import e from"../../abstract/ldapFlat.js";import{transformSchemas as s}from"../../lib/utils.js";class LdapFlatInstance extends e{constructor(t,a){super(t,a),this.name="ldapFlatInstance",this.roles=["api"],this.dependencies={departmentSync:"core/ldap/departmentSync"}}}class LdapFlatGeneric extends a{constructor(a){super(a),this.name="ldapFlatGeneric",this.roles=["configurable"],this.instances=[],this.schemaPaths=[];const e=this.config.ldap_flat_schema||[];0!==e.length?e.forEach(e=>{try{const i=t.readFileSync(e,"utf8"),n=JSON.parse(s(i,this.config));if(!n.entity)throw new Error(`Schema ${e} is missing "entity" metadata section`);const r=["name","mainAttribute","objectClass","singularName","pluralName","base"];for(const t of r)if(!n.entity[t])throw new Error(`Schema ${e} is missing entity.${t}`);let l=n.entity.base;l=l.replace(/\{([^}]+)\}/g,(t,a)=>{const e=a,s=this.config[e];return"string"==typeof s?s:t});const c=new LdapFlatInstance(a,{base:l,mainAttribute:n.entity.mainAttribute,objectClass:n.entity.objectClass,defaultAttributes:n.entity.defaultAttributes||{},schemaPath:e,singularName:n.entity.singularName,pluralName:n.entity.pluralName,hookPrefix:`ldap${n.entity.name}`});c.name=`ldapFlat:${n.entity.name}`,this.instances.push(c),this.schemaPaths.push(e),this.logger.info(`Created ldapFlat instance for "${n.entity.name}" (${n.entity.pluralName})`)}catch(t){this.logger.error(`Failed to load schema ${e}: ${t}`)}}):this.logger.warn("No schemas provided for ldapFlatGeneric plugin")}api(t){this.instances.forEach(a=>{a.api(t)})}getConfigApiData(){const t=this.config.api_prefix||"/api",a=this.config.static_name||"static";return{flatResources:this.instances.map((e,s)=>{let i;if(this.server.loadedPlugins.static&&this.schemaPaths[s]){const t=this.schemaPaths[s],e=t.indexOf("/schemas/");if(-1!==e){const s=t.substring(e);i=`/${a}${s}`}}return{name:e.name.replace("ldapFlat:",""),singularName:e.singularName,pluralName:e.pluralName,mainAttribute:e.mainAttribute,objectClass:e.objectClass,base:e.base,schema:e.schema||{strict:!1,attributes:{}},schemaUrl:i,endpoints:{list:`${t}/v1/ldap/${e.pluralName}`,get:`${t}/v1/ldap/${e.pluralName}/:id`,create:`${t}/v1/ldap/${e.pluralName}`,update:`${t}/v1/ldap/${e.pluralName}/:id`,delete:`${t}/v1/ldap/${e.pluralName}/:id`}}})}}}export{LdapFlatGeneric as default};
@@ -0,0 +1 @@
1
+ import e from"fs";import t from"../../abstract/plugin.js";import{wantJson as r,badRequest as i,ok as s,serverError as a,jsonBody as o,tryMethod as n,notFound as d}from"../../lib/expressFormatedResponses.js";import{transformSchemas as c,launchHooksChained as h,launchHooks as p}from"../../lib/utils.js";class LdapGroups extends t{constructor(t){if(super(t),this.name="ldapGroups",this.roles=["api","configurable"],this.hooks={ldapdeleterequest:async e=>{let t=e;return Array.isArray(t)||(t=[t]),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)}),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(c(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`,async(e,t)=>{if(r(e,t))try{const r={};if(e.query.match&&("string"!=typeof e.query.match||!/^[\w*=()&|]+$/.test(e.query.match)))return i(t,"Invalid match query");e.query.match&&(r.filter=/=/.test(e.query.match)?`${e.query.match}`:`(${this.cn}=${e.query.match})`),e.query.attributes&&"string"==typeof e.query.attributes&&(r.attributes=e.query.attributes.split(","));const a=await this.listGroups(r);return s(t,a)}catch(e){return a(t,e)}}),e.get(`${this.config.api_prefix}/v1/ldap/groups/:cn`,async(e,t)=>this.apiGet(e,t)),e.post(`${this.config.api_prefix}/v1/ldap/groups`,async(e,t)=>this.apiAdd(e,t,this.cn)),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn`,async(e,t)=>this.apiDelete(e,t)),e.put(`${this.config.api_prefix}/v1/ldap/groups/:cn`,async(e,t)=>this.apiModify(e,t)),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/members`,async(e,t)=>{const r=decodeURIComponent(e.params.cn);if(!r)return i(t,"cn is required");const s=o(e,t,"member");s&&await n(t,this.addMember.bind(this),r,s.member)}),e.delete(`${this.config.api_prefix}/v1/ldap/groups/:cn/members/:member`,async(e,t)=>{if(!r(e,t))return;const s=decodeURIComponent(e.params.cn),a=decodeURIComponent(e.params.member);if(!s||!a)return i(t,"cn and member are required");await n(t,this.deleteMember.bind(this),s,a)}),this.config.ldap_organization_link_attribute&&this.config.ldap_organization_path_attribute&&e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/move`,async(e,t)=>{if(!r(e,t))return;const s=decodeURIComponent(e.params.cn);if(!s)return i(t,"cn is required");const a=o(e,t,"targetOrgDn");a&&await n(t,this.moveGroup.bind(this),s,a.targetOrgDn,e)}),e.post(`${this.config.api_prefix}/v1/ldap/groups/:cn/rename`,async(e,t)=>{if(!r(e,t))return;const s=decodeURIComponent(e.params.cn);if(!s)return i(t,"cn is required");const a=o(e,t,"newCn");a&&await n(t,this.renameGroup.bind(this),s,a.newCn)})}async apiGet(e,t){if(!r(e,t))return;const i=decodeURIComponent(e.params.cn);try{const e=/,/.test(i)?i:`${this.cn}=${i},${this.base}`,r=await this.ldap.search({paged:!1,scope:"base"},e);if(0===r.searchEntries.length)return d(t,"Group not found");t.json(r.searchEntries[0])}catch(e){return e.code&&32===e.code?d(t,"Group not found"):a(t,e)}}async apiAdd(e,t,...r){const i=o(e,t,...r);if(!i)return;const s=i[this.cn],a=i.member?i.member:[],d=Object.fromEntries(Object.entries(i).filter(([e,t])=>e!==this.cn&&"member"!==e&&void 0!==t&&!(!0===this.schema?.attributes[e]?.fixed)));await n(t,this.addGroup.bind(this),s,a,d)}async apiDelete(e,t){if(!r(e,t))return;const s=decodeURIComponent(e.params.cn);if(!s)return i(t,"cn is required");await n(t,this.deleteGroup.bind(this),s)}async apiModify(e,t){const r=o(e,t);if(!r)return;const s=this.fixDn(decodeURIComponent(e.params.cn));if(!s)return i(t);const a=Object.fromEntries(Object.entries(r).filter(([e,t])=>!(!0===this.schema?.attributes[e]?.fixed)));await n(t,this.modifyGroup.bind(this),s,a)}async addGroup(e,t=[],r={}){let i;new RegExp(`^${this.cn}=`).test(e)?(i=e,e=e.replace(new RegExp(`^${this.cn}=([^,]+).*`),"$1")):i=`${this.cn}=${e},${this.base}`,await this.validateMembers(i,t),await this.validateNewGroup(i,{objectClass:this.config.group_class,cn:e,member:t,...r});let s,a={objectClass:this.config.group_class,...this.config.group_default_attributes,[this.cn]:e,member:[this.config.group_dummy_user],...r};t.length&&a.member.push(...t),[i,a]=await h(this.registeredHooks.ldapgroupadd,[i,a]);try{s=await this.ldap.add(i,a)}catch(e){throw new Error(`Failed to add group ${i}: ${e}`)}return p(this.registeredHooks.ldapgroupadddone,[i,a]),s}async modifyGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;const i=this.opNumber();if([r,t]=await h(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 p(this.registeredHooks.ldapgroupmodifydone,[r,t,i]),s}async renameGroup(e,t){let r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`,i=/,/.test(t)?t:`${this.cn}=${t},${this.base}`;[r,i]=await h(this.registeredHooks.ldapgrouprename,[r,i]);const s=await this.ldap.rename(r,i);return p(this.registeredHooks.ldapgrouprenamedone,[r,i]),s}async deleteGroup(e){let t=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;t=await h(this.registeredHooks.ldapgroupdelete,t);const r=await this.ldap.delete(t);return p(this.registeredHooks.ldapgroupdeletedone,t),r}async addMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;return Array.isArray(t)||(t=[t]),[e,t]=await h(this.registeredHooks.ldapgroupaddmember,[e,t]),await this.validateMembers(r,t),await this.ldap.modify(r,{add:{member:t}}).catch(e=>{throw new Error(`Failed to add member(s) to ${r}: ${e}`)})}async deleteMember(e,t){const r=/,/.test(e)?e:`${this.cn}=${e},${this.base}`;if([e,t]=await h(this.registeredHooks.ldapgroupdeletemember,[e,t]),t===this.config.group_dummy_user)throw new Error("Cannot delete dummy member from group");return await this.ldap.modify(r,{delete:{member:t}}).catch(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}=${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 d;try{d=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===d.searchEntries.length)throw new Error(`Target organization ${t} not found`);const c=d.searchEntries[0][s];if(!c)throw new Error(`Target organization ${t} does not have ${s} attribute`);return await this.ldap.modify(a,{replace:{[i]:t,[s]:c}},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)e.searchEntries.map(e=>{const t=e[this.cn];t&&(i[t]=e),Array.isArray(i[t].member)||(i[t].member=[i[t].member]),i[t].member=i[t].member.filter(e=>e!==this.config.group_dummy_user)});return i=await h(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}=${e},${this.base}`)}async validateMembers(e,t){if([e,t]=await h(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 h(this.server.hooks.ldapgroupvalidatenew,[e,t]);for(const[e,r]of Object.entries(t))if(!await this._validateOneChange(e,r))throw new Error(`Invalid value for field ${e}`);for(const[e,r]of Object.entries(this.schema.attributes))if(r.required&&null==t[e])throw new Error(`Missing required field ${e}`);return!0}async validateChanges(e,t){if(!this.schema)return!0;if([e,t]=await h(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 Error(`Field ${e} is not allowed`);return!0}if(null==t){if(r.required)throw new Error(`Field ${e} is required`);return!0}if("array"===r.type){if(!Array.isArray(t))throw new Error(`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){"string"==typeof r.items.test&&(r.items.test=new RegExp(r.items.test));for(let i of t){if(typeof i!==r.items.type)throw new Error(`Field ${e} must be of type ${r.items.type}`);if("string"!=typeof i&&(i=i.toString()),r.items.test&&!r.items.test.test(i))throw new Error(`Field ${e} has invalid value ${i}`)}}}else if("pointer"===r.type){if("string"!=typeof t)throw new Error(`Field ${e} must be a string (DN pointer)`);const i=t;if(r.branch&&r.branch.length>0){if(!r.branch.some(e=>new RegExp(`,?${e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}$`,"i").test(i)))throw new Error(`Field ${e} must point to a DN within allowed branches: ${r.branch.join(", ")}`)}try{const t=await this.ldap.search({paged:!1,scope:"base"},i);if(!t||!t.searchEntries||0===t.searchEntries.length)throw new Error(`Field ${e} points to non-existent DN: ${i}`)}catch(t){throw new Error(`Field ${e} points to invalid or non-existent DN: ${i}: ${t}`)}if(r.test&&("string"==typeof r.test&&(r.test=new RegExp(r.test)),r.test&&!r.test.test(i)))throw new Error(`Field ${e} has invalid value ${i}`)}else{if(typeof t!==r.type)return!1;if("string"!=typeof t&&(t=t.toString()),r.test&&("string"==typeof r.test&&(r.test=new RegExp(r.test)),r.test&&!r.test.test(t)))throw new Error(`Field ${e} has invalid value ${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};
@@ -0,0 +1 @@
1
+ import t from"../../abstract/plugin.js";import{launchHooks as e}from"../../lib/utils.js";const r={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.hooks={ldapmodifyrequest:async([t,e,r])=>{const n=await this.server.ldap.search({paged:!1},t);return 1==n.searchEntries.length?this.stack[r]=n.searchEntries[0]:this.logger.warn(`Could not find unique entry ${t} before modification, got ${n.searchEntries.length} entries`),[t,e,r]},ldapmodifydone:([t,e,r])=>{const n=this.stack[r];if(!n)return delete this.stack[r],void this.logger.warn(`Received a ldapmodifydone for an unknown operation (${r})`);const a={};if(e.add)for(const[t,r]of Object.entries(e.add))a[t]=[null,r];if(e.delete)if(Array.isArray(e.delete))for(const t of e.delete)a[t]=[n[t],null];else for(const[t,r]of Object.entries(e.delete))a[t]=[r,null];if(e.replace)for(const[t,r]of Object.entries(e.replace))a[t]=[n[t],r];this.notify(t,a)}}}notify(t,n){e(this.server.hooks.onLdapChange,t,n);for(const[e,a]of Object.entries(r))this.config[e]&&n[this.config[e]]&&("onLdapQuotaChange"===a||"onLdapForwardChange"===a||"onLdapAliasChange"===a?this.notifyAttributeChangeWithMail(this.config[e],a,t,n):"onLdapMailChange"===a&&this.notifyAttributeChange(this.config[e],a,t,n));if(n.cn||n.givenName||n.sn){const r=this.reconstructDisplayName(n,0),a=this.reconstructDisplayName(n,1);e(this.server.hooks.onLdapDisplayNameChange,t,r,a)}}notifyAttributeChange(t,r,n,a,i=!1){const[s,o]=a[t]||[];void 0===s&&void 0===o||(i&&(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[r],n,s,o))}async notifyAttributeChangeWithMail(t,r,n,a){const[i,s]=a[t]||[];if(void 0===i&&void 0===s)return;const o=this.config.mail_attribute||"mail",h=a[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]},n);if(1!==t.searchEntries.length)return void this.logger.warn(`Could not find mail for ${n}, skipping ${r} 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 ${n}:`,t)}if("onLdapQuotaChange"===r){const t=i?Number(i):0,a=s?Number(s):0;t!==a&&e(this.server.hooks[r],n,l,t,a)}else if("onLdapForwardChange"===r||"onLdapAliasChange"===r){const t=i?Array.isArray(i)?i:[i]:[],a=s?Array.isArray(s)?s:[s]:[];(t.length>0||a.length>0)&&e(this.server.hooks[r],n,l,t,a)}}reconstructDisplayName(t,e){const getValue=r=>{if(!t[r])return null;const n=t[r][e];return n?Array.isArray(n)?n.length>0?String(n[0]):null:String(n):null},r=getValue("cn");if(r)return r;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};
@@ -0,0 +1 @@
1
+ import t from"fs";import i from"../../abstract/plugin.js";import{tryMethodData as a,badRequest as e,jsonBody as r,tryMethod as n,wantJson as s}from"../../lib/expressFormatedResponses.js";import{transformSchemas as o,launchHooksChained as h}from"../../lib/utils.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),void 0!==a?[t,i,a]:[t,i]),ldapmodifyrequest:async([t,i,a])=>{let e,r={},n={};const checkIsOu=async()=>{if(void 0!==e)return e;if(i.replace?.objectClass)e=this.isOu({objectClass:i.replace.objectClass});else if(i.add?.objectClass)e=this.isOu({objectClass:i.add.objectClass});else{const i=await this.server.ldap.search({paged:!1,scope:"base"},t);e=i.searchEntries.length>0&&this.isOu(i.searchEntries[0])}return e};if(i.delete){const t=Array.isArray(i.delete)?i.delete.includes(this.linkAttr):i.delete[this.linkAttr],a=Array.isArray(i.delete)?i.delete.includes(this.pathAttr):i.delete[this.pathAttr];if(t||a){if(!await checkIsOu()&&t)throw new Error("An organization link cannot be deleted");if(a)throw new Error("An organization path cannot be deleted")}}if(i.replace&&(i.replace[this.linkAttr]&&(r={...i.replace}),i.replace[this.pathAttr]&&(n={...i.replace})),i.add&&(i.add[this.linkAttr]&&(r={...r,...i.add}),i.add[this.pathAttr]&&(n={...n,...i.add})),Object.keys(r).length>0){await checkIsOu()||await this.checkDeptLink(r)}if(Object.keys(n).length>0){await checkIsOu()&&await this.checkDeptPath(n)}return[t,i,a]},ldapdeleterequest:async([t])=>(/^ou=/.test(t)&&await this.isEmptyOrganization(t),[t]),ldaprenamerequest:([t,i])=>[t,i]},!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(o(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,this.getOrganisationTop.bind(this),t)}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,i)=>{const e=decodeURIComponent(t.params.dn);await a(i,this.getOrganisationByDn.bind(this),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,this.getOrganisationSubnodes.bind(this),e,t)}),t.get(`${this.config.api_prefix}/v1/ldap/organizations/:dn/subnodes/search`,async(t,i)=>{const r=decodeURIComponent(t.params.dn),n=t.query.q;if(!n)return e(i,'query parameter "q" is required');await a(i,this.searchOrganisationSubnodes.bind(this),r,n)}),t.post(`${this.config.api_prefix}/v1/ldap/organizations`,async(t,i)=>this.apiAdd(t,i)),t.put(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,i)=>this.apiModify(t,i)),t.delete(`${this.config.api_prefix}/v1/ldap/organizations/:dn`,async(t,i)=>this.apiDelete(t,i)),t.post(`${this.config.api_prefix}/v1/ldap/organizations/:dn/move`,async(t,i)=>this.apiMove(t,i))}async apiAdd(t,i){const a=r(t,i,"ou");if(!a)return;const e=a.parentDn||this.config.ldap_top_organization,s=`ou=${a.ou},${e}`,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),s,o,t)}async apiModify(t,i){const a=r(t,i);if(!a)return;const s=decodeURIComponent(t.params.dn);if(!s)return e(i,"dn is required");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)return e(i,"dn is required");await n(i,this.deleteOrganization.bind(this),a)}async apiMove(t,i){if(!s(t,i))return;const n=r(t,i,"targetOrgDn");if(!n)return;const o=decodeURIComponent(t.params.dn);if(!o)return e(i,"dn is required");const{targetOrgDn:h}=n;if(!h||"string"!=typeof h)return e(i,"Missing or invalid targetOrgDn in request body");await a(i,this.moveOrganization.bind(this),o,h,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 Error(`Organization ${a} does not exist`);if(!new RegExp(`(.*,)?${this.config.ldap_top_organization}`).test(e.searchEntries[0].dn))throw new Error(`Entry ${a} isn't in top organization branch`)}}async checkDeptPath(t){if(t[this.pathAttr]){const i=t[this.pathAttr],a=Array.isArray(i)?i[0]:i,e=this.config.ldap_organization_path_separator||" / ";let r=a;if(this.isOu(t)){const i=t.ou,n=Array.isArray(i)?i[0]:i;if(!a.startsWith(n+e))throw new Error(`Organization path must start with its own name followed by separator "${e}"`);r=a.slice(n.length+e.length)}const[n,s]=r.split(e,2),o=await this.server.ldap.search({paged:!1,filter:`(ou=${n})`,scope:"sub"},this.config.ldap_top_organization);if(0===o.searchEntries.length)throw new Error(`Invalid organization path ${a}`);if(s){let t=!1;for(const i of o.searchEntries){const a=i[this.pathAttr],e=Array.isArray(a)?a[0]:a;if(e&&e===s){t=!0;break}}if(!t)throw new Error(`Invalid organization path ${a}: no matching entry for ${n} with path ${s}`)}else{let t=!1;for(const i of o.searchEntries){const a=i.dn,r=this.config.ldap_top_organization;if(a.toLowerCase()===r.toLowerCase()||a.toLowerCase().endsWith(`,${r.toLowerCase()}`)){const a=i[this.pathAttr],r=Array.isArray(a)?a[0]:a;if(!r||r===n||!r.includes(e)){t=!0;break}}}if(!t)throw new Error(`Invalid organization path ${a}: no matching top-level entry for ${n}`)}}}async isEmptyOrganization(t){if((await this.server.ldap.search({paged:!1,filter:`(${this.config.ldap_organization_link_attribute}=${t})`})).searchEntries.length>0)throw new Error(`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 Error("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 Error("Top organization not found");const[,a]=await h(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 Error(`Organization ${t} not found`);return a.searchEntries[0]}async getOrganisationSubnodes(t,i){const a=[],e=this.config.ldap_organization_max_subnodes||50,r=i?.query?.objectClass;if(!r||"organizationalUnit"===r)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 n=this.config.ldap_top_organization.replace(/^ou=[^,]+,/,"");let s=`(${this.config.ldap_organization_link_attribute}=${t})`;r&&(s=`(&${s}(objectClass=${r}))`),this.server.logger.debug(`Searching for linked entities with filter: ${s} in ${n}`);const o=await this.server.ldap.search({paged:!0,filter:s},n,i);let h=0,c=0;for await(const t of o){c+=t.searchEntries.length;const i=e-h;if(i>0){const e=t.searchEntries.slice(0,i);a.push(...e),h+=e.length}}return this.server.logger.debug(`Found ${c} linked entities for ${t}, returning ${h}`),c>e&&a.push({dn:`more-${t}`,cn:[`... ${c-e} more elements`],objectClass:["moreIndicator"],_isMoreIndicator:"true",_totalCount:c.toString(),_displayedCount:e.toString()}),a}async addOrganization(t,i,a){return this.validateNewOrganization(t,i),await this.server.ldap.add(t,i,a)}async modifyOrganization(t,i){return this.validateChanges(t,i),await this.server.ldap.modify(t,i)}validateNewOrganization(t,i){if(!this.schema)return!0;for(const[t,a]of Object.entries(i))if(!this._validateOneChange(t,a))throw new Error(`Invalid value for field ${t}`);for(const[t,a]of Object.entries(this.schema.attributes))if(a.required&&null==i[t])throw new Error(`Missing required field ${t}`);return!0}validateChanges(t,i){if(!this.schema)return!0;if(i.add)for(const[t,a]of Object.entries(i.add))if(!this._validateOneChange(t,a))throw new Error(`Invalid value for field ${t}`);if(i.replace)for(const[t,a]of Object.entries(i.replace))if(!this._validateOneChange(t,a))throw new Error(`Invalid value for field ${t}`);return!0}_validateOneChange(t,i){if(!this.schema)return!0;const a=this.schema.attributes[t];if(!a){if(this.schema.strict)throw new Error(`Field ${t} is not allowed`);return!0}if(null==i){if(a.required)throw new Error(`Field ${t} is required`);return!0}if(a.type)switch(a.type){case"string":if(Array.isArray(i))throw new Error(`Field ${t} must be a single value`);break;case"array":if(!Array.isArray(i))throw new Error(`Field ${t} must be an array`)}if(a.test){const e=Array.isArray(i)?i[0]:i;if(!("string"==typeof a.test?new RegExp(a.test):a.test).test(String(e)))throw new Error(`Field ${t} does not match required pattern`)}return!0}async moveOrganization(t,i,a){try{const t=await this.server.ldap.search({paged:!1,scope:"base"},i);if(!t.searchEntries||0===t.searchEntries.length)throw new Error(`Target organization ${i} not found`);if(!this.isOu(t.searchEntries[0]))throw new Error(`Target ${i} is not an organizational unit`)}catch(t){throw new Error(`Invalid target organization: ${t}`)}const e=`${t.split(",")[0]},${i}`;try{const i=await this.server.ldap.search({paged:!1,scope:"base"},t);if(!i.searchEntries||0===i.searchEntries.length)throw new Error(`Source organization ${t} not found`)}catch(t){throw new Error(`Source organization not found: ${t}`)}if(e===t)throw new Error("Cannot move organization to its current location");if(i===t||i.endsWith(`,${t}`))throw new Error("Cannot move organization into itself or its descendant");try{await this.server.ldap.rename(t,e,a),this.logger.info(`Moved organization from ${t} to ${e}`)}catch(t){throw new Error(`Failed to move organization: ${t}`)}return{newDn:e}}async deleteOrganization(t){return await this.server.ldap.delete(t)}async searchOrganisationSubnodes(t,i){const a=[];try{const e=await this.server.ldap.search({paged:!1,scope:"one",filter:`(&(objectClass=organizationalUnit)(|(ou=*${i}*)(description=*${i}*)))`},t);this.server.logger.debug(`Found ${e.searchEntries.length} sub-OUs matching "${i}" for ${t}`),a.push(...e.searchEntries)}catch(a){this.server.logger.debug(`No sub-OUs found matching "${i}" for ${t}: ${a}`)}const e=this.config.ldap_top_organization.replace(/^ou=[^,]+,/,""),r=`(&(${this.config.ldap_organization_link_attribute}=${t})(|(uid=*${i}*)(cn=*${i}*)(mail=*${i}*)(sn=*${i}*)(givenName=*${i}*)))`;this.server.logger.debug(`Searching for linked entities with filter: ${r} in ${e}`);const n=await this.server.ldap.search({paged:!0,filter:r},e);let s=0;for await(const t of n)s+=t.searchEntries.length,a.push(...t.searchEntries);return this.server.logger.debug(`Found ${s} 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`,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};
@@ -0,0 +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=>{const s=Array.isArray(t)?t:[t],a=[];for(const t of s){if(!this.isWatched(t)){this.logger.debug(`Trash plugin: ${t} not in watched branches, allowing native delete`),a.push(t);continue}this.logger.info(`Trash plugin: Intercepting delete of ${t}`),await this.ensureTrashExists();const s=this.buildTrashDN(t);await this.removeOldTrashEntry(s);try{this.logger.debug(`Moving ${t} to ${s} using LDAP move (atomic)`),await this.server.ldap.move(t,s),this.logger.info(`Entry ${t} moved to trash at ${s}`),await this.addMetadataToTrash(s,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)?a:a[0]??void 0}};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};
@@ -0,0 +1 @@
1
+ var e=["core/weblogs","core/auth/crowdsec","core/auth/rateLimit","core/auth/token","core/auth/llng","core/auth/openidconnect"];export{e as default};
@@ -0,0 +1 @@
1
+ import s from"fs";import{resolve as t,join as e}from"path";import a from"express";import r from"../abstract/plugin.js";import{notFound as i}from"../lib/expressFormatedResponses.js";import{transformSchemas as n}from"../lib/utils.js";class Static extends r{constructor(){super(...arguments),this.name="static",this.roles=["api"]}api(r){const c=this.config.static_path;if(!c)throw new Error("--static-path is not defined");try{if(!s.statSync(c).isDirectory())throw new Error(`${c} isn't a directory`);s.accessSync(c,s.constants.R_OK)}catch(s){throw new Error(`Bad directory ${c}: ${s}`)}r.get(`/${this.config.static_name}/schemas/:name`,(a,r)=>{if(!/^[\w-]+\.json$/.test(a.params.name))return r.status(400).send("Invalid schema name");const o=t(e(c,"schemas",a.params.name)),m=t(e(c,"schemas"));if(!o.startsWith(m+"/"))return r.status(403).send("Access denied");s.readFile(o,(s,t)=>{if(s)return i(r,"Schema not found");const e=n(t,this.config);r.type("json").send(e)})}),r.get(`/${this.config.static_name}/schemas/:dir/:name`,(a,r)=>{if(!/^[\w-]+$/.test(a.params.dir)||!/^[\w-]+\.json$/.test(a.params.name))return r.status(400).send("Invalid schema name");const o=t(e(c,"schemas",a.params.dir,a.params.name)),m=t(e(c,"schemas"));if(!o.startsWith(m+"/"))return r.status(403).send("Access denied");s.readFile(o,(s,t)=>{if(s)return i(r,"Schema not found");const e=n(t,this.config);r.type("json").send(e)})}),r.use(`/${this.config.static_name}`,a.static(c))}}export{Static as default};
@@ -0,0 +1 @@
1
+ import{TwakePlugin as e}from"../../abstract/twakePlugin.js";class CalendarResources extends e{constructor(e){super(e,"calendar_webadmin_url","calendar_webadmin_token","calendar_concurrency"),this.name="calendarResources",this.roles=["consistency"],this.hooks={ldapcalendarResourceadddone:async e=>{const[r,s]=e;if(!this.isResource(r,s))return;const a=this.buildResourceData(r,s);a?await this.callWebAdminApi("ldapcalendarResourceadddone",`${this.webadminUrl}/resources`,"POST",r,JSON.stringify(a),{resourceName:a.name}):this.logger.debug(`Skipping resource creation for ${r}: missing required fields`)},ldapcalendarResourcemodifydone:async e=>{const[r,s]=e,a=this.getResourceId(r);if(!a)return;const c={};s.replace?.cn&&(c.name=Array.isArray(s.replace.cn)?String(s.replace.cn[0]):String(s.replace.cn)),s.replace?.description&&(c.description=Array.isArray(s.replace.description)?String(s.replace.description[0]):String(s.replace.description)),0!==Object.keys(c).length?await this.callWebAdminApi("ldapcalendarResourcemodifydone",`${this.webadminUrl}/resources/${a}`,"PATCH",r,JSON.stringify(c),{resourceId:a,...c}):this.logger.debug(`No relevant changes for resource ${r}`)},ldapcalendarResourcedeletedone:async e=>{const r=this.getResourceId(e);r&&await this.callWebAdminApi("ldapcalendarResourcedeletedone",`${this.webadminUrl}/resources/${r}`,"DELETE",e,null,{resourceId:r})}},this.resourceBase=this.config.calendar_resource_base||"",this.resourceObjectClass=this.config.calendar_resource_objectclass||"",this.resourceCreator=this.config.calendar_resource_creator||"admin@example.com",this.resourceDomain=this.config.calendar_resource_domain||""}isResource(e,r){if(this.resourceBase&&!e.toLowerCase().includes(this.resourceBase.toLowerCase()))return!1;const s=r.objectClass;if(this.resourceObjectClass){return(Array.isArray(s)?s:[s]).some(e=>String(e).toLowerCase()===this.resourceObjectClass.toLowerCase())}return!0}buildResourceData(e,r){const s=r.cn,a=this.attributeToString(s);if(!a)return null;const c=this.attributeToString(r.description)||void 0,t=this.resourceCreator,i=this.resourceDomain||this.extractDomainFromDn(e),o=this.getResourceId(e)||a.toLowerCase().replace(/\s+/g,"-");return{name:a,description:c,creator:t,domain:i,id:o}}getResourceId(e){const r=e.match(/(?:cn|uid)=([^,]+)/i);return r?r[1]:null}}export{CalendarResources as default};
@@ -0,0 +1 @@
1
+ import i from"node-fetch";import{TwakePlugin as t}from"../../abstract/twakePlugin.js";class James extends t{constructor(i){super(i,"james_webadmin_url","james_webadmin_token","james_concurrency"),this.name="james",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange",ldapGroups:"core/ldap/groups"},this.hooks={ldapadddone:async i=>{const[t,e]=i,a=e[this.mailAttr],s=e[this.quotaAttr],r=e[this.aliasAttr];if(!a)return;const n=Array.isArray(a)?String(a[0]):String(a);if(this.initDelay>0&&await new Promise(i=>setTimeout(i,this.initDelay)),s){const i=Array.isArray(s)?Number(s[0]):Number(s);!isNaN(i)&&i>0&&await this.callWebAdminApi("ldapadddone:quota",`${this.webadminUrl}/quota/users/${n}/size`,"PUT",t,i.toString(),{mail:n,quota:i})}if(r){const i=this.getAliases(r);await Promise.all(i.map(i=>this.callWebAdminApi("ldapadddone:alias",`${this.webadminUrl}/address/aliases/${n}/sources/${i}`,"PUT",t,null,{mail:n,alias:i})))}const l=this.getDisplayNameFromAttributes(e);l&&await this.updateJamesIdentity(t,n,l)},onLdapMailChange:async(i,t,e)=>{if(t&&"undefined"!==t){await this.callWebAdminApi("onLdapMailChange",`${this.webadminUrl}/users/${t}/rename/${e}?action=rename`,"POST",i,null,{oldmail:t,newmail:e});try{const a=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.aliasAttr]},i);if(a.searchEntries&&a.searchEntries.length>0){const s=this.getAliases(a.searchEntries[0][this.aliasAttr]);s.length>0&&await Promise.all([...s.map(e=>this.callWebAdminApi("onLdapMailChange-delete",`${this.webadminUrl}/address/aliases/${t}/sources/${e}`,"DELETE",i,null,{oldmail:t,alias:e})),...s.map(t=>this.callWebAdminApi("onLdapMailChange-create",`${this.webadminUrl}/address/aliases/${e}/sources/${t}`,"PUT",i,null,{newmail:e,alias:t}))])}}catch(i){this.logger.debug("Could not fetch aliases for mail change:",i)}}else this.logger.debug(`Skipping mail rename for ${i}: oldmail is empty (mail attribute was added, not changed)`)},onLdapAliasChange:async(i,t,e,a)=>{const s=e.map(i=>this.normalizeAlias(i)),r=a.map(i=>this.normalizeAlias(i)),n=s.filter(i=>!r.includes(i)),l=r.filter(i=>!s.includes(i));await Promise.all([...n.map(e=>this.callWebAdminApi("onLdapAliasChange-delete",`${this.webadminUrl}/address/aliases/${t}/sources/${e}`,"DELETE",i,null,{mail:t,alias:e,action:"delete"})),...l.map(e=>this.callWebAdminApi("onLdapAliasChange-add",`${this.webadminUrl}/address/aliases/${t}/sources/${e}`,"PUT",i,null,{mail:t,alias:e,action:"add"}))])},onLdapQuotaChange:(i,t,e,a)=>this.callWebAdminApi("onLdapQuotaChange",`${this.webadminUrl}/quota/users/${t}/size`,"PUT",i,a.toString(),{oldQuota:e,newQuota:a}),onLdapForwardChange:async(i,t,e,a)=>{const s=this.extractMailDomain(t);if(!s)return void this.logger.error(`Cannot extract domain from mail ${t} for forward management`);const r=e.filter(i=>!a.includes(i)),n=a.filter(i=>!e.includes(i));await Promise.all([...r.map(e=>this.callWebAdminApi("onLdapForwardChange-delete",`${this.webadminUrl}/domains/${s}/forwards/${t}/${e}`,"DELETE",i,null,{mail:t,forward:e,domain:s,action:"delete"})),...n.map(e=>this.callWebAdminApi("onLdapForwardChange-add",`${this.webadminUrl}/domains/${s}/forwards/${t}/${e}`,"PUT",i,null,{mail:t,forward:e,domain:s,action:"add"}))])},onLdapChange:async(i,t)=>{this.config.delegation_attribute&&t[this.config.delegation_attribute]&&await this._handleDelegationChange(i,t)},onLdapDisplayNameChange:async i=>{const t=[this.mailAttr,this.displayNameAttr,"cn","givenName","sn"],e=await this.ldapGetAttributes(i,t);if(!e)return void this.logger.warn(`Cannot update James identity: entry not found for ${i}`);const a=this.attributeToString(e[this.mailAttr]);if(!a)return void this.logger.warn(`Cannot update James identity: no mail found for ${i}`);const s=this.getDisplayNameFromAttributes(e);if(s)return this.updateJamesIdentity(i,a,s);this.logger.warn(`Cannot update James identity: no display name found for ${i}`)},ldapgroupadddone:async i=>{const[t,e]=i,a=e.mail;if(!a)return void this.logger.debug(`Group ${t} has no mail attribute, skipping James sync`);const s=this.getMailboxType(e),r=Array.isArray(a)?String(a[0]):String(a);if("teamMailbox"===s){const i=this.config.james_mailing_list_branch||[];if(i.length>0&&this.isInAllowedBranches(t,i))return void this.logger.error({plugin:this.name,event:"ldapgroupadddone",dn:t,mailboxType:"teamMailbox",error:`Team mailbox cannot be in mailing list branches: ${i.join(", ")}`});await this.createTeamMailbox(t,e,r)}else if("mailingList"===s||null===s){const i=this.config.james_mailing_list_branch||[];if(i.length>0&&!this.isInAllowedBranches(t,i))return void this.logger.error({plugin:this.name,event:"ldapgroupadddone",dn:t,mailboxType:s||"mailingList (default)",error:`Mailing list must be in allowed branches: ${i.join(", ")}`});await this.createMailingList(t,e,r)}},ldapgroupmodifydone:async i=>{const[t,e]=i;if(e.replace?.[this.mailboxTypeAttr]||e.add?.[this.mailboxTypeAttr])return void await this.handleMailboxTypeTransition(t,e);const a=await this.getGroupMail(t);if(!a)return void this.logger.debug(`Group ${t} has no mail attribute, skipping James sync`);const s="teamMailbox"===await this.getGroupMailboxType(t),r=s?this.extractMailDomain(a):null,n=[];if(e.add?.member){const i=e.add.member,l=Array.isArray(i)?i:[i],o=await this.getMemberEmails(l);n.push(...o.map(i=>s&&r?this.callWebAdminApi("ldapgroupmodifydone:teamMailbox",`${this.webadminUrl}/domains/${r}/team-mailboxes/${a}/members/${i}`,"PUT",t,null,{domain:r,groupMail:a,memberMail:i,action:"add"}):this.callWebAdminApi("ldapgroupmodifydone:mailingList",`${this.webadminUrl}/address/groups/${a}/${i}`,"PUT",t,null,{groupMail:a,memberMail:i,action:"add"})))}if(this.isAttributesList(e.delete)){const i=e.delete.member;if(i){const e=Array.isArray(i)?i:[i],l=await this.getMemberEmails(e);n.push(...l.map(i=>s&&r?this.callWebAdminApi("ldapgroupmodifydone:teamMailbox",`${this.webadminUrl}/domains/${r}/team-mailboxes/${a}/members/${i}`,"DELETE",t,null,{domain:r,groupMail:a,memberMail:i,action:"delete"}):this.callWebAdminApi("ldapgroupmodifydone:mailingList",`${this.webadminUrl}/address/groups/${a}/${i}`,"DELETE",t,null,{groupMail:a,memberMail:i,action:"delete"})))}}await Promise.all(n)},ldapgroupdeletedone:async i=>{const t=await this.getGroupMail(i);if(!t)return void this.logger.debug(`Group ${i} has no mail attribute, skipping James sync`);"teamMailbox"===await this.getGroupMailboxType(i)?await this.removeAllTeamMailboxMembers(i,t):await this.callWebAdminApi("ldapgroupdeletedone:mailingList",`${this.webadminUrl}/address/groups/${t}`,"DELETE",i,null,{groupMail:t})}},this.quotaAttr=this.config.quota_attribute||"mailQuotaSize",this.aliasAttr=this.config.alias_attribute||"mailAlternateAddress",this.mailboxTypeAttr=this.config.james_mailbox_type_attribute||"twakeMailboxType",this.initDelay="number"==typeof this.config.james_init_delay?this.config.james_init_delay:1e3}async handleMailboxTypeTransition(i,t){const e=await this.ldapGetAttributes(i,["mail","member",this.mailboxTypeAttr]);if(!e)return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:i,error:"Could not fetch group attributes"});const a=e.mail;if(!a)return void this.logger.debug(`Group ${i} has no mail attribute, skipping mailbox type transition`);const s=Array.isArray(a)?String(a[0]):String(a),r=t.replace?.[this.mailboxTypeAttr]||t.add?.[this.mailboxTypeAttr],n=Array.isArray(r)?String(r[0]):String(r),l=this.getMailboxType({[this.mailboxTypeAttr]:n}),o=this.getMailboxType(e);if(this.logger.info({plugin:this.name,event:"handleMailboxTypeTransition",dn:i,mail:s,oldType:o||"none",newType:l||"none"}),o!==l)if("mailingList"===o?await this.deleteMailingList(i,s):"teamMailbox"===o&&await this.removeAllTeamMailboxMembers(i,s),"mailingList"===l){const t=this.config.james_mailing_list_branch||[];if(t.length>0&&!this.isInAllowedBranches(i,t))return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:i,newType:l,error:`Mailing list must be in allowed branches: ${t.join(", ")}`});await this.createMailingList(i,e,s)}else if("teamMailbox"===l){const t=this.config.james_mailing_list_branch||[];if(t.length>0&&this.isInAllowedBranches(i,t))return void this.logger.error({plugin:this.name,event:"handleMailboxTypeTransition",dn:i,newType:l,error:`Team mailbox cannot be in mailing list branches: ${t.join(", ")}`});await this.createTeamMailbox(i,e,s)}}getDisplayNameFromAttributes(i){const t=this.attributeToString(i[this.displayNameAttr]);if(t)return t;const e=this.attributeToString(i.cn);if(e)return e;const a=this.attributeToString(i.givenName),s=this.attributeToString(i.sn);if(a||s){const i=[];return a&&i.push(a),s&&i.push(s),i.join(" ")}const r=this.attributeToString(i[this.mailAttr]);return r||null}async getMailFromDN(i){const t=await this.ldapGetAttributes(i,[this.mailAttr]);return t?this.attributeToString(t[this.mailAttr]):null}async getDisplayNameFromDN(i){const t=[this.displayNameAttr,"cn","givenName","sn",this.mailAttr],e=await this.ldapGetAttributes(i,t);return e?this.getDisplayNameFromAttributes(e):null}async generateSignature(i){const t=this.config.james_signature_template;if(!t)return null;const e=await this.ldapGetAttributes(i);if(!e)return null;let a=t;return a=a.replace(/\{(\w+)\}/g,(i,t)=>"string"==typeof t&&e&&"object"==typeof e&&Object.prototype.hasOwnProperty.call(e,t)&&this.attributeToString(e[t])||""),a}async updateJamesIdentity(t,e,a){const s={plugin:this.name,event:"onLdapDisplayNameChange",result:"error",dn:t,mail:e,displayName:a};try{const r=`${this.webadminUrl}/jmap/identities/${e}`,n=await this.requestLimit(()=>i(r,{method:"GET",headers:this.createHeaders()}));if(!n.ok)return void this.logger.error({...s,step:"get_identities",http_status:n.status,http_status_text:n.statusText});const l=await n.json(),o=l.find(i=>i.email===e)||l[0];if(!o)return void this.logger.warn({...s,step:"find_identity",message:"No identity found for user"});const m=await this.generateSignature(t),d=`${this.webadminUrl}/jmap/identities/${e}/${o.id}`,h={id:o.id,email:o.email,name:a};m&&(h.htmlSignature=m);const g=await this.requestLimit(()=>i(d,{method:"PUT",headers:this.createHeaders("application/json"),body:JSON.stringify(h)}));g.ok?this.logger.info({...s,result:"success",http_status:g.status}):this.logger.error({...s,step:"update_identity",http_status:g.status,http_status_text:g.statusText})}catch(i){this.logger.error({...s,error:`${i}`})}}isAttributesList(i){return void 0!==i&&"object"==typeof i&&!Array.isArray(i)}getMailboxType(i){const t=i[this.mailboxTypeAttr];if(!t)return null;const e=(Array.isArray(t)?String(t[0]):String(t)).match(/^cn=([^,]+)/);if(!e)return null;const a=e[1];return"group"===a||"mailingList"===a||"teamMailbox"===a?a:null}isInAllowedBranches(i,t){return!t||0===t.length||t.some(t=>i===t||i.endsWith(","+t))}async createMailingList(i,t,e){const a=t.member||[],s=Array.isArray(a)?a:[a];this.logger.debug(`Creating mailing list ${e} in James with ${s.length} members`);const r=await this.getMemberEmails(s);await Promise.all(r.map(t=>this.callWebAdminApi("ldapgroupadddone:mailingList",`${this.webadminUrl}/address/groups/${e}/${t}`,"PUT",i,null,{groupMail:e,memberMail:t})))}async createTeamMailbox(i,t,e){const a=this.extractMailDomain(e);if(!a)return void this.logger.error({plugin:this.name,event:"createTeamMailbox",dn:i,mail:e,error:"Cannot extract domain from mail address"});const s=t.member||[],r=Array.isArray(s)?s:[s];this.logger.debug(`Creating team mailbox ${e} in James with ${r.length} members`);const n=await this.getMemberEmails(r),l=[];l.push(this.callWebAdminApi("ldapgroupadddone:teamMailbox:create",`${this.webadminUrl}/domains/${a}/team-mailboxes/${e}`,"PUT",i,null,{domain:a,teamMailbox:e}));for(const t of n)l.push(this.callWebAdminApi("ldapgroupadddone:teamMailbox:addMember",`${this.webadminUrl}/domains/${a}/team-mailboxes/${e}/members/${t}`,"PUT",i,null,{domain:a,teamMailbox:e,memberMail:t}));await Promise.all(l)}async deleteTeamMailbox(i,t){const e=this.extractMailDomain(t);e?await this.callWebAdminApi("ldapgroupdeletedone:teamMailbox",`${this.webadminUrl}/domains/${e}/team-mailboxes/${t}`,"DELETE",i,null,{domain:e,teamMailbox:t}):this.logger.error({plugin:this.name,event:"deleteTeamMailbox",dn:i,mail:t,error:"Cannot extract domain from mail address"})}async removeAllTeamMailboxMembers(i,t){const e=this.extractMailDomain(t);if(e)try{const a=await this.server.ldap.search({paged:!1,scope:"base",attributes:["member"]},i);if(a.searchEntries&&a.searchEntries.length>0){const s=a.searchEntries[0].member||[],r=Array.isArray(s)?s:[s],n=await this.getMemberEmails(r);await Promise.all(n.map(a=>this.callWebAdminApi("removeAllTeamMailboxMembers",`${this.webadminUrl}/domains/${e}/team-mailboxes/${t}/members/${a}`,"DELETE",i,null,{domain:e,teamMailbox:t,memberMail:a}))),this.logger.info({plugin:this.name,event:"removeAllTeamMailboxMembers",dn:i,teamMailbox:t,membersRemoved:n.length,message:"All members removed from team mailbox (mailbox preserved)"})}}catch(i){const e=i instanceof Error?i.message:String(i);this.logger.error(`Failed to remove members from team mailbox ${t}: ${e}`)}else this.logger.error({plugin:this.name,event:"removeAllTeamMailboxMembers",dn:i,mail:t,error:"Cannot extract domain from mail address"})}async deleteMailingList(i,t){await this.callWebAdminApi("deleteMailingList",`${this.webadminUrl}/address/groups/${t}`,"DELETE",i,null,{groupMail:t})}async getMemberEmails(i){const t=i.filter(i=>i!==this.config.group_dummy_user).map(i=>this.server.ldap.queryLimit(async()=>{const t=await this.ldapGetAttributes(i,[this.mailAttr]);return t?this.attributeToString(t[this.mailAttr]):null}));return(await Promise.all(t)).filter(i=>null!==i)}async getGroupMail(i){const t=await this.ldapGetAttributes(i,["mail"]);return t?this.attributeToString(t.mail):null}async getGroupMailboxType(i){try{const t=await this.server.ldap.search({paged:!1,scope:"base",attributes:[this.mailboxTypeAttr]},i);if(t.searchEntries&&t.searchEntries.length>0)return this.getMailboxType(t.searchEntries[0])}catch(t){const e=t instanceof Error?t.message:String(t);this.logger.debug(`Could not get mailbox type for group ${i}: ${e}`)}return null}async _handleDelegationChange(i,t){const e=await this.ldapGetAttributes(i,[this.mailAttr]);if(!e)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:i,message:"Could not find user entry to get mail attribute"});const a=e[this.mailAttr];if(!a||"string"!=typeof a)return void this.logger.warn({plugin:this.name,event:"onLdapChange",dn:i,message:"User has no mail attribute, cannot manage delegation"});const s=this.config.delegation_attribute;if(!s)return;const[r,n]=t[s]||[],l=this._normalizeToArray(r),o=this._normalizeToArray(n),m=o.filter(i=>!l.includes(i)),d=l.filter(i=>!o.includes(i)),h=m.map(i=>this.server.ldap.queryLimit(async()=>{const t=await this._getDelegateEmail(i);return{dn:i,email:t}})),g=d.map(i=>this.server.ldap.queryLimit(async()=>{const t=await this._getDelegateEmail(i);return{dn:i,email:t}})),[u,p]=await Promise.all([Promise.all(h),Promise.all(g)]),c=[];for(const{dn:t,email:e}of u)e&&c.push(this.callWebAdminApi("onLdapChange:addDelegation",`${this.webadminUrl}/users/${a}/authorizedUsers/${e}`,"PUT",i,null,{userMail:a,delegateEmail:e,delegateDN:t,action:"add"}));for(const{dn:t,email:e}of p)e&&c.push(this.callWebAdminApi("onLdapChange:removeDelegation",`${this.webadminUrl}/users/${a}/authorizedUsers/${e}`,"DELETE",i,null,{userMail:a,delegateEmail:e,delegateDN:t,action:"remove"}));await Promise.all(c)}async _getDelegateEmail(i){const t=await this.ldapGetAttributes(i,[this.mailAttr]);return t?this.attributeToString(t[this.mailAttr]):(this.logger.warn({plugin:this.name,event:"getDelegateEmail",dn:i,message:"Could not resolve delegate DN to email"}),null)}_normalizeToArray(i){return i?Array.isArray(i)?i:[i]:[]}}export{James as default};
@@ -0,0 +1 @@
1
+ import o from"../abstract/plugin.js";class WebLogs extends o{constructor(){super(...arguments),this.name="weblogs",this.roles=["logging"]}api(o){o.use((o,e,s)=>{let r=!0;this.logger.debug(`Incoming request: ${o.method} ${o.originalUrl}`);const t=Date.now();e.on("finish",()=>{const s={};o.user&&(s.user=o.user),r&&this.log(o,e,t,s),r=!1}),e.on("error",s=>{r&&this.log(o,e,t,{error:s.message}),r=!1}),e.on("close",()=>{e.writableEnded||(r&&this.log(o,e,t,{error:`Connection closed before response was sent for ${o.method} ${o.originalUrl}`}),r=!1)}),s()})}log(o,e,s,r){const t=Date.now()-s,n={method:o.method,url:o.originalUrl,status:e.statusCode,duration:t,...r};this.logger.info(n)}}export{WebLogs as default};
@@ -0,0 +1,94 @@
1
+ import type { Express, Request, Response } from 'express';
2
+ import type { DM } from '../bin';
3
+ import type ldapActions from '../lib/ldapActions';
4
+ import type { AttributesList, AttributeValue, LdapList, ModifyRequest } from '../lib/ldapActions';
5
+ import type { Schema } from '../config/schema';
6
+ import DmPlugin from './plugin';
7
+ export interface LdapFlatConfig {
8
+ /**
9
+ * LDAP branch where entries are stored
10
+ */
11
+ base: string;
12
+ /**
13
+ * Main attribute used as entry identifier (e.g., 'uid', 'cn')
14
+ */
15
+ mainAttribute: string;
16
+ /**
17
+ * ObjectClass(es) for new entries
18
+ */
19
+ objectClass: string[];
20
+ /**
21
+ * Default attributes to add to new entries
22
+ */
23
+ defaultAttributes?: AttributesList;
24
+ /**
25
+ * Optional schema file path for validation
26
+ */
27
+ schemaPath?: string;
28
+ /**
29
+ * Singular name for API routes (e.g., 'user', 'position')
30
+ */
31
+ singularName: string;
32
+ /**
33
+ * Plural name for API routes (e.g., 'users', 'positions')
34
+ */
35
+ pluralName: string;
36
+ /**
37
+ * Hook name prefix (e.g., 'ldapuser', 'ldapposition')
38
+ */
39
+ hookPrefix: string;
40
+ }
41
+ export default abstract class LdapFlat extends DmPlugin {
42
+ base: string;
43
+ ldap: ldapActions;
44
+ mainAttribute: string;
45
+ objectClass: string[];
46
+ defaultAttributes: AttributesList;
47
+ schema?: Schema;
48
+ singularName: string;
49
+ pluralName: string;
50
+ hookPrefix: string;
51
+ constructor(server: DM, config: LdapFlatConfig);
52
+ /**
53
+ * API routes
54
+ */
55
+ api(app: Express): void;
56
+ apiGet(req: Request, res: Response): Promise<void>;
57
+ apiAdd(req: Request, res: Response): Promise<void>;
58
+ apiDelete(req: Request, res: Response): Promise<void>;
59
+ apiModify(req: Request, res: Response): Promise<void>;
60
+ apiMove(req: Request, res: Response): Promise<void>;
61
+ addEntry(id: string, additional?: AttributesList, req?: Request): Promise<boolean>;
62
+ modifyEntry(id: string, changes: ModifyRequest): Promise<boolean>;
63
+ renameEntry(id: string, newId: string): Promise<boolean>;
64
+ deleteEntry(id: string): Promise<boolean>;
65
+ /**
66
+ * Move entry to a different organization
67
+ * Updates department link and path attributes
68
+ */
69
+ moveEntry(id: string, targetOrgDn: string, req?: Request): Promise<{
70
+ departmentPath: string;
71
+ departmentLink: string;
72
+ }>;
73
+ /**
74
+ * Get department path from an organization DN
75
+ * Fetches the path attribute directly from the organization entry
76
+ */
77
+ private getDepartmentPath;
78
+ /**
79
+ * List entries from LDAP
80
+ */
81
+ listEntries({ filter, attributes, }: {
82
+ filter?: string;
83
+ attributes?: string[];
84
+ }): Promise<LdapList>;
85
+ searchEntriesByName(name: string, partial?: boolean, attrs?: string[]): Promise<LdapList>;
86
+ validateNewEntry(dn: string, entry: AttributesList): Promise<boolean>;
87
+ validateChanges(dn: string, changes: ModifyRequest): Promise<boolean>;
88
+ _validateOneChange(field: string, value: AttributeValue | null): Promise<boolean>;
89
+ /**
90
+ * Escape special characters in DN values for LDAP attributes
91
+ * Escape spaces and special characters with backslashes
92
+ */
93
+ private escapeDnValue;
94
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Abstract class for plugins
3
+ * @author Xavier Guimard <xguimard@linagora.com>
4
+ */
5
+ import type { Express } from 'express';
6
+ import type winston from 'winston';
7
+ import type { Config, DM } from '../bin';
8
+ import type { Hooks, MaybePromise } from '../hooks';
9
+ export type Role = 'auth' | 'authz' | 'protect' | 'api' | 'logging' | 'demo' | 'consistency' | 'configurable';
10
+ export default abstract class DmPlugin {
11
+ /**
12
+ * Properties inherited from parent (DM)
13
+ */
14
+ server: DM;
15
+ config: Config;
16
+ logger: winston.Logger;
17
+ registeredHooks: {
18
+ [K in keyof Hooks]?: Function[];
19
+ };
20
+ /**
21
+ * Interfaces
22
+ */
23
+ hooks?: Hooks;
24
+ dependencies?: Record<string, string>;
25
+ roles?: Role[] | undefined;
26
+ api?(app: Express): MaybePromise<void>;
27
+ getConfigApiData?(): Record<string, unknown> | undefined;
28
+ abstract name: string;
29
+ /**
30
+ * Constructor
31
+ * @param server DM object
32
+ */
33
+ constructor(server: DM);
34
+ /**
35
+ * Uniq ID, to be used when calling hooks
36
+ *
37
+ * Example: plugin/ldap/groups has 2 events for some operations,
38
+ * it uses opNumber to permit to plugin to link the 2 calls
39
+ *
40
+ * @returns uniq operation number
41
+ */
42
+ opNumber(): number;
43
+ }
@@ -0,0 +1,90 @@
1
+ import pLimit from 'p-limit';
2
+ import type { DM } from '../bin';
3
+ import type { AttributesList } from '../lib/ldapActions';
4
+ import DmPlugin, { type Role } from './plugin';
5
+ /**
6
+ * Abstract base class for all Twake-related plugins (James, Calendar, Matrix, etc.)
7
+ * Consolidates common patterns for HTTP communication with Twake WebAdmin APIs
8
+ */
9
+ export declare abstract class TwakePlugin extends DmPlugin {
10
+ abstract name: string;
11
+ abstract roles: Role[];
12
+ protected webadminUrl: string;
13
+ protected webadminToken: string;
14
+ protected requestLimit: ReturnType<typeof pLimit>;
15
+ protected mailAttr: string;
16
+ protected displayNameAttr: string;
17
+ /**
18
+ * Constructor - initializes common Twake plugin configuration
19
+ * @param server DM server instance
20
+ * @param urlConfig Configuration key for WebAdmin URL
21
+ * @param tokenConfig Configuration key for WebAdmin token
22
+ * @param concurrencyConfig Configuration key for concurrency limit
23
+ */
24
+ constructor(server: DM, urlConfig: string, tokenConfig: string, concurrencyConfig: string);
25
+ /**
26
+ * Create HTTP headers with optional Authorization token
27
+ * @param contentType Optional Content-Type header value
28
+ * @returns Headers object
29
+ */
30
+ protected createHeaders(contentType?: string): {
31
+ Authorization?: string;
32
+ 'Content-Type'?: string;
33
+ };
34
+ /**
35
+ * Call Twake WebAdmin API with concurrency control and error handling
36
+ * @param hookname Hook name for logging
37
+ * @param url Full URL to call
38
+ * @param method HTTP method
39
+ * @param dn LDAP DN for logging
40
+ * @param body Request body (string or null)
41
+ * @param fields Additional fields for logging
42
+ */
43
+ protected callWebAdminApi(hookname: string, url: string, method: string, dn: string, body: string | null, fields: object): Promise<void>;
44
+ /**
45
+ * Override this method to customize error handling for specific status codes
46
+ * @param statusCode HTTP status code
47
+ * @param hookname Hook name that triggered the error
48
+ * @returns true if error should be ignored
49
+ */
50
+ protected shouldIgnoreError(statusCode: number, hookname: string): boolean;
51
+ /**
52
+ * Helper to convert LDAP attribute value to string
53
+ * @param value LDAP attribute value (string, Buffer, array, etc.)
54
+ * @returns String value or null
55
+ */
56
+ protected attributeToString(value: unknown): string | null;
57
+ /**
58
+ * Generic LDAP search utility to fetch specific attributes from a DN
59
+ * Uses cache automatically for base-scope searches
60
+ * @param dn The DN to fetch attributes from
61
+ * @param attributes Optional array of attribute names to fetch
62
+ * @returns AttributesList or null if not found
63
+ */
64
+ protected ldapGetAttributes(dn: string, attributes?: string[]): Promise<AttributesList | null>;
65
+ /**
66
+ * Extract aliases from LDAP attribute value (handles AD format smtp:alias@domain.com)
67
+ * @param value LDAP attribute value
68
+ * @returns Array of normalized aliases
69
+ */
70
+ protected getAliases(value: string | string[] | Buffer | Buffer[] | undefined): string[];
71
+ /**
72
+ * Normalize email alias - handle AD format (smtp:alias@domain.com)
73
+ * @param alias Email alias
74
+ * @returns Normalized alias
75
+ */
76
+ protected normalizeAlias(alias: string): string;
77
+ /**
78
+ * Extract domain from email address
79
+ * @param email Email address
80
+ * @returns Domain name or null
81
+ */
82
+ protected extractMailDomain(email: string): string | null;
83
+ /**
84
+ * Extract domain from LDAP DN (dc=example,dc=com -> example.com)
85
+ * @param dn LDAP DN
86
+ * @returns Domain name
87
+ */
88
+ protected extractDomainFromDn(dn: string): string;
89
+ }
90
+ export default TwakePlugin;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @packageDocumentation ldap-rest
3
+ * @author Xavier Guimard <xguimard@linagora.com>
4
+ *
5
+ * Main server file
6
+ * It loads plugins, setup express app,... and start the server
7
+ *
8
+ * @example
9
+ * const server = new DM();
10
+ *
11
+ * await server.ready;
12
+ * await server.run();
13
+ */
14
+ import express from 'express';
15
+ import type winston from 'winston';
16
+ import { type Config } from '../config/args';
17
+ import type { Hooks } from '../hooks';
18
+ import ldapActions from '../lib/ldapActions';
19
+ import type DmPlugin from '../abstract/plugin';
20
+ export type { Config };
21
+ /**
22
+ * @class DM
23
+ */
24
+ export declare class DM {
25
+ app: express.Express;
26
+ config: Config;
27
+ ready: Promise<void>;
28
+ server?: import('http').Server;
29
+ hooks: {
30
+ [K in keyof Hooks]?: Function[];
31
+ };
32
+ loadedPlugins: {
33
+ [key: string]: DmPlugin;
34
+ };
35
+ ldap: ldapActions;
36
+ operationSequence: number;
37
+ logger: winston.Logger;
38
+ constructor();
39
+ run(): Promise<void>;
40
+ stop(): void;
41
+ loadPlugin(pluginName: string): Promise<boolean>;
42
+ registerPlugin(pluginName: string, obj: DmPlugin, name?: string): Promise<boolean>;
43
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * LDAP Group Editor - Main class for editing group properties
3
+ * @author Xavier Guimard
4
+ */
5
+ import type { GroupEditorOptions, Config } from './types';
6
+ import { GroupApiClient } from './api/GroupApiClient';
7
+ export declare class LdapGroupEditor {
8
+ private options;
9
+ private api;
10
+ private container;
11
+ private groupTree;
12
+ private groupEditor;
13
+ private currentGroupDn;
14
+ private currentOrgDn;
15
+ private config;
16
+ constructor(options: GroupEditorOptions);
17
+ init(): Promise<void>;
18
+ private render;
19
+ private initComponents;
20
+ private onOrgSelected;
21
+ private onGroupSelected;
22
+ private handleCreateGroup;
23
+ private escapeHtml;
24
+ private handleError;
25
+ getApi(): GroupApiClient;
26
+ getConfig(): Config | null;
27
+ getCurrentOrgDn(): string | null;
28
+ getCurrentUserDn(): string | null;
29
+ createUser(data: Record<string, unknown>): Promise<void>;
30
+ deleteUser(dn: string): Promise<void>;
31
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * API Client for LDAP Group operations
3
+ */
4
+ import type { Config, LdapGroup } from '../types';
5
+ export declare class GroupApiClient {
6
+ private baseUrl;
7
+ constructor(baseUrl: string);
8
+ getConfig(): Promise<Config>;
9
+ getOrganizations(): Promise<{
10
+ dn: string;
11
+ }>;
12
+ getGroups(orgDn: string): Promise<LdapGroup[]>;
13
+ getGroup(dn: string): Promise<LdapGroup>;
14
+ updateGroup(dn: string, data: Partial<LdapGroup>): Promise<void>;
15
+ createEntry(dn: string, data: Record<string, unknown>): Promise<void>;
16
+ deleteEntry(dn: string): Promise<void>;
17
+ moveGroup(cn: string, targetOrgDn: string): Promise<void>;
18
+ renameGroup(cn: string, newCn: string): Promise<void>;
19
+ getPointerOptions(branch: string): Promise<Array<{
20
+ dn: string;
21
+ label: string;
22
+ }>>;
23
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Create Group Modal Component - Modal for creating a new group
3
+ */
4
+ import type { GroupApiClient } from '../api/GroupApiClient';
5
+ import type { Config } from '../types';
6
+ export declare class CreateGroupModal {
7
+ private api;
8
+ private config;
9
+ private orgDn;
10
+ private onCreated;
11
+ private modal;
12
+ constructor(api: GroupApiClient, config: Config, orgDn: string, onCreated: () => Promise<void>);
13
+ show(): Promise<void>;
14
+ private getOrgPath;
15
+ private handleSubmit;
16
+ private showError;
17
+ private close;
18
+ private escapeHtml;
19
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Group Property Editor Component - Edits properties of a selected group
3
+ */
4
+ import type { GroupApiClient } from '../api/GroupApiClient';
5
+ export declare class GroupPropertyEditor {
6
+ private container;
7
+ private api;
8
+ private groupDn;
9
+ private group;
10
+ private schema;
11
+ private saveCallback;
12
+ constructor(container: HTMLElement, api: GroupApiClient, groupDn: string);
13
+ init(): Promise<void>;
14
+ private loadGroup;
15
+ private loadSchema;
16
+ private createDefaultSchema;
17
+ onSave(callback: () => void): void;
18
+ private render;
19
+ private renderFields;
20
+ private renderField;
21
+ private getFieldLabel;
22
+ private attachEventListeners;
23
+ private searchUsers;
24
+ private handleAddMember;
25
+ private handleDeleteMember;
26
+ private handleSubmit;
27
+ private handleMove;
28
+ private handleDelete;
29
+ private escapeHtml;
30
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Group Tree Component - Shows organizations tree (same as UserTree)
3
+ */
4
+ export declare class GroupTree {
5
+ private container;
6
+ private apiBaseUrl;
7
+ private onOrgSelect;
8
+ private expandedNodes;
9
+ private selectedDn;
10
+ private rootDn;
11
+ constructor(container: HTMLElement, apiBaseUrl: string, onOrgSelect: (orgDn: string) => void);
12
+ init(): Promise<void>;
13
+ private renderTree;
14
+ private renderNode;
15
+ private attachEventListeners;
16
+ private escapeHtml;
17
+ }