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
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/linagora/ldap-rest)
2
+
3
+ # LDAP-Rest
4
+
5
+ Lite LinID directory manager is a lightweight directory manager that provides LDAP integration through a plugin-based architecture.
6
+ This system enables directory management operations with configurable authentication, extensible functionality through events/hooks,
7
+ and extensible REST API.
8
+
9
+ Core plugins also provide plugins that ensure LDAP data consistency.
10
+
11
+ ## How it works
12
+
13
+ All configuration is done via command-line arguments and/or environment variables.
14
+ Example:
15
+
16
+ ```shell
17
+ npx ldap-rest --ldap-base 'dc=example,dc=com' \
18
+ --ldap-dn 'cn=admin,dc=example,dc=com' --ldap-pwd admin \
19
+ --ldap-url ldap://localhost \
20
+ --plugin core/ldap/groups --ldap-group-base 'ou=groups,dc=example,dc=com' \
21
+ --plugin core/ldap/externalUsersInGroups
22
+ ```
23
+
24
+ ## Command-line options and environment variables
25
+
26
+ [ToDo]
27
+
28
+ See also [Typescript declarations](./src/config/args.ts).
29
+
30
+ ## Documentation
31
+
32
+ ### For Application Developers
33
+
34
+ - **[Developer Guide](./docs/DEVELOPER_GUIDE.md)** - Complete guide for building web applications using LDAP-Rest APIs and browser libraries
35
+
36
+ ### For Plugin Developers
37
+
38
+ - **[Contributing Guide](./CONTRIBUTING.md)** - How to contribute and develop plugins
39
+ - [Plugins](./src/plugins/README.md)
40
+ - [Hooks](./HOOKS.md)
41
+
42
+ ## Copyright and license
43
+
44
+ [![Powered by LINAGORA](./docs/linagora.png)](https://linagora.com)
45
+
46
+ Copyright 2025-present [LINAGORA](https://linagora.com)
47
+
48
+ Licensed under [GNU AGPL-3.0](./LICENSE])
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Cleanup script for external users that are no longer referenced in any group
5
+ *
6
+ * Usage:
7
+ * cleanup-external-users.mjs [--dry-run] [--verbose] [--quiet]
8
+ *
9
+ * Options:
10
+ * --dry-run Show what would be deleted without actually deleting
11
+ * --verbose Show detailed information about each user checked
12
+ * --quiet Only show errors and deletions (WOULD DELETE in dry-run mode)
13
+ */
14
+
15
+ import { parseConfig } from '../dist/lib/parseConfig.js';
16
+ import configArgs from '../dist/config/args.js';
17
+ import ldapActions from '../dist/lib/ldapActions.js';
18
+ import { buildLogger } from '../dist/logger/winston.js';
19
+
20
+ // Parse CLI arguments
21
+ const args = process.argv.slice(2);
22
+
23
+ if (args.includes('--help') || args.includes('-h')) {
24
+ console.log(`
25
+ cleanup-external-users - Remove orphaned external users from LDAP
26
+
27
+ Usage:
28
+ cleanup-external-users [OPTIONS]
29
+
30
+ Description:
31
+ Removes external users from the contacts branch that are no longer
32
+ referenced in any group. Useful for maintaining a clean LDAP directory.
33
+
34
+ Options:
35
+ --dry-run Show what would be deleted without actually deleting
36
+ --verbose Show detailed information about each user checked
37
+ --quiet Only show errors and deletions (WOULD DELETE in dry-run mode)
38
+ --help, -h Show this help message
39
+
40
+ Examples:
41
+ # Preview what would be deleted
42
+ cleanup-external-users --dry-run
43
+
44
+ # Delete orphaned users with detailed output
45
+ cleanup-external-users --verbose
46
+
47
+ # Delete orphaned users silently (for cron)
48
+ cleanup-external-users --quiet
49
+
50
+ # Preview in quiet mode (only show what would be deleted)
51
+ cleanup-external-users --dry-run --quiet
52
+ `);
53
+ process.exit(0);
54
+ }
55
+
56
+ const dryRun = args.includes('--dry-run');
57
+ const verbose = args.includes('--verbose');
58
+ const quiet = args.includes('--quiet');
59
+
60
+ // Parse config
61
+ const config = parseConfig(configArgs);
62
+ const logger = buildLogger(config);
63
+
64
+ // Simple LDAP wrapper to match what DM provides
65
+ const server = {
66
+ config,
67
+ logger,
68
+ hooks: {},
69
+ loadedPlugins: {},
70
+ operationSequence: 0,
71
+ };
72
+
73
+ const ldap = new ldapActions(server);
74
+
75
+ async function main() {
76
+ try {
77
+ if (!quiet) {
78
+ logger.info('Starting cleanup of external users');
79
+ }
80
+
81
+ if (!config.external_members_branch) {
82
+ logger.error('external_members_branch not configured');
83
+ process.exit(1);
84
+ }
85
+
86
+ if (!config.ldap_group_base) {
87
+ logger.error('ldap_group_base not configured');
88
+ process.exit(1);
89
+ }
90
+
91
+ if (dryRun && !quiet) {
92
+ logger.info('DRY RUN MODE - no changes will be made');
93
+ }
94
+
95
+ // Get all external users
96
+ if (!quiet) {
97
+ logger.info(
98
+ `Searching for external users in ${config.external_members_branch}`
99
+ );
100
+ }
101
+ const externalUsersResult = await ldap.search(
102
+ {
103
+ paged: false,
104
+ scope: 'one',
105
+ attributes: ['dn', 'mail'],
106
+ },
107
+ config.external_members_branch
108
+ );
109
+
110
+ const externalUsers = externalUsersResult.searchEntries;
111
+ if (!quiet) {
112
+ logger.info(`Found ${externalUsers.length} external users`);
113
+ }
114
+
115
+ if (externalUsers.length === 0) {
116
+ if (!quiet) {
117
+ logger.info('No external users to check');
118
+ }
119
+ return;
120
+ }
121
+
122
+ // Get all groups
123
+ if (!quiet) {
124
+ logger.info(`Searching for groups in ${config.ldap_group_base}`);
125
+ }
126
+ const groupsResult = await ldap.search(
127
+ {
128
+ paged: false,
129
+ scope: 'sub',
130
+ attributes: ['dn', config.ldap_group_member_attribute || 'member'],
131
+ },
132
+ config.ldap_group_base
133
+ );
134
+
135
+ const groups = groupsResult.searchEntries;
136
+ if (!quiet) {
137
+ logger.info(`Found ${groups.length} groups`);
138
+ }
139
+
140
+ // Build a set of all referenced member DNs
141
+ const memberAttr = config.ldap_group_member_attribute || 'member';
142
+ const referencedMembers = new Set();
143
+
144
+ for (const group of groups) {
145
+ const members = group[memberAttr];
146
+ if (members) {
147
+ const memberList = Array.isArray(members) ? members : [members];
148
+ memberList.forEach(member => {
149
+ // Normalize DN (remove spaces)
150
+ referencedMembers.add(member.replace(/\s/g, ''));
151
+ });
152
+ }
153
+ }
154
+
155
+ if (!quiet) {
156
+ logger.info(
157
+ `Found ${referencedMembers.size} unique member references across all groups`
158
+ );
159
+ }
160
+
161
+ // Check each external user
162
+ let toDelete = 0;
163
+ let kept = 0;
164
+
165
+ for (const user of externalUsers) {
166
+ const userDn = user.dn.replace(/\s/g, '');
167
+ const isReferenced = referencedMembers.has(userDn);
168
+
169
+ if (isReferenced) {
170
+ kept++;
171
+ if (verbose) {
172
+ logger.info(
173
+ `KEEP: ${user.dn} (${user.mail || 'no mail'}) - still referenced`
174
+ );
175
+ }
176
+ } else {
177
+ toDelete++;
178
+ if (dryRun) {
179
+ logger.info(
180
+ `WOULD DELETE: ${user.dn} (${user.mail || 'no mail'}) - not referenced`
181
+ );
182
+ } else {
183
+ if (!quiet) {
184
+ logger.info(`DELETING: ${user.dn} (${user.mail || 'no mail'})`);
185
+ }
186
+ try {
187
+ await ldap.del(user.dn);
188
+ } catch (err) {
189
+ logger.error(`Failed to delete ${user.dn}: ${err}`);
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ // Summary
196
+ if (!quiet) {
197
+ logger.info('');
198
+ logger.info('=== CLEANUP SUMMARY ===');
199
+ logger.info(`Total external users: ${externalUsers.length}`);
200
+ logger.info(`Kept (still referenced): ${kept}`);
201
+ logger.info(`${dryRun ? 'Would delete' : 'Deleted'}: ${toDelete}`);
202
+
203
+ if (dryRun && toDelete > 0) {
204
+ logger.info('');
205
+ logger.info('Run without --dry-run to actually delete these users');
206
+ }
207
+ }
208
+ } catch (err) {
209
+ logger.error(`Error during cleanup: ${err}`);
210
+ process.exit(1);
211
+ } finally {
212
+ process.exit(0);
213
+ }
214
+ }
215
+
216
+ main().catch(err => {
217
+ console.error('Fatal error:', err);
218
+ process.exit(1);
219
+ });
package/bin/index.mjs ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { DM } from '../dist/bin/index.js';
4
+
5
+ // Parse CLI arguments
6
+ const args = process.argv.slice(2);
7
+
8
+ if (args.includes('--help') || args.includes('-h')) {
9
+ console.log(`
10
+ ldap-rest - Lightweight LDAP directory manager
11
+
12
+ Usage:
13
+ ldap-rest [OPTIONS]
14
+
15
+ Description:
16
+ Starts the ldap-rest server which provides a REST API and web interface
17
+ for managing LDAP directories. Configuration is done through environment
18
+ variables or command-line arguments.
19
+
20
+ Main options:
21
+ --help, -h Show this help message
22
+ --port PORT Server port (default: 8080)
23
+ --ldap-url URL LDAP server URL
24
+ --ldap-dn DN LDAP bind DN
25
+ --ldap-pwd PASSWORD LDAP bind password
26
+ --ldap-base BASE LDAP base DN
27
+ --plugin PLUGIN Load plugin (can be specified multiple times)
28
+ --api-prefix PREFIX API prefix (default: /api)
29
+ --log-level LEVEL Log level (error, warn, info, debug)
30
+
31
+ See plugin documentation for additional options.
32
+
33
+ Environment Variables:
34
+ All command-line options can also be set via environment variables
35
+ by prefixing with DM_ and using uppercase with underscores.
36
+ Example: --ldap-url becomes DM_LDAP_URL
37
+
38
+ Examples:
39
+ # Start server with default configuration
40
+ npx ldap-rest
41
+
42
+ # Start with specific LDAP server
43
+ npx ldap-rest --ldap-url ldap://localhost:389
44
+
45
+ # Load specific plugins
46
+ npx ldap-rest --plugin ldapGroups --plugin ldapOrganization
47
+ `);
48
+ process.exit(0);
49
+ }
50
+
51
+ const server = new DM();
52
+
53
+ await server.ready;
54
+ await server.run();
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Sync utility to verify and fix consistency between LDAP and James
4
+ * Ensures James quotas match LDAP mailQuota values
5
+ * @author Generated with Claude Code
6
+ */
7
+
8
+ import fetch from 'node-fetch';
9
+ import { DM } from '../dist/bin/index.js';
10
+
11
+ // Parse command line arguments
12
+ const args = process.argv.slice(2);
13
+ const quiet = args.includes('--quiet') || args.includes('-q');
14
+ const dryRun = args.includes('--dry-run') || args.includes('-n');
15
+
16
+ if (args.includes('--help') || args.includes('-h')) {
17
+ console.log(`
18
+ Usage: sync-james [options]
19
+
20
+ Synchronizes James quotas with LDAP mailQuota values.
21
+ LDAP is considered the source of truth.
22
+
23
+ Options:
24
+ --quiet, -q Only show summary and errors
25
+ --dry-run, -n Show what would be changed without making changes
26
+ --help, -h Show this help message
27
+
28
+ Environment variables:
29
+ DM_JAMES_WEBADMIN_URL James WebAdmin URL (required)
30
+ DM_JAMES_WEBADMIN_TOKEN James WebAdmin authentication token
31
+ DM_LDAP_BASE LDAP search base
32
+ DM_MAIL_ATTRIBUTE Mail attribute name (default: mail)
33
+ DM_QUOTA_ATTRIBUTE Quota attribute name (default: mailQuota)
34
+ `);
35
+ process.exit(0);
36
+ }
37
+
38
+ async function syncJamesQuotas() {
39
+ const dm = new DM();
40
+ await dm.ready;
41
+
42
+ const mailAttr = dm.config.mail_attribute || 'mail';
43
+ const quotaAttr = dm.config.quota_attribute || 'mailQuota';
44
+ const jamesUrl = dm.config.james_webadmin_url;
45
+ const jamesToken = dm.config.james_webadmin_token;
46
+
47
+ if (!jamesUrl) {
48
+ dm.logger.error('DM_JAMES_WEBADMIN_URL is not configured');
49
+ process.exit(1);
50
+ }
51
+
52
+ if (!quiet) {
53
+ dm.logger.info('Starting LDAP to James quota synchronization...');
54
+ dm.logger.info(`LDAP base: ${dm.config.ldap_base}`);
55
+ dm.logger.info(`James URL: ${jamesUrl}`);
56
+ if (dryRun) {
57
+ dm.logger.info('DRY RUN MODE: No changes will be made');
58
+ }
59
+ dm.logger.info('');
60
+ }
61
+
62
+ try {
63
+ // Search for all users with mail and quota attributes (paginated for large directories)
64
+ const resultGenerator = await dm.ldap.search({
65
+ paged: true,
66
+ filter: `(&(${mailAttr}=*)(${quotaAttr}=*))`,
67
+ attributes: [mailAttr, quotaAttr, 'dn'],
68
+ });
69
+
70
+ let checked = 0;
71
+ let synced = 0;
72
+ let errors = 0;
73
+
74
+ // Process results page by page
75
+ for await (const result of resultGenerator) {
76
+ if (!result.searchEntries || result.searchEntries.length === 0) {
77
+ continue;
78
+ }
79
+
80
+ if (!quiet) {
81
+ dm.logger.info(
82
+ `Processing batch of ${result.searchEntries.length} users...`
83
+ );
84
+ }
85
+
86
+ for (const entry of result.searchEntries) {
87
+ const dn = entry.dn;
88
+ const mail = Array.isArray(entry[mailAttr])
89
+ ? entry[mailAttr][0]
90
+ : entry[mailAttr];
91
+ const ldapQuota = Array.isArray(entry[quotaAttr])
92
+ ? Number(entry[quotaAttr][0])
93
+ : Number(entry[quotaAttr]);
94
+
95
+ if (!mail || isNaN(ldapQuota)) {
96
+ dm.logger.warn(`Skipping ${dn}: invalid mail or quota`);
97
+ continue;
98
+ }
99
+
100
+ checked++;
101
+
102
+ try {
103
+ // Get James quota
104
+ const getUrl = `${jamesUrl}/quota/users/${mail}/size`;
105
+ const headers = {};
106
+ if (jamesToken) {
107
+ headers.Authorization = `Bearer ${jamesToken}`;
108
+ }
109
+
110
+ const getRes = await fetch(getUrl, { method: 'GET', headers });
111
+
112
+ if (!getRes.ok) {
113
+ if (getRes.status === 404) {
114
+ dm.logger.warn(
115
+ `User ${mail} not found in James, skipping (DN: ${dn})`
116
+ );
117
+ } else {
118
+ dm.logger.error(
119
+ `Error getting quota for ${mail}: ${getRes.status} ${getRes.statusText}`
120
+ );
121
+ errors++;
122
+ }
123
+ continue;
124
+ }
125
+
126
+ const jamesQuota = Number(await getRes.text());
127
+
128
+ if (jamesQuota === ldapQuota) {
129
+ if (!quiet) {
130
+ dm.logger.info(`${mail}: quota OK (${ldapQuota})`);
131
+ }
132
+ } else {
133
+ dm.logger.warn(
134
+ `${mail}: quota mismatch - LDAP: ${ldapQuota}, James: ${jamesQuota}`
135
+ );
136
+ if (dryRun) {
137
+ dm.logger.info(` Would update James quota to ${ldapQuota}`);
138
+ synced++;
139
+ } else {
140
+ if (!quiet) {
141
+ dm.logger.info(` Updating James quota to ${ldapQuota}...`);
142
+ }
143
+
144
+ // Update James quota
145
+ const putUrl = `${jamesUrl}/quota/users/${mail}/size`;
146
+ const putRes = await fetch(putUrl, {
147
+ method: 'PUT',
148
+ headers,
149
+ body: ldapQuota.toString(),
150
+ });
151
+
152
+ if (putRes.ok) {
153
+ if (!quiet) {
154
+ dm.logger.info(` Updated successfully`);
155
+ }
156
+ synced++;
157
+ } else {
158
+ dm.logger.error(
159
+ ` Failed to update: ${putRes.status} ${putRes.statusText}`
160
+ );
161
+ errors++;
162
+ }
163
+ }
164
+ }
165
+ } catch (err) {
166
+ dm.logger.error(`Error processing ${mail}: ${err.message}`);
167
+ errors++;
168
+ }
169
+ }
170
+ }
171
+
172
+ dm.logger.info('\n' + '='.repeat(60));
173
+ dm.logger.info('Synchronization summary:');
174
+ dm.logger.info(` Users checked: ${checked}`);
175
+ dm.logger.info(` Quotas ${dryRun ? 'needing sync' : 'synced'}: ${synced}`);
176
+ dm.logger.info(` Errors: ${errors}`);
177
+ dm.logger.info('='.repeat(60));
178
+ } catch (err) {
179
+ dm.logger.error('Error during synchronization:', err);
180
+ process.exit(1);
181
+ } finally {
182
+ await dm.ldap.unbind();
183
+ }
184
+ }
185
+
186
+ // Run the sync
187
+ syncJamesQuotas().catch(err => {
188
+ console.error('Fatal error:', err);
189
+ process.exit(1);
190
+ });
@@ -0,0 +1 @@
1
+ import t from"fs";import{wantJson as e,serverError as i,notFound as r,jsonBody as s,created as a,tryMethod as n}from"../lib/expressFormatedResponses.js";import{transformSchemas as o,launchHooksChained as h,launchHooks as c}from"../lib/utils.js";import d from"./plugin.js";class LdapFlat extends d{constructor(e,i){if(super(e),this.ldap=e.ldap,this.base=i.base,this.mainAttribute=i.mainAttribute,this.objectClass=i.objectClass,this.defaultAttributes=i.defaultAttributes||{},this.singularName=i.singularName,this.pluralName=i.pluralName,this.hookPrefix=i.hookPrefix,!this.base)throw new Error(`LDAP base is not defined for ${this.singularName}`);if(i.schemaPath)try{const e=t.readFileSync(i.schemaPath,"utf8");this.schema=JSON.parse(o(e,this.config)),this.logger.info(`${this.singularName} schema loaded from ${i.schemaPath}`)}catch(t){this.logger.error(`Failed to load ${this.singularName} schema from ${i.schemaPath}: ${t}`)}}api(t){t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,async(t,r)=>{if(e(t,r))try{const e={};t.query.match&&"string"==typeof t.query.match&&t.query.attribute&&"string"==typeof t.query.attribute&&(e.filter=`(${t.query.attribute}=*${t.query.match}*)`),t.query.attributes&&"string"==typeof t.query.attributes&&(e.attributes=t.query.attributes.split(","));const i=await this.listEntries(e);r.json(i)}catch(t){return i(r,t)}}),t.get(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,async(t,e)=>this.apiGet(t,e)),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}`,async(t,e)=>this.apiAdd(t,e)),t.delete(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,async(t,e)=>this.apiDelete(t,e)),t.put(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id`,async(t,e)=>this.apiModify(t,e)),t.post(`${this.config.api_prefix}/v1/ldap/${this.pluralName}/:id/move`,async(t,e)=>this.apiMove(t,e))}async apiGet(t,s){if(!e(t,s))return;const a=decodeURIComponent(t.params.id);try{const t=/,/.test(a)?a:`${this.mainAttribute}=${a},${this.base}`,e=await this.ldap.search({paged:!1,scope:"base"},t);if(0===e.searchEntries.length)return r(s,`${this.singularName} not found`);s.json(e.searchEntries[0])}catch(t){return t.code&&32===t.code?r(s,`${this.singularName} not found`):i(s,t)}}async apiAdd(t,e){const r=s(t,e,this.mainAttribute);if(!r)return;const n=r[this.mainAttribute],o={...r};delete o[this.mainAttribute],delete o.dn;try{await this.addEntry(n,o,t);const i=await this.searchEntriesByName(n,!1);return a(e,i[n])}catch(t){return i(e,t)}}async apiDelete(t,i){if(!e(t,i))return;const r=decodeURIComponent(t.params.id);await n(i,this.deleteEntry.bind(this),r)}async apiModify(t,e){const i=s(t,e);if(!i)return;const r=decodeURIComponent(t.params.id);await n(e,this.modifyEntry.bind(this),r,i)}async apiMove(t,r){if(!e(t,r))return;const a=s(t,r,"targetOrgDn");if(!a)return;const n=decodeURIComponent(t.params.id),{targetOrgDn:o}=a;if(o&&"string"==typeof o)try{const e=await this.moveEntry(n,o,t);r.json({success:!0,...e})}catch(t){return i(r,t)}else r.status(400).json({error:"Missing or invalid targetOrgDn in request body"})}async addEntry(t,e={},i){let r;if(new RegExp(`^${this.mainAttribute}=`).test(t)){r=t;const e=`,${this.base}`;if(!r.endsWith(e))throw new Error(`DN must be in the flat branch "${this.base}". Provided DN "${r}" does not end with "${e}"`);t=t.replace(new RegExp(`^${this.mainAttribute}=([^,]+).*`),"$1")}else r=`${this.mainAttribute}=${t},${this.base}`;await this.validateNewEntry(r,{objectClass:this.objectClass,[this.mainAttribute]:t,...e});let s,a={objectClass:this.objectClass,...this.defaultAttributes,[this.mainAttribute]:t,...e};this.schema?this.logger.debug("Schema loaded, attributes:",Object.keys(this.schema.attributes)):this.logger.warn("No schema available"),[r,a]=await h(this.registeredHooks[`${this.hookPrefix}add`],[r,a]),this.logger.debug("Adding LDAP entry:",{dn:r,entry:JSON.stringify(a,null,2)}),this.logger.debug("Entry attributes breakdown:");for(const[t,e]of Object.entries(a))this.logger.debug(` ${t}: ${typeof e} = ${JSON.stringify(e)}`);try{s=await this.ldap.add(r,a,i)}catch(t){throw this.logger.error("LDAP add failed:",{dn:r,entry:JSON.stringify(a,null,2),error:t}),new Error(`Failed to add ${this.singularName} ${r}: ${t}`)}return c(this.registeredHooks[`${this.hookPrefix}adddone`],[r,a]),s}async modifyEntry(t,e){let i=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`;const r=this.opNumber();if([i,e]=await h(this.registeredHooks[`${this.hookPrefix}modify`],[i,e,r]),e.add&&e.add[this.mainAttribute])throw new Error(`${this.mainAttribute} attribute is unique, cannot add`);if(e.delete){if(e.delete instanceof Object&&e.delete[this.mainAttribute])throw new Error(`Cannot delete ${this.mainAttribute} attribute`);if(Array.isArray(e.delete)&&e.delete.includes(this.mainAttribute))throw new Error(`Cannot delete ${this.mainAttribute} attribute`)}if(e.replace&&e.replace[this.mainAttribute])throw new Error(`Use dedicated API to change ${this.mainAttribute} attribute`);await this.validateChanges(i,e);const s=await this.ldap.modify(i,e);return c(this.registeredHooks[`${this.hookPrefix}modifydone`],[i,e,r]),s}async renameEntry(t,e){let i=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`,r=/,/.test(e)?e:`${this.mainAttribute}=${e},${this.base}`;[i,r]=await h(this.registeredHooks[`${this.hookPrefix}rename`],[i,r]);const s=await this.ldap.rename(i,r);return c(this.registeredHooks[`${this.hookPrefix}renamedone`],[i,r]),s}async deleteEntry(t){let e=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`;e=await h(this.registeredHooks[`${this.hookPrefix}delete`],e);const i=await this.ldap.delete(e);return c(this.registeredHooks[`${this.hookPrefix}deletedone`],e),i}async moveEntry(t,e,i){const r=/,/.test(t)?t:`${this.mainAttribute}=${t},${this.base}`,s=this.config.ldap_organization_link_attribute||"twakeDepartmentLink",a=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";if(!this.schema?.attributes[s]||!this.schema?.attributes[a])throw new Error(`Schema for ${this.singularName} does not support move operation (missing ${s} or ${a})`);const n=await this.ldap.search({paged:!1,scope:"base",attributes:[s]},r,i);if(!n.searchEntries||0===n.searchEntries.length)throw new Error(`Entry ${r} not found`);const o=await this.getDepartmentPath(e,i);[,e]=await h(this.registeredHooks[`${this.hookPrefix}move`],[r,e,i]);const c={replace:{[s]:e,[a]:o}};return await this.modifyEntry(t,c),{departmentPath:o,departmentLink:e}}async getDepartmentPath(t,e){const i=this.config.ldap_organization_path_attribute||"twakeDepartmentPath";try{const r=await this.ldap.search({paged:!1,scope:"base",attributes:[i,"ou","o"]},t,e);if(!r.searchEntries||0===r.searchEntries.length)throw new Error(`Organization ${t} not found`);const s=r.searchEntries[0];if(s[i]){const t=s[i];return Array.isArray(t)?String(t[0]):String(t)}const a=s.ou||s.o;if(a){return`/${Array.isArray(a)?String(a[0]):String(a)}`}return this.logger.warn(`Organization ${t} has no ${i} attribute, using DN`),t}catch(e){throw new Error(`Failed to fetch organization ${t}: ${e}`)}}async listEntries({filter:t,attributes:e}){const i={paged:!0,filter:t=t||"(objectClass=*)"};e&&e.length>0&&(i.attributes=e);const r=await this.ldap.search(i,this.base),s={};for await(const t of r)t.searchEntries.forEach(t=>{if(t[this.mainAttribute]){const e=t[this.mainAttribute];let i;i=Array.isArray(e)?"string"==typeof e[0]?e[0]:String(e[0]):"string"==typeof e?e:String(e),s[i]=t}});return s}async searchEntriesByName(t,e=!1,i=[this.mainAttribute]){const r=e?`(${this.mainAttribute}=*${t}*)`:`(${this.mainAttribute}=${t})`;return await this.listEntries({filter:r,attributes:i})}async validateNewEntry(t,e){if(!this.schema)return!0;for(const[t,i]of Object.entries(this.schema.attributes))i.fixed&&void 0!==i.default&&(e[t]=i.default);for(const[t,i]of Object.entries(e)){if(!this.schema.attributes[t]){if(this.schema.strict)throw new Error(`Unknown attribute "${t}" for ${this.singularName}`);continue}const e=this.schema.attributes[t];if(e.fixed&&void 0!==e.default){const r=JSON.stringify(e.default);if(r!==JSON.stringify(i))throw new Error(`Attribute "${t}" is fixed and cannot be modified. Expected: ${r}`)}if(!await this._validateOneChange(t,i))throw new Error(`Invalid value for attribute "${t}"`);if(e.required&&!i)throw new Error(`Attribute "${t}" is required`)}for(const[t,i]of Object.entries(this.schema.attributes))if(i.required&&!e[t])throw new Error(`Attribute "${t}" is required`);return!0}async validateChanges(t,e){if(!this.schema)return!0;const checkFixed=(t,e)=>{const i=this.schema?.attributes[t];if(i?.fixed)throw new Error(`Attribute "${t}" is fixed and cannot be modified`)};if(e.add)for(const[t,i]of Object.entries(e.add))if(checkFixed(t),!await this._validateOneChange(t,i))throw new Error(`Invalid value for attribute "${t}"`);if(e.replace)for(const[t,i]of Object.entries(e.replace))if(checkFixed(t),!await this._validateOneChange(t,i))throw new Error(`Invalid value for attribute "${t}"`);if(e.delete){const t=Array.isArray(e.delete)?e.delete:Object.keys(e.delete);for(const e of t){const t=this.schema.attributes[e];if(t?.fixed)throw new Error(`Attribute "${e}" is fixed and cannot be deleted`)}}return!0}async _validateOneChange(t,e){if(!this.schema)return!0;const i=this.schema.attributes[t];if(!i)return!this.schema.strict;if(!e)return!0;if("pointer"===i.type){if("string"!=typeof e)throw new Error(`Field ${t} must be a string (DN pointer)`);const r=e;if(i.branch&&i.branch.length>0){if(!i.branch.some(t=>new RegExp(`,?${t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}$`,"i").test(r)))throw new Error(`Field ${t} must point to a DN within allowed branches: ${i.branch.join(", ")}`)}try{const e=await this.ldap.search({paged:!1,scope:"base"},r);if(!e||!e.searchEntries||0===e.searchEntries.length)throw new Error(`Field ${t} points to non-existent DN: ${r}`)}catch(e){throw new Error(`Field ${t} points to invalid or non-existent DN: ${r}`)}}if(i.test){const t=new RegExp(i.test);return Array.isArray(e)?e.every(e=>t.test(e)):t.test(e)}return!0}escapeDnValue(t){if(!t)return t;return t.split(",").map(t=>{const e=t.trim(),i=e.indexOf("=");if(-1===i)return e;const r=e.substring(0,i);let s=e.substring(i+1);return s=s.replace(/\\/g,"\\\\").replace(/ /g,"\\ ").replace(/#/g,"\\#").replace(/\+/g,"\\+").replace(/,/g,"\\,").replace(/;/g,"\\;").replace(/</g,"\\<").replace(/>/g,"\\>"),`${r}=${s}`}).join(",")}}export{LdapFlat as default};
@@ -0,0 +1 @@
1
+ class DmPlugin{constructor(e){this.registeredHooks={},this.server=e,this.config=e.config,this.logger=e.logger,this.registeredHooks=e.hooks}opNumber(){return this.server.operationSequence++}}export{DmPlugin as default};
@@ -0,0 +1 @@
1
+ import t from"node-fetch";import r from"p-limit";import e from"./plugin.js";class TwakePlugin extends e{constructor(t,e,s,i){if(super(t),this.webadminUrl=this.config[e]||"",this.webadminToken=this.config[s]||"",!this.webadminUrl)throw new Error(`Twake plugin: ${e} is required`);const n="number"==typeof this.config[i]?this.config[i]:10;this.requestLimit=r(n),this.logger.info(`Twake plugin HTTP request concurrency limit set to ${n}`),this.mailAttr=this.config.mail_attribute||"mail",this.displayNameAttr=this.config.display_name_attribute||"displayName"}createHeaders(t){const r={};return this.webadminToken&&(r.Authorization=`Bearer ${this.webadminToken}`),t&&(r["Content-Type"]=t),r}async callWebAdminApi(r,e,s,i,n,a){return this.requestLimit(async()=>{const o={plugin:this.name,event:r,result:"error",dn:i,...a};try{const i={method:s,headers:this.createHeaders(n?"application/json":void 0)};n&&(i.body=n);const a=await t(e,i);a.ok?this.logger.info({...o,result:"success",http_status:a.status,url:e}):this.shouldIgnoreError(a.status,r)?this.logger.debug({...o,result:"ignored",http_status:a.status,http_status_text:a.statusText,url:e}):this.logger.error({...o,http_status:a.status,http_status_text:a.statusText,url:e})}catch(t){this.logger.error({...o,error:t,url:e})}})}shouldIgnoreError(t,r){return 409===t&&r.includes("Alias")}attributeToString(t){return t?Array.isArray(t)?t.length>0?String(t[0]):null:String(t):null}async ldapGetAttributes(t,r){try{const e=r&&r.length>0?r:void 0,s=await this.server.ldap.search({paged:!1,scope:"base",attributes:e},t);return s.searchEntries&&s.searchEntries.length>0?s.searchEntries[0]:null}catch(r){return this.logger.debug(`Could not fetch attributes from DN ${t}: ${r}`),null}}getAliases(t){if(!t)return[];return(Array.isArray(t)?t:[t]).map(t=>Buffer.isBuffer(t)?t.toString("utf-8"):String(t)).map(t=>this.normalizeAlias(t))}normalizeAlias(t){return t.toLowerCase().startsWith("smtp:")?t.substring(5):t}extractMailDomain(t){const r=t.split("@");return 2===r.length?r[1]:null}extractDomainFromDn(t){const r=t.match(/dc=([^,]+)/gi);return r?r.map(t=>t.substring(3)).join("."):"example.com"}}export{TwakePlugin,TwakePlugin as default};
@@ -0,0 +1 @@
1
+ import{fileURLToPath as i}from"url";import{join as e,dirname as s}from"path";import o from"express";import r from"body-parser";import{parseConfig as t}from"../lib/parseConfig.js";import n from"../config/args.js";import g from"../lib/ldapActions.js";import{buildLogger as l}from"../logger/winston.js";import{setLogger as a}from"../lib/expressFormatedResponses.js";import h from"../plugins/priority.json.js";class DM{constructor(){this.hooks={},this.loadedPlugins={},this.config=t(n),this.app=o(),this.app.use(r.json()),this.app.use(r.urlencoded({extended:!0})),this.logger=l(this.config),this.ldap=new g(this),a(this.logger),this.operationSequence=0;const i=[];if(this.config.plugin){const e=this.config.plugin.find(i=>i.includes("configApi"));let s=this.config.plugin.filter(i=>!i.includes("configApi"));for(const e of h)s.includes(e)&&(s=s.filter(i=>i!==e),i.push(this.loadPlugin(e)));for(const e of s)i.push(this.loadPlugin(e));e&&i.push(Promise.all(i).then(()=>this.loadPlugin(e)))}this.ready=new Promise((e,s)=>{i.length>0?Promise.all(i).then(()=>e()).catch(i=>s(new Error("Error loading plugins: "+i))):e()})}run(){return new Promise((i,e)=>{this.server=this.app.listen(this.config.port,s=>{s?(this.logger.error(`Error starting server: ${s}`),e(s)):(this.logger.debug(`Server started on port ${this.config.port}`),i())})})}stop(){this.app.removeAllListeners(),this.server?.close(),this.logger.debug("Server stopped")}loadPlugin(o){let r,t;if(/:/.test(o)){let i=o.substring(o.indexOf(":")+1);if(o=o.substring(0,o.indexOf(":")),/:/.test(i)){r=i.substring(0,i.indexOf(":")),r||(r=void 0),i=i.substring(i.indexOf(":")+1);try{t=JSON.parse(i),"object"!=typeof t?(this.logger.error(`Overrides for plugin ${o} are not valid: ${i}`),t=void 0):this.logger.debug(`Overrides for plugin ${r||o}: ${i}`)}catch(e){this.logger.error(`Failed to parse overrides for plugin ${o}: ${e}, using ${i}`),t=void 0}}else r=i,r||(r=void 0)}else r=void 0;return this.logger.debug(`Loading plugin ${o}`),o.startsWith("core/")&&(o=o.replace("core/",e(s(i(import.meta.url)),"..","plugins")+"/").replace(/$/,".js")),new Promise((i,e)=>{import(o).then(async s=>{let n;if(s&&s.default&&(s=s.default),t){const i={...this.config,...t};n=new s({...this,config:i})}else n=new s(this);if(!n)return e(new Error(`Unable to load ${o}`));i(await this.registerPlugin(o,n,r)),this.logger.debug(`Plugin ${n.name} loaded`)}).catch(i=>e(new Error(`Failed to load plugin ${o}: ${i}`)))})}async registerPlugin(i,e,s){if(e.name||(e.name=i),s&&(e.name=s),this.loadedPlugins[e.name])return this.logger.info(`Plugin ${i} already loaded as ${e.name}`),!1;if(this.logger.debug(`Registering plugin ${i} as ${e.name}`),e.dependencies)for(const i in e.dependencies)this.loadedPlugins[i]||(this.logger.debug(`Plugin ${e.name} depends on ${i}, loading it first`),await this.loadPlugin(e.dependencies[i]));if(e.api&&(this.logger.debug(`Plugin ${e.name} has API, registering it`),await e.api(this.app)),e.hooks)for(const i in e.hooks){this.logger.debug(`Plugin ${e.name} has hook ${i}, registering it`);const s=e.hooks[i];if(this.hooks[i]||(this.hooks[i]=[]),!s||"function"!=typeof s)throw new Error(`Plugin ${e.name}: hook ${i} is invalid`);this.hooks[i].push(s)}return this.loadedPlugins[e.name]=e,!0}}export{DM};
@@ -0,0 +1 @@
1
+ import{fileURLToPath as a}from"url";import{join as _,dirname as e}from"path";const t=[["--port","DM_PORT",8081,"number"],["--plugin","DM_PLUGINS",[],"array","--plugins"],["--log-level","DM_LOG_LEVEL","info"],["--logger","DM_LOGGER","console"],["--api-prefix","DM_API_PREFIX","/api"],["--mail-domain","DM_MAIL_DOMAIN",[],"array","--mail-domains"],["--ldap-base","DM_LDAP_BASE",""],["--ldap-dn","DM_LDAP_DN","cn=admin,dc=example,dc=com"],["--ldap-pwd","DM_LDAP_PWD","admin"],["--ldap-url","DM_LDAP_URL","ldap://localhost"],["--ldap-user-main-attribute","DM_LDAP_USER_ATTRIBUTE","uid"],["--ldap-cache-max","DM_LDAP_CACHE_MAX",1e3,"number"],["--ldap-cache-ttl","DM_LDAP_CACHE_TTL",300,"number"],["--ldap-pool-size","DM_LDAP_POOL_SIZE",5,"number"],["--ldap-connection-ttl","DM_LDAP_CONNECTION_TTL",60,"number"],["--schemas-path","DM_SCHEMAS_PATH",_(e(a(import.meta.url)),"..","..","static","schemas")],["--mail-attribute","DM_MAIL_ATTRIBUTE","mail"],["--quota-attribute","DM_QUOTA_ATTRIBUTE","mailQuota"],["--delegation-attribute","DM_DELEGATION_ATTRIBUTE","twakeDelegatedUsers"],["--alias-attribute","DM_ALIAS_ATTRIBUTE","mailAlternateAddress"],["--forward-attribute","DM_FORWARD_ATTRIBUTE","mailForwardingAddress"],["--display-name-attribute","DM_DISPLAY_NAME_ATTRIBUTE","displayName"],["--user-class","DM_USER_CLASSES",["top","twakeAccount","twakeWhitePages"],"array","--user-classes"],["--ldap-top-organization","DM_LDAP_TOP_ORGANIZATION",""],["--ldap-organization-class","DM_LDAP_ORGANIZATION_CLASSES",["top","organizationalUnit","twakeDepartment"],"array","--ldap-organization-classes"],["--ldap-organization-link-attribute","DM_LDAP_ORGANIZATION_LINK_ATTRIBUTE","twakeDepartmentLink"],["--ldap-organization-path-attribute","DM_LDAP_ORGANIZATION_PATH_ATTRIBUTE","twakeDepartmentPath"],["--ldap-organization-path-separator","DM_LDAP_ORGANIZATION_PATH_SEPARATOR"," / "],["--ldap-organization-max-subnodes","DM_LDAP_ORGANIZATION_MAX_SUBNODES",50,"number"],["--ldap-group-base","DM_LDAP_GROUP_BASE",""],["--ldap-groups-main-attribute","DM_LDAP_GROUPS_MAIN_ATTRIBUTE","cn"],["--group-class","DM_GROUP_CLASSES",["top","groupOfNames"],"array","--group-classes"],["--group-allow-unexistent-members","DM_ALLOW_UNEXISTENT_MEMBERS",!1,"boolean"],["--group-default-attributes","DM_GROUP_DEFAULT_ATTRIBUTES",{},"json"],["--group-dummy-user","DM_GROUP_DUMMY_USER","cn=fakeuser"],["--group-schema","DM_GROUP_SCHEMA",_(e(a(import.meta.url)),"..","..","static","schemas","twake","groups.json")],["--external-members-branch","DM_EXTERNAL_MEMBERS_BRANCH","ou=contacts,dc=example,dc=com"],["--external-branch-class","DM_EXTERNAL_BRANCH_CLASSES",["top","inetOrgPerson"],"array","--external-branch-classes"],["--static-path","DM_STATIC_PATH",_(e(a(import.meta.url)),"..","..","static")],["--static-name","DM_STATIC_NAME","static"],["--ldap-flat-schema","DM_LDAP_FLAT_SCHEMA",[],"array","--ldap-flat-schemas"],["--bulk-import-schemas","DM_BULK_IMPORT_SCHEMAS",""],["--bulk-import-max-file-size","DM_BULK_IMPORT_MAX_FILE_SIZE","10485760"],["--bulk-import-batch-size","DM_BULK_IMPORT_BATCH_SIZE","100"],["--james-webadmin-url","DM_JAMES_WEBADMIN_URL","http://localhost:8000"],["--james-webadmin-token","DM_JAMES_WEBADMIN_TOKEN",""],["--james-signature-template","DM_JAMES_SIGNATURE_TEMPLATE",""],["--ldap-concurrency","DM_LDAP_CONCURRENCY",10,"number"],["--james-concurrency","DM_JAMES_CONCURRENCY",10,"number"],["--james-init-delay","DM_JAMES_INIT_DELAY",1e3,"number"],["--james-mailing-list-branch","DM_JAMES_MAILING_LIST_BRANCHES",[],"array","--james-mailing-list-branches"],["--james-mailbox-type-attribute","DM_JAMES_MAILBOX_TYPE_ATTRIBUTE","twakeMailboxType"],["--calendar-webadmin-url","DM_CALENDAR_WEBADMIN_URL","http://localhost:8080"],["--calendar-webadmin-token","DM_CALENDAR_WEBADMIN_TOKEN",""],["--calendar-concurrency","DM_CALENDAR_CONCURRENCY",10,"number"],["--calendar-resource-base","DM_CALENDAR_RESOURCE_BASE",""],["--calendar-resource-objectclass","DM_CALENDAR_RESOURCE_OBJECTCLASS",""],["--calendar-resource-creator","DM_CALENDAR_RESOURCE_CREATOR",""],["--calendar-resource-domain","DM_CALENDAR_RESOURCE_DOMAIN",""],["--trash-base","DM_TRASH_BASE",""],["--trash-watched-bases","DM_TRASH_WATCHED_BASES",""],["--trash-add-metadata","DM_TRASH_ADD_METADATA","true"],["--trash-auto-create","DM_TRASH_AUTO_CREATE","true"],["--llng-ini","DM_LLNG_INI","/etc/lemonldap-ng/lemonldap-ng.ini"],["--auth-token","DM_AUTH_TOKENS",[],"array","--auth-tokens"],["--authz-per-branch-config","DM_AUTHZ_PER_BRANCH_CONFIG",{default:{read:!0,write:!1,delete:!1}},"json"],["--authz-per-branch-cache-ttl","DM_AUTHZ_PER_BRANCH_CACHE_TTL",60,"number"],["--authz-local-admin-attribute","DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE","twakeLocalAdminLink"],["--oidc-server","DM_OIDC_SERVER",""],["--oidc-client-id","DM_OIDC_CLIENT_ID",""],["--oidc-client-secret","DM_OIDC_CLIENT_SECRET",""],["--base-url","DM_BASE_URL",""],["--rate-limit-window-ms","DM_RATE_LIMIT_WINDOW_MS",9e5,"number"],["--rate-limit-max","DM_RATE_LIMIT_MAX",100,"number"],["--crowdsec-url","DM_CROWDSEC_URL","http://localhost:8080/v1/decisions"],["--crowdsec-api-key","DM_CROWDSEC_API_KEY",""],["--crowdsec-cache-ttl","DM_CROWDSEC_CACHE_TTL",60,"number"]];export{t as default};
@@ -0,0 +1 @@
1
+ import t from"../../abstract/plugin.js";import{serverError as s}from"../expressFormatedResponses.js";import{launchHooksChained as e}from"../utils.js";class AuthBase extends t{api(t){t.use(async(t,r,a)=>{try{[t,r]=await e(this.server.hooks.beforeAuth,[t,r])}catch(t){return s(r,t)}this.authMethod(t,r,async()=>{try{this.hooks?.onAuth&&([t,r]=await e(this.server.hooks.afterAuth,[t,r])),a()}catch(t){s(r,t)}})})}}export{AuthBase as default};
@@ -0,0 +1 @@
1
+ import r from"../../abstract/plugin.js";import{getParentDn as e}from"../utils.js";class AuthzBase extends r{constructor(){super(...arguments),this.roles=["authz"],this.hooks={ldapmodifyrequest:async([r,e,s,t])=>{if(this.shouldSkipAuthorization(t))return[r,e,s,t];const i=await this.resolveUser(t.user);if(!i)return this.logger.warn(`User ${t.user} could not be resolved`),[r,e,s,t];const o=this.config.ldap_organization_link_attribute;if(o&&e.replace?.[o]){try{const e=await this.server.ldap.search({paged:!1,scope:"base",attributes:[o]},r);if(e.searchEntries.length>0){const r=e.searchEntries[0][o],s=Array.isArray(r)?String(r[0]):String(r);if(!(await this.getUserPermissions(i,s)).read)throw new Error(`User ${t.user} does not have read permission for source branch ${s}`)}}catch(e){const s=this.extractBranchDn(r);if(!(await this.getUserPermissions(i,s)).read)throw new Error(`User ${t.user} does not have read permission for source branch ${s}`)}const s=e.replace[o],n=Array.isArray(s)?String(s[0]):String(s);if(!(await this.getUserPermissions(i,n)).write)throw new Error(`User ${t.user} does not have write permission for destination branch ${n}`)}else{const e=this.extractBranchDn(r);if(!(await this.getUserPermissions(i,e)).write)throw new Error(`User ${t.user} does not have write permission for branch ${e}`)}return[r,e,s,t]},ldapaddrequest:async([r,e,s])=>{if(this.shouldSkipAuthorization(s))return[r,e,s];const t=await this.resolveUser(s.user);if(!t)return this.logger.warn(`User ${s.user} could not be resolved`),[r,e,s];let i;const o=this.config.ldap_organization_link_attribute;if(o&&e[o]){const r=e[o];i=Array.isArray(r)?String(r[0]):String(r)}else i=this.extractBranchDn(r);if(!(await this.getUserPermissions(t,i)).write)throw new Error(`User ${s.user} does not have write permission for branch ${i}`);return[r,e,s]},ldapsearchrequest:async([r,e,s])=>{if(this.shouldSkipAuthorization(s))return[r,e,s];const t=await this.resolveUser(s.user);if(!t)return this.logger.warn(`User ${s.user} could not be resolved`),[r,e,s];if(r===this.config.ldap_top_organization&&"base"===e.scope)return[r,e,s];if(!(await this.getUserPermissions(t,r)).read)throw new Error(`User ${s.user} does not have read permission for branch ${r}`);return[r,e,s]},getOrganisationTop:async([r,e])=>{if(this.shouldSkipAuthorization(r))return[r,e];const s=await this.resolveUser(r.user);if(!s)return[r,e];const t=await this.getAuthorizedBranches(s);if(t.length>0){const e=[];for(const s of t)try{const t=await this.server.ldap.search({paged:!1,scope:"base"},s,r);1===t.searchEntries.length&&e.push(t.searchEntries[0])}catch(r){this.logger.warn(`Failed to fetch authorized branch ${s}: ${r}`)}if(1===e.length)return[r,e[0]];if(e.length>1)return[r,e[0]]}return[r,e]},ldaprenamerequest:async([r,e,s])=>{if(this.shouldSkipAuthorization(s))return[r,e,s];const t=await this.resolveUser(s.user);if(!t)return this.logger.warn(`User ${s.user} could not be resolved`),[r,e,s];const i=this.extractBranchDn(r),o=this.extractBranchDn(e);if(!(await this.getUserPermissions(t,i)).read)throw new Error(`User ${s.user} does not have read permission for source branch ${i}`);if(!(await this.getUserPermissions(t,o)).write)throw new Error(`User ${s.user} does not have write permission for destination branch ${o}`);return[r,e,s]}}}extractBranchDn(r){return e(r)}shouldSkipAuthorization(r){return!r?.user}}export{AuthzBase as default};
@@ -0,0 +1 @@
1
+ let r;const setLogger=t=>{r=t},getLogger=()=>r,tryMethod=async(r,t,...e)=>{try{return await t(...e),ok(r)}catch(t){return serverError(r,t)}},tryMethodData=async(r,t,...e)=>{try{const n=await t(...e);return ok(r,n)}catch(t){return serverError(r,t)}},_rejectResponse=(r,t,e)=>{t.status(r).json({error:e})},ok=(r,t={success:!0})=>{r.status(200).json(t)},created=(r,t)=>{r.status(201).json(t)},badRequest=(r,t="Bad request")=>_rejectResponse(400,r,t),unauthorized=(r,t="Unauthorized")=>_rejectResponse(401,r,t),notFound=(r,t="Not found")=>_rejectResponse(404,r,t),serverError=(t,e)=>{e instanceof Error?r.error(e.message,e):"string"==typeof e?r.error(e):r.error("Server error",e),t.status(500).json({error:"check logs"})},wantJson=(r,t)=>!1!==r.accepts("json")||(badRequest(t),!1),jsonBody=(r,t,...e)=>{try{if(!wantJson(r,t))return!1;const n=r.body;if(e.length>0)for(let r=0;r<e.length;r++)if(null==n[e[r]])return badRequest(t,"Bad content"),!1;return n}catch(r){return badRequest(t,"Bad content"),!1}};export{badRequest,created,getLogger,jsonBody,notFound,ok,serverError,setLogger,tryMethod,tryMethodData,unauthorized,wantJson};
@@ -0,0 +1 @@
1
+ import{Client as e,Attribute as t,Change as n}from"ldapts";import{LRUCache as i}from"lru-cache";import o from"p-limit";import{launchHooksChained as s,launchHooks as r}from"./utils.js";const a={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}};class ldapActions{constructor(e){this.connectionPool=[],this.parent=e,this.logger=e.logger,this.config=e.config,this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=o(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const n="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,s=1e3*(this.config.ldap_cache_ttl||300);if(this.searchCache=new i({max:n,ttl:s,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(`LDAP search cache initialized: max=${n}, ttl=${s/1e3}s`),!e.config.ldap_url)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP DN is not defined");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined");e.config.ldap_base?this.base=e.config.ldap_base:(this.base=e.config.ldap_dn.split(",",2)[1],this.logger.warn(`LDAP base is not defined, using "${this.base}"`)),this.options={url:e.config.ldap_url,timeout:0,connectTimeout:0,strictDN:!1},e.config.ldap_url.startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const t=new e(this.options);try{await t.bind(this.dn,this.pwd)}catch(e){throw this.logger.error("LDAP bind error:",e),new Error("LDAP bind error")}if(!t)throw new Error("LDAP connection error");return t}cleanupExpiredConnections(){const e=Date.now(),t=[];for(let n=this.connectionPool.length-1;n>=0;n--){const i=this.connectionPool[n];!i.inUse&&e-i.createdAt>this.connectionTtl&&(t.push(i),this.connectionPool.splice(n,1))}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}async acquireConnection(){this.cleanupExpiredConnections();const e=this.connectionPool.find(e=>!e.inUse);if(e)return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e;if(this.connectionPool.length<this.poolSize){const e={client:await this.createConnection(),createdAt:Date.now(),inUse:!0};return this.connectionPool.push(e),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),e}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise(e=>{const t=setInterval(()=>{this.cleanupExpiredConnections();const n=this.connectionPool.find(e=>!e.inUse);n&&(clearInterval(t),n.inUse=!0,e(n))},50)})}releaseConnection(e){e.inUse=!1,this.logger.debug("Released LDAP connection back to pool")}async connect(){return await this.createConnection()}getCacheKey(e,t){const n=t.attributes?[...t.attributes].sort().join(","):"*",i="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return`${e}:${t.scope||"sub"}:${i}:${n}`}invalidateCache(e){for(const t of this.searchCache.keys())t.startsWith(e)&&this.searchCache.delete(t)}async search(e,t=this.base,n){let i={...a,...e};if(i=await s(this.parent.hooks.ldapsearchopts,i),[t,i]=await s(this.parent.hooks.ldapsearchrequest,[t,i,n]),!i.paged&&"base"===i.scope){const e=this.getCacheKey(t,i),n=this.searchCache.get(e);if(n)return this.logger.debug(`LDAP search cache hit: ${e}`),n}const o=await this.acquireConnection();try{let e=i.paged?o.client.searchPaginated(t,i):o.client.search(t,i);if(e=await s(this.parent.hooks.ldapsearchresult,e),!i.paged&&"base"===i.scope&&e instanceof Promise){const n=await e,o=this.getCacheKey(t,i);return this.searchCache.set(o,n),this.logger.debug(`LDAP search cached: ${o}`),n}return i.paged?this.wrapPaginatedSearch(e,o):e}finally{i.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){try{for await(const t of e)yield t}finally{this.releaseConnection(t)}}async add(e,n,i){e=this.setDn(e),n.objectClass&&0!==n.objectClass.length||!this.config.user_class||(n.objectClass=this.config.user_class);const o={};for(const[e,t]of Object.entries(n))Buffer.isBuffer(t)?o[e]=t.toString():Array.isArray(t)&&t.length>0&&Buffer.isBuffer(t[0])?o[e]=t.map(e=>e.toString()):o[e]=t;[e,n]=await s(this.parent.hooks.ldapaddrequest,[e,o,i]);const a=[];for(const[e,n]of Object.entries(o)){const i=Array.isArray(n)?n:[n];a.push(new t({type:e,values:i}))}const c=await this.acquireConnection();try{return await c.client.add(e,a),this.invalidateCache(e),r(this.parent.hooks.ldapadddone,[e,n]),!0}catch(e){throw new Error(`LDAP add error: ${e}`)}finally{this.releaseConnection(c)}}async modify(e,i,o){e=this.setDn(e);const a=[],c=this.opNumber();if([e,i]=await s(this.parent.hooks.ldapmodifyrequest,[e,i,c,o]),i.add)for(const[e,o]of Object.entries(i.add))a.push(new n({operation:"add",modification:new t({type:e,values:Array.isArray(o)?o:[o]})}));if(i.replace)for(const[e,o]of Object.entries(i.replace))a.push(new n({operation:"replace",modification:new t({type:e,values:Array.isArray(o)?o:[o]})}));if(i.delete)if(Array.isArray(i.delete))for(const e of i.delete)e&&a.push(new n({operation:"delete",modification:new t({type:e,values:[]})}));else for(const[e,o]of Object.entries(i.delete)){const i=new n({operation:"delete",modification:new t(o?{type:e,values:Array.isArray(o)?o:[o]}:{type:e})});a.push(i)}if(0===a.length)return this.logger.error("No changes to apply"),r(this.parent.hooks.ldapmodifydone,[e,{},c]),!1;{const t=await this.acquireConnection();try{return await t.client.modify(e,a),this.invalidateCache(e),r(this.parent.hooks.ldapmodifydone,[e,i,c]),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(a)}`),new Error(`LDAP modify error: ${t}`)}finally{this.releaseConnection(t)}}}async rename(e,t,n){e=this.setDn(e),t=this.setDn(t),[e,t]=await s(this.parent.hooks.ldaprenamerequest,[e,t,n]);const i=await this.acquireConnection();try{return await i.client.modifyDN(e,t),r(this.parent.hooks.ldaprenamedone,[e,t]),!0}catch(e){throw new Error(`LDAP rename error: ${e}`)}finally{this.releaseConnection(i)}}async move(e,t){e=this.setDn(e),t=this.setDn(t);const n=await this.acquireConnection();try{return await n.client.modifyDN(e,t),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(e){throw new Error(`LDAP move error: ${e}`)}finally{this.releaseConnection(n)}}async delete(e){e=Array.isArray(e)?e.map(e=>this.setDn(e)):this.setDn(e),Array.isArray(e)||(e=[e]),e=await s(this.parent?.hooks.ldapdeleterequest,e);const t=await this.acquireConnection();try{for(const n of e){try{await t.client.del(n),this.invalidateCache(n)}catch(e){throw new Error(`LDAP delete error: ${e}`)}r(this.parent.hooks.ldapdeletedone,n)}return!0}finally{this.releaseConnection(t)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${e},${this.base}`,e}opNumber(){return this.parent.operationSequence++}}export{ldapActions as default};
@@ -0,0 +1 @@
1
+ class ConfigParser{constructor(r){this.config=r}parse(r=process.argv){const e={},s=this.parseCliArgs(r);for(const r of this.config){const t=this.getKeyFromCliArg(r[0]);let n=r[2];if(void 0!==r[1]){const e=process.env[r[1]];if(void 0!==e)if("boolean"===r[3])n="true"===e.toLowerCase();else if("number"===r[3])n=parseInt(e);else if("array"===r[3]){const r=e.indexOf(";")>0?";":",";n=e.split(new RegExp(`[${r}\\s]+`)).map(r=>r.trim()).filter(r=>r.length>0)}else if("json"===r[3])try{n=JSON.parse(e)}catch(e){throw new Error(`Error parsing JSON from environment variable ${r[1]}: ${e}`)}else n=e}if(s.has(r[0])){const e=s.get(r[0]);if("boolean"===r[3])n=!0;else if("number"===r[3])n=parseInt(e);else if("array"===r[3])n=Array.isArray(n)?n.concat(e):e;else if("json"===r[3])try{n=JSON.parse(e)}catch(e){throw new Error(`Error parsing JSON from command line argument ${r[0]}: ${e}`)}else n=e;s.delete(r[0])}if("array"===r[3]&&r[4]&&s.has(r[4])){const e=s.get(r[4])||"";n=Array.isArray(n)?n.concat(e.split(/[,\s]+/).filter(r=>r.length>0)):e.split(/[,\s]+/).filter(r=>r.length>0),s.delete(r[4])}e[t]=n}return s.forEach((r,s)=>{if(e[s])throw new Error(`Error in command line: ${s} redefined`);e[s.replace(/^-+/,"").replace(/-/g,"_")]=r}),e}parseCliArgs(r){const e=new Map;for(let s=2;s<r.length;s++){const t=r[s];if(t.startsWith("--")||t.startsWith("-")){const n=this.config.find(r=>r[0]===t);if(n&&"boolean"===n[3])e.set(t,!0);else if(n&&"number"===n[3])e.set(t,parseInt(r[s+1]));else if(n&&"array"===n[3]){const n=e.get(t)||[],i=r[s+1];n.push(i),e.set(t,n)}else{const n=r[s+1];e.set(t,n),s++}}}return e}getKeyFromCliArg(r){return r.startsWith("--")?r.substring(2).replace(/-/g,"_"):r.startsWith("-")?r.substring(1).replace(/-/g,"_"):r}}function parseConfig(r,e=process.argv){return new ConfigParser(r).parse(e)}export{ConfigParser,parseConfig};
@@ -0,0 +1 @@
1
+ import{getLogger as t}from"./expressFormatedResponses.js";const r=t(),launchHooks=async(t,...e)=>{if(t)for(const n of t)if(n)try{await n(...e)}catch(t){r.error("Hook error",t)}},launchHooksChained=async(t,r)=>{if(t)for(const e of t)e&&(r=await e(r));return r},transformSchemas=(t,r)=>t.toString().replace(/__(\S+)__/g,(t,e)=>{if(!e||"string"!=typeof e)return t;const n=e.trim().toLowerCase();return r[n]?"object"!=typeof r[n]?r[n]:JSON.stringify(r[n]):t});function parseDn(t){const r=[];let e="",n=!1;for(let o=0;o<t.length;o++){const s=t[o];n?(e+=s,n=!1):"\\"===s?(e+=s,n=!0):","===s?(r.push(e.trim()),e=""):e+=s}return e&&r.push(e.trim()),r}function getParentDn(t){const r=parseDn(t);return r.length<=1?t:r.slice(1).join(",")}export{getParentDn,launchHooks,launchHooksChained,parseDn,transformSchemas};
@@ -0,0 +1 @@
1
+ import e from"winston";const buildLogger=r=>e.createLogger({level:r.log_level,format:e.format.combine(e.format.timestamp(),e.format.printf(({timestamp:e,level:r,message:o})=>`${e} [${r}]: ${o}`)),transports:["console"===r.logger?new e.transports.Console({stderrLevels:["error","warn"],format:e.format.json()}):new e.transports.File({filename:"error.log"})]});export{buildLogger};
@@ -0,0 +1 @@
1
+ import s from"../../lib/authz/base.js";class AuthzLinid1 extends s{constructor(s){super(s),this.name="authzLinid1",this.permissionsCache=new Map,this.cacheTTL=3e5;const e=this.config.authz_local_admin_attribute||"twakeLocalAdminLink";this.logger.info(`AuthzLinid1: Authorization based on ${e} attribute enabled`)}async resolveUser(s){return this.getUserDn(s)}async getUserDn(s){try{const e=`(${this.config.ldap_user_main_attribute||"uid"}=${s})`,t=await this.server.ldap.search({paged:!1,filter:e,attributes:["dn"],scope:"sub"},this.config.ldap_base||"");if(t.searchEntries&&t.searchEntries.length>0){const s=t.searchEntries[0].dn;return"string"==typeof s?s:String(s)}}catch(e){this.logger.error(`Failed to get DN for user ${s}: ${e}`)}return null}async getUserPermissions(s,e){const t=Date.now(),i=this.permissionsCache.get(s);if(i&&t-i.timestamp<this.cacheTTL){const s=this.findBranchPermissions(i.branches,e);if(s)return s}await this.refreshUserPermissions(s);const r=this.permissionsCache.get(s);if(r){const s=this.findBranchPermissions(r.branches,e);if(s)return s}return{read:!1,write:!1,delete:!1}}async refreshUserPermissions(s){const e=new Map;try{const t=`(${this.config.authz_local_admin_attribute||"twakeLocalAdminLink"}=${s})`,i=this.config.ldap_top_organization||this.config.ldap_base||"",r=await this.server.ldap.search({paged:!1,filter:t,attributes:["dn"],scope:"sub"},i);if(r.searchEntries)for(const s of r.searchEntries){const t="string"==typeof s.dn?s.dn:String(s.dn);e.set(t,{read:!0,write:!0,delete:!0})}}catch(e){this.logger.error(`Failed to refresh permissions for user ${s}: ${e}`)}this.permissionsCache.set(s,{branches:e,timestamp:Date.now()})}async getAuthorizedBranches(s){const e=Date.now(),t=this.permissionsCache.get(s);(!t||e-t.timestamp>=this.cacheTTL)&&await this.refreshUserPermissions(s);const i=this.permissionsCache.get(s);return i?Array.from(i.branches.keys()):[]}findBranchPermissions(s,e){if(s.has(e))return s.get(e)||null;for(const[t,i]of s.entries())if(e.toLowerCase().endsWith(`,${t.toLowerCase()}`))return i;return null}}export{AuthzLinid1 as default};
@@ -0,0 +1 @@
1
+ import e from"../../lib/authz/base.js";class AuthzPerBranch extends e{constructor(e){super(e),this.name="authzPerBranch",this.groupCache=new Map,this.cacheTTL=1e3*(this.config.authz_per_branch_cache_ttl||60),this.authConfig=this.config.authz_per_branch_config,this.authConfig&&this.logger.info("Authorization config loaded")}resolveUser(e){return Promise.resolve(e)}shouldSkipAuthorization(e){return!e?.user||!this.authConfig}async getUserPermissions(e,t){if(!this.authConfig)return{read:!0,write:!0,delete:!0};let r={read:this.authConfig.default?.read??!1,write:this.authConfig.default?.write??!1,delete:this.authConfig.default?.delete??!1};if(this.authConfig.users?.[e]){const s=this.findBranchPermissions(this.authConfig.users[e],t);s&&(r=this.mergePermissions(r,s))}const s=await this.getUserGroups(e);for(const e of s)if(this.authConfig.groups?.[e]){const s=this.findBranchPermissions(this.authConfig.groups[e],t);s&&(r=this.mergePermissions(r,s))}return r}async getAuthorizedBranches(e){return this.getAuthorizedBranchesForPermission(e,"read")}async getAuthorizedBranchesForPermission(e,t){if(!this.authConfig)return[];const r=[];if(this.authConfig.users?.[e])for(const[s,i]of Object.entries(this.authConfig.users[e]))i[t]&&r.push(s);const s=await this.getUserGroups(e);for(const e of s)if(this.authConfig.groups?.[e])for(const[s,i]of Object.entries(this.authConfig.groups[e]))i[t]&&!r.includes(s)&&r.push(s);return r}findBranchPermissions(e,t){if(e[t])return e[t];for(const[r,s]of Object.entries(e))if(t.toLowerCase().endsWith(`,${r.toLowerCase()}`))return s;return null}mergePermissions(e,t){return{read:e.read||t.read||!1,write:e.write||t.write||!1,delete:e.delete||t.delete||!1}}buildBranchFilter(e,t){if(t.some(t=>e.toLowerCase()===t.toLowerCase()||e.toLowerCase().endsWith(`,${t.toLowerCase()}`)))return null;if(1===t.length)return`(entryDN=*,${t[0]})`;if(t.length>1){return`(|${t.map(e=>`(entryDN=*,${e})`).join("")})`}return null}async getUserGroups(e){const t=Date.now(),r=this.groupCache.get(e);if(r&&t-r.timestamp<this.cacheTTL)return r.groups;const s=[];try{const t=`(${this.config.ldap_group_member_attribute||"member"}=${this.config.ldap_user_main_attribute}=${e},*)`,r=await this.server.ldap.search({paged:!1,filter:t,attributes:["dn"]},this.server.ldap.base);if(r.searchEntries)for(const e of r.searchEntries)e.dn&&s.push("string"==typeof e.dn?e.dn:String(e.dn))}catch(t){this.logger.error(`Failed to resolve groups for user ${e}: ${t}`)}return this.groupCache.set(e,{groups:s,timestamp:t}),s}}export{AuthzPerBranch as default};
@@ -0,0 +1 @@
1
+ import e from"node-fetch";import r from"../../abstract/plugin.js";class CrowdSec extends r{constructor(...e){if(super(...e),this.name="crowdsec",this.roles=["protect"],this.cache=new Map,this.apiUrl=this.config.crowdsec_url||"http://localhost:8080/v1/decisions",this.apiKey=this.config.crowdsec_api_key,this.cacheTtl=this.config.crowdsec_cache_ttl||60,!this.apiKey)throw new Error("CrowdSec plugin requires crowdsec_api_key configuration");this.logger.info(`CrowdSec integration enabled with API at ${this.apiUrl} (cache TTL: ${this.cacheTtl}s)`)}api(e){e.use(async(e,r,t)=>{const s=this.getClientIp(e);try{if(await this.checkIpBanned(s))return this.logger.warn(`Blocked request from CrowdSec-banned IP: ${s}`),r.status(403).json({error:"Access denied",reason:"IP address is banned by security policies"});t()}catch(e){this.logger.error(`CrowdSec API error for IP ${s}: ${e instanceof Error?e.message:String(e)}`),t()}})}async checkIpBanned(r){const t=this.cache.get(r),s=Date.now();if(t&&s<t.expiresAt)return this.logger.debug(`CrowdSec cache hit for IP ${r}: ${t.isBanned}`),t.isBanned;const o=`${this.apiUrl}?ip=${encodeURIComponent(r)}`;this.logger.debug(`Querying CrowdSec API: ${o}`);const i=await e(o,{headers:{"X-Api-Key":this.apiKey}});if(!i.ok)throw new Error(`CrowdSec API returned status ${i.status}: ${i.statusText}`);let c;try{const e=await i.text();c="null"===e||""===e?null:JSON.parse(e)}catch(e){throw this.logger.error(`Failed to parse CrowdSec API response: ${e instanceof Error?e.message:String(e)}`),new Error("Invalid JSON response from CrowdSec API")}let n=!1;return null!==c&&Array.isArray(c)&&(n=c.some(e=>"ban"===e.type)),this.cache.set(r,{isBanned:n,expiresAt:s+1e3*this.cacheTtl}),this.logger.debug(`CrowdSec decision for IP ${r}: ${n?"BANNED":"ALLOWED"}`),n}getClientIp(e){const r=e.headers["x-forwarded-for"];if(r){return(Array.isArray(r)?r[0]:r).split(",")[0].trim()}return e.socket.remoteAddress||"unknown"}}export{CrowdSec as default};
@@ -0,0 +1 @@
1
+ import*as e from"lemonldap-ng-handler";import t from"../../lib/auth/base.js";class AuthLLNG extends t{constructor(){super(...arguments),this.name="authLemonldapNg",this.roles=["auth"]}authMethod(t,s,a){e.run(t,s,()=>{t.user=t.headers["Lm-Remote-User"],a()})}}export{AuthLLNG as default};