ldap-rest 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/bin/sync-james.mjs +13 -2
  2. package/dist/abstract/ldapFlat.js +1 -1
  3. package/dist/abstract/plugin.js +1 -1
  4. package/dist/abstract/twakePlugin.js +1 -1
  5. package/dist/bin/index.js +1 -1
  6. package/dist/config/args.js +1 -1
  7. package/dist/config/options.js +1 -0
  8. package/dist/config/schema.js +1 -1
  9. package/dist/lib/auth/base.js +1 -1
  10. package/dist/lib/authz/base.js +1 -1
  11. package/dist/lib/entitySchema/validator.js +1 -1
  12. package/dist/lib/errors.js +1 -1
  13. package/dist/lib/expressFormatedResponses.js +1 -1
  14. package/dist/lib/health.js +1 -1
  15. package/dist/lib/hookBus.js +1 -0
  16. package/dist/lib/instanceProviders.js +1 -1
  17. package/dist/lib/ldapActions.js +1 -1
  18. package/dist/lib/ldapSchema.js +1 -1
  19. package/dist/lib/parseConfig.js +1 -1
  20. package/dist/lib/route.js +1 -0
  21. package/dist/lib/schemaFile.js +1 -1
  22. package/dist/lib/storage/store.js +1 -1
  23. package/dist/lib/utils.js +1 -1
  24. package/dist/lib/version.js +1 -0
  25. package/dist/logger/winston.js +1 -1
  26. package/dist/plugins/auth/auth.options.js +1 -0
  27. package/dist/plugins/auth/authzDynamic.js +1 -1
  28. package/dist/plugins/auth/authzLinid1.js +1 -1
  29. package/dist/plugins/auth/authzPerBranch.js +1 -1
  30. package/dist/plugins/auth/authzPerRoute.js +1 -1
  31. package/dist/plugins/auth/authzScope.js +1 -1
  32. package/dist/plugins/auth/crowdsec.js +1 -1
  33. package/dist/plugins/auth/fake.js +1 -1
  34. package/dist/plugins/auth/hmac.js +1 -1
  35. package/dist/plugins/auth/llng.js +1 -1
  36. package/dist/plugins/auth/openidconnect.js +1 -1
  37. package/dist/plugins/auth/rateLimit.js +1 -1
  38. package/dist/plugins/auth/token.js +1 -1
  39. package/dist/plugins/auth/totp.js +1 -1
  40. package/dist/plugins/auth/trustedProxy.js +1 -1
  41. package/dist/plugins/bcl.js +1 -1
  42. package/dist/plugins/bcl.options.js +1 -0
  43. package/dist/plugins/configApi.js +1 -1
  44. package/dist/plugins/demo/helloworld.js +1 -1
  45. package/dist/plugins/ldap/accountLifecycle.js +1 -1
  46. package/dist/plugins/ldap/bulkImport.js +1 -1
  47. package/dist/plugins/ldap/departmentSync.js +1 -1
  48. package/dist/plugins/ldap/enterpriseRules.js +1 -1
  49. package/dist/plugins/ldap/externalUsersInGroups.js +1 -1
  50. package/dist/plugins/ldap/flatGeneric.js +1 -1
  51. package/dist/plugins/ldap/groups.js +1 -1
  52. package/dist/plugins/ldap/ldap.options.js +1 -0
  53. package/dist/plugins/ldap/onChange.js +1 -1
  54. package/dist/plugins/ldap/organizations.js +1 -1
  55. package/dist/plugins/ldap/passwordPolicy.js +1 -1
  56. package/dist/plugins/ldap/raw.js +1 -1
  57. package/dist/plugins/ldap/trash.js +1 -1
  58. package/dist/plugins/metrics.js +1 -1
  59. package/dist/plugins/metrics.options.js +1 -0
  60. package/dist/plugins/rabbitmq.js +1 -1
  61. package/dist/plugins/rabbitmq.options.js +1 -0
  62. package/dist/plugins/scim/baseResolver.js +1 -1
  63. package/dist/plugins/scim/bulk.js +1 -1
  64. package/dist/plugins/scim/discovery.js +1 -1
  65. package/dist/plugins/scim/errors.js +1 -1
  66. package/dist/plugins/scim/filter.js +1 -1
  67. package/dist/plugins/scim/groups.js +1 -1
  68. package/dist/plugins/scim/mapping.js +1 -1
  69. package/dist/plugins/scim/patch.js +1 -1
  70. package/dist/plugins/scim/projection.js +1 -1
  71. package/dist/plugins/scim/scim.js +1 -1
  72. package/dist/plugins/scim/scim.options.js +1 -0
  73. package/dist/plugins/scim/users.js +1 -1
  74. package/dist/plugins/static.js +1 -1
  75. package/dist/plugins/static.options.js +1 -0
  76. package/dist/plugins/storage/file.js +1 -1
  77. package/dist/plugins/storage/ldap.js +1 -1
  78. package/dist/plugins/storage/postgres.js +1 -1
  79. package/dist/plugins/storage/valkey.js +1 -1
  80. package/dist/plugins/storage.js +1 -1
  81. package/dist/plugins/storage.options.js +1 -0
  82. package/dist/plugins/twake/appAccounts.options.js +1 -0
  83. package/dist/plugins/twake/appAccountsApi.js +1 -1
  84. package/dist/plugins/twake/appAccountsConsistency.js +1 -1
  85. package/dist/plugins/twake/calendar.js +1 -1
  86. package/dist/plugins/twake/calendar.options.js +1 -0
  87. package/dist/plugins/twake/calendarResources.js +1 -1
  88. package/dist/plugins/twake/cloudery.options.js +1 -0
  89. package/dist/plugins/twake/clouderyProvision.js +1 -1
  90. package/dist/plugins/twake/cozy.options.js +1 -0
  91. package/dist/plugins/twake/cozyProvision.js +1 -1
  92. package/dist/plugins/twake/drive.js +1 -1
  93. package/dist/plugins/twake/drive.options.js +1 -0
  94. package/dist/plugins/twake/groups.js +1 -1
  95. package/dist/plugins/twake/instances.js +1 -1
  96. package/dist/plugins/twake/james.js +1 -1
  97. package/dist/plugins/twake/james.options.js +1 -0
  98. package/dist/plugins/twake/lifecycleAttributes.js +1 -1
  99. package/dist/plugins/twake/lifecycleEvents.js +1 -1
  100. package/dist/plugins/twake/migrationProxy.js +1 -1
  101. package/dist/plugins/twake/migrationProxy.options.js +1 -0
  102. package/dist/plugins/twake/spaces.js +1 -1
  103. package/dist/plugins/twake/tombstone.js +1 -1
  104. package/dist/plugins/twake/twakeGroups.options.js +1 -0
  105. package/dist/plugins/twake/twakeInstances.options.js +1 -0
  106. package/dist/plugins/twake/twakeLifecycle.options.js +1 -0
  107. package/dist/plugins/twake/twakeSpaces.options.js +1 -0
  108. package/dist/plugins/twake/twakeTombstone.options.js +1 -0
  109. package/dist/plugins/weblogs.js +1 -1
  110. package/dist/src/abstract/ldapFlat.d.ts +28 -10
  111. package/dist/src/abstract/plugin.d.ts +10 -5
  112. package/dist/src/abstract/twakePlugin.d.ts +14 -4
  113. package/dist/src/bin/index.d.ts +17 -4
  114. package/dist/src/config/args.d.ts +2 -4
  115. package/dist/src/config/options.d.ts +89 -0
  116. package/dist/src/config/schema.d.ts +50 -1
  117. package/dist/src/hooks.d.ts +60 -22
  118. package/dist/src/lib/authz/base.d.ts +4 -51
  119. package/dist/src/lib/authz/composition.d.ts +2 -2
  120. package/dist/src/lib/entitySchema/validator.d.ts +63 -6
  121. package/dist/src/lib/errors.d.ts +56 -1
  122. package/dist/src/lib/expressFormatedResponses.d.ts +49 -7
  123. package/dist/src/lib/hookBus.d.ts +226 -0
  124. package/dist/src/lib/instanceProviders.d.ts +0 -8
  125. package/dist/src/lib/ldapActions.d.ts +11 -2
  126. package/dist/src/lib/parseConfig.d.ts +5 -0
  127. package/dist/src/lib/route.d.ts +99 -0
  128. package/dist/src/lib/utils.d.ts +37 -36
  129. package/dist/src/lib/version.d.ts +12 -0
  130. package/dist/src/plugins/auth/auth.options.d.ts +88 -0
  131. package/dist/src/plugins/auth/authzDynamic.d.ts +10 -1
  132. package/dist/src/plugins/auth/authzLinid1.d.ts +6 -0
  133. package/dist/src/plugins/auth/authzPerBranch.d.ts +9 -0
  134. package/dist/src/plugins/auth/authzPerRoute.d.ts +3 -0
  135. package/dist/src/plugins/auth/authzScope.d.ts +23 -14
  136. package/dist/src/plugins/auth/crowdsec.d.ts +5 -0
  137. package/dist/src/plugins/auth/fake.d.ts +3 -0
  138. package/dist/src/plugins/auth/hmac.d.ts +4 -0
  139. package/dist/src/plugins/auth/llng.d.ts +4 -0
  140. package/dist/src/plugins/auth/openidconnect.d.ts +7 -0
  141. package/dist/src/plugins/auth/rateLimit.d.ts +4 -0
  142. package/dist/src/plugins/auth/token.d.ts +3 -0
  143. package/dist/src/plugins/auth/totp.d.ts +5 -0
  144. package/dist/src/plugins/auth/trustedProxy.d.ts +4 -0
  145. package/dist/src/plugins/bcl.d.ts +3 -0
  146. package/dist/src/plugins/bcl.options.d.ts +8 -0
  147. package/dist/src/plugins/configApi.d.ts +24 -5
  148. package/dist/src/plugins/demo/helloworld.d.ts +8 -19
  149. package/dist/src/plugins/ldap/accountLifecycle.d.ts +17 -0
  150. package/dist/src/plugins/ldap/bulkImport.d.ts +6 -76
  151. package/dist/src/plugins/ldap/departmentSync.d.ts +1 -0
  152. package/dist/src/plugins/ldap/enterpriseRules.d.ts +8 -1
  153. package/dist/src/plugins/ldap/externalUsersInGroups.d.ts +4 -0
  154. package/dist/src/plugins/ldap/flatGeneric.d.ts +4 -0
  155. package/dist/src/plugins/ldap/groups.d.ts +27 -11
  156. package/dist/src/plugins/ldap/ldap.options.d.ts +75 -0
  157. package/dist/src/plugins/ldap/onChange.d.ts +10 -2
  158. package/dist/src/plugins/ldap/organizations.d.ts +16 -0
  159. package/dist/src/plugins/ldap/passwordPolicy.d.ts +32 -1
  160. package/dist/src/plugins/ldap/raw.d.ts +50 -19
  161. package/dist/src/plugins/ldap/trash.d.ts +6 -0
  162. package/dist/src/plugins/metrics.d.ts +3 -0
  163. package/dist/src/plugins/metrics.options.d.ts +8 -0
  164. package/dist/src/plugins/rabbitmq.d.ts +3 -0
  165. package/dist/src/plugins/rabbitmq.options.d.ts +8 -0
  166. package/dist/src/plugins/scim/baseResolver.d.ts +2 -1
  167. package/dist/src/plugins/scim/bulk.d.ts +6 -7
  168. package/dist/src/plugins/scim/discovery.d.ts +3 -0
  169. package/dist/src/plugins/scim/errors.d.ts +4 -1
  170. package/dist/src/plugins/scim/groups.d.ts +6 -4
  171. package/dist/src/plugins/scim/scim.d.ts +37 -3
  172. package/dist/src/plugins/scim/scim.options.d.ts +35 -0
  173. package/dist/src/plugins/scim/types.d.ts +2 -1
  174. package/dist/src/plugins/scim/users.d.ts +6 -7
  175. package/dist/src/plugins/static.d.ts +9 -0
  176. package/dist/src/plugins/static.options.d.ts +13 -0
  177. package/dist/src/plugins/storage.d.ts +12 -1
  178. package/dist/src/plugins/storage.options.d.ts +16 -0
  179. package/dist/src/plugins/twake/appAccounts.options.d.ts +13 -0
  180. package/dist/src/plugins/twake/appAccountsApi.d.ts +18 -0
  181. package/dist/src/plugins/twake/appAccountsConsistency.d.ts +8 -0
  182. package/dist/src/plugins/twake/calendar.d.ts +25 -2
  183. package/dist/src/plugins/twake/calendar.options.d.ts +19 -0
  184. package/dist/src/plugins/twake/cloudery.options.d.ts +26 -0
  185. package/dist/src/plugins/twake/clouderyProvision.d.ts +18 -0
  186. package/dist/src/plugins/twake/cozy.options.d.ts +23 -0
  187. package/dist/src/plugins/twake/cozyProvision.d.ts +14 -0
  188. package/dist/src/plugins/twake/drive.d.ts +7 -0
  189. package/dist/src/plugins/twake/drive.options.d.ts +15 -0
  190. package/dist/src/plugins/twake/groups.d.ts +30 -3
  191. package/dist/src/plugins/twake/instances.d.ts +43 -10
  192. package/dist/src/plugins/twake/james.d.ts +17 -0
  193. package/dist/src/plugins/twake/james.options.d.ts +21 -0
  194. package/dist/src/plugins/twake/lifecycleEvents.d.ts +12 -0
  195. package/dist/src/plugins/twake/migrationProxy.d.ts +11 -11
  196. package/dist/src/plugins/twake/migrationProxy.options.d.ts +16 -0
  197. package/dist/src/plugins/twake/spaces.d.ts +30 -0
  198. package/dist/src/plugins/twake/tombstone.d.ts +16 -14
  199. package/dist/src/plugins/twake/twakeGroups.options.d.ts +20 -0
  200. package/dist/src/plugins/twake/twakeInstances.options.d.ts +43 -0
  201. package/dist/src/plugins/twake/twakeLifecycle.options.d.ts +21 -0
  202. package/dist/src/plugins/twake/twakeSpaces.options.d.ts +19 -0
  203. package/dist/src/plugins/twake/twakeTombstone.options.d.ts +17 -0
  204. package/docs/README.md +1 -0
  205. package/docs/client-development/api/rest-api.md +9 -3
  206. package/docs/client-development/schemas/README.md +96 -32
  207. package/docs/plugin-development/README.md +45 -94
  208. package/docs/plugin-development/dependencies.md +11 -6
  209. package/docs/plugin-development/hooks.md +142 -23
  210. package/docs/plugin-development/openapi.md +14 -1
  211. package/docs/plugin-development/options.md +109 -0
  212. package/docs/plugin-development/routes.md +152 -0
  213. package/docs/plugin-development/testing.md +3 -3
  214. package/docs/usage/configuration.md +31 -24
  215. package/docs/usage/operations.md +19 -4
  216. package/docs/usage/plugins/integrations/groups.md +2 -2
  217. package/docs/usage/plugins/integrations/scim.md +12 -1
  218. package/docs/usage/plugins/integrations/spaces.md +1 -1
  219. package/docs/usage/plugins/ldap/enterprise-rules.md +10 -1
  220. package/docs/usage/plugins/ldap/flat-generic.md +4 -0
  221. package/docs/usage/plugins/ldap/raw.md +17 -1
  222. package/docs/usage/upgrading.md +207 -0
  223. package/package.json +358 -277
  224. package/static/browser/ldap-browser/LdapBrowser.js +8 -6
  225. package/static/browser/ldap-browser/LdapBrowser.js.map +1 -1
  226. package/static/browser/ldap-browser/api/RawApiClient.js +2 -0
  227. package/static/browser/ldap-browser/api/RawApiClient.js.map +1 -1
  228. package/static/browser/ldap-browser/components/EntryTree.js +9 -5
  229. package/static/browser/ldap-browser/components/EntryTree.js.map +1 -1
  230. package/static/browser/ldap-browser/components/EntryView.js +2 -0
  231. package/static/browser/ldap-browser/components/EntryView.js.map +1 -1
  232. package/static/browser/ldap-browser/schema.js +3 -2
  233. package/static/browser/ldap-browser/schema.js.map +1 -1
  234. package/static/browser/ldap-browser.esm.js +27 -18
  235. package/static/browser/ldap-browser.esm.js.map +1 -1
  236. package/static/browser/ldap-browser.js +27 -18
  237. package/static/browser/ldap-browser.js.map +1 -1
  238. package/static/browser/ldap-browser.min.js +1 -1
  239. package/static/browser/ldap-browser.min.js.map +1 -1
  240. package/static/browser/ldap-group-editor/LdapGroupEditor.js +8 -6
  241. package/static/browser/ldap-group-editor/LdapGroupEditor.js.map +1 -1
  242. package/static/browser/ldap-group-editor/api/GroupApiClient.js +1 -0
  243. package/static/browser/ldap-group-editor/api/GroupApiClient.js.map +1 -1
  244. package/static/browser/ldap-group-editor/components/CreateGroupModal.js +5 -1
  245. package/static/browser/ldap-group-editor/components/CreateGroupModal.js.map +1 -1
  246. package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js +6 -3
  247. package/static/browser/ldap-group-editor/components/GroupPropertyEditor.js.map +1 -1
  248. package/static/browser/ldap-group-editor/components/GroupTree.js +6 -3
  249. package/static/browser/ldap-group-editor/components/GroupTree.js.map +1 -1
  250. package/static/browser/ldap-group-editor/components/MoveGroupModal.js +6 -3
  251. package/static/browser/ldap-group-editor/components/MoveGroupModal.js.map +1 -1
  252. package/static/browser/ldap-group-editor.esm.js +32 -16
  253. package/static/browser/ldap-group-editor.esm.js.map +1 -1
  254. package/static/browser/ldap-group-editor.js +32 -16
  255. package/static/browser/ldap-group-editor.js.map +1 -1
  256. package/static/browser/ldap-group-editor.min.js +1 -1
  257. package/static/browser/ldap-group-editor.min.js.map +1 -1
  258. package/static/browser/ldap-resource-editor/api/ResourceApiClient.js +3 -0
  259. package/static/browser/ldap-resource-editor/api/ResourceApiClient.js.map +1 -1
  260. package/static/browser/ldap-resource-editor/cache/CacheManager.js +3 -1
  261. package/static/browser/ldap-resource-editor/cache/CacheManager.js.map +1 -1
  262. package/static/browser/ldap-tree-viewer/LdapTreeViewer.js +6 -1
  263. package/static/browser/ldap-tree-viewer/LdapTreeViewer.js.map +1 -1
  264. package/static/browser/ldap-tree-viewer/api/LdapApiClient.js +2 -0
  265. package/static/browser/ldap-tree-viewer/api/LdapApiClient.js.map +1 -1
  266. package/static/browser/ldap-tree-viewer/components/TreeNode.js +4 -0
  267. package/static/browser/ldap-tree-viewer/components/TreeNode.js.map +1 -1
  268. package/static/browser/ldap-tree-viewer/components/TreeRoot.js +3 -0
  269. package/static/browser/ldap-tree-viewer/components/TreeRoot.js.map +1 -1
  270. package/static/browser/ldap-tree-viewer/store/Store.js +3 -1
  271. package/static/browser/ldap-tree-viewer/store/Store.js.map +1 -1
  272. package/static/browser/ldap-tree-viewer.esm.js +18 -2
  273. package/static/browser/ldap-tree-viewer.esm.js.map +1 -1
  274. package/static/browser/ldap-tree-viewer.js +18 -2
  275. package/static/browser/ldap-tree-viewer.js.map +1 -1
  276. package/static/browser/ldap-tree-viewer.min.js +1 -1
  277. package/static/browser/ldap-tree-viewer.min.js.map +1 -1
  278. package/static/browser/ldap-unit-editor/LdapUnitEditor.js +7 -5
  279. package/static/browser/ldap-unit-editor/LdapUnitEditor.js.map +1 -1
  280. package/static/browser/ldap-unit-editor/api/UnitApiClient.js +1 -0
  281. package/static/browser/ldap-unit-editor/api/UnitApiClient.js.map +1 -1
  282. package/static/browser/ldap-unit-editor/components/MoveUnitModal.js +7 -4
  283. package/static/browser/ldap-unit-editor/components/MoveUnitModal.js.map +1 -1
  284. package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js +7 -4
  285. package/static/browser/ldap-unit-editor/components/UnitPropertyEditor.js.map +1 -1
  286. package/static/browser/ldap-unit-editor/components/UnitTree.js +6 -3
  287. package/static/browser/ldap-unit-editor/components/UnitTree.js.map +1 -1
  288. package/static/browser/ldap-unit-editor.esm.js +28 -16
  289. package/static/browser/ldap-unit-editor.esm.js.map +1 -1
  290. package/static/browser/ldap-unit-editor.js +28 -16
  291. package/static/browser/ldap-unit-editor.js.map +1 -1
  292. package/static/browser/ldap-unit-editor.min.js +1 -1
  293. package/static/browser/ldap-unit-editor.min.js.map +1 -1
  294. package/static/browser/ldap-user-editor/LdapUserEditor.js +9 -7
  295. package/static/browser/ldap-user-editor/LdapUserEditor.js.map +1 -1
  296. package/static/browser/ldap-user-editor/api/UserApiClient.js +2 -0
  297. package/static/browser/ldap-user-editor/api/UserApiClient.js.map +1 -1
  298. package/static/browser/ldap-user-editor/cache/CacheManager.js +3 -1
  299. package/static/browser/ldap-user-editor/cache/CacheManager.js.map +1 -1
  300. package/static/browser/ldap-user-editor/components/MoveUserModal.js +7 -4
  301. package/static/browser/ldap-user-editor/components/MoveUserModal.js.map +1 -1
  302. package/static/browser/ldap-user-editor/components/PointerField.js +6 -1
  303. package/static/browser/ldap-user-editor/components/PointerField.js.map +1 -1
  304. package/static/browser/ldap-user-editor/components/UserEditor.js +10 -5
  305. package/static/browser/ldap-user-editor/components/UserEditor.js.map +1 -1
  306. package/static/browser/ldap-user-editor/components/UserList.js +8 -4
  307. package/static/browser/ldap-user-editor/components/UserList.js.map +1 -1
  308. package/static/browser/ldap-user-editor/components/UserTree.js +7 -3
  309. package/static/browser/ldap-user-editor/components/UserTree.js.map +1 -1
  310. package/static/browser/ldap-user-editor.esm.js +55 -30
  311. package/static/browser/ldap-user-editor.esm.js.map +1 -1
  312. package/static/browser/ldap-user-editor.js +55 -30
  313. package/static/browser/ldap-user-editor.js.map +1 -1
  314. package/static/browser/ldap-user-editor.min.js +1 -1
  315. package/static/browser/ldap-user-editor.min.js.map +1 -1
  316. package/static/browser/shared/components/DisposableComponent.js +3 -5
  317. package/static/browser/shared/components/DisposableComponent.js.map +1 -1
  318. package/static/browser/shared/components/Modal.js +2 -1
  319. package/static/browser/shared/components/Modal.js.map +1 -1
  320. package/static/browser/shared/components/StatusMessage.js +3 -2
  321. package/static/browser/shared/components/StatusMessage.js.map +1 -1
  322. package/static/browser/shared/utils/form.js.map +1 -1
  323. package/static/browser/shared/utils/hmac.js +1 -0
  324. package/static/browser/shared/utils/hmac.js.map +1 -1
  325. package/static/browser/shared/utils/totp.js +1 -0
  326. package/static/browser/shared/utils/totp.js.map +1 -1
  327. package/static/browser/shared.esm.js +9 -8
  328. package/static/browser/shared.esm.js.map +1 -1
  329. package/static/browser/shared.js +9 -8
  330. package/static/browser/shared.js.map +1 -1
  331. package/static/browser/shared.min.js +1 -1
  332. package/static/browser/shared.min.js.map +1 -1
  333. package/static/schemas/ad/groups.json +1 -1
  334. package/static/schemas/ad/users.json +3 -3
  335. package/static/schemas/example/organizations.json +2 -2
  336. package/static/schemas/example/users.json +2 -2
  337. package/static/schemas/obm/users.json +2 -2
  338. package/static/schemas/standard/groups.json +1 -1
  339. package/static/schemas/standard/users.json +2 -2
  340. package/static/schemas/twake/groups.json +4 -4
  341. package/static/schemas/twake/teamMailboxes.json +3 -3
  342. package/static/schemas/twake/users.json +7 -7
@@ -1 +1 @@
1
- import{escapeDnValue as t,launchHooksChained as e,validateDnValue as s,launchHooks as i,isChildOf as r,escapeLdapFilter as a}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as o}from"./types.js";import{resolveLockConfig as c,mergeMapping as h,loadMappingFile as n,DEFAULT_USER_MAPPING as l,requiredLdapAttributes as u,ldapToScimUser as d,scimUserToLdap as p,isLocked as m}from"./mapping.js";import{scimFilterToLdap as f}from"./filter.js";import{pagedSearch as b}from"./list.js";import{writePatch as g,patchToModifyRequest as w}from"./patch.js";import{scimNotFound as k,ScimError as y,scimInvalidValue as A,scimUniqueness as R}from"./errors.js";import{extractLdapCode as $}from"../../lib/ldapCodes.js";class ScimUsers{constructor(t){this.ldap=t.ldap,this.config=t.config,this.logger=t.logger,this.baseResolver=t.baseResolver,this.hooks=t.hooks,this.rdnAttribute=this.config.scim_user_rdn_attribute||"uid",this.objectClass=this.config.scim_user_object_class,this.idAttribute=this.config.scim_id_attribute||"rdn",this.maxResults=this.config.scim_max_results||200,this.maxScanned=this.config.scim_max_scanned||1e4,this.scimPrefix=this.config.scim_prefix||"/scim/v2";const e=c(this.config.scim_user_lock_attribute||"",this.config.scim_user_lock_value||"");this.lockAttribute=e.attribute,this.lockValue=e.value;const s=this.config.scim_user_mapping||"";this.mapping=h(l,s?n(s):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"User",baseUrl:this.config.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:this.lockAttribute,lockValue:this.lockValue}}ldapAttributes(){return u(this.mapping,[this.lockAttribute])}dnForId(e,s){const i=this.baseResolver.userBase(s);return`${this.rdnAttribute}=${t(e)},${i}`}async get(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===$(t))throw k(`User ${e} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw k(`User ${e} not found`);return d(i.searchEntries[0],this.mapping,this.ctx(t))}async list(t,e){const s=this.baseResolver.userBase(t),i=Math.max(1,e.startIndex||1),r=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let a,c=`(${this.objectClass.includes("inetOrgPerson")?"objectClass=inetOrgPerson":`objectClass=${this.objectClass[0]}`})`;if(e.filter){const t=f(e.filter,this.mapping,{lockAttribute:this.lockAttribute,lockValue:this.lockValue,supportsActive:!0});t.idEquals?a=t.idEquals:c=`(&${c}${t.ldapFilter})`}if(a)try{const e=await this.get(t,a);return{schemas:[o],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof y&&404===t.statusCode)return{schemas:[o],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:h,totalResults:n}=await b({directory:this.ldap.forRequest(t),base:s,filter:c,attributes:this.ldapAttributes(),startIndex:i,count:r,maxScanned:this.maxScanned});e.sortBy,e.sortOrder;const l=h.map(e=>d(e,this.mapping,this.ctx(t)));return{schemas:[o],totalResults:n,startIndex:i,itemsPerPage:l.length,Resources:l}}async create(r,a){if(!a.userName)throw A("userName is required");const o=(await e(this.hooks.scimusercreate,[a,r]))[0],{rdn:c,attributes:h}=p(o,this.mapping,this.ctx(r),this.objectClass);if(!c)throw A("userName is required");s(c,this.rdnAttribute),h[this.rdnAttribute]=c;const n=this.baseResolver.userBase(r),l=`${this.rdnAttribute}=${t(c)},${n}`;try{await this.ldap.forRequest(r).add(l,h)}catch(t){if(68===$(t))throw R(`User ${c} already exists`);if(void 0!==h[this.lockAttribute])throw this.lockSchemaError(t);throw t}const u=await this.get(r,c);return i(this.hooks.scimusercreatedone,u),u}async replace(t,s,r){const a=this.dnForId(s,t);let o;try{o=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},a)}catch(t){if(32===$(t))throw k(`User ${s} not found`);throw t}if(!o.searchEntries?.length)throw k(`User ${s} not found`);const c=o.searchEntries[0],h=(await e(this.hooks.scimuserupdate,[s,r,t]))[1],{attributes:n,active:l}=p(h,this.mapping,this.ctx(t),this.objectClass),u={replace:{},delete:[]},d=new Set(["objectClass","entryUUID","createTimestamp","modifyTimestamp",this.rdnAttribute,this.lockAttribute]),hasAttrValue=t=>null!=t&&("string"==typeof t?t.length>0:!Array.isArray(t)||t.length>0&&t.some(t=>null!=t));for(const t of this.ldapAttributes())d.has(t)||(null!=n[t]?u.replace[t]=n[t]:hasAttrValue(c[t])&&u.delete.push(t));if(!1===l?u.replace[this.lockAttribute]=this.lockValue:!0===l&&m(c,{lockAttribute:this.lockAttribute,lockValue:this.lockValue})&&u.delete.push(this.lockAttribute),0===Object.keys(u.replace||{}).length&&delete u.replace,0===u.delete.length&&delete u.delete,u.replace||u.delete)try{await this.ldap.forRequest(t).modify(a,u)}catch(t){if(void 0!==l)throw this.lockSchemaError(t);if(16!==$(t))throw t;throw new y(409,`${a} changed while this PUT was being applied; nothing was written. Retry the request.`)}const f=await this.get(t,s);return i(this.hooks.scimuserupdatedone,s,f),f}async patch(t,e,s){const r=this.dnForId(e,t);await g(r,async()=>{const i=await this.currentEntry(t,e),a=await w(s,{mapping:this.mapping,lockAttribute:this.lockAttribute,lockValue:this.lockValue,supportsActive:!0,current:i});try{await this.ldap.forRequest(t).modify(r,a,{permissive:!0})}catch(t){if(function(t,e){const s=e.toLowerCase(),i=t.delete;return[...Object.keys(t.replace||{}),...Object.keys(t.add||{}),...Array.isArray(i)?i:Object.keys(i||{})].some(t=>t.toLowerCase()===s)}(a,this.lockAttribute))throw this.lockSchemaError(t);throw t}});const a=await this.get(t,e);return i(this.hooks.scimuserupdatedone,e,a),a}lockSchemaError(t){const e=$(t);if(17!==e&&65!==e)return t;const s=t instanceof Error?t.message:String(t);return A(`The directory rejected '${this.lockAttribute}', the attribute backing 'active': ${s}. Point --scim-user-lock-attribute and --scim-user-lock-value at an attribute this directory defines (nsAccountLock / TRUE on 389-ds); the default needs the ppolicy overlay.`)}async currentEntry(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===$(t))throw k(`User ${e} not found`);throw t}if(!i.searchEntries?.length)throw k(`User ${e} not found`);return i.searchEntries[0]}async delete(t,s){await this.get(t,s);const r=(await e(this.hooks.scimuserdelete,[s,t]))[0],a=this.dnForId(r,t);await this.ldap.forRequest(t).delete(a),i(this.hooks.scimuserdeletedone,r)}async resolveRef(e,s){if(!s)return;const i=this.baseResolver.userBase(e),o=s.includes("=")&&s.includes(",");if(o&&!r(s,i))return;const c=o?s:`${this.rdnAttribute}=${t(s)},${i}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},c);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==$(t))throw t}if(!o)try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${a(s)})`,scope:"sub",paged:!1,attributes:["dn"]},i);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==$(t))throw t}}get userMapping(){return this.mapping}}export{ScimUsers};
1
+ import{escapeDnValue as t,validateDnValue as e,isChildOf as s,escapeLdapFilter as i}from"../../lib/utils.js";import{SCHEMA_LIST_RESPONSE as r}from"./types.js";import{resolveLockConfig as o,mergeMapping as a,loadMappingFile as c,DEFAULT_USER_MAPPING as n,requiredLdapAttributes as h,ldapToScimUser as u,scimUserToLdap as l,isLocked as d}from"./mapping.js";import{scimFilterToLdap as p}from"./filter.js";import{pagedSearch as m}from"./list.js";import{writePatch as b,patchToModifyRequest as f}from"./patch.js";import{scimNotFound as g,ScimError as k,scimInvalidValue as w,scimUniqueness as y,kindOf as A}from"./errors.js";import{extractLdapCode as R}from"../../lib/ldapCodes.js";class ScimUsers{ldap;options;logger;baseResolver;hookBus;mapping;rdnAttribute;objectClass;idAttribute;maxResults;maxScanned;scimPrefix;lockAttribute;lockValue;constructor(t){this.ldap=t.ldap,this.options=t.options,this.logger=t.logger,this.baseResolver=t.baseResolver,this.hookBus=t.hookBus,this.rdnAttribute=this.options.scim_user_rdn_attribute,this.objectClass=this.options.scim_user_object_class,this.idAttribute=this.options.scim_id_attribute,this.maxResults=this.options.scim_max_results,this.maxScanned=this.options.scim_max_scanned,this.scimPrefix=this.options.scim_prefix;const e=o(this.options.scim_user_lock_attribute,this.options.scim_user_lock_value);this.lockAttribute=e.attribute,this.lockValue=e.value;const s=this.options.scim_user_mapping;this.mapping=a(n,s?c(s):void 0)}ctx(t){return{idAttribute:this.idAttribute,rdnAttribute:this.rdnAttribute,resourceType:"User",baseUrl:this.options.scim_base_url||(t?.protocol&&t.get?`${t.protocol}://${String(t.get("host")||"")}`:""),scimPrefix:this.scimPrefix,lockAttribute:this.lockAttribute,lockValue:this.lockValue}}ldapAttributes(){return h(this.mapping,[this.lockAttribute])}dnForId(e,s){const i=this.baseResolver.userBase(s);return`${this.rdnAttribute}=${t(e)},${i}`}async get(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===R(t))throw g(`User ${e} not found`);throw t}if(!i.searchEntries||0===i.searchEntries.length)throw g(`User ${e} not found`);return u(i.searchEntries[0],this.mapping,this.ctx(t))}async list(t,e){const s=this.baseResolver.userBase(t),i=Math.max(1,e.startIndex||1),o=Math.min(this.maxResults,Math.max(0,e.count??this.maxResults));let a,c=`(${this.objectClass.includes("inetOrgPerson")?"objectClass=inetOrgPerson":`objectClass=${this.objectClass[0]}`})`;if(e.filter){const t=p(e.filter,this.mapping,{lockAttribute:this.lockAttribute,lockValue:this.lockValue,supportsActive:!0});t.idEquals?a=t.idEquals:c=`(&${c}${t.ldapFilter})`}if(a)try{const e=await this.get(t,a);return{schemas:[r],totalResults:1,startIndex:1,itemsPerPage:1,Resources:[e]}}catch(t){if(t instanceof k&&404===t.statusCode)return{schemas:[r],totalResults:0,startIndex:1,itemsPerPage:0,Resources:[]};throw t}const{entries:n,totalResults:h}=await m({directory:this.ldap.forRequest(t),base:s,filter:c,attributes:this.ldapAttributes(),startIndex:i,count:o,maxScanned:this.maxScanned});e.sortBy,e.sortOrder;const l=n.map(e=>u(e,this.mapping,this.ctx(t)));return{schemas:[r],totalResults:h,startIndex:i,itemsPerPage:l.length,Resources:l}}async create(s,i){if(!i.userName)throw w("userName is required");l(i,this.mapping,this.ctx(s),this.objectClass);const[r]=await this.hookBus.chain("scimusercreate",[i,s]),{rdn:o,attributes:a}=l(r,this.mapping,this.ctx(s),this.objectClass);if(!o)throw w("userName is required");e(o,this.rdnAttribute),a[this.rdnAttribute]=o;const c=this.baseResolver.userBase(s),n=`${this.rdnAttribute}=${t(o)},${c}`;try{await this.ldap.forRequest(s).add(n,a)}catch(t){if(68===R(t))throw y(`User ${o} already exists`);if(void 0!==a[this.lockAttribute])throw this.lockSchemaError(t);throw t}const h=await this.get(s,o);return this.hookBus.notify("scimusercreatedone",h),h}async replace(t,e,s){const i=this.dnForId(e,t);let r;try{r=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},i)}catch(t){if(32===R(t))throw g(`User ${e} not found`);throw t}if(!r.searchEntries?.length)throw g(`User ${e} not found`);const o=r.searchEntries[0],a=(await this.hookBus.chain("scimuserupdate",[e,s,t]))[1],{attributes:c,active:n}=l(a,this.mapping,this.ctx(t),this.objectClass),h={replace:{},delete:[]},u=new Set(["objectClass","entryUUID","createTimestamp","modifyTimestamp",this.rdnAttribute,this.lockAttribute]),hasAttrValue=t=>null!=t&&("string"==typeof t?t.length>0:!Array.isArray(t)||t.length>0&&t.some(t=>null!=t));for(const t of this.ldapAttributes())u.has(t)||(null!=c[t]?h.replace[t]=c[t]:hasAttrValue(o[t])&&h.delete.push(t));if(!1===n?h.replace[this.lockAttribute]=this.lockValue:!0===n&&d(o,{lockAttribute:this.lockAttribute,lockValue:this.lockValue})&&h.delete.push(this.lockAttribute),0===Object.keys(h.replace||{}).length&&delete h.replace,0===h.delete.length&&delete h.delete,h.replace||h.delete)try{await this.ldap.forRequest(t).modify(i,h)}catch(t){if(void 0!==n)throw this.lockSchemaError(t);if(16!==R(t))throw t;throw new k(409,`${i} changed while this PUT was being applied; nothing was written. Retry the request.`)}const p=await this.get(t,e);return this.hookBus.notify("scimuserupdatedone",e,p),p}async patch(t,e,s){const i=this.dnForId(e,t);await b(i,async()=>{const r=await this.currentEntry(t,e),o=await f(s,{mapping:this.mapping,lockAttribute:this.lockAttribute,lockValue:this.lockValue,supportsActive:!0,current:r});try{await this.ldap.forRequest(t).modify(i,o,{permissive:!0})}catch(t){if(function(t,e){const s=e.toLowerCase(),i=t.delete;return[...Object.keys(t.replace||{}),...Object.keys(t.add||{}),...Array.isArray(i)?i:Object.keys(i||{})].some(t=>t.toLowerCase()===s)}(o,this.lockAttribute))throw this.lockSchemaError(t);throw t}});const r=await this.get(t,e);return this.hookBus.notify("scimuserupdatedone",e,r),r}lockSchemaError(t){const e=R(t);if(17!==e&&65!==e)return t;const s=t instanceof Error?t.message:String(t);return w(`The directory rejected '${this.lockAttribute}', the attribute backing 'active': ${s}. Point --scim-user-lock-attribute and --scim-user-lock-value at an attribute this directory defines (nsAccountLock / TRUE on 389-ds); the default needs the ppolicy overlay.`)}async currentEntry(t,e){const s=this.dnForId(e,t);let i;try{i=await this.ldap.forRequest(t).search({paged:!1,scope:"base",attributes:this.ldapAttributes()},s)}catch(t){if(32===R(t))throw g(`User ${e} not found`);throw t}if(!i.searchEntries?.length)throw g(`User ${e} not found`);return i.searchEntries[0]}async delete(t,e){await this.get(t,e);const[s]=await this.hookBus.chain("scimuserdelete",[e,t]),i=this.dnForId(s,t);await this.ldap.forRequest(t).delete(i),this.hookBus.notify("scimuserdeletedone",s)}async resolveRef(e,r){if("string"!=typeof r)throw w(`A member value must be a string, got ${A(r)}`);if(!r)return;const o=this.baseResolver.userBase(e),a=r.includes("=")&&r.includes(",");if(a&&!s(r,o))return;const c=a?r:`${this.rdnAttribute}=${t(r)},${o}`;try{const t=await this.ldap.forRequest(e).search({paged:!1,scope:"base",attributes:["dn"]},c);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==R(t))throw t}if(!a)try{const t=await this.ldap.forRequest(e).search({filter:`(${this.rdnAttribute}=${i(r)})`,scope:"sub",paged:!1,attributes:["dn"]},o);if(t.searchEntries&&t.searchEntries.length>0)return t.searchEntries[0].dn}catch(t){if(32!==R(t))throw t}}get userMapping(){return this.mapping}}export{ScimUsers};
@@ -1 +1 @@
1
- import t from"fs";import{resolve as s,join as e,relative as n,sep as r,isAbsolute as i}from"path";import{validateHeaderValue as o}from"http";import c from"express";import a from"../abstract/plugin.js";import{STATIC_CSP as h,STATIC_FRAME_OPTIONS as f}from"../config/args.js";import{notFound as m,serverError as u}from"../lib/expressFormatedResponses.js";import{staticName as p,configuredSchemaFiles as d,schemaUrl as l,loadSchemaFileAsync as g}from"../lib/schemaFile.js";const headerValue=(t,s,e,n)=>{if(!1===e)return;if(null!=e&&"string"!=typeof e)throw new Error(`${t}: a text is expected, or false for none`);const r=("string"==typeof e?e:"").replace(/\s+/g," ").trim();if(!r)return n;if("off"!==r.toLowerCase()){try{o(s,r)}catch{throw new Error(`${t}: "${r}" cannot be sent as ${s}`)}return r}};class Static extends a{constructor(){super(...arguments),this.name="static",this.roles=["api","configurable"],this.configured=new Map}api(s){const e=this.config.static_path;if(!e)throw new Error("--static-path is not defined");try{if(!t.statSync(e).isDirectory())throw new Error(`${e} isn't a directory`);t.accessSync(e,t.constants.R_OK)}catch(t){throw new Error(`Bad directory ${e}: ${t}`)}const n=this.config.static_name;if(null!=n&&"string"!=typeof n)throw new Error("--static-name: a text is expected, such as static");const r=p(this.config);if(r.split("/").some(t=>!t||/^\.+$/.test(t)||/[^\x21-\x7e]|[:*?+!()[\]{}\\#%"<>|^`]/.test(t)))throw new Error(`--static-name: "${r}" is not a path to serve under, such as static`);const i=`/${r}`,o=this.schemaServer(e),a=headerValue("--static-csp","Content-Security-Policy",this.config.static_csp,h),m=headerValue("--static-frame-options","X-Frame-Options",this.config.static_frame_options,f);s.use(i,(t,s,e)=>{a&&s.setHeader("Content-Security-Policy",a),m&&s.setHeader("X-Frame-Options",m),e()}),s.get(`${i}/schemas/:name`,(t,s,e)=>o([t.params.name],t,s,e)),s.get(`${i}/schemas/:dir/:name`,(t,s,e)=>o([t.params.dir,t.params.name],t,s,e)),s.get(`${i}/schemas/*path`,(t,s,e)=>o(t.params.path,t,s,e)),s.use(i,c.static(e))}schemaServer(t){const n=this.configured=this.configuredSchemas(t),r=s(e(t,"schemas"));return(t,i,o,c)=>{const a=n.get((t=>{try{return decodeURIComponent(t)}catch{return t}})(i.path));if(a)return this.sendSchema(a,o);if(!t[t.length-1].endsWith(".json"))return c();if(!t.every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t)))return void o.status(400).send("Invalid schema name");const h=s(e(r,...t));h.startsWith(r+"/")?this.sendSchema(h,o):o.status(403).send("Access denied")}}configuredSchemas(t){const n=new Map;for(const r of d(this.config)){const i=l(this.config,r);if(!i)continue;const o=e(t,i.substring(i.indexOf("/schemas/")));if(s(o)===s(r))continue;const c=n.get(i);c&&s(c)!==s(r)?this.logger.warn(`${r} is not served: ${c} already has its URL ${i}`):n.set(i,r)}return n}sendSchema(t,s){g(t,{config:this.config}).then(t=>s.type("json").send(JSON.stringify(t)),t=>{if(t.code)return m(s,"Schema not found");u(s,t)})}urlOf(e){const o=s(e);let c;try{if(!t.statSync(o).isFile())return;c=t.realpathSync(o)}catch{return}const same=e=>{if(s(e)===o)return!0;try{return t.realpathSync(e)===c}catch{return!1}},a=this.servedPath(),url=t=>`${a}/${t.map(encodeURIComponent).join("/")}`;for(const[t,s]of this.configured)if(same(s)){const s=t.slice(a.length+1).split("/");if(s.some(t=>["",".",".."].includes(t)))continue;return url(s)}const h=this.config.static_path;if(!h)return;let f;try{f=t.realpathSync(h)}catch{return}const m=[n(s(h),o),n(f,c)].find(t=>!!t&&".."!==t&&!t.startsWith(`..${r}`)&&!i(t));if(!m)return;const u=m.split(r);return u.some(t=>t.startsWith("."))||"schemas"===u[0].toLowerCase()&&"schemas"!==u[0]&&m.endsWith(".json")||"schemas"===u[0]&&m.endsWith(".json")&&!u.slice(1).every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t))||this.configured.has(`${a}/${u.join("/")}`)?void 0:url(u)}schemaUrlOf(o){const realOf=s=>{try{return t.statSync(s).isFile()?t.realpathSync(s):void 0}catch{return}},c=realOf(o);if(!c)return;const a=this.servedPath(),url=t=>`${a}/${t.map(encodeURIComponent).join("/")}`;for(const[t,s]of this.configured)if(realOf(s)===c){const s=t.slice(a.length+1).split("/");if(s.some(t=>["",".",".."].includes(t)))continue;return url(s)}const h=this.config.static_path;if(!h)return;const f=e(h,"schemas"),m=[];for(const t of[s(o),c]){const s=t.split(r);s.forEach((t,e)=>{"schemas"===t&&m.push(s.slice(e+1))})}try{const s=n(t.realpathSync(f),c);!s||".."===s||s.startsWith(`..${r}`)||i(s)||m.push(s.split(r))}catch{}for(const t of m){if(!t.length||!t[t.length-1].endsWith(".json")||!t.every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t)))continue;const s=["schemas",...t];if(!this.configured.has(`${a}/${s.join("/")}`)&&realOf(e(f,...t))===c)return url(s)}}servedPath(){return`/${p(this.config)}`}getConfigApiData(){const t=this.servedPath();return{enabled:!0,staticPath:t,endpoints:{schema:`${t}/schemas/:name`,schemaInSubdir:`${t}/schemas/:dir/:name`,files:`${t}/*`}}}}export{Static as default};
1
+ import t from"fs";import{resolve as s,join as e,relative as n,sep as r,isAbsolute as i}from"path";import{validateHeaderValue as o}from"http";import c from"express";import a from"../abstract/plugin.js";import"../config/args.js";import{notFound as h,serverError as f}from"../lib/expressFormatedResponses.js";import{staticName as m,configuredSchemaFiles as p,schemaUrl as u,loadSchemaFileAsync as d}from"../lib/schemaFile.js";import{staticOptions as l,STATIC_CSP as g,STATIC_FRAME_OPTIONS as $}from"./static.options.js";import{defineRoute as y}from"../lib/route.js";const headerValue=(t,s,e,n)=>{if(!1===e)return;if(null!=e&&"string"!=typeof e)throw new Error(`${t}: a text is expected, or false for none`);const r=("string"==typeof e?e:"").replace(/\s+/g," ").trim();if(!r)return n;if("off"!==r.toLowerCase()){try{o(s,r)}catch{throw new Error(`${t}: "${r}" cannot be sent as ${s}`)}return r}};class Static extends a{static optionDefinitions=l;name="static";roles=["api","configurable"];configured=new Map;settings;constructor(t){super(t),this.settings=this.options(l)}api(s){const e=this.settings.static_path;try{if(!t.statSync(e).isDirectory())throw new Error(`${e} isn't a directory`);t.accessSync(e,t.constants.R_OK)}catch(t){throw new Error(`Bad directory ${e}: ${t}`)}const n=m(this.config);if(n.split("/").some(t=>!t||/^\.+$/.test(t)||/[^\x21-\x7e]|[:*?+!()[\]{}\\#%"<>|^`]/.test(t)))throw new Error(`--static-name: "${n}" is not a path to serve under, such as static`);const r=`/${n}`,i=this.schemaServer(e),o=headerValue("--static-csp","Content-Security-Policy",this.settings.static_csp,g),a=headerValue("--static-frame-options","X-Frame-Options",this.settings.static_frame_options,$);s.use(r,(t,s,e)=>{o&&s.setHeader("Content-Security-Policy",o),a&&s.setHeader("X-Frame-Options",a),e()}),y(s,{method:"get",path:`${r}/schemas/:name`,json:!1,handler:({req:t,res:s,next:e})=>i([t.params.name],t,s,e)}),y(s,{method:"get",path:`${r}/schemas/:dir/:name`,json:!1,handler:({req:t,res:s,next:e})=>i([t.params.dir,t.params.name],t,s,e)}),y(s,{method:"get",path:`${r}/schemas/*path`,json:!1,handler:({req:t,res:s,next:e})=>i(t.params.path,t,s,e)}),s.use(r,c.static(e))}schemaServer(t){const n=this.configured=this.configuredSchemas(t),r=s(e(t,"schemas"));return(t,i,o,c)=>{const a=n.get((t=>{try{return decodeURIComponent(t)}catch{return t}})(i.path));if(a)return this.sendSchema(a,o);if(!t[t.length-1].endsWith(".json"))return c();if(!t.every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t)))return void o.status(400).send("Invalid schema name");const h=s(e(r,...t));h.startsWith(r+"/")?this.sendSchema(h,o):o.status(403).send("Access denied")}}configuredSchemas(t){const n=new Map;for(const r of p(this.config)){const i=u(this.config,r);if(!i)continue;const o=e(t,i.substring(i.indexOf("/schemas/")));if(s(o)===s(r))continue;const c=n.get(i);c&&s(c)!==s(r)?this.logger.warn(`${r} is not served: ${c} already has its URL ${i}`):n.set(i,r)}return n}sendSchema(t,s){d(t,{config:this.config}).then(t=>s.type("json").send(JSON.stringify(t)),t=>{if(t.code)return h(s,"Schema not found");f(s,t)})}urlOf(e){const o=s(e);let c;try{if(!t.statSync(o).isFile())return;c=t.realpathSync(o)}catch{return}const same=e=>{if(s(e)===o)return!0;try{return t.realpathSync(e)===c}catch{return!1}},a=this.servedPath(),url=t=>`${a}/${t.map(encodeURIComponent).join("/")}`;for(const[t,s]of this.configured)if(same(s)){const s=t.slice(a.length+1).split("/");if(s.some(t=>["",".",".."].includes(t)))continue;return url(s)}const h=this.settings.static_path;if(!h)return;let f;try{f=t.realpathSync(h)}catch{return}const m=[n(s(h),o),n(f,c)].find(t=>!!t&&".."!==t&&!t.startsWith(`..${r}`)&&!i(t));if(!m)return;const p=m.split(r);return p.some(t=>t.startsWith("."))||"schemas"===p[0].toLowerCase()&&"schemas"!==p[0]&&m.endsWith(".json")||"schemas"===p[0]&&m.endsWith(".json")&&!p.slice(1).every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t))||this.configured.has(`${a}/${p.join("/")}`)?void 0:url(p)}schemaUrlOf(o){const realOf=s=>{try{return t.statSync(s).isFile()?t.realpathSync(s):void 0}catch{return}},c=realOf(o);if(!c)return;const a=this.servedPath(),url=t=>`${a}/${t.map(encodeURIComponent).join("/")}`;for(const[t,s]of this.configured)if(realOf(s)===c){const s=t.slice(a.length+1).split("/");if(s.some(t=>["",".",".."].includes(t)))continue;return url(s)}const h=this.settings.static_path;if(!h)return;const f=e(h,"schemas"),m=[];for(const t of[s(o),c]){const s=t.split(r);s.forEach((t,e)=>{"schemas"===t&&m.push(s.slice(e+1))})}try{const s=n(t.realpathSync(f),c);!s||".."===s||s.startsWith(`..${r}`)||i(s)||m.push(s.split(r))}catch{}for(const t of m){if(!t.length||!t[t.length-1].endsWith(".json")||!t.every(t=>/^[\w-]+(\.[\w-]+)*$/.test(t)))continue;const s=["schemas",...t];if(!this.configured.has(`${a}/${s.join("/")}`)&&realOf(e(f,...t))===c)return url(s)}}servedPath(){return`/${m(this.config)}`}getConfigApiData(){const t=this.servedPath();return{enabled:!0,staticPath:t,endpoints:{schema:`${t}/schemas/:name`,schemaInSubdir:`${t}/schemas/:dir/:name`,files:`${t}/*`}}}}export{Static as default};
@@ -0,0 +1 @@
1
+ import{join as t,dirname as s}from"path";import{fileURLToPath as i}from"url";import{defineOptions as a,opt as r,readOptions as n}from"../config/options.js";const e="frame-ancestors 'self'",o="SAMEORIGIN",f=t(s(i(import.meta.url)),"..","..","static"),_=a({static_path:r.string({env:"DM_STATIC_PATH",default:f}),static_name:r.string({env:"DM_STATIC_NAME",default:"static"}),static_csp:r.stringOrFalse({env:"DM_STATIC_CSP",default:e}),static_frame_options:r.stringOrFalse({env:"DM_STATIC_FRAME_OPTIONS",default:o})});function staticOptionsOf(t){return n(_,t)}export{e as STATIC_CSP,o as STATIC_FRAME_OPTIONS,_ as staticOptions,staticOptionsOf};
@@ -1 +1 @@
1
- import{createHash as t}from"node:crypto";import{promises as e,constants as a}from"node:fs";import{join as r}from"node:path";import i from"../../lib/storage/store.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t);class FileStore extends i{constructor(t,e,a){super(t),this.name="storage/file",this.dir=e,this.abandonedAfter=1e3*Math.max(a,60)}async ready(){await e.mkdir(this.dir,{recursive:!0,mode:448})}async open(){await this.ready()}async health(){if(!(await e.stat(this.dir)).isDirectory())throw new Error(`${this.dir} is not a directory`);return await e.access(this.dir,a.W_OK|a.X_OK),"ok"}path(e){return r(this.dir,t("sha256").update(e).digest("hex"))}static encode(t,e){return`${e.deadline} ${FileStore.readable(t)}\n${e.value}\n`}static decode(t){const e=t.indexOf("\n");if(e<0)return null;const a=parseInt(t.slice(0,e).split(" ")[0],10);return Number.isFinite(a)?{deadline:a,value:t.slice(e+1).replace(/\n$/,"")}:null}async load(t){try{return FileStore.decode(await e.readFile(this.path(t),"utf8"))}catch(t){return"ENOENT"!==t?.code&&this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,a){await this.ready();const r=this.path(t),i=`${r}.${process.pid}.tmp`;try{await e.writeFile(i,FileStore.encode(t,a),{mode:384}),await e.rename(i,r)}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),await e.unlink(i).catch(()=>{}),t}}async drop(t){await e.unlink(this.path(t)).catch(t=>{"ENOENT"!==t?.code&&this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)})}async sweep(){const t=Date.now();let a,i=0;try{a=await e.readdir(this.dir)}catch{return 0}for(const n of a){const a=r(this.dir,n);if(n.endsWith(".tmp")){await e.stat(a).then(e=>t-e.mtimeMs).catch(()=>0)>this.abandonedAfter&&(await e.unlink(a).catch(()=>{}),i++);continue}let s=null;try{s=FileStore.decode(await e.readFile(a,"utf8"))}catch{continue}null!==s&&s.deadline>t||(await e.unlink(a).catch(()=>{}),i++)}return i}}export{FileStore as default};
1
+ import{createHash as t}from"node:crypto";import{promises as e,constants as a}from"node:fs";import{join as r}from"node:path";import i from"../../lib/storage/store.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t);class FileStore extends i{name="storage/file";dir;abandonedAfter;constructor(t,e,a){super(t),this.dir=e,this.abandonedAfter=1e3*Math.max(a,60)}async ready(){await e.mkdir(this.dir,{recursive:!0,mode:448})}async open(){await this.ready()}async health(){if(!(await e.stat(this.dir)).isDirectory())throw new Error(`${this.dir} is not a directory`);return await e.access(this.dir,a.W_OK|a.X_OK),"ok"}path(e){return r(this.dir,t("sha256").update(e).digest("hex"))}static encode(t,e){return`${e.deadline} ${FileStore.readable(t)}\n${e.value}\n`}static decode(t){const e=t.indexOf("\n");if(e<0)return null;const a=parseInt(t.slice(0,e).split(" ")[0],10);return Number.isFinite(a)?{deadline:a,value:t.slice(e+1).replace(/\n$/,"")}:null}async load(t){try{return FileStore.decode(await e.readFile(this.path(t),"utf8"))}catch(t){return"ENOENT"!==t?.code&&this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,a){await this.ready();const r=this.path(t),i=`${r}.${process.pid}.tmp`;try{await e.writeFile(i,FileStore.encode(t,a),{mode:384}),await e.rename(i,r)}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),await e.unlink(i).catch(()=>{}),t}}async drop(t){await e.unlink(this.path(t)).catch(t=>{"ENOENT"!==t?.code&&this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)})}async sweep(){const t=Date.now();let a,i=0;try{a=await e.readdir(this.dir)}catch{return 0}for(const n of a){const a=r(this.dir,n);if(n.endsWith(".tmp")){await e.stat(a).then(e=>t-e.mtimeMs).catch(()=>0)>this.abandonedAfter&&(await e.unlink(a).catch(()=>{}),i++);continue}let s=null;try{s=FileStore.decode(await e.readFile(a,"utf8"))}catch{continue}null!==s&&s.deadline>t||(await e.unlink(a).catch(()=>{}),i++)}return i}}export{FileStore as default};
@@ -1 +1 @@
1
- import{createHash as e}from"node:crypto";import s from"../../lib/storage/store.js";const errorText=e=>e instanceof Error?e.message:JSON.stringify(e);class LdapStore extends s{constructor(e,s,t){super(e.logger),this.name="storage/ldap",this.server=e,this.base=s,this.objectClass=t}dn(s){return`cn=${e("sha256").update(s).digest("hex")},${this.base}`}async load(e){try{const s=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:["description"]},this.dn(e)),t=s.searchEntries?.[0];return t?LdapStore.decodeRecord(t.description):null}catch(e){return 32!==e?.code&&this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(e)}`),null}}async save(e,s){const t=this.dn(e),r=LdapStore.encodeRecord(e,s);try{await this.server.ldap.system.add(t,{objectClass:["top",this.objectClass],cn:t.slice(3,t.indexOf(",")),description:r})}catch(e){if(68!==e?.code)throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(e)}`),e;await this.server.ldap.system.modify(t,{replace:{description:r}})}}async drop(e){await this.server.ldap.system.delete(this.dn(e)).catch(e=>{32!==e?.code&&this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(e)}`)})}async sweep(){const e=Date.now();let s=0;const t=await this.server.ldap.system.search({paged:!1,scope:"one",filter:`(objectClass=${this.objectClass})`,attributes:["description"]},this.base);for(const r of t.searchEntries||[]){const t=LdapStore.decodeRecord(r.description);null!==t&&t.deadline>e||(await this.server.ldap.system.delete(String(r.dn)).catch(()=>{}),s++)}return s}}export{LdapStore as default};
1
+ import{createHash as e}from"node:crypto";import s from"../../lib/storage/store.js";const errorText=e=>e instanceof Error?e.message:JSON.stringify(e);class LdapStore extends s{name="storage/ldap";server;base;objectClass;constructor(e,s,t){super(e.logger),this.server=e,this.base=s,this.objectClass=t}dn(s){return`cn=${e("sha256").update(s).digest("hex")},${this.base}`}async load(e){try{const s=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:["description"]},this.dn(e)),t=s.searchEntries?.[0];return t?LdapStore.decodeRecord(t.description):null}catch(e){return 32!==e?.code&&this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(e)}`),null}}async save(e,s){const t=this.dn(e),r=LdapStore.encodeRecord(e,s);try{await this.server.ldap.system.add(t,{objectClass:["top",this.objectClass],cn:t.slice(3,t.indexOf(",")),description:r})}catch(e){if(68!==e?.code)throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(e)}`),e;await this.server.ldap.system.modify(t,{replace:{description:r}})}}async drop(e){await this.server.ldap.system.delete(this.dn(e)).catch(e=>{32!==e?.code&&this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(e)}`)})}async sweep(){const e=Date.now();let s=0;const t=await this.server.ldap.system.search({paged:!1,scope:"one",filter:`(objectClass=${this.objectClass})`,attributes:["description"]},this.base);for(const r of t.searchEntries||[]){const t=LdapStore.decodeRecord(r.description);null!==t&&t.deadline>e||(await this.server.ldap.system.delete(String(r.dn)).catch(()=>{}),s++)}return s}}export{LdapStore as default};
@@ -1 +1 @@
1
- import{AsyncResource as t}from"node:async_hooks";import e from"../../lib/storage/store.js";import{PROBE_TIMEOUT_MS as n}from"../../lib/health.js";import{outsideAnyRequest as a}from"../../lib/requestContext.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t),quote=t=>`"${t.replace(/"/g,'""')}"`;class PostgresStore extends e{constructor(t,e,n){super(t),this.name="storage/postgres",this.url=e,this.table=n,this.quoted=n.split(".").map(quote).join(".")}async driver(){return import("pg")}async open(){await this.pool()}async health(){const t=new((await this.driver()).Client)({connectionString:this.url,connectionTimeoutMillis:n,query_timeout:n});t.on("error",()=>{});try{return await t.connect(),await t.query("SELECT 1"),"ok"}finally{await t.end().catch(()=>{})}}async close(){await super.close();const t=this.connection;this.connection=void 0,t&&await((await t.catch(()=>{}))?.end())}pool(){return this.closed?Promise.reject(new Error(`${this.name}: closed`)):(this.connection??(this.connection=a(()=>this.connect()).catch(t=>{throw this.connection=void 0,t})),this.connection)}async connect(){let e;try{e=await this.driver()}catch(t){throw new Error(`${this.name}: requires the optional dependency "pg", which is not installed. Install it to keep the records in PostgreSQL, or choose another --storage-backend. (${errorText(t)})`)}const n=new e.Pool({connectionString:this.url,connectionTimeoutMillis:1e4,allowExitOnIdle:!0});n.on("error",t.bind(t=>this.logger.warn(`${this.name}: connection lost: ${errorText(t)}`)));try{await this.ensureTable(n)}catch(t){throw await n.end().catch(()=>{}),new Error(`${this.name}: cannot use table ${this.table}: ${errorText(t)}`)}return n}async ensureTable(t){const e=await t.query("SELECT to_regclass($1) AS t",[this.quoted]);if(e.rows[0]?.t)return;const n=this.quoted,a=quote(`${this.table.split(".").pop()}_expires_at`);await t.query(`CREATE TABLE IF NOT EXISTS ${n} (\n namespace text NOT NULL,\n key text NOT NULL,\n value text NOT NULL,\n expires_at timestamptz NOT NULL,\n PRIMARY KEY (namespace, key)\n )`),await t.query(`CREATE INDEX IF NOT EXISTS ${a} ON ${n} (expires_at)`),this.logger.info(`${this.name}: created table ${this.table}`)}static parts(t){const e=t.indexOf("\0"),n=t.slice(e+1);return n.includes("\0")?null:[t.slice(0,e),n]}async load(t){const e=PostgresStore.parts(t);if(!e)return null;try{const t=await this.pool(),n=(await t.query(`SELECT value, expires_at FROM ${this.quoted}\n WHERE namespace = $1 AND key = $2`,e)).rows[0];return n?{value:n.value,deadline:n.expires_at.getTime()}:null}catch(t){return this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,e){const n=PostgresStore.parts(t);if(!n)throw new Error(`${this.name}: cannot keep a key holding a NUL`);try{const t=await this.pool();await t.query(`INSERT INTO ${this.quoted}\n (namespace, key, value, expires_at) VALUES ($1, $2, $3, $4)\n ON CONFLICT (namespace, key) DO UPDATE\n SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,[...n,e.value,new Date(e.deadline)])}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),t}}async drop(t){const e=PostgresStore.parts(t);if(e)try{const t=await this.pool();await t.query(`DELETE FROM ${this.quoted}\n WHERE namespace = $1 AND key = $2`,e)}catch(t){this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)}}async sweep(){const t=await this.pool();return(await t.query(`DELETE FROM ${this.quoted} WHERE expires_at <= $1`,[new Date])).rowCount??0}}export{PostgresStore as default};
1
+ import{AsyncResource as t}from"node:async_hooks";import e from"../../lib/storage/store.js";import{PROBE_TIMEOUT_MS as n}from"../../lib/health.js";import{outsideAnyRequest as a}from"../../lib/requestContext.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t),quote=t=>`"${t.replace(/"/g,'""')}"`;class PostgresStore extends e{name="storage/postgres";url;table;quoted;connection;constructor(t,e,n){super(t),this.url=e,this.table=n,this.quoted=n.split(".").map(quote).join(".")}async driver(){return import("pg")}async open(){await this.pool()}async health(){const t=new((await this.driver()).Client)({connectionString:this.url,connectionTimeoutMillis:n,query_timeout:n});t.on("error",()=>{});try{return await t.connect(),await t.query("SELECT 1"),"ok"}finally{await t.end().catch(()=>{})}}async close(){await super.close();const t=this.connection;this.connection=void 0,t&&await((await t.catch(()=>{}))?.end())}pool(){return this.closed?Promise.reject(new Error(`${this.name}: closed`)):(this.connection??=a(()=>this.connect()).catch(t=>{throw this.connection=void 0,t}),this.connection)}async connect(){let e;try{e=await this.driver()}catch(t){throw new Error(`${this.name}: requires the optional dependency "pg", which is not installed. Install it to keep the records in PostgreSQL, or choose another --storage-backend. (${errorText(t)})`)}const n=new e.Pool({connectionString:this.url,connectionTimeoutMillis:1e4,allowExitOnIdle:!0});n.on("error",t.bind(t=>this.logger.warn(`${this.name}: connection lost: ${errorText(t)}`)));try{await this.ensureTable(n)}catch(t){throw await n.end().catch(()=>{}),new Error(`${this.name}: cannot use table ${this.table}: ${errorText(t)}`)}return n}async ensureTable(t){const e=await t.query("SELECT to_regclass($1) AS t",[this.quoted]);if(e.rows[0]?.t)return;const n=this.quoted,a=quote(`${this.table.split(".").pop()}_expires_at`);await t.query(`CREATE TABLE IF NOT EXISTS ${n} (\n namespace text NOT NULL,\n key text NOT NULL,\n value text NOT NULL,\n expires_at timestamptz NOT NULL,\n PRIMARY KEY (namespace, key)\n )`),await t.query(`CREATE INDEX IF NOT EXISTS ${a} ON ${n} (expires_at)`),this.logger.info(`${this.name}: created table ${this.table}`)}static parts(t){const e=t.indexOf("\0"),n=t.slice(e+1);return n.includes("\0")?null:[t.slice(0,e),n]}async load(t){const e=PostgresStore.parts(t);if(!e)return null;try{const t=await this.pool(),n=(await t.query(`SELECT value, expires_at FROM ${this.quoted}\n WHERE namespace = $1 AND key = $2`,e)).rows[0];return n?{value:n.value,deadline:n.expires_at.getTime()}:null}catch(t){return this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,e){const n=PostgresStore.parts(t);if(!n)throw new Error(`${this.name}: cannot keep a key holding a NUL`);try{const t=await this.pool();await t.query(`INSERT INTO ${this.quoted}\n (namespace, key, value, expires_at) VALUES ($1, $2, $3, $4)\n ON CONFLICT (namespace, key) DO UPDATE\n SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,[...n,e.value,new Date(e.deadline)])}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),t}}async drop(t){const e=PostgresStore.parts(t);if(e)try{const t=await this.pool();await t.query(`DELETE FROM ${this.quoted}\n WHERE namespace = $1 AND key = $2`,e)}catch(t){this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)}}async sweep(){const t=await this.pool();return(await t.query(`DELETE FROM ${this.quoted} WHERE expires_at <= $1`,[new Date])).rowCount??0}}export{PostgresStore as default};
@@ -1 +1 @@
1
- import{AsyncResource as t}from"node:async_hooks";import e from"../../lib/storage/store.js";import{outsideAnyRequest as n}from"../../lib/requestContext.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t);class ValkeyStore extends e{constructor(t,e,n){super(t),this.name="storage/valkey",this.down=!1,this.url=e,this.prefix=n}async driver(){return import("iovalkey")}async open(){await this.client()}async health(){return await(await this.client()).ping(),"ok"}async close(){await super.close();const t=this.connection;this.connection=void 0;const e=await(t?.catch(()=>{}));e&&await e.quit().catch(()=>e.disconnect())}client(){return this.closed?Promise.reject(new Error(`${this.name}: closed`)):(this.connection??(this.connection=n(()=>this.connect()).catch(t=>{throw this.connection=void 0,t})),this.connection)}async connect(){let e;try{e=await this.driver()}catch(t){throw new Error(`${this.name}: requires the optional dependency "iovalkey", which is not installed. Install it to keep the records in Valkey, or choose another --storage-backend. (${errorText(t)})`)}const n=new e.Valkey(this.url,{lazyConnect:!0,enableOfflineQueue:!1,maxRetriesPerRequest:1,commandTimeout:5e3});let o,i=!1;n.on("error",t.bind(t=>{o=t,i&&!this.down&&(this.down=!0,this.logger.warn(`${this.name}: connection lost: ${errorText(t)}`))})),n.on("ready",t.bind(()=>{this.down&&this.logger.info(`${this.name}: connection back`),this.down=!1}));try{await n.connect(),i=!0}catch(t){throw n.disconnect(),new Error(`${this.name}: cannot connect: ${errorText(o??t)}`)}return n}key(t){return`${this.prefix}${t}`}async load(t){try{const e=await(await this.client()).get(this.key(t));return null===e?null:ValkeyStore.decodeRecord(e)}catch(t){return this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,e){const n=Math.ceil(e.deadline-Date.now());try{const o=await this.client();if(n<=0)return void await o.del(this.key(t));await o.set(this.key(t),ValkeyStore.encodeRecord(t,e),"PX",n)}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),t}}async drop(t){try{await(await this.client()).del(this.key(t))}catch(t){this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)}}async sweep(){return 0}}export{ValkeyStore as default};
1
+ import{AsyncResource as t}from"node:async_hooks";import e from"../../lib/storage/store.js";import{outsideAnyRequest as n}from"../../lib/requestContext.js";const errorText=t=>t instanceof Error?t.message:JSON.stringify(t);class ValkeyStore extends e{name="storage/valkey";url;prefix;connection;down=!1;constructor(t,e,n){super(t),this.url=e,this.prefix=n}async driver(){return import("iovalkey")}async open(){await this.client()}async health(){return await(await this.client()).ping(),"ok"}async close(){await super.close();const t=this.connection;this.connection=void 0;const e=await(t?.catch(()=>{}));e&&await e.quit().catch(()=>e.disconnect())}client(){return this.closed?Promise.reject(new Error(`${this.name}: closed`)):(this.connection??=n(()=>this.connect()).catch(t=>{throw this.connection=void 0,t}),this.connection)}async connect(){let e;try{e=await this.driver()}catch(t){throw new Error(`${this.name}: requires the optional dependency "iovalkey", which is not installed. Install it to keep the records in Valkey, or choose another --storage-backend. (${errorText(t)})`)}const n=new e.Valkey(this.url,{lazyConnect:!0,enableOfflineQueue:!1,maxRetriesPerRequest:1,commandTimeout:5e3});let o,i=!1;n.on("error",t.bind(t=>{o=t,i&&!this.down&&(this.down=!0,this.logger.warn(`${this.name}: connection lost: ${errorText(t)}`))})),n.on("ready",t.bind(()=>{this.down&&this.logger.info(`${this.name}: connection back`),this.down=!1}));try{await n.connect(),i=!0}catch(t){throw n.disconnect(),new Error(`${this.name}: cannot connect: ${errorText(o??t)}`)}return n}key(t){return`${this.prefix}${t}`}async load(t){try{const e=await(await this.client()).get(this.key(t));return null===e?null:ValkeyStore.decodeRecord(e)}catch(t){return this.logger.warn(`${this.name}: cannot read a record, the answer is a guess: ${errorText(t)}`),null}}async save(t,e){const n=Math.ceil(e.deadline-Date.now());try{const o=await this.client();if(n<=0)return void await o.del(this.key(t));await o.set(this.key(t),ValkeyStore.encodeRecord(t,e),"PX",n)}catch(t){throw this.logger.warn(`${this.name}: cannot write a record: ${errorText(t)}`),t}}async drop(t){try{await(await this.client()).del(this.key(t))}catch(t){this.logger.warn(`${this.name}: cannot drop a record, it keeps counting: ${errorText(t)}`)}}async sweep(){return 0}}export{ValkeyStore as default};
@@ -1 +1 @@
1
- import e from"../abstract/plugin.js";import r from"./storage/file.js";import s from"./storage/ldap.js";import t from"./storage/postgres.js";import o from"./storage/valkey.js";let a;const i="ldap, file, postgres, valkey";class Storage extends e{constructor(e){if(super(e),this.name="storage",this.degraded=!1,this.probes=0,a)throw new Error(`storage: an instance is already loaded, keeping its records in ${a.store.name}. A second one would put the same record in two places and leave registration order to decide which answers. Load one, and let its consumers share it.`);const n=e.config.storage_backend||"";switch(n){case"ldap":{const r=e.config.storage_ldap_base;if(!r)throw new Error("storage: the ldap backend needs --storage-ldap-base, the branch it writes its records to. It holds records and nothing else, so it belongs outside the branches the directory serves.");this.store=new s(e,r,e.config.storage_ldap_object_class||"applicationProcess");break}case"file":{const s=e.config.storage_file_directory;if(!s)throw new Error("storage: the file backend needs --storage-file-directory, the directory it writes its records to. It writes one file per record, so nothing else should be writing there.");this.store=new r(e.logger,s,e.config.storage_sweep_interval||600);break}case"postgres":{const r=e.config.storage_postgres_url;if(!r)throw new Error("storage: the postgres backend needs --storage-postgres-url, the database it writes its records to.");this.store=new t(e.logger,r,e.config.storage_postgres_table||"ldap_rest_storage");break}case"valkey":{const r=e.config.storage_valkey_url;if(!r)throw new Error("storage: the valkey backend needs --storage-valkey-url, the server it writes its records to.");this.store=new o(e.logger,r,e.config.storage_valkey_prefix??"");break}default:throw new Error(n?`storage: unknown backend "${n}". Known: ${i}.`:`storage: --storage-backend says which sub-plugin keeps the records. Known: ${i}.`)}a=this,this.logger.info(`storage: records kept by ${this.store.name}`),this.store.open().catch(e=>this.logger.error(e instanceof Error?e.message:String(e))),this.store.startSweeping(e.config.storage_sweep_interval||0)}async close(){await this.store.close()}async health(){const e=++this.probes;try{const r=await this.store.health();return e===this.probes&&(this.degraded&&this.logger.info(`${this.store.name}: answers again`),this.degraded=!1),r}catch(r){return e===this.probes&&(this.degraded||this.logger.warn(`${this.store.name}: does not answer: ${r instanceof Error?r.message:String(r)}`),this.degraded=!0),"degraded"}}static release(e){a===e&&(a=void 0)}}export{Storage as default};
1
+ import e from"../abstract/plugin.js";import r from"./storage/file.js";import s from"./storage/ldap.js";import t from"./storage/postgres.js";import o from"./storage/valkey.js";import{storageOptions as a}from"./storage.options.js";let i;const n="ldap, file, postgres, valkey";class Storage extends e{static optionDefinitions=a;name="storage";store;constructor(e){if(super(e),i)throw new Error(`storage: an instance is already loaded, keeping its records in ${i.store.name}. A second one would put the same record in two places and leave registration order to decide which answers. Load one, and let its consumers share it.`);const g=this.options(a),d=g.storage_backend;switch(d){case"ldap":{const r=g.storage_ldap_base;if(!r)throw new Error("storage: the ldap backend needs --storage-ldap-base, the branch it writes its records to. It holds records and nothing else, so it belongs outside the branches the directory serves.");this.store=new s(e,r,g.storage_ldap_object_class);break}case"file":{const s=g.storage_file_directory;if(!s)throw new Error("storage: the file backend needs --storage-file-directory, the directory it writes its records to. It writes one file per record, so nothing else should be writing there.");this.store=new r(e.logger,s,g.storage_sweep_interval);break}case"postgres":{const r=g.storage_postgres_url;if(!r)throw new Error("storage: the postgres backend needs --storage-postgres-url, the database it writes its records to.");this.store=new t(e.logger,r,g.storage_postgres_table);break}case"valkey":{const r=g.storage_valkey_url;if(!r)throw new Error("storage: the valkey backend needs --storage-valkey-url, the server it writes its records to.");this.store=new o(e.logger,r,g.storage_valkey_prefix);break}default:throw new Error(d?`storage: unknown backend "${d}". Known: ${n}.`:`storage: --storage-backend says which sub-plugin keeps the records. Known: ${n}.`)}i=this,this.logger.info(`storage: records kept by ${this.store.name}`),this.store.open().catch(e=>this.logger.error(e instanceof Error?e.message:String(e))),this.store.startSweeping(g.storage_sweep_interval)}async close(){await this.store.close()}degraded=!1;probes=0;async health(){const e=++this.probes;try{const r=await this.store.health();return e===this.probes&&(this.degraded&&this.logger.info(`${this.store.name}: answers again`),this.degraded=!1),r}catch(r){return e===this.probes&&(this.degraded||this.logger.warn(`${this.store.name}: does not answer: ${r instanceof Error?r.message:String(r)}`),this.degraded=!0),"degraded"}}static release(e){i===e&&(i=void 0)}}export{Storage as default};
@@ -0,0 +1 @@
1
+ import{defineOptions as e,opt as t,readOptions as _}from"../config/options.js";const s=e({storage_backend:t.string({env:"DM_STORAGE_BACKEND",default:""}),storage_sweep_interval:t.number({env:"DM_STORAGE_SWEEP_INTERVAL",default:600}),storage_ldap_base:t.string({env:"DM_STORAGE_LDAP_BASE",default:""}),storage_ldap_object_class:t.string({env:"DM_STORAGE_LDAP_OBJECT_CLASS",default:"applicationProcess"}),storage_file_directory:t.string({env:"DM_STORAGE_FILE_DIRECTORY",default:""}),storage_postgres_url:t.string({env:"DM_STORAGE_POSTGRES_URL",default:""}),storage_postgres_table:t.string({env:"DM_STORAGE_POSTGRES_TABLE",default:"ldap_rest_storage"}),storage_valkey_url:t.string({env:"DM_STORAGE_VALKEY_URL",default:""}),storage_valkey_prefix:t.string({env:"DM_STORAGE_VALKEY_PREFIX",default:"ldap-rest:",emptyIsValue:!0})});function storageOptionsOf(e){return _(s,e)}export{s as storageOptions,storageOptionsOf};
@@ -0,0 +1 @@
1
+ import{defineOptions as a,opt as t}from"../../config/options.js";const e=a({applicative_account_base:t.string({env:"DM_APPLICATIVE_ACCOUNT_BASE",default:""}),max_app_accounts:t.number({env:"DM_MAX_APP_ACCOUNTS",default:5}),app_accounts_user_attribute:t.string({env:"DM_APP_ACCOUNTS_USER_ATTRIBUTE",default:""}),applicative_account_attribute:t.array({env:"DM_APPLICATIVE_ACCOUNT_ATTRIBUTES",default:["objectClass","cn","sn","givenName","displayName","description"],plural:"applicative-account-attributes",kind:"identifiers"}),ldap_operational_attribute:t.array({env:"DM_LDAP_OPERATIONAL_ATTRIBUTES",default:["dn","controls","structuralObjectClass","entryUUID","entryDN","subschemaSubentry","modifyTimestamp","modifiersName","createTimestamp","creatorsName","userPassword"],plural:"ldap-operational-attributes",kind:"identifiers"})});export{e as appAccountsOptions};
@@ -1 +1 @@
1
- import{randomInt as t}from"crypto";import r from"../../abstract/plugin.js";import{wantJson as s}from"../../lib/expressFormatedResponses.js";import{HttpError as e,errorBody as i,AUTHZ_REFUSED as a}from"../../lib/errors.js";import{escapeLdapFilter as o,isDnInBranch as n,escapeDnValue as c,isAuthzRefusal as u}from"../../lib/utils.js";const answeredRefusal=(t,r)=>u(t)?(r.status(403).json(i(t,a)),!0):t instanceof e&&t.statusCode<500&&(r.status(t.statusCode).json(i(t,t.message)),!0);class AppAccountsApi extends r{constructor(t){if(super(t),this.name="appAccountsApi",this.roles=["api","configurable"],this.dependencies={appAccountsConsistency:"core/twake/appAccountsConsistency"},this.applicativeAccountBase=this.config.applicative_account_base,this.maxAppAccounts=this.config.max_app_accounts||5,this.mailAttr=this.config.mail_attribute||"mail",this.userAttr=this.config.app_accounts_user_attribute||this.mailAttr,!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}, max_app_accounts=${this.maxAppAccounts}, user_attribute=${this.userAttr}`)}api(t){const r=this.config.api_prefix||"/api";t.get(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.listAccounts(t,r)),t.post(`${r}/v1/users/:user/app-accounts`,(t,r)=>this.createAccount(t,r)),t.delete(`${r}/v1/users/:user/app-accounts/:uid`,(t,r)=>this.deleteAccount(t,r)),this.logger.info(`AppAccounts API registered at ${r}/v1/users/:user/app-accounts`)}async resolvePrincipal(t,r,s,e){let i=((await this.server.ldap.system.search({scope:"sub",filter:`(${this.userAttr}=${o(t)})`,paged:!1,...e?{attributes:e}:{}},this.config.ldap_base||"")).searchEntries||[]).filter(t=>!n(t.dn,this.applicativeAccountBase));const a=[];for(const t of i)await this.server.ldap.readableBy(String(t.dn),r)&&a.push(t);return i=a,0===i.length?(s.status(404).json({error:`User ${t} not found`}),null):i.length>1?(this.logger.error(`${this.name}: Ambiguous principal lookup for ${t}: ${i.length} entries share ${this.userAttr}`),s.status(409).json({error:`Multiple users share ${this.userAttr} ${t}`}),null):i[0]}async listAccounts(t,r){if(!s(t,r))return;const e=t.params.user;try{const s=await this.resolvePrincipal(e,t,r,[this.mailAttr]);if(!s)return;const i=s[this.mailAttr];if(!i)return void r.status(400).json({error:`User ${e} has no mail attribute`});const a=Array.isArray(i)?String(i[0]):String(i),n=await this.server.ldap.system.search({scope:"sub",filter:`(${this.mailAttr}=${o(a)})`,paged:!1},this.applicativeAccountBase),c=(n.searchEntries||[]).filter(t=>{const r=t.uid;return(Array.isArray(r)?String(r[0]):String(r)).toLowerCase()!==a.toLowerCase()}).map(t=>{const r=t.uid,s=Array.isArray(r)?String(r[0]):String(r),e=t.description,i=e?Array.isArray(e)?String(e[0]):String(e):void 0;return{uid:s,...i?{name:i}:{}}}).sort((t,r)=>t.uid.localeCompare(r.uid));r.json(c)}catch(t){this.logger.error(`${this.name}: Failed to list accounts for ${e}:`,t),r.status(500).json({error:"Internal server error"})}}async createAccount(t,r){if(!s(t,r))return;const e=t.params.user,{name:i}=t.body||{};try{const s=await this.resolvePrincipal(e,t,r);if(!s)return;const a=s[this.mailAttr];if(!a)return void r.status(400).json({error:`User ${e} has no mail attribute`});const n=Array.isArray(a)?String(a[0]):String(a),u=this.appUidPrefix(e),p=(await this.server.ldap.system.search({scope:"sub",filter:`(${this.mailAttr}=${o(n)})`,paged:!1},this.applicativeAccountBase)).searchEntries||[];if(p.filter(t=>{const r=t.uid;return(Array.isArray(r)?String(r[0]):String(r)).toLowerCase()!==n.toLowerCase()}).length>=this.maxAppAccounts)return void r.status(400).json({error:`Maximum number of accounts (${this.maxAppAccounts}) reached`});let l,h=0;do{if(l=`${u}_${this.generateAccountId()}`,h++,h>100)throw new Error("Failed to generate unique account ID after 100 attempts")}while(await this.uidExists(l));const d=this.generatePassword(),A={objectClass:["inetOrgPerson"],uid:l,userPassword:d},g=[...new Set([...this.config.applicative_account_attribute||[],this.mailAttr])].filter(t=>"objectClass"!==t&&"description"!==t);for(const t of g)if(s[t]){const r=s[t];Array.isArray(r)?A[t]=r.map(t=>Buffer.isBuffer(t)?t.toString():String(t)):A[t]=Buffer.isBuffer(r)?r.toString():String(r)}i&&(A.description=i);const f=`uid=${c(l)},${this.applicativeAccountBase}`;await this.server.ldap.add(f,A,t),this.logger.info(`${this.name}: Created applicative account ${f} for user ${e}`);const m=`uid=${c(n)},${this.applicativeAccountBase}`;try{await this.server.ldap.modify(m,{add:{userPassword:d}},t)}catch(s){this.logger.error(`${this.name}: Failed to add password to principal account ${m}:`,s);try{await this.server.ldap.delete(f,t)}catch(t){this.logger.error(`${this.name}: Failed to roll back applicative account ${f}:`,t)}return void r.status(500).json({error:"Failed to register the password on the principal account"})}r.json({uid:l,pwd:d,mail:n})}catch(t){if(answeredRefusal(t,r))return;this.logger.error(`${this.name}: Failed to create account for ${e}:`,t),r.status(500).json({error:"Internal server error"})}}appUidPrefix(t){return t.replace(/[^A-Za-z0-9_-]/g,"_")}async uidExists(t){return((await this.server.ldap.system.search({scope:"sub",filter:`(uid=${o(t)})`,paged:!1,attributes:["uid"]},this.applicativeAccountBase)).searchEntries||[]).length>0}async deleteAccount(t,r){if(!s(t,r))return;const i=t.params.user,a=t.params.uid;try{await this.server.ldap.authorize("ldapdeleterequest",[`uid=${c(a)},${this.applicativeAccountBase}`,t]);const s=await this.server.ldap.system.search({scope:"sub",filter:`(uid=${o(a)})`,paged:!1,attributes:["uid",this.mailAttr,"userPassword"]},this.applicativeAccountBase),n=s.searchEntries?.[0];if(!n)return this.logger.warn(`${this.name}: Account ${a} already deleted or not found`),void r.json({uid:a});const p=n[this.mailAttr],l=p?Array.isArray(p)?String(p[0]):String(p):null;let h=i;if(this.userAttr!==this.mailAttr){const s=await this.resolvePrincipal(i,t,r,[this.mailAttr]);if(!s)return;const e=s[this.mailAttr],a=e?Array.isArray(e)?String(e[0]):String(e):null;if(!a)return void r.status(400).json({error:`User ${i} has no mail attribute`});h=a}if(!l||l.toLowerCase()!==h.toLowerCase())return void r.status(403).json({error:`Account ${a} does not belong to user ${i}`});const d=n.userPassword;if(!d)return this.logger.error(`${this.name}: Account ${a} has no readable userPassword, refusing to delete: the bind DN needs read access on userPassword in the applicative branch`),void r.status(500).json({error:`Cannot revoke account ${a}: its password is not readable`});const A=`uid=${c(l)},${this.applicativeAccountBase}`;try{const r=Array.isArray(d)?d[0]:d;await this.server.ldap.modify(A,{delete:{userPassword:r}},t)}catch(t){const s=String(t.message??"");if(16!==t.code&&!/no ?such ?attribute/i.test(s))return u(t)||t instanceof e&&403===t.statusCode?void answeredRefusal(t,r):(this.logger.error(`${this.name}: Failed to delete password from principal account ${A}:`,t),void r.status(500).json({error:`Failed to revoke the password of account ${a}`}));this.logger.warn(`${this.name}: Password of ${a} was already absent from principal account ${A}, resuming deletion`)}const g=n.dn;await this.server.ldap.delete(g,t),this.logger.info(`${this.name}: Deleted applicative account ${g}`),r.json({uid:a})}catch(t){if(answeredRefusal(t,r))return;this.logger.error(`${this.name}: Failed to delete account ${a} for ${i}:`,t),r.status(500).json({error:"Internal server error"})}}generateAccountId(){return`c${t(0,1e8).toString().padStart(8,"0")}`}generatePassword(){const r="ABCDEFGHJKMNPQRSTUVWXYZ",s="abcdefghjkmnpqrstuvwxyz",e="23456789",i="@!#$%",a=[];for(let o=0;o<6;o++){const o=t(0,23),n=t(0,23),c=t(0,8),u=t(0,5),p=[r[o],s[n],e[c],i[u]];for(let r=p.length-1;r>0;r--){const s=t(0,r+1);[p[r],p[s]]=[p[s],p[r]]}a.push(p.join(""))}return a.join("-")}getConfigApiData(){const t=this.config.api_prefix||"/api";return{enabled:!0,base:this.applicativeAccountBase,maxAccounts:this.maxAppAccounts,mailAttribute:this.mailAttr,endpoints:{list:`${t}/v1/users/:user/app-accounts`,create:`${t}/v1/users/:user/app-accounts`,delete:`${t}/v1/users/:user/app-accounts/:uid`}}}}export{AppAccountsApi as default};
1
+ import{randomInt as t}from"crypto";import{Type as s}from"@sinclair/typebox";import r from"../../abstract/plugin.js";import{isAuthzRefusal as e,HttpError as i,errorBody as a,AUTHZ_REFUSED as o}from"../../lib/errors.js";import{escapeLdapFilter as n,isDnInBranch as c,escapeDnValue as u}from"../../lib/utils.js";import{appAccountsOptions as p}from"./appAccounts.options.js";import{defineRoute as l}from"../../lib/route.js";const h=s.Object({user:s.String({minLength:1})}),d=s.Object({user:s.String({minLength:1}),uid:s.String({minLength:1})}),g=s.Object({name:s.Optional(s.String())}),answeredRefusal=(t,s)=>e(t)?(s.status(403).json(a(t,o)),!0):t instanceof i&&t.statusCode<500&&(s.status(t.statusCode).json(a(t,t.message)),!0);class AppAccountsApi extends r{static optionDefinitions=p;name="appAccountsApi";roles=["api","configurable"];dependencies={appAccountsConsistency:"core/twake/appAccountsConsistency"};settings;applicativeAccountBase;maxAppAccounts;mailAttr;userAttr;constructor(t){if(super(t),this.settings=this.options(p),this.applicativeAccountBase=this.settings.applicative_account_base,this.maxAppAccounts=this.settings.max_app_accounts||5,this.mailAttr=this.config.mail_attribute||"mail",this.userAttr=this.settings.app_accounts_user_attribute||this.mailAttr,!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}, max_app_accounts=${this.maxAppAccounts}, user_attribute=${this.userAttr}`)}api(t){const s=this.config.api_prefix||"/api";l(t,{method:"get",path:`${s}/v1/users/:user/app-accounts`,params:h,handler:async({req:t,res:s})=>this.listAccounts(t,s)}),l(t,{method:"post",path:`${s}/v1/users/:user/app-accounts`,params:h,body:g,handler:async({req:t,res:s})=>this.createAccount(t,s)}),l(t,{method:"delete",path:`${s}/v1/users/:user/app-accounts/:uid`,params:d,handler:async({req:t,res:s})=>this.deleteAccount(t,s)}),this.logger.info(`AppAccounts API registered at ${s}/v1/users/:user/app-accounts`)}async resolvePrincipal(t,s,r,e){let i=((await this.server.ldap.system.search({scope:"sub",filter:`(${this.userAttr}=${n(t)})`,paged:!1,...e?{attributes:e}:{}},this.config.ldap_base||"")).searchEntries||[]).filter(t=>!c(t.dn,this.applicativeAccountBase));const a=[];for(const t of i)await this.server.ldap.readableBy(String(t.dn),s)&&a.push(t);return i=a,0===i.length?(r.status(404).json({error:`User ${t} not found`}),null):i.length>1?(this.logger.error(`${this.name}: Ambiguous principal lookup for ${t}: ${i.length} entries share ${this.userAttr}`),r.status(409).json({error:`Multiple users share ${this.userAttr} ${t}`}),null):i[0]}async listAccounts(t,s){const r=t.params.user;try{const e=await this.resolvePrincipal(r,t,s,[this.mailAttr]);if(!e)return;const i=e[this.mailAttr];if(!i)return void s.status(400).json({error:`User ${r} has no mail attribute`});const a=Array.isArray(i)?String(i[0]):String(i),o=await this.server.ldap.system.search({scope:"sub",filter:`(${this.mailAttr}=${n(a)})`,paged:!1},this.applicativeAccountBase),c=(o.searchEntries||[]).filter(t=>{const s=t.uid;return(Array.isArray(s)?String(s[0]):String(s)).toLowerCase()!==a.toLowerCase()}).map(t=>{const s=t.uid,r=Array.isArray(s)?String(s[0]):String(s),e=t.description,i=e?Array.isArray(e)?String(e[0]):String(e):void 0;return{uid:r,...i?{name:i}:{}}}).sort((t,s)=>t.uid.localeCompare(s.uid));s.json(c)}catch(t){this.logger.error(`${this.name}: Failed to list accounts for ${r}:`,t),s.status(500).json({error:"Internal server error"})}}async createAccount(t,s){const r=t.params.user,{name:e}=t.body||{};try{const i=await this.resolvePrincipal(r,t,s);if(!i)return;const a=i[this.mailAttr];if(!a)return void s.status(400).json({error:`User ${r} has no mail attribute`});const o=Array.isArray(a)?String(a[0]):String(a),c=this.appUidPrefix(r),p=(await this.server.ldap.system.search({scope:"sub",filter:`(${this.mailAttr}=${n(o)})`,paged:!1},this.applicativeAccountBase)).searchEntries||[];if(p.filter(t=>{const s=t.uid;return(Array.isArray(s)?String(s[0]):String(s)).toLowerCase()!==o.toLowerCase()}).length>=this.maxAppAccounts)return void s.status(400).json({error:`Maximum number of accounts (${this.maxAppAccounts}) reached`});let l,h=0;do{if(l=`${c}_${this.generateAccountId()}`,h++,h>100)throw new Error("Failed to generate unique account ID after 100 attempts")}while(await this.uidExists(l));const d=this.generatePassword(),g={objectClass:["inetOrgPerson"],uid:l,userPassword:d},A=[...new Set([...this.settings.applicative_account_attribute,this.mailAttr])].filter(t=>"objectClass"!==t&&"description"!==t);for(const t of A)if(i[t]){const s=i[t];Array.isArray(s)?g[t]=s.map(t=>Buffer.isBuffer(t)?t.toString():String(t)):g[t]=Buffer.isBuffer(s)?s.toString():String(s)}e&&(g.description=e);const m=`uid=${u(l)},${this.applicativeAccountBase}`;await this.server.ldap.add(m,g,t),this.logger.info(`${this.name}: Created applicative account ${m} for user ${r}`);const f=`uid=${u(o)},${this.applicativeAccountBase}`;try{await this.server.ldap.modify(f,{add:{userPassword:d}},t)}catch(r){this.logger.error(`${this.name}: Failed to add password to principal account ${f}:`,r);try{await this.server.ldap.delete(m,t)}catch(t){this.logger.error(`${this.name}: Failed to roll back applicative account ${m}:`,t)}return void s.status(500).json({error:"Failed to register the password on the principal account"})}s.json({uid:l,pwd:d,mail:o})}catch(t){if(answeredRefusal(t,s))return;this.logger.error(`${this.name}: Failed to create account for ${r}:`,t),s.status(500).json({error:"Internal server error"})}}appUidPrefix(t){return t.replace(/[^A-Za-z0-9_-]/g,"_")}async uidExists(t){return((await this.server.ldap.system.search({scope:"sub",filter:`(uid=${n(t)})`,paged:!1,attributes:["uid"]},this.applicativeAccountBase)).searchEntries||[]).length>0}async deleteAccount(t,s){const r=t.params.user,a=t.params.uid;try{await this.server.ldap.authorize("ldapdeleterequest",[`uid=${u(a)},${this.applicativeAccountBase}`,t]);const o=await this.server.ldap.system.search({scope:"sub",filter:`(uid=${n(a)})`,paged:!1,attributes:["uid",this.mailAttr,"userPassword"]},this.applicativeAccountBase),c=o.searchEntries?.[0];if(!c)return this.logger.warn(`${this.name}: Account ${a} already deleted or not found`),void s.json({uid:a});const p=c[this.mailAttr],l=p?Array.isArray(p)?String(p[0]):String(p):null;let h=r;if(this.userAttr!==this.mailAttr){const e=await this.resolvePrincipal(r,t,s,[this.mailAttr]);if(!e)return;const i=e[this.mailAttr],a=i?Array.isArray(i)?String(i[0]):String(i):null;if(!a)return void s.status(400).json({error:`User ${r} has no mail attribute`});h=a}if(!l||l.toLowerCase()!==h.toLowerCase())return void s.status(403).json({error:`Account ${a} does not belong to user ${r}`});const d=c.userPassword;if(!d)return this.logger.error(`${this.name}: Account ${a} has no readable userPassword, refusing to delete: the bind DN needs read access on userPassword in the applicative branch`),void s.status(500).json({error:`Cannot revoke account ${a}: its password is not readable`});const g=`uid=${u(l)},${this.applicativeAccountBase}`;try{const s=Array.isArray(d)?d[0]:d;await this.server.ldap.modify(g,{delete:{userPassword:s}},t)}catch(t){const r=String(t.message??"");if(16!==t.code&&!/no ?such ?attribute/i.test(r))return e(t)||t instanceof i&&403===t.statusCode?void answeredRefusal(t,s):(this.logger.error(`${this.name}: Failed to delete password from principal account ${g}:`,t),void s.status(500).json({error:`Failed to revoke the password of account ${a}`}));this.logger.warn(`${this.name}: Password of ${a} was already absent from principal account ${g}, resuming deletion`)}const A=c.dn;await this.server.ldap.delete(A,t),this.logger.info(`${this.name}: Deleted applicative account ${A}`),s.json({uid:a})}catch(t){if(answeredRefusal(t,s))return;this.logger.error(`${this.name}: Failed to delete account ${a} for ${r}:`,t),s.status(500).json({error:"Internal server error"})}}generateAccountId(){return`c${t(0,1e8).toString().padStart(8,"0")}`}generatePassword(){const s="ABCDEFGHJKMNPQRSTUVWXYZ",r="abcdefghjkmnpqrstuvwxyz",e="23456789",i="@!#$%",a=[];for(let o=0;o<6;o++){const o=t(0,23),n=t(0,23),c=t(0,8),u=t(0,5),p=[s[o],r[n],e[c],i[u]];for(let s=p.length-1;s>0;s--){const r=t(0,s+1);[p[s],p[r]]=[p[r],p[s]]}a.push(p.join(""))}return a.join("-")}getConfigApiData(){const t=this.config.api_prefix||"/api";return{enabled:!0,base:this.applicativeAccountBase,maxAccounts:this.maxAppAccounts,mailAttribute:this.mailAttr,endpoints:{list:`${t}/v1/users/:user/app-accounts`,create:`${t}/v1/users/:user/app-accounts`,delete:`${t}/v1/users/:user/app-accounts/:uid`}}}}export{g as AppAccountCreateBody,d as AppAccountParams,h as AppAccountsParams,AppAccountsApi as default};
@@ -1 +1 @@
1
- import t from"../../abstract/plugin.js";import{isDnInBranch as e,escapeDnValue as i,escapeLdapFilter as a}from"../../lib/utils.js";function isNoSuchObject(t){if("object"!=typeof t||null===t)return!1;const{code:e,name:i,message:a}=t;return 32===e||"NoSuchObjectError"===i||"string"==typeof a&&a.includes("Code: 0x20")}class AppAccountsConsistency extends t{constructor(t){super(t),this.name="appAccountsConsistency",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange"},this.hooks={onLdapMailChange:async(t,e,i)=>{if(this.isInApplicativeBranch(t))this.logger.debug(`${this.name}: Ignoring mail change event originating in the applicative branch`);else try{const a=null!=e?Array.isArray(e)?String(e[0]):String(e):null,s=null!=i?Array.isArray(i)?String(i[0]):String(i):null;if(!a&&s)return void await this.createApplicativeAccount(t,s);if(a&&!s)return void await this.deleteApplicativeAccount(a);if(a&&s&&a!==s)return void await this.updateApplicativeAccount(t,a,s)}catch(e){this.logger.error(`${this.name}: Failed to handle mail change for ${t}:`,e)}}},this.mailAttr=this.config.mail_attribute||"mail",this.applicativeAccountBase=this.config.applicative_account_base,this.operationalAttributes=this.config.ldap_operational_attribute||[];const e=this.config.applicative_account_attribute||[],i=["objectClass",this.mailAttr],a=i.filter(t=>!e.includes(t));if(e.length>0&&a.length>0&&this.logger.warn(`${this.name}: applicative_account_attribute omits ${a.join(", ")} — copied anyway, an applicative entry cannot be created without them`),this.copiedAttributes=new Set([...e,...i]),!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}`)}shouldExcludeAttribute(t){return"dn"===t||this.operationalAttributes.includes(t)}async deleteApplicativeEntry(t){try{await this.server.ldap.system.delete(t),this.logger.info(`${this.name}: Deleted applicative account ${t}`)}catch(e){if(isNoSuchObject(e))return void this.logger.debug(`${this.name}: Applicative account ${t} already deleted`);throw e}}pickCopiedAttributes(t){const e={};for(const[i,a]of Object.entries(t))this.copiedAttributes.has(i)&&null!=a&&(Array.isArray(a)&&0===a.length||(e[i]=a));return e}isInApplicativeBranch(t){return e(t,this.applicativeAccountBase)}async createApplicativeAccount(t,e){const a=`uid=${i(e)},${this.applicativeAccountBase}`;try{const i=await this.server.ldap.system.search({scope:"base",paged:!1},t),s=i.searchEntries?.[0];if(!s)return void this.logger.warn(`${this.name}: Could not find user ${t} to create applicative account`);const c=this.pickCopiedAttributes(s);c.uid=e,await this.server.ldap.system.add(a,c),this.logger.info(`${this.name}: Created applicative account ${a} for user ${t}`)}catch(e){if(68===e.code||e.message?.includes("AlreadyExists"))return void this.logger.debug(`${this.name}: Applicative account ${a} already exists`);this.logger.error(`${this.name}: Failed to create applicative account for ${t}:`,e)}}async deleteApplicativeAccount(t){try{const e=`(${this.mailAttr}=${a(t)})`,i=(await this.server.ldap.system.search({filter:e,scope:"sub",paged:!1},this.applicativeAccountBase)).searchEntries||[];if(0===i.length)return void this.logger.debug(`${this.name}: No applicative accounts found for mail ${t}`);for(const t of i){const e=t.dn;try{await this.deleteApplicativeEntry(e)}catch(t){this.logger.error(`${this.name}: Failed to delete applicative account ${e}:`,t)}}}catch(e){if(isNoSuchObject(e))return void this.logger.debug(`${this.name}: Applicative account base does not exist or no accounts found for mail ${t}`);this.logger.error(`${this.name}: Failed to delete applicative account for mail ${t}:`,e)}}async updateApplicativeAccount(t,e,s){try{const c=`(${this.mailAttr}=${a(e)})`,r=(await this.server.ldap.system.search({filter:c,scope:"sub",paged:!1},this.applicativeAccountBase)).searchEntries||[];if(0===r.length)return void this.logger.debug(`${this.name}: No applicative account found for old mail ${e}`);for(const a of r){const c=a.dn,r=Array.isArray(a.uid)?String(a.uid[0]):String(a.uid);if(!(r===e)){await this.deleteApplicativeEntry(c);continue}const n=`uid=${i(s)},${this.applicativeAccountBase}`;try{const i={};for(const[t,e]of Object.entries(a))this.shouldExcludeAttribute(t)||null!=e&&(Array.isArray(e)&&0===e.length||(i[t]=e));await this.deleteApplicativeEntry(c);const r={...i},o=await this.server.ldap.system.search({scope:"base",paged:!1},t),l=o.searchEntries?.[0];if(!l)return void this.logger.warn(`${this.name}: Could not find user ${t} to update applicative account`);Object.assign(r,this.pickCopiedAttributes(l)),r.uid=s,await this.server.ldap.system.add(n,r),this.logger.info(`${this.name}: Created new applicative account ${n} (mail changed from ${e} to ${s})`)}catch(t){if(68===t.code||t.message?.includes("AlreadyExists"))return void this.logger.debug(`${this.name}: Applicative account ${n} already exists after mail change`);this.logger.error(`${this.name}: Failed to update applicative account for mail change:`,t)}}}catch(e){this.logger.error(`${this.name}: Failed to update applicative account for ${t}:`,e)}}}export{AppAccountsConsistency as default};
1
+ import t from"../../abstract/plugin.js";import{isDnInBranch as e,escapeDnValue as i,escapeLdapFilter as a}from"../../lib/utils.js";import{appAccountsOptions as s}from"./appAccounts.options.js";function isNoSuchObject(t){if("object"!=typeof t||null===t)return!1;const{code:e,name:i,message:a}=t;return 32===e||"NoSuchObjectError"===i||"string"==typeof a&&a.includes("Code: 0x20")}class AppAccountsConsistency extends t{static optionDefinitions=s;name="appAccountsConsistency";roles=["consistency"];dependencies={onLdapChange:"core/ldap/onChange"};settings;mailAttr;applicativeAccountBase;operationalAttributes;copiedAttributes;constructor(t){super(t),this.settings=this.options(s),this.mailAttr=this.config.mail_attribute||"mail",this.applicativeAccountBase=this.settings.applicative_account_base,this.operationalAttributes=this.settings.ldap_operational_attribute;const e=this.settings.applicative_account_attribute,i=["objectClass",this.mailAttr],a=i.filter(t=>!e.includes(t));if(e.length>0&&a.length>0&&this.logger.warn(`${this.name}: applicative_account_attribute omits ${a.join(", ")} — copied anyway, an applicative entry cannot be created without them`),this.copiedAttributes=new Set([...e,...i]),!this.applicativeAccountBase)throw new Error(`${this.name}: applicative_account_base configuration is required`);this.logger.info(`${this.name}: initialized with applicative_account_base=${this.applicativeAccountBase}`)}shouldExcludeAttribute(t){return"dn"===t||this.operationalAttributes.includes(t)}async deleteApplicativeEntry(t){try{await this.server.ldap.system.delete(t),this.logger.info(`${this.name}: Deleted applicative account ${t}`)}catch(e){if(isNoSuchObject(e))return void this.logger.debug(`${this.name}: Applicative account ${t} already deleted`);throw e}}pickCopiedAttributes(t){const e={};for(const[i,a]of Object.entries(t))this.copiedAttributes.has(i)&&null!=a&&(Array.isArray(a)&&0===a.length||(e[i]=a));return e}isInApplicativeBranch(t){return e(t,this.applicativeAccountBase)}hooks={onLdapMailChange:async(t,e,i)=>{if(this.isInApplicativeBranch(t))this.logger.debug(`${this.name}: Ignoring mail change event originating in the applicative branch`);else try{const a=null!=e?Array.isArray(e)?String(e[0]):String(e):null,s=null!=i?Array.isArray(i)?String(i[0]):String(i):null;if(!a&&s)return void await this.createApplicativeAccount(t,s);if(a&&!s)return void await this.deleteApplicativeAccount(a);if(a&&s&&a!==s)return void await this.updateApplicativeAccount(t,a,s)}catch(e){this.logger.error(`${this.name}: Failed to handle mail change for ${t}:`,e)}}};async createApplicativeAccount(t,e){const a=`uid=${i(e)},${this.applicativeAccountBase}`;try{const i=await this.server.ldap.system.search({scope:"base",paged:!1},t),s=i.searchEntries?.[0];if(!s)return void this.logger.warn(`${this.name}: Could not find user ${t} to create applicative account`);const c=this.pickCopiedAttributes(s);c.uid=e,await this.server.ldap.system.add(a,c),this.logger.info(`${this.name}: Created applicative account ${a} for user ${t}`)}catch(e){if(68===e.code||e.message?.includes("AlreadyExists"))return void this.logger.debug(`${this.name}: Applicative account ${a} already exists`);this.logger.error(`${this.name}: Failed to create applicative account for ${t}:`,e)}}async deleteApplicativeAccount(t){try{const e=`(${this.mailAttr}=${a(t)})`,i=(await this.server.ldap.system.search({filter:e,scope:"sub",paged:!1},this.applicativeAccountBase)).searchEntries||[];if(0===i.length)return void this.logger.debug(`${this.name}: No applicative accounts found for mail ${t}`);for(const t of i){const e=t.dn;try{await this.deleteApplicativeEntry(e)}catch(t){this.logger.error(`${this.name}: Failed to delete applicative account ${e}:`,t)}}}catch(e){if(isNoSuchObject(e))return void this.logger.debug(`${this.name}: Applicative account base does not exist or no accounts found for mail ${t}`);this.logger.error(`${this.name}: Failed to delete applicative account for mail ${t}:`,e)}}async updateApplicativeAccount(t,e,s){try{const c=`(${this.mailAttr}=${a(e)})`,r=(await this.server.ldap.system.search({filter:c,scope:"sub",paged:!1},this.applicativeAccountBase)).searchEntries||[];if(0===r.length)return void this.logger.debug(`${this.name}: No applicative account found for old mail ${e}`);for(const a of r){const c=a.dn,r=Array.isArray(a.uid)?String(a.uid[0]):String(a.uid);if(!(r===e)){await this.deleteApplicativeEntry(c);continue}const n=`uid=${i(s)},${this.applicativeAccountBase}`;try{const i={};for(const[t,e]of Object.entries(a))this.shouldExcludeAttribute(t)||null!=e&&(Array.isArray(e)&&0===e.length||(i[t]=e));await this.deleteApplicativeEntry(c);const r={...i},o=await this.server.ldap.system.search({scope:"base",paged:!1},t),l=o.searchEntries?.[0];if(!l)return void this.logger.warn(`${this.name}: Could not find user ${t} to update applicative account`);Object.assign(r,this.pickCopiedAttributes(l)),r.uid=s,await this.server.ldap.system.add(n,r),this.logger.info(`${this.name}: Created new applicative account ${n} (mail changed from ${e} to ${s})`)}catch(t){if(68===t.code||t.message?.includes("AlreadyExists"))return void this.logger.debug(`${this.name}: Applicative account ${n} already exists after mail change`);this.logger.error(`${this.name}: Failed to update applicative account for mail change:`,t)}}}catch(e){this.logger.error(`${this.name}: Failed to update applicative account for ${t}:`,e)}}}export{AppAccountsConsistency as default};
@@ -1 +1 @@
1
- import{TwakePlugin as e}from"../../abstract/twakePlugin.js";import{isDnInBranch as t,rdnValue as r}from"../../lib/utils.js";function isRegisteredUser(e){return"object"==typeof e&&null!==e&&"string"==typeof e.id}class Calendar extends e{constructor(e){super(e,"calendar_webadmin_url","calendar_webadmin_token","calendar_concurrency"),this.name="calendar",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange"},this.hooks={ldapcalendarResourceadddone:async e=>{const[t,r]=e;if(!this.isResource(t,r))return;const s=this.buildResourceData(t,r);s?await this.callWebAdminApi("ldapcalendarResourceadddone",`${this.webadminUrl}/resources`,"POST",t,JSON.stringify(s),{resourceName:s.name}):this.logger.debug(`Skipping resource creation for ${t}: missing required fields`)},ldapcalendarResourcemodifydone:async e=>{const[t,r]=e;if(!this.isInResourceBranch(t))return;const s=this.getResourceId(t);if(!s)return;const i={};r.replace?.cn&&(i.name=Array.isArray(r.replace.cn)?String(r.replace.cn[0]):String(r.replace.cn)),r.replace?.description&&(i.description=Array.isArray(r.replace.description)?String(r.replace.description[0]):String(r.replace.description)),0!==Object.keys(i).length?await this.callWebAdminApi("ldapcalendarResourcemodifydone",this.resourceUrl(s),"PATCH",t,JSON.stringify(i),{resourceId:s,...i}):this.logger.debug(`No relevant changes for resource ${t}`)},ldapcalendarResourcedeletedone:async e=>{if(!this.isInResourceBranch(e))return;const t=this.getResourceId(e);t&&await this.callWebAdminApi("ldapcalendarResourcedeletedone",this.resourceUrl(t),"DELETE",e,null,{resourceId:t})},onLdapChange:async(e,t)=>{const r=t[this.mailAttr];if(r){const t=this.attributeToString(r[0]),s=this.attributeToString(r[1]);return void(t&&s&&t!==s&&await this.syncRegisteredUser("onLdapMailChange",e,t))}(t[this.firstnameAttr]||t[this.lastnameAttr])&&await this.syncRegisteredUser("onLdapDisplayNameChange",e)}},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||"";const r=this.config.ldap_base;this.resourceBase&&r&&!t(this.resourceBase,r)&&this.logger.warn(`Calendar plugin: calendar_resource_base (${this.resourceBase}) is not a DN under ${r}; no entry will be taken for a resource`),this.firstnameAttr=this.config.calendar_firstname_attribute||"givenName",this.lastnameAttr=this.config.calendar_lastname_attribute||"sn"}isResource(e,t){if(!this.isInResourceBranch(e))return!1;const r=t.objectClass;if(this.resourceObjectClass){return(Array.isArray(r)?r:[r]).some(e=>String(e).toLowerCase()===this.resourceObjectClass.toLowerCase())}return!0}isInResourceBranch(e){return!this.resourceBase||t(e,this.resourceBase)}resourceUrl(e){return`${this.webadminUrl}/resources/${encodeURIComponent(e)}`}buildResourceData(e,t){const r=t.cn,s=this.attributeToString(r);if(!s)return null;const i=this.attributeToString(t.description)||void 0,a=this.resourceCreator,n=this.resourceDomain||this.extractDomainFromDn(e),o=this.getResourceId(e);return o?{name:s,description:i,creator:a,domain:n,id:o}:null}getResourceId(e){return r(e)||null}async findRegisteredUser(e,t){const r={...t,step:"find_registered_user",searchEmail:e},s=new URL(`${this.webadminUrl}/registeredUsers`);s.searchParams.set("email",e);const i=await this.requestLimit(()=>this.fetch(s.toString(),{method:"GET",headers:this.createHeaders()}));if(404!==i.status){if(!i.ok)return this.logger.error({...r,http_status:i.status,http_status_text:i.statusText}),null;let t;try{t=await i.json()}catch(e){return this.logger.error({...r,http_status:i.status,error:`unreadable answer: ${e}`}),null}if(!Array.isArray(t))return isRegisteredUser(t)?t:(this.logger.error({...r,http_status:i.status,error:"answer is not a registered user"}),null);const s=t.find(t=>isRegisteredUser(t)&&t.email?.toLowerCase()===e.toLowerCase());if(s)return s}return this.logger.warn({...r,message:"user not registered in Calendar"}),null}async syncRegisteredUser(e,t,r){const s={plugin:this.name,event:e,result:"error",dn:t},i=await this.ldapGetAttributes(t,[this.mailAttr,this.firstnameAttr,this.lastnameAttr]);if(!i)return void this.logger.warn({...s,message:`Cannot sync registered user: entry not found for ${t}`});const a=this.attributeToString(i[this.mailAttr]);if(!a)return void this.logger.warn({...s,message:`Cannot sync registered user: no mail found for ${t}`});const n=this.attributeToString(i[this.firstnameAttr]),o=this.attributeToString(i[this.lastnameAttr]),c=r||a;try{const e=await this.findRegisteredUser(c,s);if(!e)return;const t=new URL(`${this.webadminUrl}/registeredUsers`);t.searchParams.set("id",e.id);const r={email:a};n&&(r.firstname=n),o&&(r.lastname=o);const i=await this.requestLimit(()=>this.fetch(t.toString(),{method:"PATCH",headers:this.createHeaders("application/json"),body:JSON.stringify(r)}));i.ok?this.logger.info({...s,result:"success",id:e.id,http_status:i.status,...r}):this.logger.error({...s,step:"patch_registered_user",id:e.id,http_status:i.status,http_status_text:i.statusText})}catch(e){this.logger.error({...s,error:`${e}`})}}async deleteUserData(e){const t={plugin:this.name,event:"deleteUserData",mail:e};try{const r=new URL(`${this.webadminUrl}/users/${e}`);r.searchParams.set("action","deleteData");const s=await this.requestLimit(()=>this.fetch(r.toString(),{method:"POST",headers:this.createHeaders()}));if(!s.ok)return this.logger.error({...t,http_status:s.status,http_status_text:s.statusText}),null;const i=await s.json();return this.logger.info({...t,http_status:s.status,taskId:i.taskId}),i}catch(e){return this.logger.error({...t,error:`${e}`}),null}}}export{Calendar as default};
1
+ import{TwakePlugin as e}from"../../abstract/twakePlugin.js";import{isDnInBranch as t,rdnValue as r}from"../../lib/utils.js";import{calendarOptions as s,calendarOptionsOf as a}from"./calendar.options.js";function isRegisteredUser(e){return"object"==typeof e&&null!==e&&"string"==typeof e.id}class Calendar extends e{static optionDefinitions=s;name="calendar";roles=["consistency"];dependencies={onLdapChange:"core/ldap/onChange"};resourceBase;resourceObjectClass;resourceCreator;resourceDomain;firstnameAttr;lastnameAttr;constructor(e){const r=a(e.config);super(e,{url:r.calendar_webadmin_url,token:r.calendar_webadmin_token,concurrency:r.calendar_concurrency,urlOption:"calendar_webadmin_url",tokenOption:"calendar_webadmin_token",concurrencyOption:"calendar_concurrency"}),this.resourceBase=r.calendar_resource_base,this.resourceObjectClass=r.calendar_resource_objectclass,this.resourceCreator=r.calendar_resource_creator,this.resourceDomain=r.calendar_resource_domain;const s=this.config.ldap_base;this.resourceBase&&s&&!t(this.resourceBase,s)&&this.logger.warn(`Calendar plugin: calendar_resource_base (${this.resourceBase}) is not a DN under ${s}; no entry will be taken for a resource`),this.firstnameAttr=r.calendar_firstname_attribute,this.lastnameAttr=r.calendar_lastname_attribute}hooks={ldapcalendarResourceadddone:async(e,t)=>{if(!this.isResource(e,t))return;const r=this.buildResourceData(e,t);r?await this.callWebAdminApi("ldapcalendarResourceadddone",`${this.webadminUrl}/resources`,"POST",e,JSON.stringify(r),{resourceName:r.name}):this.logger.debug(`Skipping resource creation for ${e}: missing required fields`)},ldapcalendarResourcemodifydone:async(e,t,r)=>{if(!this.isInResourceBranch(e))return;const s=this.getResourceId(e);if(!s)return;const a={};t.replace?.cn&&(a.name=Array.isArray(t.replace.cn)?String(t.replace.cn[0]):String(t.replace.cn)),t.replace?.description&&(a.description=Array.isArray(t.replace.description)?String(t.replace.description[0]):String(t.replace.description)),0!==Object.keys(a).length?await this.callWebAdminApi("ldapcalendarResourcemodifydone",this.resourceUrl(s),"PATCH",e,JSON.stringify(a),{resourceId:s,...a}):this.logger.debug(`No relevant changes for resource ${e}`)},ldapcalendarResourcedeletedone:async e=>{if(!this.isInResourceBranch(e))return;const t=this.getResourceId(e);t&&await this.callWebAdminApi("ldapcalendarResourcedeletedone",this.resourceUrl(t),"DELETE",e,null,{resourceId:t})},onLdapChange:async(e,t)=>{const r=t[this.mailAttr];if(r){const t=this.attributeToString(r[0]),s=this.attributeToString(r[1]);return void(t&&s&&t!==s&&await this.syncRegisteredUser("onLdapMailChange",e,t))}(t[this.firstnameAttr]||t[this.lastnameAttr])&&await this.syncRegisteredUser("onLdapDisplayNameChange",e)}};isResource(e,t){if(!this.isInResourceBranch(e))return!1;const r=t.objectClass;if(this.resourceObjectClass){return(Array.isArray(r)?r:[r]).some(e=>String(e).toLowerCase()===this.resourceObjectClass.toLowerCase())}return!0}isInResourceBranch(e){return!this.resourceBase||t(e,this.resourceBase)}resourceUrl(e){return`${this.webadminUrl}/resources/${encodeURIComponent(e)}`}buildResourceData(e,t){const r=t.cn,s=this.attributeToString(r);if(!s)return null;const a=this.attributeToString(t.description)||void 0,i=this.resourceCreator,n=this.resourceDomain||this.extractDomainFromDn(e),o=this.getResourceId(e);return o?{name:s,description:a,creator:i,domain:n,id:o}:null}getResourceId(e){return r(e)||null}async findRegisteredUser(e,t){const r={...t,step:"find_registered_user",searchEmail:e},s=new URL(`${this.webadminUrl}/registeredUsers`);s.searchParams.set("email",e);const a=await this.requestLimit(()=>this.fetch(s.toString(),{method:"GET",headers:this.createHeaders()}));if(404!==a.status){if(!a.ok)return this.logger.error({...r,http_status:a.status,http_status_text:a.statusText}),null;let t;try{t=await a.json()}catch(e){return this.logger.error({...r,http_status:a.status,error:`unreadable answer: ${e}`}),null}if(!Array.isArray(t))return isRegisteredUser(t)?t:(this.logger.error({...r,http_status:a.status,error:"answer is not a registered user"}),null);const s=t.find(t=>isRegisteredUser(t)&&t.email?.toLowerCase()===e.toLowerCase());if(s)return s}return this.logger.warn({...r,message:"user not registered in Calendar"}),null}async syncRegisteredUser(e,t,r){const s={plugin:this.name,event:e,result:"error",dn:t},a=await this.ldapGetAttributes(t,[this.mailAttr,this.firstnameAttr,this.lastnameAttr]);if(!a)return void this.logger.warn({...s,message:`Cannot sync registered user: entry not found for ${t}`});const i=this.attributeToString(a[this.mailAttr]);if(!i)return void this.logger.warn({...s,message:`Cannot sync registered user: no mail found for ${t}`});const n=this.attributeToString(a[this.firstnameAttr]),o=this.attributeToString(a[this.lastnameAttr]),c=r||i;try{const e=await this.findRegisteredUser(c,s);if(!e)return;const t=new URL(`${this.webadminUrl}/registeredUsers`);t.searchParams.set("id",e.id);const r={email:i};n&&(r.firstname=n),o&&(r.lastname=o);const a=await this.requestLimit(()=>this.fetch(t.toString(),{method:"PATCH",headers:this.createHeaders("application/json"),body:JSON.stringify(r)}));a.ok?this.logger.info({...s,result:"success",id:e.id,http_status:a.status,...r}):this.logger.error({...s,step:"patch_registered_user",id:e.id,http_status:a.status,http_status_text:a.statusText})}catch(e){this.logger.error({...s,error:`${e}`})}}async deleteUserData(e){const t={plugin:this.name,event:"deleteUserData",mail:e};try{const r=new URL(`${this.webadminUrl}/users/${e}`);r.searchParams.set("action","deleteData");const s=await this.requestLimit(()=>this.fetch(r.toString(),{method:"POST",headers:this.createHeaders()}));if(!s.ok)return this.logger.error({...t,http_status:s.status,http_status_text:s.statusText}),null;const a=await s.json();return this.logger.info({...t,http_status:s.status,taskId:a.taskId}),a}catch(e){return this.logger.error({...t,error:`${e}`}),null}}}export{Calendar as default};
@@ -0,0 +1 @@
1
+ import{defineOptions as e,opt as n,readOptions as a}from"../../config/options.js";const _=e({calendar_webadmin_url:n.string({env:"DM_CALENDAR_WEBADMIN_URL",default:"http://localhost:8080"}),calendar_webadmin_token:n.string({env:"DM_CALENDAR_WEBADMIN_TOKEN",default:""}),calendar_concurrency:n.number({env:"DM_CALENDAR_CONCURRENCY",default:10,min:1}),calendar_resource_base:n.string({env:"DM_CALENDAR_RESOURCE_BASE",default:""}),calendar_resource_objectclass:n.string({env:"DM_CALENDAR_RESOURCE_OBJECTCLASS",default:""}),calendar_resource_creator:n.string({env:"DM_CALENDAR_RESOURCE_CREATOR",default:"admin@example.com"}),calendar_resource_domain:n.string({env:"DM_CALENDAR_RESOURCE_DOMAIN",default:""}),calendar_firstname_attribute:n.string({env:"DM_CALENDAR_FIRSTNAME_ATTRIBUTE",default:"givenName"}),calendar_lastname_attribute:n.string({env:"DM_CALENDAR_LASTNAME_ATTRIBUTE",default:"sn"})});function calendarOptionsOf(e){return a(_,e)}export{_ as calendarOptions,calendarOptionsOf};
@@ -1 +1 @@
1
- import e from"./calendar.js";console.warn("[ldap-rest] Plugin `core/twake/calendarResources` is deprecated; replace it with `core/twake/calendar` — do not load both, each would sync Calendar.");class CalendarResources extends e{constructor(){super(...arguments),this.name="calendarResources"}}export{CalendarResources as default};
1
+ import e from"./calendar.js";console.warn("[ldap-rest] Plugin `core/twake/calendarResources` is deprecated; replace it with `core/twake/calendar` — do not load both, each would sync Calendar.");class CalendarResources extends e{name="calendarResources"}export{CalendarResources as default};
@@ -0,0 +1 @@
1
+ import{defineOptions as e,opt as _,readOptions as r}from"../../config/options.js";const t=e({cloudery_manager_url:_.string({env:"DM_CLOUDERY_MANAGER_URL",default:""}),cloudery_manager_token:_.string({env:"DM_CLOUDERY_MANAGER_TOKEN",default:""}),cloudery_offer:_.string({env:"DM_CLOUDERY_OFFER",default:"b2b_twake_default"}),cloudery_domain:_.string({env:"DM_CLOUDERY_DOMAIN",default:""}),cloudery_user_branch:_.string({env:"DM_CLOUDERY_USER_BRANCH",default:""}),cloudery_org_id_header:_.string({env:"DM_CLOUDERY_ORG_ID_HEADER",default:"x-cloudery-org-id"}),cloudery_org_role_header:_.string({env:"DM_CLOUDERY_ORG_ROLE_HEADER",default:"x-cloudery-org-role"}),cloudery_default_org_role:_.string({env:"DM_CLOUDERY_DEFAULT_ORG_ROLE",default:"member"}),cloudery_fqdn_attribute:_.string({env:"DM_CLOUDERY_FQDN_ATTRIBUTE",default:"twakeWorkspaceUrl"}),cloudery_org_id_attribute:_.string({env:"DM_CLOUDERY_ORG_ID_ATTRIBUTE",default:"twakeOrganizationId"}),cloudery_org_role_attribute:_.string({env:"DM_CLOUDERY_ORG_ROLE_ATTRIBUTE",default:"twakeOrganizationRole"}),cloudery_phones_attribute:_.string({env:"DM_CLOUDERY_PHONES_ATTRIBUTE",default:"twakePhones"}),cloudery_invited_attribute:_.string({env:"DM_CLOUDERY_INVITED_ATTRIBUTE",default:"twakeInvited"}),cloudery_default_locale:_.string({env:"DM_CLOUDERY_DEFAULT_LOCALE",default:"en"}),cloudery_workflow_poll_interval_ms:_.number({env:"DM_CLOUDERY_WORKFLOW_POLL_INTERVAL_MS",default:2e3,min:1}),cloudery_workflow_max_attempts:_.number({env:"DM_CLOUDERY_WORKFLOW_MAX_ATTEMPTS",default:60,min:1})});function clouderyOptionsOf(e){return r(t,e)}export{t as clouderyOptions,clouderyOptionsOf};
@@ -1 +1 @@
1
- import{setTimeout as e}from"node:timers/promises";import t from"../../abstract/plugin.js";import{instanceSlug as r}from"../../lib/instanceProviders.js";import{escapeDnValue as i,isChildOf as s}from"../../lib/utils.js";import{BaseResolver as n}from"../scim/baseResolver.js";const a=new Set(["admin","owner","member"]),o=1e3;class ClouderyProvision extends t{constructor(e){super(e),this.name="clouderyProvision",this.roles=["consistency"],this.dependencies={rabbitmq:"core/rabbitmq"},this.createStash=new Map,this.deleteStash=new Map,this.hooks={scimusercreate:e=>(this.normalizeUserName(e[0]),this.captureCreate(e[0],e[1]),e),scimusercreatedone:async e=>{await this.provision(e)},scimuserdelete:async e=>(e[0]=e[0].toLowerCase(),await this.captureDelete(e[0],e[1]),e),scimuserdeletedone:async e=>{await this.deprovision(e)}};const t=this.config;this.managerUrl=(t.cloudery_manager_url||"").replace(/\/$/,""),this.managerToken=t.cloudery_manager_token||"",this.offer=t.cloudery_offer||"b2b_twake_default",this.clouderyDomain=t.cloudery_domain||"",this.userBranch=t.cloudery_user_branch||"",this.orgIdHeader=(t.cloudery_org_id_header||"x-cloudery-org-id").toLowerCase(),this.orgRoleHeader=(t.cloudery_org_role_header||"x-cloudery-org-role").toLowerCase();const r=t.cloudery_default_org_role||"member",i=r.trim().toLowerCase();a.has(i)?this.defaultOrgRole=i:(this.defaultOrgRole="member",this.logger.warn(`${this.name}: cloudery_default_org_role "${r}" is not one of ${[...a].join(", ")} — using "member"`)),this.fqdnAttribute=t.cloudery_fqdn_attribute||"twakeWorkspaceUrl",this.orgIdAttribute=t.cloudery_org_id_attribute||"twakeOrganizationId",this.orgRoleAttribute=t.cloudery_org_role_attribute||"twakeOrganizationRole",this.phonesAttribute=t.cloudery_phones_attribute||"twakePhones",this.invitedAttribute=t.cloudery_invited_attribute||"twakeInvited",this.defaultLocale=t.cloudery_default_locale||"en",this.rdnAttribute=t.scim_user_rdn_attribute||"uid",this.authExchange=t.cozy_auth_exchange||"auth",this.b2bExchange=t.cozy_b2b_exchange||"b2b",this.userCreatedRoutingKey=t.cozy_user_created_routing_key||"user.created",this.userDeletedRoutingKey=t.cozy_user_deleted_routing_key??"domain.user.deleted",this.pollIntervalMs=Number(t.cloudery_workflow_poll_interval_ms)||2e3,this.maxPollAttempts=Number(t.cloudery_workflow_max_attempts)||60,this.baseResolver=new n(this.config),this.managerUrl||this.logger.warn(`${this.name}: cloudery_manager_url is empty — instance provisioning will be skipped`),this.userBranch||this.logger.warn(`${this.name}: cloudery_user_branch is empty — no users will be provisioned`),this.logger.info({plugin:this.name,event:"init",managerUrl:this.managerUrl||void 0,userBranch:this.userBranch||void 0,orgIdHeader:this.orgIdHeader,orgRoleHeader:this.orgRoleHeader,defaultOrgRole:this.defaultOrgRole,fqdnAttribute:this.fqdnAttribute,orgIdAttribute:this.orgIdAttribute,orgRoleAttribute:this.orgRoleAttribute,phonesAttribute:this.phonesAttribute,invitedAttribute:this.invitedAttribute,pollIntervalMs:this.pollIntervalMs,maxPollAttempts:this.maxPollAttempts})}captureCreate(e,t){this.pruneStash();const r=this.extractId(e),i=this.baseResolver.userBase(t);if(this.logger.info({plugin:this.name,event:"captureCreate",userName:r||void 0,base:i,hasManagerUrl:Boolean(this.managerUrl),hasUserBranch:Boolean(this.userBranch)}),!this.managerUrl||!this.userBranch)return void this.logger.info({plugin:this.name,event:"captureCreate",userName:r||void 0,base:i,result:"skipped",reason:"missing_configuration"});if(!r)return void this.logger.info({plugin:this.name,event:"captureCreate",base:i,result:"skipped",reason:"missing_user_name"});if(!this.isAtOrUnder(i,this.userBranch))return void this.logger.info({plugin:this.name,event:"captureCreate",userName:r,base:i,result:"skipped",reason:"outside_b2b_branch"});const s=this.readHeader(t,this.orgIdHeader);if(!s)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:`B2B branch create without ${this.orgIdHeader} — skipping provisioning`});const n=this.extractPrimaryEmail(e);if(!n)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:"B2B branch create without a primary email — skipping provisioning"});const a=n.trim(),o=this.resolveRole(t,r);this.createStash.set(a.toLowerCase(),{orgId:s.trim(),email:a,role:o,base:i,ts:Date.now()}),this.logger.info({plugin:this.name,event:"captureCreate",userName:r,base:i,orgId:s.trim(),email:a,role:o,result:"stashed"})}async provision(e){const t=this.extractId(e);if(this.logger.info({plugin:this.name,event:"provision",userName:t||void 0}),!t)return void this.logger.info({plugin:this.name,event:"provision",result:"skipped",reason:"missing_user_name"});const s=this.extractPrimaryEmail(e);if(!s)return void this.logger.info({plugin:this.name,event:"provision",userName:t,result:"skipped",reason:"missing_primary_email"});const n=s.trim().toLowerCase(),a=this.createStash.get(n);if(!a)return void this.logger.info({plugin:this.name,event:"provision",userName:t,email:s.trim(),result:"skipped",reason:"no_stashed_context"});this.createStash.delete(n),this.logger.info({plugin:this.name,event:"provision",userName:t,email:a.email,orgId:a.orgId,base:a.base,role:a.role,result:"context_loaded"});const o=a.email,l=emailDomain(o),u=r(t+a.orgId);if(!u)return void this.logger.error({plugin:this.name,event:"provision",userName:t,result:"no_slug"});const h=this.extractPrimaryPhone(e),g=this.extractPhones(e),d=this.extractPublicName(e)||t,c={plugin:this.name,event:"provision",userName:t,orgId:a.orgId,slug:u},m=`${u}.${this.clouderyDomain}`.toLowerCase();let p=null;try{p=await this.findInstanceByFqdn(m)}catch(e){this.logger.warn({...c,result:"existence_lookup_failed",fqdn:m,error:`${e}`})}if(p&&(p.internal_email&&p.internal_email.toLowerCase()!==o.toLowerCase()||p.oidc&&p.oidc.toLowerCase()!==u))return void this.logger.error({...c,result:"slug_taken",fqdn:m,uuid:p._id});const f=p?._id??null;let w;if(f)this.logger.info({...c,result:"instance_exists",fqdn:m,uuid:f}),w=m;else{let t;try{this.logger.info({...c,result:"creating_instance",orgDomain:l,email:o,mobile:h||void 0}),t=await this.createInstance({email:o,publicName:d,locale:this.extractLocale(e),oidc:u,phone:h||"",slug:u,public_name:d,offer:this.offer,domain:this.clouderyDomain,skip_email_validation:!0,internal_email:o,org_domain:l,org_id:a.orgId})}catch(e){return void this.logger.error({...c,result:"error",error:`${e}`})}if(!(!t.workflow||await this.waitForWorkflow(t.workflow)))return void this.logger.error({...c,result:"workflow_failed",workflow:t.workflow});w=t.fqdn}const v=`${this.rdnAttribute}=${i(t)},${a.base}`,b={[this.fqdnAttribute]:w,[this.orgIdAttribute]:a.orgId,[this.orgRoleAttribute]:a.role,[this.invitedAttribute]:"TRUE",cn:t};g.length>0&&(b[this.phonesAttribute]=JSON.stringify(g));try{this.logger.info({...c,result:"writing_fqdn",dn:v,hasPhones:g.length>0}),await this.server.ldap.system.modify(v,{replace:b},void 0,{declaredClasses:this.config.user_class}),this.logger.info({...c,result:"written_fqdn",dn:v})}catch(e){this.logger.error({...c,event:"writeFqdn",dn:v,error:`${e}`})}await this.publishUserCreated({twakeId:t,orgDomain:l,orgId:a.orgId,fqdn:w,email:o,mobile:h}),this.logger.info({...c,result:"success",fqdn:w})}async captureDelete(e,t){const r=this.baseResolver.userBase(t);if(this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,hasManagerUrl:Boolean(this.managerUrl),hasUserBranch:Boolean(this.userBranch)}),!this.managerUrl||!this.userBranch)return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,result:"skipped",reason:"missing_configuration"});if(!this.isAtOrUnder(r,this.userBranch))return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,result:"skipped",reason:"outside_b2b_branch"});const s=`${this.rdnAttribute}=${i(e)},${r}`;try{this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:s,result:"searching"});const t=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[this.fqdnAttribute,"mail"]},s),r=t.searchEntries?.[0],i=r?firstValue(r[this.fqdnAttribute]):void 0,n=i?i.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i,"").split("/")[0]:void 0;if(!n)return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:s,result:"skipped",reason:"missing_fqdn"});const a=r?firstValue(r.mail):void 0,o=a?emailDomain(a):"";this.deleteStash.set(e,{fqdn:n,domain:o}),this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:s,fqdn:n,domain:o,result:"stashed"})}catch(t){this.logger.warn({plugin:this.name,event:"captureDelete",id:e,error:`${t}`})}}async deprovision(e){this.logger.info({plugin:this.name,event:"deprovision",id:e});const t=this.deleteStash.get(e);if(!t)return void this.logger.info({plugin:this.name,event:"deprovision",id:e,result:"skipped",reason:"no_stashed_context"});this.deleteStash.delete(e);const r={plugin:this.name,event:"deprovision",id:e,fqdn:t.fqdn};let i=!1;try{this.logger.info({...r,result:"resolving_uuid"});const e=(await this.findInstanceByFqdn(t.fqdn,"_id"))?._id;e?(this.logger.info({...r,result:"deleting_instance",uuid:e}),await this.deleteInstance(e),i=!0,this.logger.info({...r,result:"deleted",uuid:e})):this.logger.warn({...r,result:"not_found"})}catch(e){this.logger.error({...r,result:"error",error:`${e}`})}i&&await this.publishUserDeleted(t.fqdn,t.domain)}async createInstance(e){this.logger.info({plugin:this.name,event:"createInstance",url:`${this.managerUrl}/api/v1/instances`});const t=await this.fetch(`${this.managerUrl}/api/v1/instances`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.managerToken}`},body:JSON.stringify(e)});if(!t.ok){const e=(await t.text().catch(()=>"")).slice(0,o);throw this.logger.error({plugin:this.name,event:"createInstance",result:"error",http_status:t.status,http_status_text:t.statusText,body:e||void 0}),new Error(`Cloudery createInstance HTTP ${t.status}`)}const r=await t.json();return this.logger.info({plugin:this.name,event:"createInstance",result:"success",id:r.id,fqdn:r.fqdn,workflow:r.workflow}),r}async waitForWorkflow(t){this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,maxPollAttempts:this.maxPollAttempts,pollIntervalMs:this.pollIntervalMs});for(let r=1;r<=this.maxPollAttempts;r++){try{const e=await this.fetch(`${this.managerUrl}/api/v1/workflows/${encodeURIComponent(t)}`,{headers:{Authorization:`Bearer ${this.managerToken}`}});if(e.ok){const i=await e.json();if(this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,status:i.status}),"succeeded"===i.status)return this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,result:"success"}),!0;if("failed"===i.status||"error"===i.status)return this.logger.warn({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,result:"failed",status:i.status}),!1}else{const i=(await e.text().catch(()=>"")).slice(0,o);this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,result:"http_error",http_status:e.status,http_status_text:e.statusText,body:i||void 0})}}catch(e){this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,error:`${e}`})}r<this.maxPollAttempts&&(this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,result:"retrying"}),await e(this.pollIntervalMs))}return this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,result:"timeout",maxPollAttempts:this.maxPollAttempts}),!1}async findInstanceByFqdn(e,t){this.logger.info({plugin:this.name,event:"findInstanceByFqdn",fqdn:e});const r=new URL(`${this.managerUrl}/api/v2/instances`);r.searchParams.set("fqdn",e),t&&r.searchParams.set("only",t),r.searchParams.set("limit","1");const i=await this.fetch(r.toString(),{headers:{Authorization:`Bearer ${this.managerToken}`}});if(!i.ok){const t=(await i.text().catch(()=>"")).slice(0,o);throw this.logger.error({plugin:this.name,event:"findInstanceByFqdn",fqdn:e,result:"error",http_status:i.status,http_status_text:i.statusText,body:t||void 0}),new Error(`Cloudery searchInstances HTTP ${i.status}`)}const s=await i.json(),n=s.items?.[0]??null,a=n?._id;return this.logger.info({plugin:this.name,event:"findInstanceByFqdn",fqdn:e,result:a?"found":"not_found",uuid:a||void 0}),n}async deleteInstance(e){const t=new URL(`${this.managerUrl}/api/v1/instances/${encodeURIComponent(e)}`);t.searchParams.set("user_request","true"),this.logger.info({plugin:this.name,event:"deleteInstance",uuid:e,url:t.toString()});const r=await this.fetch(t.toString(),{method:"DELETE",headers:{Authorization:`Bearer ${this.managerToken}`}});if(!r.ok){const t=(await r.text().catch(()=>"")).slice(0,o);throw this.logger.error({plugin:this.name,event:"deleteInstance",uuid:e,result:"error",http_status:r.status,http_status_text:r.statusText,body:t||void 0}),new Error(`Cloudery deleteInstance HTTP ${r.status}`)}this.logger.info({plugin:this.name,event:"deleteInstance",uuid:e,result:"success",http_status:r.status})}async publishUserCreated(e){const t=this.requirePlugin("rabbitmq");if(this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,orgId:e.orgId,orgDomain:e.orgDomain,hasRabbitmq:Boolean(t)}),!t)return void this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,result:"skipped",reason:"missing_rabbitmq"});const r={twakeId:e.twakeId,domain:e.orgDomain,organizationDomain:e.orgDomain,workplaceFqdn:e.fqdn,organizationId:e.orgId};e.email&&(r.internalEmail=e.email),e.mobile&&(r.mobile=e.mobile);try{await t.publish(this.authExchange,this.userCreatedRoutingKey,r),this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,exchange:this.authExchange,routingKey:this.userCreatedRoutingKey,result:"success"})}catch(t){this.logger.error({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,error:`${t}`})}}async publishUserDeleted(e,t){if(!this.userDeletedRoutingKey)return;const r=this.requirePlugin("rabbitmq");if(this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,domain:t,hasRabbitmq:Boolean(r)}),r)try{await r.publish(this.b2bExchange,this.userDeletedRoutingKey,{workplaceFqdn:e,domain:t}),this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,exchange:this.b2bExchange,routingKey:this.userDeletedRoutingKey,result:"success"})}catch(t){this.logger.error({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,error:`${t}`})}else this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,result:"skipped",reason:"missing_rabbitmq"})}readHeader(e,t){if(!e)return;const r=e.get?e.get(t):void 0;if("string"==typeof r&&r.length>0)return r;const i=e.query?.[t];return"string"==typeof i&&i.length>0?i:void 0}resolveRole(e,t){const r=this.readHeader(e,this.orgRoleHeader)?.trim().toLowerCase();return r?a.has(r)?(this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,input:r,result:"accepted",role:r}),r):(this.logger.warn({plugin:this.name,event:"captureCreate",userName:t,message:`Unknown ${this.orgRoleHeader} "${r}" — falling back to ${this.defaultOrgRole}`}),this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,input:r,result:"default",role:this.defaultOrgRole}),this.defaultOrgRole):(this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,result:"default",role:this.defaultOrgRole}),this.defaultOrgRole)}isAtOrUnder(e,t){return e.trim().toLowerCase()===t.trim().toLowerCase()||s(e,t)}pruneStash(){const e=Date.now()-3e5;for(const[t,r]of this.createStash)r.ts<e&&this.createStash.delete(t)}normalizeUserName(e){"string"==typeof e.userName&&(e.userName=e.userName.toLowerCase())}extractId(e){return"string"==typeof e.userName&&e.userName.length>0?e.userName:"string"==typeof e.id&&e.id.length>0?e.id:null}extractPrimaryEmail(e){const t=e.emails;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPrimaryPhone(e){const t=e.phoneNumbers;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPhones(e){const t=e.phoneNumbers;return t&&0!==t.length?t.filter(e=>"string"==typeof e.value&&e.value.trim().length>0).map(e=>({number:e.value.trim(),primary:Boolean(e.primary)})):[]}extractPublicName(e){if("string"==typeof e.displayName&&e.displayName.trim().length>0)return e.displayName.trim();const t=e.name?.formatted;if("string"==typeof t&&t.trim().length>0)return t.trim();const r=`${e.name?.givenName?.trim()??""} ${e.name?.familyName?.trim()??""}`.trim();return r.length>0?r:null}extractLocale(e){return"string"==typeof e.locale&&e.locale.length>0?e.locale:"string"==typeof e.preferredLanguage&&e.preferredLanguage.length>0?e.preferredLanguage:this.defaultLocale}}function emailDomain(e){const t=e.lastIndexOf("@");return t>=0?e.slice(t+1):""}function firstValue(e){if(Array.isArray(e)){const t=e[0];return"string"==typeof t?t:void 0}return"string"==typeof e?e:void 0}export{ClouderyProvision as default};
1
+ import{setTimeout as e}from"node:timers/promises";import t from"../../abstract/plugin.js";import{instanceSlug as r}from"../../lib/instanceProviders.js";import{escapeDnValue as i,isChildOf as s}from"../../lib/utils.js";import{BaseResolver as n}from"../scim/baseResolver.js";import{scimOptionsOf as o}from"../scim/scim.options.js";import{clouderyOptions as a,clouderyOptionsOf as l}from"./cloudery.options.js";import{cozyOptionsOf as u}from"./cozy.options.js";const h=new Set(["admin","owner","member"]),g=1e3;class ClouderyProvision extends t{static optionDefinitions=a;name="clouderyProvision";roles=["consistency"];dependencies={rabbitmq:"core/rabbitmq"};managerUrl;managerToken;offer;clouderyDomain;userBranch;orgIdHeader;orgRoleHeader;defaultOrgRole;fqdnAttribute;orgIdAttribute;orgRoleAttribute;phonesAttribute;invitedAttribute;defaultLocale;rdnAttribute;authExchange;b2bExchange;userCreatedRoutingKey;userDeletedRoutingKey;pollIntervalMs;maxPollAttempts;baseResolver;createStash=new Map;deleteStash=new Map;constructor(e){super(e);const t=l(e.config),r=u(e.config),i=o(e.config);this.managerUrl=t.cloudery_manager_url.replace(/\/$/,""),this.managerToken=t.cloudery_manager_token,this.offer=t.cloudery_offer,this.clouderyDomain=t.cloudery_domain,this.userBranch=t.cloudery_user_branch,this.orgIdHeader=t.cloudery_org_id_header.toLowerCase(),this.orgRoleHeader=t.cloudery_org_role_header.toLowerCase();const s=t.cloudery_default_org_role,a=s.trim().toLowerCase();h.has(a)?this.defaultOrgRole=a:(this.defaultOrgRole="member",this.logger.warn(`${this.name}: cloudery_default_org_role "${s}" is not one of ${[...h].join(", ")} — using "member"`)),this.fqdnAttribute=t.cloudery_fqdn_attribute,this.orgIdAttribute=t.cloudery_org_id_attribute,this.orgRoleAttribute=t.cloudery_org_role_attribute,this.phonesAttribute=t.cloudery_phones_attribute,this.invitedAttribute=t.cloudery_invited_attribute,this.defaultLocale=t.cloudery_default_locale,this.rdnAttribute=i.scim_user_rdn_attribute,this.authExchange=r.cozy_auth_exchange,this.b2bExchange=r.cozy_b2b_exchange,this.userCreatedRoutingKey=r.cozy_user_created_routing_key,this.userDeletedRoutingKey=r.cozy_user_deleted_routing_key,this.pollIntervalMs=t.cloudery_workflow_poll_interval_ms,this.maxPollAttempts=t.cloudery_workflow_max_attempts,this.baseResolver=new n(this.config),this.managerUrl||this.logger.warn(`${this.name}: cloudery_manager_url is empty — instance provisioning will be skipped`),this.userBranch||this.logger.warn(`${this.name}: cloudery_user_branch is empty — no users will be provisioned`),this.logger.info({plugin:this.name,event:"init",managerUrl:this.managerUrl||void 0,userBranch:this.userBranch||void 0,orgIdHeader:this.orgIdHeader,orgRoleHeader:this.orgRoleHeader,defaultOrgRole:this.defaultOrgRole,fqdnAttribute:this.fqdnAttribute,orgIdAttribute:this.orgIdAttribute,orgRoleAttribute:this.orgRoleAttribute,phonesAttribute:this.phonesAttribute,invitedAttribute:this.invitedAttribute,pollIntervalMs:this.pollIntervalMs,maxPollAttempts:this.maxPollAttempts})}hooks={scimusercreate:e=>(this.normalizeUserName(e[0]),this.captureCreate(e[0],e[1]),e),scimusercreatedone:async e=>{await this.provision(e)},scimuserdelete:async e=>(e[0]=e[0].toLowerCase(),await this.captureDelete(e[0],e[1]),e),scimuserdeletedone:async e=>{await this.deprovision(e)}};captureCreate(e,t){this.pruneStash();const r=this.extractId(e),i=this.baseResolver.userBase(t);if(this.logger.info({plugin:this.name,event:"captureCreate",userName:r||void 0,base:i,hasManagerUrl:Boolean(this.managerUrl),hasUserBranch:Boolean(this.userBranch)}),!this.managerUrl||!this.userBranch)return void this.logger.info({plugin:this.name,event:"captureCreate",userName:r||void 0,base:i,result:"skipped",reason:"missing_configuration"});if(!r)return void this.logger.info({plugin:this.name,event:"captureCreate",base:i,result:"skipped",reason:"missing_user_name"});if(!this.isAtOrUnder(i,this.userBranch))return void this.logger.info({plugin:this.name,event:"captureCreate",userName:r,base:i,result:"skipped",reason:"outside_b2b_branch"});const s=this.readHeader(t,this.orgIdHeader);if(!s)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:`B2B branch create without ${this.orgIdHeader} — skipping provisioning`});const n=this.extractPrimaryEmail(e);if(!n)return void this.logger.warn({plugin:this.name,event:"captureCreate",userName:r,message:"B2B branch create without a primary email — skipping provisioning"});const o=n.trim(),a=this.resolveRole(t,r);this.createStash.set(o.toLowerCase(),{orgId:s.trim(),email:o,role:a,base:i,ts:Date.now()}),this.logger.info({plugin:this.name,event:"captureCreate",userName:r,base:i,orgId:s.trim(),email:o,role:a,result:"stashed"})}async provision(e){const t=this.extractId(e);if(this.logger.info({plugin:this.name,event:"provision",userName:t||void 0}),!t)return void this.logger.info({plugin:this.name,event:"provision",result:"skipped",reason:"missing_user_name"});const s=this.extractPrimaryEmail(e);if(!s)return void this.logger.info({plugin:this.name,event:"provision",userName:t,result:"skipped",reason:"missing_primary_email"});const n=s.trim().toLowerCase(),o=this.createStash.get(n);if(!o)return void this.logger.info({plugin:this.name,event:"provision",userName:t,email:s.trim(),result:"skipped",reason:"no_stashed_context"});this.createStash.delete(n),this.logger.info({plugin:this.name,event:"provision",userName:t,email:o.email,orgId:o.orgId,base:o.base,role:o.role,result:"context_loaded"});const a=o.email,l=emailDomain(a),u=r(t+o.orgId);if(!u)return void this.logger.error({plugin:this.name,event:"provision",userName:t,result:"no_slug"});const h=this.extractPrimaryPhone(e),g=this.extractPhones(e),d=this.extractPublicName(e)||t,c={plugin:this.name,event:"provision",userName:t,orgId:o.orgId,slug:u},m=`${u}.${this.clouderyDomain}`.toLowerCase();let p=null;try{p=await this.findInstanceByFqdn(m)}catch(e){this.logger.warn({...c,result:"existence_lookup_failed",fqdn:m,error:`${e}`})}if(p&&(p.internal_email&&p.internal_email.toLowerCase()!==a.toLowerCase()||p.oidc&&p.oidc.toLowerCase()!==u))return void this.logger.error({...c,result:"slug_taken",fqdn:m,uuid:p._id});const f=p?._id??null;let v;if(f)this.logger.info({...c,result:"instance_exists",fqdn:m,uuid:f}),v=m;else{let t;try{this.logger.info({...c,result:"creating_instance",orgDomain:l,email:a,mobile:h||void 0}),t=await this.createInstance({email:a,publicName:d,locale:this.extractLocale(e),oidc:u,phone:h||"",slug:u,public_name:d,offer:this.offer,domain:this.clouderyDomain,skip_email_validation:!0,internal_email:a,org_domain:l,org_id:o.orgId})}catch(e){return void this.logger.error({...c,result:"error",error:`${e}`})}if(!(!t.workflow||await this.waitForWorkflow(t.workflow)))return void this.logger.error({...c,result:"workflow_failed",workflow:t.workflow});v=t.fqdn}const w=`${this.rdnAttribute}=${i(t)},${o.base}`,b={[this.fqdnAttribute]:v,[this.orgIdAttribute]:o.orgId,[this.orgRoleAttribute]:o.role,[this.invitedAttribute]:"TRUE",cn:t};g.length>0&&(b[this.phonesAttribute]=JSON.stringify(g));try{this.logger.info({...c,result:"writing_fqdn",dn:w,hasPhones:g.length>0}),await this.server.ldap.system.modify(w,{replace:b},void 0,{declaredClasses:this.config.user_class}),this.logger.info({...c,result:"written_fqdn",dn:w})}catch(e){this.logger.error({...c,event:"writeFqdn",dn:w,error:`${e}`})}await this.publishUserCreated({twakeId:t,orgDomain:l,orgId:o.orgId,fqdn:v,email:a,mobile:h}),this.logger.info({...c,result:"success",fqdn:v})}async captureDelete(e,t){const r=this.baseResolver.userBase(t);if(this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,hasManagerUrl:Boolean(this.managerUrl),hasUserBranch:Boolean(this.userBranch)}),!this.managerUrl||!this.userBranch)return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,result:"skipped",reason:"missing_configuration"});if(!this.isAtOrUnder(r,this.userBranch))return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,base:r,result:"skipped",reason:"outside_b2b_branch"});const s=`${this.rdnAttribute}=${i(e)},${r}`;try{this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:s,result:"searching"});const t=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:[this.fqdnAttribute,"mail"]},s),r=t.searchEntries?.[0],i=r?firstValue(r[this.fqdnAttribute]):void 0,n=i?i.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i,"").split("/")[0]:void 0;if(!n)return void this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:s,result:"skipped",reason:"missing_fqdn"});const o=r?firstValue(r.mail):void 0,a=o?emailDomain(o):"";this.deleteStash.set(e,{fqdn:n,domain:a}),this.logger.info({plugin:this.name,event:"captureDelete",id:e,dn:s,fqdn:n,domain:a,result:"stashed"})}catch(t){this.logger.warn({plugin:this.name,event:"captureDelete",id:e,error:`${t}`})}}async deprovision(e){this.logger.info({plugin:this.name,event:"deprovision",id:e});const t=this.deleteStash.get(e);if(!t)return void this.logger.info({plugin:this.name,event:"deprovision",id:e,result:"skipped",reason:"no_stashed_context"});this.deleteStash.delete(e);const r={plugin:this.name,event:"deprovision",id:e,fqdn:t.fqdn};let i=!1;try{this.logger.info({...r,result:"resolving_uuid"});const e=(await this.findInstanceByFqdn(t.fqdn,"_id"))?._id;e?(this.logger.info({...r,result:"deleting_instance",uuid:e}),await this.deleteInstance(e),i=!0,this.logger.info({...r,result:"deleted",uuid:e})):this.logger.warn({...r,result:"not_found"})}catch(e){this.logger.error({...r,result:"error",error:`${e}`})}i&&await this.publishUserDeleted(t.fqdn,t.domain)}async createInstance(e){this.logger.info({plugin:this.name,event:"createInstance",url:`${this.managerUrl}/api/v1/instances`});const t=await this.fetch(`${this.managerUrl}/api/v1/instances`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.managerToken}`},body:JSON.stringify(e)});if(!t.ok){const e=(await t.text().catch(()=>"")).slice(0,g);throw this.logger.error({plugin:this.name,event:"createInstance",result:"error",http_status:t.status,http_status_text:t.statusText,body:e||void 0}),new Error(`Cloudery createInstance HTTP ${t.status}`)}const r=await t.json();return this.logger.info({plugin:this.name,event:"createInstance",result:"success",id:r.id,fqdn:r.fqdn,workflow:r.workflow}),r}async waitForWorkflow(t){this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,maxPollAttempts:this.maxPollAttempts,pollIntervalMs:this.pollIntervalMs});for(let r=1;r<=this.maxPollAttempts;r++){try{const e=await this.fetch(`${this.managerUrl}/api/v1/workflows/${encodeURIComponent(t)}`,{headers:{Authorization:`Bearer ${this.managerToken}`}});if(e.ok){const i=await e.json();if(this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,status:i.status}),"succeeded"===i.status)return this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,result:"success"}),!0;if("failed"===i.status||"error"===i.status)return this.logger.warn({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,result:"failed",status:i.status}),!1}else{const i=(await e.text().catch(()=>"")).slice(0,g);this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,result:"http_error",http_status:e.status,http_status_text:e.statusText,body:i||void 0})}}catch(e){this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,error:`${e}`})}r<this.maxPollAttempts&&(this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,attempt:r,result:"retrying"}),await e(this.pollIntervalMs))}return this.logger.info({plugin:this.name,event:"waitForWorkflow",workflow:t,result:"timeout",maxPollAttempts:this.maxPollAttempts}),!1}async findInstanceByFqdn(e,t){this.logger.info({plugin:this.name,event:"findInstanceByFqdn",fqdn:e});const r=new URL(`${this.managerUrl}/api/v2/instances`);r.searchParams.set("fqdn",e),t&&r.searchParams.set("only",t),r.searchParams.set("limit","1");const i=await this.fetch(r.toString(),{headers:{Authorization:`Bearer ${this.managerToken}`}});if(!i.ok){const t=(await i.text().catch(()=>"")).slice(0,g);throw this.logger.error({plugin:this.name,event:"findInstanceByFqdn",fqdn:e,result:"error",http_status:i.status,http_status_text:i.statusText,body:t||void 0}),new Error(`Cloudery searchInstances HTTP ${i.status}`)}const s=await i.json(),n=s.items?.[0]??null,o=n?._id;return this.logger.info({plugin:this.name,event:"findInstanceByFqdn",fqdn:e,result:o?"found":"not_found",uuid:o||void 0}),n}async deleteInstance(e){const t=new URL(`${this.managerUrl}/api/v1/instances/${encodeURIComponent(e)}`);t.searchParams.set("user_request","true"),this.logger.info({plugin:this.name,event:"deleteInstance",uuid:e,url:t.toString()});const r=await this.fetch(t.toString(),{method:"DELETE",headers:{Authorization:`Bearer ${this.managerToken}`}});if(!r.ok){const t=(await r.text().catch(()=>"")).slice(0,g);throw this.logger.error({plugin:this.name,event:"deleteInstance",uuid:e,result:"error",http_status:r.status,http_status_text:r.statusText,body:t||void 0}),new Error(`Cloudery deleteInstance HTTP ${r.status}`)}this.logger.info({plugin:this.name,event:"deleteInstance",uuid:e,result:"success",http_status:r.status})}async publishUserCreated(e){const t=this.requirePlugin("rabbitmq");if(this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,orgId:e.orgId,orgDomain:e.orgDomain,hasRabbitmq:Boolean(t)}),!t)return void this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,result:"skipped",reason:"missing_rabbitmq"});const r={twakeId:e.twakeId,domain:e.orgDomain,organizationDomain:e.orgDomain,workplaceFqdn:e.fqdn,organizationId:e.orgId};e.email&&(r.internalEmail=e.email),e.mobile&&(r.mobile=e.mobile);try{await t.publish(this.authExchange,this.userCreatedRoutingKey,r),this.logger.info({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,exchange:this.authExchange,routingKey:this.userCreatedRoutingKey,result:"success"})}catch(t){this.logger.error({plugin:this.name,event:"publishUserCreated",twakeId:e.twakeId,error:`${t}`})}}async publishUserDeleted(e,t){if(!this.userDeletedRoutingKey)return;const r=this.requirePlugin("rabbitmq");if(this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,domain:t,hasRabbitmq:Boolean(r)}),r)try{await r.publish(this.b2bExchange,this.userDeletedRoutingKey,{workplaceFqdn:e,domain:t}),this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,exchange:this.b2bExchange,routingKey:this.userDeletedRoutingKey,result:"success"})}catch(t){this.logger.error({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,error:`${t}`})}else this.logger.info({plugin:this.name,event:"publishUserDeleted",workplaceFqdn:e,result:"skipped",reason:"missing_rabbitmq"})}readHeader(e,t){if(!e)return;const r=e.get?e.get(t):void 0;if("string"==typeof r&&r.length>0)return r;const i=e.query?.[t];return"string"==typeof i&&i.length>0?i:void 0}resolveRole(e,t){const r=this.readHeader(e,this.orgRoleHeader)?.trim().toLowerCase();return r?h.has(r)?(this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,input:r,result:"accepted",role:r}),r):(this.logger.warn({plugin:this.name,event:"captureCreate",userName:t,message:`Unknown ${this.orgRoleHeader} "${r}" — falling back to ${this.defaultOrgRole}`}),this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,input:r,result:"default",role:this.defaultOrgRole}),this.defaultOrgRole):(this.logger.debug({plugin:this.name,event:"resolveRole",userName:t,result:"default",role:this.defaultOrgRole}),this.defaultOrgRole)}isAtOrUnder(e,t){return e.trim().toLowerCase()===t.trim().toLowerCase()||s(e,t)}pruneStash(){const e=Date.now()-3e5;for(const[t,r]of this.createStash)r.ts<e&&this.createStash.delete(t)}normalizeUserName(e){"string"==typeof e.userName&&(e.userName=e.userName.toLowerCase())}extractId(e){return"string"==typeof e.userName&&e.userName.length>0?e.userName:"string"==typeof e.id&&e.id.length>0?e.id:null}extractPrimaryEmail(e){const t=e.emails;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPrimaryPhone(e){const t=e.phoneNumbers;if(!t||0===t.length)return null;const r=(t.find(e=>e.primary)||t[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPhones(e){const t=e.phoneNumbers;return t&&0!==t.length?t.filter(e=>"string"==typeof e.value&&e.value.trim().length>0).map(e=>({number:e.value.trim(),primary:Boolean(e.primary)})):[]}extractPublicName(e){if("string"==typeof e.displayName&&e.displayName.trim().length>0)return e.displayName.trim();const t=e.name?.formatted;if("string"==typeof t&&t.trim().length>0)return t.trim();const r=`${e.name?.givenName?.trim()??""} ${e.name?.familyName?.trim()??""}`.trim();return r.length>0?r:null}extractLocale(e){return"string"==typeof e.locale&&e.locale.length>0?e.locale:"string"==typeof e.preferredLanguage&&e.preferredLanguage.length>0?e.preferredLanguage:this.defaultLocale}}function emailDomain(e){const t=e.lastIndexOf("@");return t>=0?e.slice(t+1):""}function firstValue(e){if(Array.isArray(e)){const t=e[0];return"string"==typeof t?t:void 0}return"string"==typeof e?e:void 0}export{ClouderyProvision as default};
@@ -0,0 +1 @@
1
+ import{defineOptions as _,opt as e,readOptions as t}from"../../config/options.js";const n=_({cozy_admin_url:e.string({env:"DM_COZY_ADMIN_URL",default:""}),cozy_admin_user:e.string({env:"DM_COZY_ADMIN_USER",default:"admin"}),cozy_admin_passphrase:e.string({env:"DM_COZY_ADMIN_PASSPHRASE",default:""}),cozy_org_id:e.string({env:"DM_COZY_ORG_ID",default:""}),cozy_org_domain:e.string({env:"DM_COZY_ORG_DOMAIN",default:""}),cozy_default_locale:e.string({env:"DM_COZY_DEFAULT_LOCALE",default:"fr"}),cozy_context_name:e.string({env:"DM_COZY_CONTEXT_NAME",default:"default"}),cozy_apps:e.string({env:"DM_COZY_APPS",default:"home,drive,settings,notes,dataproxy",emptyIsValue:!0}),cozy_auth_exchange:e.string({env:"DM_COZY_AUTH_EXCHANGE",default:"auth"}),cozy_b2b_exchange:e.string({env:"DM_COZY_B2B_EXCHANGE",default:"b2b"}),cozy_user_created_routing_key:e.string({env:"DM_COZY_USER_CREATED_ROUTING_KEY",default:"user.created"}),cozy_user_deleted_routing_key:e.string({env:"DM_COZY_USER_DELETED_ROUTING_KEY",default:"domain.user.deleted",emptyIsValue:!0})});function cozyOptionsOf(_){return t(n,_)}export{n as cozyOptions,cozyOptionsOf};
@@ -1 +1 @@
1
- import{Buffer as t}from"buffer";import e from"../../abstract/plugin.js";class CozyProvision extends e{constructor(e){super(e),this.name="cozyProvision",this.roles=["consistency"],this.dependencies={rabbitmq:"core/rabbitmq"},this.hooks={scimusercreatedone:async t=>{await this.createCozyInstance(t)&&await this.publishUserCreated(t)},scimuserdeletedone:async t=>{await this.destroyCozyInstance(t)&&await this.publishUserDeleted(t)}},this.cozyAdminUrl=(this.config.cozy_admin_url||"").replace(/\/$/,""),this.cozyAdminUser=this.config.cozy_admin_user||"admin",this.cozyAdminPassphrase=this.config.cozy_admin_passphrase||"",this.cozyOrgId=this.config.cozy_org_id||"",this.cozyOrgDomain=this.config.cozy_org_domain||"",this.cozyDefaultLocale=this.config.cozy_default_locale||"fr",this.cozyContextName=this.config.cozy_context_name||"default",this.cozyApps=this.config.cozy_apps??"",this.authExchange=this.config.cozy_auth_exchange||"auth",this.b2bExchange=this.config.cozy_b2b_exchange||"b2b",this.userCreatedRoutingKey=this.config.cozy_user_created_routing_key||"user.created",this.userDeletedRoutingKey=this.config.cozy_user_deleted_routing_key??"domain.user.deleted",this.cozyAdminAuthHeader=`Basic ${t.from(`${this.cozyAdminUser}:${this.cozyAdminPassphrase}`).toString("base64")}`,this.cozyAdminUrl||this.logger.warn(`${this.name}: cozy_admin_url is empty — Cozy instance creation will be skipped`),this.cozyOrgDomain||this.logger.warn(`${this.name}: cozy_org_domain is empty — workplaceFqdn cannot be composed`)}async createCozyInstance(t){if(!this.cozyAdminUrl)return!1;const e=this.extractId(t);if(!e)return this.logger.warn({plugin:this.name,event:"createCozyInstance",message:"user has no userName/id — skipping"}),!1;if(!this.cozyOrgDomain)return this.logger.error({plugin:this.name,event:"createCozyInstance",id:e,message:"cozy_org_domain not configured — cannot compose Domain"}),!1;const s=`${e}.${this.cozyOrgDomain}`,r=new URLSearchParams;r.set("Domain",s),r.set("Locale",this.extractLocale(t));const i=this.extractPrimaryEmail(t);i&&r.set("Email",i);const n=this.extractPublicName(t);n&&r.set("PublicName",n);const o=this.extractPrimaryPhone(t);o&&r.set("Phone",o),this.cozyOrgId&&r.set("OrgID",this.cozyOrgId),this.cozyOrgDomain&&r.set("OrgDomain",this.cozyOrgDomain),this.cozyContextName&&r.set("ContextName",this.cozyContextName),this.cozyApps&&r.set("Apps",this.cozyApps),r.set("OIDCID",e);const a=`${this.cozyAdminUrl}/instances?${r.toString()}`,c={plugin:this.name,event:"createCozyInstance",id:e,domain:s,email:i||void 0};try{const t=await this.fetch(a,{method:"POST",headers:{Authorization:this.cozyAdminAuthHeader,Accept:"application/json"}});return t.ok?(this.logger.info({...c,result:"success",http_status:t.status}),await this.markInstanceOnboarded(s),!0):409===t.status?(this.logger.info({...c,result:"already_exists",http_status:t.status}),!0):(this.logger.error({...c,result:"error",http_status:t.status,http_status_text:t.statusText}),!1)}catch(t){return this.logger.error({...c,result:"error",error:`${t}`}),!1}}async markInstanceOnboarded(t){const e=`${this.cozyAdminUrl}/instances/${encodeURIComponent(t)}?OnboardingFinished=true`,s={plugin:this.name,event:"markInstanceOnboarded",domain:t};try{const t=await this.fetch(e,{method:"PATCH",headers:{Authorization:this.cozyAdminAuthHeader,Accept:"application/json"}});if(!t.ok)return void this.logger.error({...s,result:"error",http_status:t.status,http_status_text:t.statusText});this.logger.info({...s,result:"success",http_status:t.status})}catch(t){this.logger.error({...s,result:"error",error:`${t}`})}}async destroyCozyInstance(t){if(!this.cozyAdminUrl)return!1;if(!this.cozyOrgDomain)return this.logger.error({plugin:this.name,event:"destroyCozyInstance",id:t,message:"cozy_org_domain not configured — cannot compose Domain"}),!1;const e=`${t}.${this.cozyOrgDomain}`,s=`${this.cozyAdminUrl}/instances/${encodeURIComponent(e)}`,r={plugin:this.name,event:"destroyCozyInstance",id:t,domain:e};try{const t=await this.fetch(s,{method:"DELETE",headers:{Authorization:this.cozyAdminAuthHeader,Accept:"application/json"}});return t.ok||404===t.status?(this.logger.info({...r,result:t.ok?"success":"not_found",http_status:t.status}),!0):(this.logger.error({...r,result:"error",http_status:t.status,http_status_text:t.statusText}),!1)}catch(t){return this.logger.error({...r,result:"error",error:`${t}`}),!1}}async publishUserCreated(t){const e=this.rabbitmq();if(!e)return;const s=this.extractId(t);if(!s)return;const r=this.extractPrimaryEmail(t),i=this.extractPrimaryPhone(t),n={twakeId:s,domain:this.cozyOrgDomain,organizationDomain:this.cozyOrgDomain,workplaceFqdn:`${s}.${this.cozyOrgDomain}`};r&&(n.internalEmail=r),i&&(n.mobile=i);try{await e.publish(this.authExchange,this.userCreatedRoutingKey,n),this.logger.info({plugin:this.name,event:"publishUserCreated",result:"success",exchange:this.authExchange,routingKey:this.userCreatedRoutingKey,twakeId:s})}catch(t){this.logger.error({plugin:this.name,event:"publishUserCreated",twakeId:s,error:`${t}`})}}async publishUserDeleted(t){if(!this.userDeletedRoutingKey)return;const e=this.rabbitmq();if(!e)return;if(!this.cozyOrgDomain)return void this.logger.error({plugin:this.name,event:"publishUserDeleted",id:t,message:"cozy_org_domain not configured — cannot compose workplaceFqdn"});const s={workplaceFqdn:`${t}.${this.cozyOrgDomain}`,domain:this.cozyOrgDomain};try{await e.publish(this.b2bExchange,this.userDeletedRoutingKey,s),this.logger.info({plugin:this.name,event:"publishUserDeleted",result:"success",exchange:this.b2bExchange,routingKey:this.userDeletedRoutingKey,workplaceFqdn:s.workplaceFqdn})}catch(e){this.logger.error({plugin:this.name,event:"publishUserDeleted",id:t,error:`${e}`})}}rabbitmq(){return this.requirePlugin("rabbitmq")}extractId(t){return"string"==typeof t.userName&&t.userName.length>0?t.userName:"string"==typeof t.id&&t.id.length>0?t.id:null}extractPrimaryEmail(t){const e=t.emails;if(!e||0===e.length)return null;const s=(e.find(t=>t.primary)||e[0]).value;return"string"==typeof s&&s.length>0?s:null}extractPrimaryPhone(t){const e=t.phoneNumbers;if(!e||0===e.length)return null;const s=(e.find(t=>t.primary)||e[0]).value;return"string"==typeof s&&s.length>0?s:null}extractPublicName(t){if("string"==typeof t.displayName&&t.displayName.trim().length>0)return t.displayName.trim();const e=t.name?.formatted;if("string"==typeof e&&e.trim().length>0)return e.trim();const s=`${t.name?.givenName?.trim()??""} ${t.name?.familyName?.trim()??""}`.trim();return s.length>0?s:this.extractId(t)}extractLocale(t){return"string"==typeof t.locale&&t.locale.length>0?t.locale:"string"==typeof t.preferredLanguage&&t.preferredLanguage.length>0?t.preferredLanguage:this.cozyDefaultLocale}}export{CozyProvision as default};
1
+ import{Buffer as t}from"buffer";import e from"../../abstract/plugin.js";import{cozyOptions as r,cozyOptionsOf as s}from"./cozy.options.js";class CozyProvision extends e{static optionDefinitions=r;name="cozyProvision";roles=["consistency"];dependencies={rabbitmq:"core/rabbitmq"};cozyAdminUrl;cozyAdminPassphrase;cozyAdminUser;cozyOrgId;cozyOrgDomain;cozyDefaultLocale;cozyContextName;cozyApps;authExchange;b2bExchange;userCreatedRoutingKey;userDeletedRoutingKey;cozyAdminAuthHeader;constructor(e){super(e);const r=s(e.config);this.cozyAdminUrl=r.cozy_admin_url.replace(/\/$/,""),this.cozyAdminUser=r.cozy_admin_user,this.cozyAdminPassphrase=r.cozy_admin_passphrase,this.cozyOrgId=r.cozy_org_id,this.cozyOrgDomain=r.cozy_org_domain,this.cozyDefaultLocale=r.cozy_default_locale,this.cozyContextName=r.cozy_context_name,this.cozyApps=r.cozy_apps,this.authExchange=r.cozy_auth_exchange,this.b2bExchange=r.cozy_b2b_exchange,this.userCreatedRoutingKey=r.cozy_user_created_routing_key,this.userDeletedRoutingKey=r.cozy_user_deleted_routing_key,this.cozyAdminAuthHeader=`Basic ${t.from(`${this.cozyAdminUser}:${this.cozyAdminPassphrase}`).toString("base64")}`,this.cozyAdminUrl||this.logger.warn(`${this.name}: cozy_admin_url is empty — Cozy instance creation will be skipped`),this.cozyOrgDomain||this.logger.warn(`${this.name}: cozy_org_domain is empty — workplaceFqdn cannot be composed`)}hooks={scimusercreatedone:async t=>{await this.createCozyInstance(t)&&await this.publishUserCreated(t)},scimuserdeletedone:async t=>{await this.destroyCozyInstance(t)&&await this.publishUserDeleted(t)}};async createCozyInstance(t){if(!this.cozyAdminUrl)return!1;const e=this.extractId(t);if(!e)return this.logger.warn({plugin:this.name,event:"createCozyInstance",message:"user has no userName/id — skipping"}),!1;if(!this.cozyOrgDomain)return this.logger.error({plugin:this.name,event:"createCozyInstance",id:e,message:"cozy_org_domain not configured — cannot compose Domain"}),!1;const r=`${e}.${this.cozyOrgDomain}`,s=new URLSearchParams;s.set("Domain",r),s.set("Locale",this.extractLocale(t));const i=this.extractPrimaryEmail(t);i&&s.set("Email",i);const o=this.extractPublicName(t);o&&s.set("PublicName",o);const n=this.extractPrimaryPhone(t);n&&s.set("Phone",n),this.cozyOrgId&&s.set("OrgID",this.cozyOrgId),this.cozyOrgDomain&&s.set("OrgDomain",this.cozyOrgDomain),this.cozyContextName&&s.set("ContextName",this.cozyContextName),this.cozyApps&&s.set("Apps",this.cozyApps),s.set("OIDCID",e);const a=`${this.cozyAdminUrl}/instances?${s.toString()}`,c={plugin:this.name,event:"createCozyInstance",id:e,domain:r,email:i||void 0};try{const t=await this.fetch(a,{method:"POST",headers:{Authorization:this.cozyAdminAuthHeader,Accept:"application/json"}});return t.ok?(this.logger.info({...c,result:"success",http_status:t.status}),await this.markInstanceOnboarded(r),!0):409===t.status?(this.logger.info({...c,result:"already_exists",http_status:t.status}),!0):(this.logger.error({...c,result:"error",http_status:t.status,http_status_text:t.statusText}),!1)}catch(t){return this.logger.error({...c,result:"error",error:`${t}`}),!1}}async markInstanceOnboarded(t){const e=`${this.cozyAdminUrl}/instances/${encodeURIComponent(t)}?OnboardingFinished=true`,r={plugin:this.name,event:"markInstanceOnboarded",domain:t};try{const t=await this.fetch(e,{method:"PATCH",headers:{Authorization:this.cozyAdminAuthHeader,Accept:"application/json"}});if(!t.ok)return void this.logger.error({...r,result:"error",http_status:t.status,http_status_text:t.statusText});this.logger.info({...r,result:"success",http_status:t.status})}catch(t){this.logger.error({...r,result:"error",error:`${t}`})}}async destroyCozyInstance(t){if(!this.cozyAdminUrl)return!1;if(!this.cozyOrgDomain)return this.logger.error({plugin:this.name,event:"destroyCozyInstance",id:t,message:"cozy_org_domain not configured — cannot compose Domain"}),!1;const e=`${t}.${this.cozyOrgDomain}`,r=`${this.cozyAdminUrl}/instances/${encodeURIComponent(e)}`,s={plugin:this.name,event:"destroyCozyInstance",id:t,domain:e};try{const t=await this.fetch(r,{method:"DELETE",headers:{Authorization:this.cozyAdminAuthHeader,Accept:"application/json"}});return t.ok||404===t.status?(this.logger.info({...s,result:t.ok?"success":"not_found",http_status:t.status}),!0):(this.logger.error({...s,result:"error",http_status:t.status,http_status_text:t.statusText}),!1)}catch(t){return this.logger.error({...s,result:"error",error:`${t}`}),!1}}async publishUserCreated(t){const e=this.rabbitmq();if(!e)return;const r=this.extractId(t);if(!r)return;const s=this.extractPrimaryEmail(t),i=this.extractPrimaryPhone(t),o={twakeId:r,domain:this.cozyOrgDomain,organizationDomain:this.cozyOrgDomain,workplaceFqdn:`${r}.${this.cozyOrgDomain}`};s&&(o.internalEmail=s),i&&(o.mobile=i);try{await e.publish(this.authExchange,this.userCreatedRoutingKey,o),this.logger.info({plugin:this.name,event:"publishUserCreated",result:"success",exchange:this.authExchange,routingKey:this.userCreatedRoutingKey,twakeId:r})}catch(t){this.logger.error({plugin:this.name,event:"publishUserCreated",twakeId:r,error:`${t}`})}}async publishUserDeleted(t){if(!this.userDeletedRoutingKey)return;const e=this.rabbitmq();if(!e)return;if(!this.cozyOrgDomain)return void this.logger.error({plugin:this.name,event:"publishUserDeleted",id:t,message:"cozy_org_domain not configured — cannot compose workplaceFqdn"});const r={workplaceFqdn:`${t}.${this.cozyOrgDomain}`,domain:this.cozyOrgDomain};try{await e.publish(this.b2bExchange,this.userDeletedRoutingKey,r),this.logger.info({plugin:this.name,event:"publishUserDeleted",result:"success",exchange:this.b2bExchange,routingKey:this.userDeletedRoutingKey,workplaceFqdn:r.workplaceFqdn})}catch(e){this.logger.error({plugin:this.name,event:"publishUserDeleted",id:t,error:`${e}`})}}rabbitmq(){return this.requirePlugin("rabbitmq")}extractId(t){return"string"==typeof t.userName&&t.userName.length>0?t.userName:"string"==typeof t.id&&t.id.length>0?t.id:null}extractPrimaryEmail(t){const e=t.emails;if(!e||0===e.length)return null;const r=(e.find(t=>t.primary)||e[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPrimaryPhone(t){const e=t.phoneNumbers;if(!e||0===e.length)return null;const r=(e.find(t=>t.primary)||e[0]).value;return"string"==typeof r&&r.length>0?r:null}extractPublicName(t){if("string"==typeof t.displayName&&t.displayName.trim().length>0)return t.displayName.trim();const e=t.name?.formatted;if("string"==typeof e&&e.trim().length>0)return e.trim();const r=`${t.name?.givenName?.trim()??""} ${t.name?.familyName?.trim()??""}`.trim();return r.length>0?r:this.extractId(t)}extractLocale(t){return"string"==typeof t.locale&&t.locale.length>0?t.locale:"string"==typeof t.preferredLanguage&&t.preferredLanguage.length>0?t.preferredLanguage:this.cozyDefaultLocale}}export{CozyProvision as default};
@@ -1 +1 @@
1
- import{TwakePlugin as t}from"../../abstract/twakePlugin.js";class Drive extends t{constructor(t){super(t,"twake_drive_webadmin_url","twake_drive_webadmin_token","twake_drive_concurrency"),this.name="drive",this.roles=["consistency"],this.dependencies={onLdapChange:"core/ldap/onChange"},this.hooks={onLdapMailChange:async(t,e,i)=>{const n=e?Array.isArray(e)?String(e[0]):String(e):null,a=i?Array.isArray(i)?String(i[0]):String(i):null;if(!n)return void this.logger.debug(`Skipping mail change for ${t}: oldmail is empty (mail attribute was added, not changed)`);if(!a)return void this.logger.debug(`Skipping mail change for ${t}: newmail is empty (mail attribute was deleted)`);const r=await this.getCozyDomain(t);r?await this.updateCozyInstance("onLdapMailChange",r,t,{Email:a}):this.logger.debug(`Skipping mail change for ${t}: no Cozy domain attribute (${this.cozyDomainAttr})`)},onLdapDisplayNameChange:async(t,e,i)=>{const n=await this.ldapGetAttributes(t,[this.cozyDomainAttr,this.displayNameAttr,"cn","givenName","sn"]);if(!n)return void this.logger.warn(`Cannot update Cozy display name: entry not found for ${t}`);const a=this.attributeToString(n[this.cozyDomainAttr]);if(!a)return void this.logger.debug(`Skipping display name change for ${t}: no Cozy domain attribute (${this.cozyDomainAttr})`);const r=this.getDisplayNameFromAttributes(n);r?await this.updateCozyInstance("onLdapDisplayNameChange",a,t,{PublicName:r}):this.logger.warn(`Cannot update Cozy display name: no display name found for ${t}`)},onLdapDriveQuotaChange:async(t,e,i)=>{if(null===i)return void this.logger.debug(`Skipping drive quota change for ${t}: quota was deleted`);const n=await this.getCozyDomain(t);n?await this.updateCozyInstance("onLdapDriveQuotaChange",n,t,{DiskQuota:String(i)}):this.logger.debug(`Skipping drive quota change for ${t}: no Twake Drive domain attribute (${this.cozyDomainAttr})`)}},this.cozyDomainAttr=this.config.twake_drive_domain_attribute||"twakeCozyDomain",this.defaultDomainTemplate=this.config.twake_drive_default_domain_template||"",this.templateAttrs=this.defaultDomainTemplate?[...this.defaultDomainTemplate.matchAll(/\{(\w+)\}/g)].map(t=>t[1]):[],this.driveQuotaAttr=this.config.drive_quota_attribute||"twakeDriveQuota"}createHeaders(t){const e={};if(this.webadminToken){const t=Buffer.from(`:${this.webadminToken}`).toString("base64");e.Authorization=`Basic ${t}`}return t&&(e["Content-Type"]=t),e}isValidCozyDomain(t){if(!t||t.length>253)return!1;if(t.includes("/")||t.includes("\\")||t.includes("?")||t.includes("#"))return!1;return/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(t)}async getCozyDomain(t){const e=[this.cozyDomainAttr,...this.templateAttrs],i=await this.ldapGetAttributes(t,e),n=i?this.attributeToString(i[this.cozyDomainAttr]):null;if(n)return n;if(this.defaultDomainTemplate&&i){let t=this.defaultDomainTemplate;for(const e of this.templateAttrs){const n=this.attributeToString(i[e]);if(!n)return null;t=t.replace(`{${e}}`,n)}return t}return null}async updateCozyInstance(t,e,i,n){if(!this.isValidCozyDomain(e))return void this.logger.error({plugin:this.name,event:t,result:"error",dn:i,cozyDomain:e,error:"Invalid Cozy domain format (potential URL injection attempt)"});const a=new URL(`${this.webadminUrl}/instances/${e}`);a.searchParams.set("FromCloudery","true");for(const[t,e]of Object.entries(n))a.searchParams.set(t,e);return this.requestLimit(async()=>{const r={plugin:this.name,event:t,result:"error",dn:i,cozyDomain:e,...n};try{const t=await this.fetch(a.toString(),{method:"PATCH",headers:this.createHeaders("application/json")});t.ok?this.logger.info({...r,result:"success",http_status:t.status}):404===t.status?this.logger.debug({...r,result:"ignored",http_status:t.status,http_status_text:t.statusText,message:"Cozy instance not found (may not be provisioned yet)"}):this.logger.error({...r,http_status:t.status,http_status_text:t.statusText,url:a.toString()})}catch(t){this.logger.error({...r,error:t,url:a.toString()})}})}getDisplayNameFromAttributes(t){const e=this.attributeToString(t[this.displayNameAttr]);if(e)return e;const i=this.attributeToString(t.cn);if(i)return i;const n=this.attributeToString(t.givenName),a=this.attributeToString(t.sn);if(n||a){const t=[];return n&&t.push(n),a&&t.push(a),t.join(" ")}return null}async getDisplayNameFromDN(t){const e=[this.displayNameAttr,"cn","givenName","sn"],i=await this.ldapGetAttributes(t,e);return i?this.getDisplayNameFromAttributes(i):null}async getMailFromDN(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}async getDriveQuotaFromDN(t){const e=await this.ldapGetAttributes(t,[this.driveQuotaAttr]);if(!e)return null;const i=this.attributeToString(e[this.driveQuotaAttr]);return i?Number(i):null}async syncUserToCozy(t){const e=await this.ldapGetAttributes(t,[this.cozyDomainAttr,this.mailAttr,this.displayNameAttr,this.driveQuotaAttr,"cn","givenName","sn"]);if(!e)return this.logger.error(`Cannot sync user: entry not found for ${t}`),!1;const i=this.attributeToString(e[this.cozyDomainAttr]);if(!i)return this.logger.error(`Cannot sync user: no Twake Drive domain attribute for ${t}`),!1;const n=this.attributeToString(e[this.mailAttr]),a=this.getDisplayNameFromAttributes(e),r=this.attributeToString(e[this.driveQuotaAttr]),s={};return n&&(s.Email=n),a&&(s.PublicName=a),r&&(s.DiskQuota=r),0===Object.keys(s).length?(this.logger.warn(`No attributes to sync for ${t}`),!1):(await this.updateCozyInstance("syncUserToCozy",i,t,s),!0)}async blockInstance(t,e){const i=await this.getCozyDomain(t);if(!i)return this.logger.error(`Cannot block instance: no Cozy domain for ${t}`),!1;const n={Blocked:"true"};return e&&(n.BlockingReason=e),await this.updateCozyInstance("blockInstance",i,t,n),!0}async unblockInstance(t){const e=await this.getCozyDomain(t);return e?(await this.updateCozyInstance("unblockInstance",e,t,{Blocked:"false"}),!0):(this.logger.error(`Cannot unblock instance: no Cozy domain for ${t}`),!1)}async deleteInstance(t){const e=await this.getCozyDomain(t);if(!e)return this.logger.error(`Cannot delete instance: no Cozy domain for ${t}`),!1;if(!this.isValidCozyDomain(e))return this.logger.error({plugin:this.name,event:"deleteInstance",result:"error",dn:t,cozyDomain:e,error:"Invalid Cozy domain format (potential URL injection attempt)"}),!1;const i=`${this.webadminUrl}/instances/${e}`;return this.requestLimit(async()=>{try{const n=await this.fetch(i,{method:"DELETE",headers:this.createHeaders()});return n.ok?(this.logger.info({plugin:this.name,event:"deleteInstance",result:"success",dn:t,cozyDomain:e,http_status:n.status}),!0):404===n.status?(this.logger.info({plugin:this.name,event:"deleteInstance",result:"ignored",dn:t,cozyDomain:e,http_status:404,message:"Cozy instance not found (already deleted or never created)"}),!0):(this.logger.error({plugin:this.name,event:"deleteInstance",result:"error",dn:t,cozyDomain:e,http_status:n.status,http_status_text:n.statusText,url:i}),!1)}catch(n){return this.logger.error({plugin:this.name,event:"deleteInstance",result:"error",dn:t,cozyDomain:e,error:n,url:i}),!1}})}}export{Drive as default};
1
+ import{TwakePlugin as t}from"../../abstract/twakePlugin.js";import{driveOptions as e,driveOptionsOf as i}from"./drive.options.js";class Drive extends t{static optionDefinitions=e;name="drive";roles=["consistency"];dependencies={onLdapChange:"core/ldap/onChange"};cozyDomainAttr;defaultDomainTemplate;templateAttrs;driveQuotaAttr;constructor(t){const e=i(t.config);super(t,{url:e.twake_drive_webadmin_url,token:e.twake_drive_webadmin_token,concurrency:e.twake_drive_concurrency,urlOption:"twake_drive_webadmin_url",tokenOption:"twake_drive_webadmin_token",concurrencyOption:"twake_drive_concurrency"}),this.cozyDomainAttr=e.twake_drive_domain_attribute,this.defaultDomainTemplate=e.twake_drive_default_domain_template,this.templateAttrs=this.defaultDomainTemplate?[...this.defaultDomainTemplate.matchAll(/\{(\w+)\}/g)].map(t=>t[1]):[],this.driveQuotaAttr=this.config.drive_quota_attribute||"twakeDriveQuota"}createHeaders(t){const e={};if(this.webadminToken){const t=Buffer.from(`:${this.webadminToken}`).toString("base64");e.Authorization=`Basic ${t}`}return t&&(e["Content-Type"]=t),e}isValidCozyDomain(t){if(!t||t.length>253)return!1;if(t.includes("/")||t.includes("\\")||t.includes("?")||t.includes("#"))return!1;return/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(t)}async getCozyDomain(t){const e=[this.cozyDomainAttr,...this.templateAttrs],i=await this.ldapGetAttributes(t,e),n=i?this.attributeToString(i[this.cozyDomainAttr]):null;if(n)return n;if(this.defaultDomainTemplate&&i){let t=this.defaultDomainTemplate;for(const e of this.templateAttrs){const n=this.attributeToString(i[e]);if(!n)return null;t=t.replace(`{${e}}`,()=>n)}return t}return null}async updateCozyInstance(t,e,i,n){if(!this.isValidCozyDomain(e))return void this.logger.error({plugin:this.name,event:t,result:"error",dn:i,cozyDomain:e,error:"Invalid Cozy domain format (potential URL injection attempt)"});const a=new URL(`${this.webadminUrl}/instances/${e}`);a.searchParams.set("FromCloudery","true");for(const[t,e]of Object.entries(n))a.searchParams.set(t,e);return this.requestLimit(async()=>{const r={plugin:this.name,event:t,result:"error",dn:i,cozyDomain:e,...n};try{const t=await this.fetch(a.toString(),{method:"PATCH",headers:this.createHeaders("application/json")});t.ok?this.logger.info({...r,result:"success",http_status:t.status}):404===t.status?this.logger.debug({...r,result:"ignored",http_status:t.status,http_status_text:t.statusText,message:"Cozy instance not found (may not be provisioned yet)"}):this.logger.error({...r,http_status:t.status,http_status_text:t.statusText,url:a.toString()})}catch(t){this.logger.error({...r,error:t,url:a.toString()})}})}hooks={onLdapMailChange:async(t,e,i)=>{const n=e?Array.isArray(e)?String(e[0]):String(e):null,a=i?Array.isArray(i)?String(i[0]):String(i):null;if(!n)return void this.logger.debug(`Skipping mail change for ${t}: oldmail is empty (mail attribute was added, not changed)`);if(!a)return void this.logger.debug(`Skipping mail change for ${t}: newmail is empty (mail attribute was deleted)`);const r=await this.getCozyDomain(t);r?await this.updateCozyInstance("onLdapMailChange",r,t,{Email:a}):this.logger.debug(`Skipping mail change for ${t}: no Cozy domain attribute (${this.cozyDomainAttr})`)},onLdapDisplayNameChange:async(t,e,i)=>{const n=await this.ldapGetAttributes(t,[this.cozyDomainAttr,this.displayNameAttr,"cn","givenName","sn"]);if(!n)return void this.logger.warn(`Cannot update Cozy display name: entry not found for ${t}`);const a=this.attributeToString(n[this.cozyDomainAttr]);if(!a)return void this.logger.debug(`Skipping display name change for ${t}: no Cozy domain attribute (${this.cozyDomainAttr})`);const r=this.getDisplayNameFromAttributes(n);r?await this.updateCozyInstance("onLdapDisplayNameChange",a,t,{PublicName:r}):this.logger.warn(`Cannot update Cozy display name: no display name found for ${t}`)},onLdapDriveQuotaChange:async(t,e,i)=>{if(null===i)return void this.logger.debug(`Skipping drive quota change for ${t}: quota was deleted`);const n=await this.getCozyDomain(t);n?await this.updateCozyInstance("onLdapDriveQuotaChange",n,t,{DiskQuota:String(i)}):this.logger.debug(`Skipping drive quota change for ${t}: no Twake Drive domain attribute (${this.cozyDomainAttr})`)}};getDisplayNameFromAttributes(t){const e=this.attributeToString(t[this.displayNameAttr]);if(e)return e;const i=this.attributeToString(t.cn);if(i)return i;const n=this.attributeToString(t.givenName),a=this.attributeToString(t.sn);if(n||a){const t=[];return n&&t.push(n),a&&t.push(a),t.join(" ")}return null}async getDisplayNameFromDN(t){const e=[this.displayNameAttr,"cn","givenName","sn"],i=await this.ldapGetAttributes(t,e);return i?this.getDisplayNameFromAttributes(i):null}async getMailFromDN(t){const e=await this.ldapGetAttributes(t,[this.mailAttr]);return e?this.attributeToString(e[this.mailAttr]):null}async getDriveQuotaFromDN(t){const e=await this.ldapGetAttributes(t,[this.driveQuotaAttr]);if(!e)return null;const i=this.attributeToString(e[this.driveQuotaAttr]);return i?Number(i):null}async syncUserToCozy(t){const e=await this.ldapGetAttributes(t,[this.cozyDomainAttr,this.mailAttr,this.displayNameAttr,this.driveQuotaAttr,"cn","givenName","sn"]);if(!e)return this.logger.error(`Cannot sync user: entry not found for ${t}`),!1;const i=this.attributeToString(e[this.cozyDomainAttr]);if(!i)return this.logger.error(`Cannot sync user: no Twake Drive domain attribute for ${t}`),!1;const n=this.attributeToString(e[this.mailAttr]),a=this.getDisplayNameFromAttributes(e),r=this.attributeToString(e[this.driveQuotaAttr]),o={};return n&&(o.Email=n),a&&(o.PublicName=a),r&&(o.DiskQuota=r),0===Object.keys(o).length?(this.logger.warn(`No attributes to sync for ${t}`),!1):(await this.updateCozyInstance("syncUserToCozy",i,t,o),!0)}async blockInstance(t,e){const i=await this.getCozyDomain(t);if(!i)return this.logger.error(`Cannot block instance: no Cozy domain for ${t}`),!1;const n={Blocked:"true"};return e&&(n.BlockingReason=e),await this.updateCozyInstance("blockInstance",i,t,n),!0}async unblockInstance(t){const e=await this.getCozyDomain(t);return e?(await this.updateCozyInstance("unblockInstance",e,t,{Blocked:"false"}),!0):(this.logger.error(`Cannot unblock instance: no Cozy domain for ${t}`),!1)}async deleteInstance(t){const e=await this.getCozyDomain(t);if(!e)return this.logger.error(`Cannot delete instance: no Cozy domain for ${t}`),!1;if(!this.isValidCozyDomain(e))return this.logger.error({plugin:this.name,event:"deleteInstance",result:"error",dn:t,cozyDomain:e,error:"Invalid Cozy domain format (potential URL injection attempt)"}),!1;const i=`${this.webadminUrl}/instances/${e}`;return this.requestLimit(async()=>{try{const n=await this.fetch(i,{method:"DELETE",headers:this.createHeaders()});return n.ok?(this.logger.info({plugin:this.name,event:"deleteInstance",result:"success",dn:t,cozyDomain:e,http_status:n.status}),!0):404===n.status?(this.logger.info({plugin:this.name,event:"deleteInstance",result:"ignored",dn:t,cozyDomain:e,http_status:404,message:"Cozy instance not found (already deleted or never created)"}),!0):(this.logger.error({plugin:this.name,event:"deleteInstance",result:"error",dn:t,cozyDomain:e,http_status:n.status,http_status_text:n.statusText,url:i}),!1)}catch(n){return this.logger.error({plugin:this.name,event:"deleteInstance",result:"error",dn:t,cozyDomain:e,error:n,url:i}),!1}})}}export{Drive as default};
@@ -0,0 +1 @@
1
+ import{defineOptions as _,opt as e,readOptions as t}from"../../config/options.js";const n=_({twake_drive_webadmin_url:e.string({env:"DM_TWAKE_DRIVE_WEBADMIN_URL",default:""}),twake_drive_webadmin_token:e.string({env:"DM_TWAKE_DRIVE_WEBADMIN_TOKEN",default:""}),twake_drive_concurrency:e.number({env:"DM_TWAKE_DRIVE_CONCURRENCY",default:10,min:1}),twake_drive_domain_attribute:e.string({env:"DM_TWAKE_DRIVE_DOMAIN_ATTRIBUTE",default:"twakeCozyDomain"}),twake_drive_default_domain_template:e.string({env:"DM_TWAKE_DRIVE_DEFAULT_DOMAIN_TEMPLATE",default:""})});function driveOptionsOf(_){return t(n,_)}export{n as driveOptions,driveOptionsOf};