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{createHash as e}from"node:crypto";import{Client as t,Attribute as s,Change as i,Control as n}from"ldapts";import{LRUCache as a}from"lru-cache";import r from"p-limit";import{isAuthzRefusal as o,hookOwner as c,launchHooksChained as l,normalizeDn as h,isDnInBranch as d,knownAttributeName as p,launchHooks as f,rdnTypes as u,escapeDnValue as g,getParentDn as y}from"./utils.js";import{changeContext as w}from"./changeContext.js";import{runOperation as m,outsideOperation as b}from"./operation.js";import{outsideAnyRequest as C,outsideRequest as A,currentRequest as v}from"./requestContext.js";import{timedLdap as $,ldapPoolWait as D,secondsSince as L,ldapSearchCache as j,ldapResult as S,ldapOperationDuration as P}from"./metrics.js";import{BadRequestError as E,NotFoundError as x,ConflictError as O}from"./errors.js";import{extractLdapCode as R,isSchemaRefusal as _}from"./ldapCodes.js";import{SchemaIndex as k,parseSchema as U}from"./ldapSchema.js";const q={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}},T={ldapaddrequest:2,ldapmodifyrequest:3,ldaprenamerequest:2,ldapdeleterequest:1},N=new Set(["search","add","modify","rename","move","delete"]),isPlainObject=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function assertModifyRequest(e){const t=Object.keys(e).filter(e=>!["add","replace","delete"].includes(e));if(t.length>0)throw new E(`A modification carries add, replace or delete, not ${t.join(", ")}`);const{add:s,replace:i,delete:n}=e;for(const[e,t]of[["add",s],["replace",i]])if(void 0!==t&&!isPlainObject(t))throw new E(`"${e}" must map attributes to values`);if(!(void 0===n||isPlainObject(n)||Array.isArray(n)&&n.every(e=>"string"==typeof e)))throw new E('"delete" must list attribute names or map attributes to values');if(0===[s,i,n].reduce((e,t)=>e+(t?Object.keys(t).length:0),0))throw new E("A modification names at least one attribute in add, replace or delete")}function ldapError(e,t){const s=new Error(`${e}: ${String(t)}`),i=R(t);return void 0===i?s:withCode(s,i)}function withCode(e,t){return e.code=t,e}const digestKey=t=>e("sha256").update(t).digest("hex").slice(0,12),foldDn=e=>e.trim().toLowerCase().split(",").map(e=>e.trim()).join(","),B=new Set(["member","uniquemember"]),isMembershipAttribute=e=>B.has(e.toLowerCase()),membersNamed=e=>Object.entries(e??{}).flatMap(([e,t])=>isMembershipAttribute(e)?[t].flat().map(e=>Buffer.isBuffer(e)?e.toString():e).filter(e=>"string"==typeof e):[]),cloneSearchResult=e=>({searchEntries:e.searchEntries.map(e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?[...t]:t]))),searchReferences:[...e.searchReferences]}),I=["*","+","1.1"],dropSelectors=e=>{for(const t of e.searchEntries)for(const e of I){const s=t[e];Array.isArray(s)&&0===s.length&&delete t[e]}return e};function ldapRefusal(e,t,s){return withCode(new E(`${e}: ${String(s)}`),t)}function assertHasValue(e,t){if(Array.isArray(t)&&0===t.length)throw new E(`Attribute "${e}" has no value`)}function ldapWriteError(e,t,s){const i=R(s);return _(i)?ldapRefusal(`The directory refused this write to ${t}`,i,s):ldapError(e,s)}function ldapRenameError(e,t,s,i){const n=R(i);return 68===n?withCode(new O(`Entry ${s} already exists`),n):_(n)?ldapRefusal(`The directory refused to rename ${t} to ${s}`,n,i):ldapError(e,i)}class ldapActions{constructor(e){this.cacheGeneration=0,this.connectionPool=[],this.currentUrlIndex=0,this.waitingResolvers=[],this.availableConnections=[],this.isCleaningUp=!1,this.declaredClasses=new Map,this.schemaUnavailable=!1,this.spellings=new Map,this.spelledTypes=new Map,this.classWarnings=new a({max:1e4}),this.closed=!1,this.parent=e,this.logger=e.logger,this.config=e.config;for(const[e,t]of Object.entries(this.config))/_attributes?$/.test(e)&&this.knowAttributeNames([t].flat().filter(e=>"string"==typeof e));this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=r(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const s="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,i=this.config.ldap_cache_ttl,n="string"==typeof i?parseInt(i,10)||0:i??0,o=1e3*Math.max(n,0);if(this.cacheEnabled=o>0,this.searchCache=new a({max:s,ttl:this.cacheEnabled?o:void 0,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(this.cacheEnabled?`LDAP search cache initialized: max=${s}, ttl=${o/1e3}s`:"LDAP search cache disabled (--ldap-cache-ttl=0)"),this.attrSignatureCache=new a({max:1e3}),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP bind DN is not defined, please set --ldap-dn (or DM_LDAP_DN)");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined, please set --ldap-pwd (or DM_LDAP_PWD)");if(!e.config.ldap_base)throw new Error("LDAP base is not defined, please set --ldap-base (or DM_LDAP_BASE)");this.base=e.config.ldap_base,this.ldapUrls=e.config.ldap_url,this.logger.info(`LDAP failover configured with ${this.ldapUrls.length} URL(s): ${this.ldapUrls.join(", ")}`),this.options={url:this.ldapUrls[0],timeout:0,connectTimeout:0,strictDN:!1},this.ldapUrls[0].startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const e=[];for(let s=0;s<this.ldapUrls.length;s++){const i=(this.currentUrlIndex+s)%this.ldapUrls.length,n=this.ldapUrls[i];try{this.logger.debug(`Attempting connection to ${n}`);const e={...this.options,url:n,tlsOptions:n.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},s=new t(e);return await $("bind",()=>s.bind(this.dn,this.pwd)),i!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${n}`),this.currentUrlIndex=i),s}catch(t){this.logger.warn(`Failed to connect to ${n}: ${String(t)}`),e.push(t)}}throw this.logger.error(`LDAP connection failed for all ${this.ldapUrls.length} URL(s)`),new Error(`LDAP connection failed for all URLs: ${e.map(e=>e.message).join(", ")}`)}cleanupExpiredConnections(){if(!this.isCleaningUp){this.isCleaningUp=!0;try{const e=Date.now(),t=[];for(let s=this.availableConnections.length-1;s>=0;s--){const i=this.availableConnections[s];if(e-i.createdAt>this.connectionTtl){t.push(i),this.availableConnections.splice(s,1);const e=this.connectionPool.indexOf(i);-1!==e&&this.connectionPool.splice(e,1)}}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}finally{this.isCleaningUp=!1}}}async ping(e){if(this.closed)throw new Error("LDAP connections closed");const s=[];for(let i=0;i<this.ldapUrls.length;i++){const n=this.ldapUrls[(this.currentUrlIndex+i)%this.ldapUrls.length],a=new t({...this.options,url:n,timeout:e,connectTimeout:e,tlsOptions:n.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0});try{return await a.bind(this.dn,this.pwd),void await a.search("",{scope:"base",attributes:["1.1"]})}catch(e){s.push(`${n}: ${String(e)}`)}finally{await a.unbind().catch(()=>{})}}throw new Error(`No directory answered: ${s.join(", ")}`)}async close(){this.closed=!0;for(const e of this.waitingResolvers.splice(0))e(void 0);const e=this.connectionPool.splice(0);this.availableConnections.length=0,await Promise.allSettled(e.map(e=>e.client.unbind()))}async acquireConnection(){const e=process.hrtime.bigint();try{return await this.takeConnection()}finally{D.observe(L(e))}}poolStats(){return{busy:this.connectionPool.length-this.availableConnections.length,idle:this.availableConnections.length,waiting:this.waitingResolvers.length,max:this.poolSize}}async takeConnection(){if(this.closed)throw new Error("LDAP connections closed");if(this.cleanupExpiredConnections(),this.availableConnections.length>0){const e=this.availableConnections.pop();return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e}if(this.connectionPool.length<this.poolSize){const e=await C(()=>this.createConnection());if(this.closed)throw await e.unbind().catch(()=>{}),new Error("LDAP connections closed");const t={client:e,createdAt:Date.now(),inUse:!0};return this.connectionPool.push(t),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),t}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((e,t)=>{const s=globalThis.setTimeout(()=>{const e=this.waitingResolvers.indexOf(resolveWrapper);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),resolveWrapper=i=>{globalThis.clearTimeout(s),i?e(i):t(new Error("LDAP connections closed"))};this.waitingResolvers.push(resolveWrapper)})}releaseConnection(e){if(Date.now()-e.createdAt>this.connectionTtl){const t=this.connectionPool.indexOf(e);return-1!==t&&this.connectionPool.splice(t,1),e.client.unbind().catch(()=>{}),void this.logger.debug("Released expired LDAP connection (discarded)")}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t(e)}e.inUse=!1,this.availableConnections.push(e),this.logger.debug("Released LDAP connection back to pool")}getAttributeSignature(e){if(!e||0===e.length)return"*";const t=e.join("|");let s=this.attrSignatureCache.get(t);return s||(s=[...e].sort().join(","),this.attrSignatureCache.set(t,s)),s}getCacheKey(e,t){const s=this.getAttributeSignature(t.attributes),i=t.explicitBufferAttributes?.length?[...t.explicitBufferAttributes].sort().join(","):"-",n="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return[e,t.scope||"sub",n,s,i,!1===t.returnAttributeValues?"novalues":"values",t.derefAliases??"-",String(t.sizeLimit??"-")].join("\0")}invalidateCache(e){if(!this.cacheEnabled)return;this.cacheGeneration++;const t=foldDn(e),s=`,${t}`,i=[];for(const e of this.searchCache.keys()){const n=foldDn(e.split("\0")[0]);(n===t||n.endsWith(s))&&i.push(e)}for(const e of i)this.searchCache.delete(e)}invalidateReferences(e){if(!this.cacheEnabled)return;this.cacheGeneration++;const t=foldDn(e),s=`,${t}`,names=e=>{if("string"!=typeof e)return!1;const i=foldDn(e);return i===t||i.endsWith(s)},i=[];for(const[e,t]of this.searchCache.entries())t.searchEntries.some(e=>Object.values(e).some(e=>Array.isArray(e)?e.some(names):names(e)))&&i.push(e);for(const e of i)this.searchCache.delete(e)}get system(){return this.systemView??(this.systemView=new Proxy(this,{get:(e,t)=>{const s=Reflect.get(e,t,e);return"function"==typeof s&&N.has(t)?(...t)=>A(()=>s.apply(e,t)):s}})),this.systemView}forRequest(e){return{search:(t,s)=>this.search(t,s??this.base,e),add:(t,s)=>this.add(t,s,e),modify:(t,s,i)=>this.modify(t,s,e,i),rename:(t,s)=>this.rename(t,s,e),delete:t=>this.delete(t,e)}}async readableBy(e,t){if(t??(t=v()),!t)return!0;try{const s=await this.search({paged:!1,scope:"base",attributes:["1.1"]},e,t);return(s.searchEntries?.length??0)>0}catch(e){if(o(e)||403===e?.statusCode||32===R(e))return!1;throw e}}async authorize(e,t){const s=[...t];"ldapaddrequest"===e&&await this.canonicalAttributes(s[1]),"ldapmodifyrequest"===e&&await this.canonicalChanges(s[1]);const i=T[e];s[i]??(s[i]=v());const[n,...a]=s,r="ldapdeleterequest"===e?[[n].flat().map(e=>this.setDn(String(e))),...a]:[this.setDn(String(n)),...a];"ldaprenamerequest"===e&&(r[1]=this.setDn(String(r[1])));const o=(this.parent.hooks[e]??[]).filter(e=>{const t=c(e);return Boolean(t&&this.parent.loadedPlugins[t.plugin]?.roles?.includes("authz"))});await l(o,r)}declareObjectClasses(e,t,s=!1){const i=(Array.isArray(t)?t:[t]).filter(e=>"string"==typeof e&&""!==e);if(!e||0===i.length)return;const n=JSON.stringify([h(e),i.map(e=>e.toLowerCase()).sort(),s]);this.declaredClasses.set(n,{base:e,classes:i,includeBase:s})}declaredFor(e){let t=0,s=[];for(const i of this.declaredClasses.values()){if(!d(e,i.base))continue;const n=h(i.base);if(!i.includeBase&&h(e)===n)continue;const a=n.split(",").length;a>t&&(t=a,s=[]),a===t&&s.push(...i.classes)}const i=new Set;return s.filter(e=>{const t=e.toLowerCase();return!i.has(t)&&(i.add(t),!0)})}warnOnce(e){this.classWarnings.has(e)||(this.classWarnings.set(e,!0),this.logger.warn(e))}async classRepair(e,t,s=[]){if(!1===this.config.ldap_flat_auto_repair)return[];const i=[...this.declaredFor(e)];for(const e of s)i.some(t=>t.toLowerCase()===e.toLowerCase())||i.push(e);if(0===i.length)return[];const n=Array.isArray(t.delete)?t.delete:Object.entries(t.delete||{}).filter(([,e])=>!e||Array.isArray(e)&&!e.length).map(([e])=>e);if([...Object.keys(t.add||{}),...Object.keys(t.replace||{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete||{})].some(e=>"objectclass"===e.toLowerCase()))return[];let a;try{a=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){return this.schemaUnavailable||this.logger.warn(`Object classes are not repaired: the directory schema could not be read (${String(e)})`),this.schemaUnavailable=!0,[]}const attributeKey=e=>a.getAttributeType(e)?.oid??e.toLowerCase(),allowedBy=e=>{const{must:t,may:s}=a.resolveAttributes(e);return new Set([...t,...s].map(attributeKey))},valued=e=>Object.entries(e||{}).filter(([,e])=>Array.isArray(e)?e.length>0:null!=e&&""!==e).map(([e])=>e),r=[...valued(t.add),...valued(t.replace)],o=i.filter(e=>{const t=a.getObjectClass(e);if("AUXILIARY"!==t?.kind)return!1;const s=allowedBy([e]);return r.some(e=>s.has(attributeKey(e)))});if(0===o.length)return[];let c;try{c=(await this.system.search({paged:!1,scope:"base",attributes:["objectClass",...a.resolveAttributes(o).must]},e)).searchEntries[0]}catch{return[]}if(!c)return[];const l=c.objectClass??c.objectclass,h=(Array.isArray(l)?l:l?[l]:[]).map(String),d=allowedBy(h);let p=r.filter(e=>a.getAttributeType(e)&&!d.has(attributeKey(e)));if(0===p.length)return"allowed";const f=new Set(n.map(attributeKey)),u=new Set([...valued(c).filter(e=>!f.has(attributeKey(e))),...r].map(attributeKey)),{auxiliary:g,structural:y}=a.missingClasses(i,h),w=[];for(const t of g){const s=t.names[0]??t.oid,i=allowedBy([s]),n=p.filter(e=>i.has(attributeKey(e)));if(0===n.length)continue;const r=a.resolveAttributes([s]).must.filter(e=>!u.has(attributeKey(e)));r.length>0?this.warnOnce(`${e} needs ${s} for ${n.join(", ")}, and cannot be given it without ${r.join(", ")}`):(w.push(s),p=p.filter(e=>!n.includes(e)))}const m=y.filter(e=>{const t=allowedBy([e]);return p.some(e=>t.has(attributeKey(e)))});return m.length>0&&this.warnOnce(`${e} cannot hold ${p.join(", ")}: its schema declares ${m.join(", ")}, a structural class an existing entry cannot be given`),w.length>0&&this.logger.info(`${e}: adding ${w.join(", ")}, which the change needs and its schema declares`),w}async subschemaDn(){try{const e=await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["subschemaSubentry"]},""),t=e.searchEntries[0]?.subschemaSubentry,s=Array.isArray(t)?t[0]:t;if(s)return String(s)}catch(e){this.logger.warn(`Unable to read subschemaSubentry from root DSE: ${String(e)}`)}return"cn=Subschema"}async schemaIndex(){const e=1e3*(this.config.ldap_raw_schema_cache_ttl??3600);if(this.schemaCache&&Date.now()-this.schemaCache.fetchedAt<e)return this.schemaCache.index;if(this.schemaLoading)return this.schemaLoading;this.schemaLoading=(async()=>{const e=await this.subschemaDn(),t=(await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["objectClasses","attributeTypes","ldapSyntaxes","matchingRules"]},e).catch(t=>{if(32===t?.code)throw new x(`Subschema entry ${e} not found`);throw t})).searchEntries[0];if(!t)throw new x(`Subschema entry ${e} not found`);const list=e=>{if(void 0===e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString("utf8"):e)},s=new k(U({objectClasses:list(t.objectClasses),attributeTypes:list(t.attributeTypes),ldapSyntaxes:list(t.ldapSyntaxes),matchingRules:list(t.matchingRules)}));return this.schemaCache={index:s,fetchedAt:Date.now()},this.logger.info(`LDAP schema loaded from ${e}: ${s.schema.objectClasses.length} object classes, ${s.schema.attributeTypes.length} attribute types`),s})();try{return await this.schemaLoading}finally{this.schemaLoading=void 0}}knowAttributeNames(e){for(const t of e){if(!t||t.includes(";"))continue;const e=t.toLowerCase();this.spellings.has(e)||this.spellings.set(e,t);const s=(p(t)??t).toLowerCase(),i=this.spelledTypes.get(s);void 0===i?this.spelledTypes.set(s,t):i!==t&&this.logger.warn(`Attribute "${t}" is also named "${i}" in the configuration or a schema: plugins comparing with one name miss the other`)}}async canonicalAttributes(e){if(!e)return;const t=Object.keys(e),s=await this.canonicalNames(t);if(t.every((e,t)=>e===s[t]))return;const i=t.map(t=>e[t]);for(const s of t)delete e[s];s.forEach((t,s)=>e[t]=i[s])}async canonicalChanges(e){e&&(await this.canonicalAttributes(e.add),await this.canonicalAttributes(e.replace),Array.isArray(e.delete)?e.delete=await this.canonicalNames(e.delete):await this.canonicalAttributes(e.delete))}async canonicalNames(e){if(0===e.length)return e;let t;try{t=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){this.schemaUnavailable||this.logger.warn(`The directory schema could not be read (${String(e)}): attribute names are resolved from the standard types only, a name given as another OID is refused, and object classes are not repaired`),this.schemaUnavailable=!0}const s=new Map;return e.map(e=>{const i=this.canonicalName(e,t),n=s.get(i.toLowerCase());if(void 0!==n)throw new E(`"${n}" and "${e}" name the same attribute`);return s.set(i.toLowerCase(),e),i})}canonicalName(e,t){const[s,...i]=e.split(";"),n=s.replace(/^oid\./i,""),a=i.find(e=>"binary"!==e.toLowerCase());if(void 0!==a)throw new E(`Attribute option "${a}" of "${e}" is not accepted`);const r=t?.getAttributeType(n),o=t?void 0:p(n);if(!t&&!o&&/^\d+(\.\d+)+$/.test(n))throw new E(`Attribute "${e}" cannot be named by its OID here`);const c=(r?[...r.names,r.oid]:[n,...o?[o]:[]]).map(e=>this.spellings.get(e.toLowerCase())).find(Boolean)??r?.names[0]??o??n;return i.length?`${c};binary`:c}filterForCaller(e,t,s){if(!this.parent.hooks.ldapsearchfilter)return e;const i=this.parent.hooks.ldapsearchfilter,one=async e=>{const[n]=await l(i,[e,t,s]);return n??e};if("function"==typeof e[Symbol.asyncIterator]){const t=e;return async function*(){for await(const e of t)yield await one(e)}()}return one(e)}async search(e,t=this.base,s){s??(s=v());let i={...q,...e};i=await l(this.parent.hooks.ldapsearchopts,i),[t,i]=await l(this.parent.hooks.ldapsearchrequest,[t,i,s]);const n=this.cacheEnabled&&!i.paged&&"base"===i.scope,a=n?this.getCacheKey(t,i):"",r=this.cacheGeneration;if(n){const e=this.searchCache.get(a);if(j.inc({result:e?"hit":"miss"}),e)return this.logger.debug(`LDAP search cache hit: ${digestKey(a)}`),this.filterForCaller(cloneSearchResult(e),s,i)}const o=await this.acquireConnection();try{let e=i.paged?async function*(e){for await(const t of e)yield dropSelectors(t)}(o.client.searchPaginated(t,i)):$("search",()=>o.client.search(t,i)).then(dropSelectors);if(e=await l(this.parent.hooks.ldapsearchresult,e),n){const t=e;return r===this.cacheGeneration?(this.searchCache.set(a,cloneSearchResult(t)),this.logger.debug(`LDAP search cached: ${digestKey(a)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${digestKey(a)}`),this.filterForCaller(t,s,i)}return i.paged?this.filterForCaller(this.wrapPaginatedSearch(e,o),s,i):this.filterForCaller(e,s,i)}finally{i.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){let s=0,i="ok";try{for(;;){const t=process.hrtime.bigint();let n;try{n=await e.next()}catch(e){throw i=S(e),e}finally{s+=L(t)}if(n.done)return;yield n.value}}finally{try{await e.return(void 0)}catch{}finally{P.observe({operation:"search",result:i},s),this.releaseConnection(t)}}}async add(e,t,i){i??(i=v()),e=this.setDn(e),await this.canonicalAttributes(t),t.objectClass&&0!==t.objectClass.length||!this.config.user_class||(t.objectClass=this.config.user_class);const n={};for(const[e,s]of Object.entries(t))Buffer.isBuffer(s)?n[e]=s.toString():Array.isArray(s)&&s.length>0&&Buffer.isBuffer(s[0])?n[e]=s.map(e=>e.toString()):n[e]=s;[e,t]=await l(this.parent.hooks.ldapaddrequest,[e,n,i]);const a=membersNamed(t),r=[];for(const[e,i]of Object.entries(t)){assertHasValue(e,i);const t=Array.isArray(i)?i:[i];r.push(new s({type:e,values:t}))}const o=await this.acquireConnection();try{this.invalidateCache(e);for(const e of a)this.invalidateCache(e);await $("add",()=>o.client.add(e,r)),this.invalidateCache(e);for(const e of a)this.invalidateCache(e);f(this.parent.hooks.ldapadddone,[e,t],w(i))}catch(t){throw function(e,t){const s=R(t);return 68===s?withCode(new O(`Entry ${e} already exists`),s):32===s?withCode(new x(`${y(e)} does not exist`),s):20===s?ldapRefusal(`The directory refused this write to ${e}`,s,t):ldapWriteError("LDAP add error",e,t)}(e,t)}finally{this.releaseConnection(o)}return await f(this.parent.hooks.ldapaddafter,[e,t],w(i)),!0}async modify(e,t,s,i={}){s??(s=v());const n=this.opNumber(),a=[];try{return await m(n,()=>this.applyModify(e,t,s,i,n,a))}finally{this.endOperation(a,this.parent.hooks.ldapmodifyend,n)}}async applyModify(e,t,a,r,o,c){e=this.setDn(e);const h=[];if(await this.canonicalChanges(t),[e,t]=await l(this.parent.hooks.ldapmodifyrequest,[e,t,o,a]),t.add)for(const[e,n]of Object.entries(t.add))assertHasValue(e,n),h.push(new i({operation:"add",modification:new s({type:e,values:Array.isArray(n)?n:[n]})}));if(t.replace)for(const[e,n]of Object.entries(t.replace))h.push(new i({operation:"replace",modification:new s({type:e,values:Array.isArray(n)?n:[n]})}));if(t.delete)if(Array.isArray(t.delete))for(const e of t.delete)e&&h.push(new i({operation:"delete",modification:new s({type:e,values:[]})}));else for(const[e,n]of Object.entries(t.delete)){const t=new i({operation:"delete",modification:new s(n?{type:e,values:Array.isArray(n)?n:[n]}:{type:e})});h.push(t)}const d=[t.add,t.replace,t.delete].flatMap(e=>Array.isArray(e)?e:Object.keys(e??{})).some(isMembershipAttribute),invalidate=()=>{if(this.invalidateCache(e),d){this.invalidateReferences(e);for(const e of[t.add,t.replace,t.delete])for(const t of membersNamed(Array.isArray(e)?{}:e))this.invalidateCache(t)}};if(0===h.length){return Object.keys(t.add||{}).length>0||Object.keys(t.replace||{}).length>0||(Array.isArray(t.delete)?t.delete.length>0:Object.keys(t.delete||{}).length>0)?this.logger.warn(`Modify on ${e} asked for changes but emitted none; nothing was written`):this.logger.debug(`Modify on ${e} had nothing to apply`),c.push(f(this.parent.hooks.ldapmodifydone,[e,{},o],r.context??w(a))),!1}{const l=r.permissive?[new n("1.2.840.113556.1.4.1413",{critical:!1})]:void 0,send=async()=>{const t=await this.acquireConnection();try{await $("modify",()=>t.client.modify(e,h,l))}finally{this.releaseConnection(t)}};try{invalidate();try{await send()}catch(n){if(65!==R(n))throw n;const a=await this.classRepair(e,t,r.declaredClasses);if("allowed"!==a&&0===a.length)throw n;const o="allowed"===a?void 0:new i({operation:"add",modification:new s({type:"objectClass",values:a})});o&&h.push(o),this.invalidateCache(e);try{await send()}catch(e){if(!o||20!==R(e))throw e;h.splice(h.indexOf(o),1),await send()}}return invalidate(),c.push(f(this.parent.hooks.ldapmodifydone,[e,t,o],r.context??w(a))),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(h)}`),function(e,t){const s=R(t);return 32===s?withCode(new x(`Entry ${e} not found`),s):16===s?withCode(new x(`Entry ${e} does not hold the value to delete`),s):65!==s&&20!==s?ldapWriteError("LDAP modify error",e,t):withCode(new O(`Entry ${e} cannot hold this change: ${String(t)}`),s)}(e,t)}}}async rename(e,t,s){s??(s=v()),e=this.setDn(e),t=this.setDn(t);const i=u(e),n=u(t);if(1!==n.length||!i.includes(n[0]))throw new E(`${t}: an entry is renamed by one value of the attribute it is named by`);const a=this.opNumber(),r=[];try{return await m(a,()=>this.applyRename(e,t,s,r))}finally{this.endOperation(r,this.parent.hooks.ldaprenameend,a)}}async applyRename(e,t,s,i){[e,t]=await l(this.parent.hooks.ldaprenamerequest,[e,t,s]);const n=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),await $("rename",()=>n.client.modifyDN(e,t)),this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),i.push(f(this.parent.hooks.ldaprenamedone,[e,t],w(s))),!0}catch(s){throw ldapRenameError("LDAP rename error",e,t,s)}finally{this.releaseConnection(n)}}async move(e,t,s){s??(s=v()),e=this.setDn(e),t=this.setDn(t);const i=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),await $("rename",()=>i.client.modifyDN(e,t)),this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),b(()=>f(this.parent.hooks.ldaprenamedone,[e,t],w(s))),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(s){throw ldapRenameError("LDAP move error",e,t,s)}finally{this.releaseConnection(i)}}async delete(e,t,s={}){t??(t=v());const i=(Array.isArray(e)?e:[e]).map(e=>this.setDn(e)),n=this.opNumber(),a=[];try{return await m(n,()=>this.applyDelete(i,t,s,a))}finally{this.endOperation(a,this.parent?.hooks.ldapdeleteend,n)}}async applyDelete(e,t,s,i){[e]=await l(this.parent?.hooks.ldapdeleterequest,[e,t]);const n=await this.acquireConnection();try{for(const a of e){try{this.invalidateCache(a),this.invalidateReferences(a),await $("delete",()=>n.client.del(a)),this.invalidateCache(a),this.invalidateReferences(a)}catch(e){const t=R(e);throw 32===t?withCode(new x(`Entry ${a} not found`),t):ldapError("LDAP delete error",e)}i.push(f(this.parent.hooks.ldapdeletedone,a,s.context??w(t)))}return!0}finally{this.releaseConnection(n)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${g(e)},${this.base}`,e}endOperation(e,t,...s){t?.length&&Promise.all(e).then(()=>f(t,...s))}opNumber(){return this.parent.operationSequence++}}export{assertModifyRequest,ldapActions as default};
1
+ import{createHash as e}from"node:crypto";import{Client as t,Attribute as i,Change as n,Control as s}from"ldapts";import{LRUCache as a}from"lru-cache";import r from"p-limit";import{normalizeDn as o,isDnInBranch as c,knownAttributeName as l,rdnTypes as h,escapeDnValue as d,getParentDn as u}from"./utils.js";import{changeContext as f}from"./changeContext.js";import p,{hookOwner as g}from"./hookBus.js";import{runOperation as y,outsideOperation as w}from"./operation.js";import{outsideAnyRequest as m,outsideRequest as b,currentRequest as A}from"./requestContext.js";import{timedLdap as C,ldapPoolWait as v,secondsSince as $,ldapSearchCache as L,ldapResult as D,ldapOperationDuration as j}from"./metrics.js";import{BadRequestError as S,isAuthzRefusal as P,NotFoundError as E,ConflictError as x}from"./errors.js";import{extractLdapCode as R,isSchemaRefusal as O}from"./ldapCodes.js";import{SchemaIndex as k,parseSchema as _}from"./ldapSchema.js";const U={scope:"sub",filter:"(objectClass=*)",attributes:["*"],sizeLimit:0,timeLimit:10,paged:{pageSize:100}},B={ldapaddrequest:2,ldapmodifyrequest:3,ldaprenamerequest:2,ldapdeleterequest:1},q=new Set(["search","add","modify","rename","move","delete"]),isPlainObject=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function assertModifyRequest(e){const t=Object.keys(e).filter(e=>!["add","replace","delete"].includes(e));if(t.length>0)throw new S(`A modification carries add, replace or delete, not ${t.join(", ")}`);const{add:i,replace:n,delete:s}=e;for(const[e,t]of[["add",i],["replace",n]])if(void 0!==t&&!isPlainObject(t))throw new S(`"${e}" must map attributes to values`);if(!(void 0===s||isPlainObject(s)||Array.isArray(s)&&s.every(e=>"string"==typeof e)))throw new S('"delete" must list attribute names or map attributes to values');if(0===[i,n,s].reduce((e,t)=>e+(t?Object.keys(t).length:0),0))throw new S("A modification names at least one attribute in add, replace or delete")}function ldapError(e,t){const i=new Error(`${e}: ${String(t)}`),n=R(t);return void 0===n?i:withCode(i,n)}function withCode(e,t){return e.code=t,e}const digestKey=t=>e("sha256").update(t).digest("hex").slice(0,12),foldDn=e=>e.trim().toLowerCase().split(",").map(e=>e.trim()).join(","),T=new Set(["member","uniquemember"]),isMembershipAttribute=e=>T.has(e.toLowerCase()),membersNamed=e=>Object.entries(e??{}).flatMap(([e,t])=>isMembershipAttribute(e)?[t].flat().map(e=>Buffer.isBuffer(e)?e.toString():e).filter(e=>"string"==typeof e):[]),cloneSearchResult=e=>({searchEntries:e.searchEntries.map(e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[e,Array.isArray(t)?[...t]:t]))),searchReferences:[...e.searchReferences]}),W=["*","+","1.1"],dropSelectors=e=>{for(const t of e.searchEntries)for(const e of W){const i=t[e];Array.isArray(i)&&0===i.length&&delete t[e]}return e};function ldapRefusal(e,t,i){return withCode(new S(`${e}: ${String(i)}`),t)}function assertHasValue(e,t){if((e=>{const absent=e=>null==e;return Array.isArray(e)?0===e.length||e.some(e=>absent(e)):absent(e)})(t))throw new S(`Attribute "${e}" has no value`)}function ldapWriteError(e,t,i){const n=R(i);return O(n)?ldapRefusal(`The directory refused this write to ${t}`,n,i):ldapError(e,i)}function ldapRenameError(e,t,i,n){const s=R(n);return 68===s?withCode(new x(`Entry ${i} already exists`),s):O(s)?ldapRefusal(`The directory refused to rename ${t} to ${i}`,s,n):ldapError(e,n)}class ldapActions{config;options;dn;pwd;base;parent;logger;searchCache;cacheEnabled;cacheGeneration=0;queryLimit;connectionPool=[];poolSize;connectionTtl;ldapUrls;currentUrlIndex=0;attrSignatureCache;waitingResolvers=[];availableConnections=[];opening=0;isCleaningUp=!1;declaredClasses=new Map;schemaCache;schemaLoading;schemaUnavailable=!1;spellings=new Map;spelledTypes=new Map;classWarnings=new a({max:1e4});constructor(e){this.parent=e,this.logger=e.logger,this.config=e.config;for(const[e,t]of Object.entries(this.config))/_attributes?$/.test(e)&&this.knowAttributeNames([t].flat().filter(e=>"string"==typeof e));this.poolSize=this.config.ldap_pool_size||5,this.connectionTtl=1e3*(this.config.ldap_connection_ttl||60),this.logger.info(`LDAP connection pool initialized: size=${this.poolSize}, ttl=${this.connectionTtl/1e3}s`);const t=this.config.ldap_concurrency||10;this.queryLimit=r(t),this.logger.info(`Global LDAP query concurrency limit set to ${t}`);const i="string"==typeof this.config.ldap_cache_max?parseInt(this.config.ldap_cache_max,10)||1e3:this.config.ldap_cache_max??1e3,n=this.config.ldap_cache_ttl,s="string"==typeof n?parseInt(n,10)||0:n??0,o=1e3*Math.max(s,0);if(this.cacheEnabled=o>0,this.searchCache=new a({max:i,ttl:this.cacheEnabled?o:void 0,updateAgeOnGet:!1,updateAgeOnHas:!1}),this.logger.info(this.cacheEnabled?`LDAP search cache initialized: max=${i}, ttl=${o/1e3}s`:"LDAP search cache disabled (--ldap-cache-ttl=0)"),this.attrSignatureCache=new a({max:1e3}),!e.config.ldap_url||0===e.config.ldap_url.length)throw new Error("LDAP URL is not defined");if(!e.config.ldap_dn)throw new Error("LDAP bind DN is not defined, please set --ldap-dn (or DM_LDAP_DN)");if(!e.config.ldap_pwd)throw new Error("LDAP password is not defined, please set --ldap-pwd (or DM_LDAP_PWD)");if(!e.config.ldap_base)throw new Error("LDAP base is not defined, please set --ldap-base (or DM_LDAP_BASE)");this.base=e.config.ldap_base,this.ldapUrls=e.config.ldap_url,this.logger.info(`LDAP failover configured with ${this.ldapUrls.length} URL(s): ${this.ldapUrls.join(", ")}`),this.options={url:this.ldapUrls[0],timeout:0,connectTimeout:0,strictDN:!1},this.ldapUrls[0].startsWith("ldaps://")&&(this.options.tlsOptions={minVersion:"TLSv1.2"}),this.dn=e.config.ldap_dn,this.pwd=e.config.ldap_pwd}async createConnection(){const e=[];for(let i=0;i<this.ldapUrls.length;i++){const n=(this.currentUrlIndex+i)%this.ldapUrls.length,s=this.ldapUrls[n];try{this.logger.debug(`Attempting connection to ${s}`);const e={...this.options,url:s,tlsOptions:s.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0},i=new t(e);return await C("bind",()=>i.bind(this.dn,this.pwd)),n!==this.currentUrlIndex&&(this.logger.info(`LDAP failover: switched from ${this.ldapUrls[this.currentUrlIndex]} to ${s}`),this.currentUrlIndex=n),i}catch(t){this.logger.warn(`Failed to connect to ${s}: ${String(t)}`),e.push(t)}}throw this.logger.error(`LDAP connection failed for all ${this.ldapUrls.length} URL(s)`),new Error(`LDAP connection failed for all URLs: ${e.map(e=>e.message).join(", ")}`)}cleanupExpiredConnections(){if(!this.isCleaningUp){this.isCleaningUp=!0;try{const e=Date.now(),t=[];for(let i=this.availableConnections.length-1;i>=0;i--){const n=this.availableConnections[i];if(e-n.createdAt>this.connectionTtl){t.push(n),this.availableConnections.splice(i,1);const e=this.connectionPool.indexOf(n);-1!==e&&this.connectionPool.splice(e,1)}}for(const e of t)e.client.unbind().catch(e=>{this.logger.debug(`Error unbinding expired connection: ${String(e)}`)});t.length>0&&this.logger.debug(`Cleaned up ${t.length} expired LDAP connections`)}finally{this.isCleaningUp=!1}}}async ping(e){if(this.closed)throw new Error("LDAP connections closed");const i=[];for(let n=0;n<this.ldapUrls.length;n++){const s=this.ldapUrls[(this.currentUrlIndex+n)%this.ldapUrls.length],a=new t({...this.options,url:s,timeout:e,connectTimeout:e,tlsOptions:s.startsWith("ldaps://")?{minVersion:"TLSv1.2"}:void 0});try{return await a.bind(this.dn,this.pwd),void await a.search("",{scope:"base",attributes:["1.1"]})}catch(e){i.push(`${s}: ${String(e)}`)}finally{await a.unbind().catch(()=>{})}}throw new Error(`No directory answered: ${i.join(", ")}`)}closed=!1;async close(){this.closed=!0,this.refuseWaiters(new Error("LDAP connections closed"));const e=this.connectionPool.splice(0);this.availableConnections.length=0,await Promise.allSettled(e.map(e=>e.client.unbind()))}async acquireConnection(){const e=process.hrtime.bigint();try{return await this.takeConnection()}finally{v.observe($(e))}}poolStats(){return{busy:this.connectionPool.length-this.availableConnections.length,idle:this.availableConnections.length,waiting:this.waitingResolvers.length,opening:this.opening,max:this.poolSize}}async takeConnection(){if(this.closed)throw new Error("LDAP connections closed");if(this.cleanupExpiredConnections(),this.availableConnections.length>0){const e=this.availableConnections.pop();return e.inUse=!0,this.logger.debug("Reusing pooled LDAP connection"),e}if(this.connectionPool.length+this.opening<this.poolSize){this.opening++;try{const e=await m(()=>this.createConnection());if(this.closed)throw await e.unbind().catch(()=>{}),new Error("LDAP connections closed");const t={client:e,createdAt:Date.now(),inUse:!0};return this.connectionPool.push(t),this.logger.debug(`Created new LDAP connection (pool: ${this.connectionPool.length}/${this.poolSize})`),t}catch(e){throw this.refuseWaiters(e instanceof Error?e:new Error(String(e))),e}finally{this.opening--}}return this.logger.debug("LDAP connection pool full, waiting for available connection"),new Promise((e,t)=>{const i=globalThis.setTimeout(()=>{const e=this.waitingResolvers.indexOf(settle);-1!==e&&this.waitingResolvers.splice(e,1),t(new Error("LDAP connection pool timeout after 30s"))},3e4),settle=n=>{globalThis.clearTimeout(i),"error"in n?t(n.error):e("retry"in n?this.takeConnection():n.connection)};this.waitingResolvers.push(settle)})}refuseWaiters(e){for(const t of this.waitingResolvers.splice(0))t({error:e})}wakeWaiter(){this.waitingResolvers.shift()?.({retry:!0})}releaseConnection(e){if(this.closed)e.client.unbind().catch(()=>{});else{if(Date.now()-e.createdAt>this.connectionTtl){const t=this.connectionPool.indexOf(e);return-1!==t&&this.connectionPool.splice(t,1),e.client.unbind().catch(()=>{}),this.logger.debug("Released expired LDAP connection (discarded)"),void this.wakeWaiter()}if(this.waitingResolvers.length>0){const t=this.waitingResolvers.shift();return this.logger.debug("Released LDAP connection to waiting request"),void t({connection:e})}e.inUse=!1,this.availableConnections.push(e),this.logger.debug("Released LDAP connection back to pool")}}getAttributeSignature(e){if(!e||0===e.length)return"*";const t=e.join("|");let i=this.attrSignatureCache.get(t);return i||(i=[...e].sort().join(","),this.attrSignatureCache.set(t,i)),i}getCacheKey(e,t){const i=this.getAttributeSignature(t.attributes),n=t.explicitBufferAttributes?.length?[...t.explicitBufferAttributes].sort().join(","):"-",s="string"==typeof t.filter?t.filter:t.filter?t.filter.toString():"(objectClass=*)";return[e,t.scope||"sub",s,i,n,!1===t.returnAttributeValues?"novalues":"values",t.derefAliases??"-",String(t.sizeLimit??"-")].join("\0")}invalidateCache(e){if(!this.cacheEnabled)return;this.cacheGeneration++;const t=foldDn(e),i=`,${t}`,n=[];for(const e of this.searchCache.keys()){const s=foldDn(e.split("\0")[0]);(s===t||s.endsWith(i))&&n.push(e)}for(const e of n)this.searchCache.delete(e)}invalidateReferences(e){if(!this.cacheEnabled)return;this.cacheGeneration++;const t=foldDn(e),i=`,${t}`,names=e=>{if("string"!=typeof e)return!1;const n=foldDn(e);return n===t||n.endsWith(i)},n=[];for(const[e,t]of this.searchCache.entries())t.searchEntries.some(e=>Object.values(e).some(e=>Array.isArray(e)?e.some(names):names(e)))&&n.push(e);for(const e of n)this.searchCache.delete(e)}get system(){return this.systemView??=new Proxy(this,{get:(e,t)=>{const i=Reflect.get(e,t,e);return"function"==typeof i&&q.has(t)?(...t)=>b(()=>i.apply(e,t)):i}}),this.systemView}systemView;forRequest(e){return{search:(t,i)=>this.search(t,i??this.base,e),add:(t,i)=>this.add(t,i,e),modify:(t,i,n)=>this.modify(t,i,e,n),rename:(t,i)=>this.rename(t,i,e),delete:t=>this.delete(t,e)}}async readableBy(e,t){if(t??=A(),!t)return!0;try{const i=await this.search({paged:!1,scope:"base",attributes:["1.1"]},e,t);return(i.searchEntries?.length??0)>0}catch(e){if(P(e)||403===e?.statusCode||32===R(e))return!1;throw e}}async authorize(e,t){const i=[...t];"ldapaddrequest"===e&&await this.canonicalAttributes(i[1]),"ldapmodifyrequest"===e&&await this.canonicalChanges(i[1]);const n=B[e];i[n]??=A();const[s,...a]=i,r="ldapdeleterequest"===e?[[s].flat().map(e=>this.setDn(String(e))),...a]:[this.setDn(String(s)),...a];"ldaprenamerequest"===e&&(r[1]=this.setDn(String(r[1])));const o=this.parent.hookBus.subscribers(e).filter(e=>{const t=g(e);return Boolean(t&&this.parent.loadedPlugins[t.plugin]?.roles?.includes("authz"))}),c=new p({[e]:o});await c.chain(e,r)}declareObjectClasses(e,t,i=!1){const n=(Array.isArray(t)?t:[t]).filter(e=>"string"==typeof e&&""!==e);if(!e||0===n.length)return;const s=JSON.stringify([o(e),n.map(e=>e.toLowerCase()).sort(),i]);this.declaredClasses.set(s,{base:e,classes:n,includeBase:i})}declaredFor(e){let t=0,i=[];for(const n of this.declaredClasses.values()){if(!c(e,n.base))continue;const s=o(n.base);if(!n.includeBase&&o(e)===s)continue;const a=s.split(",").length;a>t&&(t=a,i=[]),a===t&&i.push(...n.classes)}const n=new Set;return i.filter(e=>{const t=e.toLowerCase();return!n.has(t)&&(n.add(t),!0)})}warnOnce(e){this.classWarnings.has(e)||(this.classWarnings.set(e,!0),this.logger.warn(e))}async classRepair(e,t,i=[]){if(!1===this.config.ldap_flat_auto_repair)return[];const n=[...this.declaredFor(e)];for(const e of i)n.some(t=>t.toLowerCase()===e.toLowerCase())||n.push(e);if(0===n.length)return[];const s=Array.isArray(t.delete)?t.delete:Object.entries(t.delete||{}).filter(([,e])=>!e||Array.isArray(e)&&!e.length).map(([e])=>e);if([...Object.keys(t.add||{}),...Object.keys(t.replace||{}),...Array.isArray(t.delete)?t.delete:Object.keys(t.delete||{})].some(e=>"objectclass"===e.toLowerCase()))return[];let a;try{a=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){return this.schemaUnavailable||this.logger.warn(`Object classes are not repaired: the directory schema could not be read (${String(e)})`),this.schemaUnavailable=!0,[]}const attributeKey=e=>a.getAttributeType(e)?.oid??e.toLowerCase(),allowedBy=e=>{const{must:t,may:i}=a.resolveAttributes(e);return new Set([...t,...i].map(attributeKey))},valued=e=>Object.entries(e||{}).filter(([,e])=>Array.isArray(e)?e.length>0:null!=e&&""!==e).map(([e])=>e),r=[...valued(t.add),...valued(t.replace)],o=n.filter(e=>{const t=a.getObjectClass(e);if("AUXILIARY"!==t?.kind)return!1;const i=allowedBy([e]);return r.some(e=>i.has(attributeKey(e)))});if(0===o.length)return[];let c;try{c=(await this.system.search({paged:!1,scope:"base",attributes:["objectClass",...a.resolveAttributes(o).must]},e)).searchEntries[0]}catch{return[]}if(!c)return[];const l=c.objectClass??c.objectclass,h=(Array.isArray(l)?l:l?[l]:[]).map(String),d=allowedBy(h);let u=r.filter(e=>a.getAttributeType(e)&&!d.has(attributeKey(e)));if(0===u.length)return"allowed";const f=new Set(s.map(attributeKey)),p=new Set([...valued(c).filter(e=>!f.has(attributeKey(e))),...r].map(attributeKey)),{auxiliary:g,structural:y}=a.missingClasses(n,h),w=[];for(const t of g){const i=t.names[0]??t.oid,n=allowedBy([i]),s=u.filter(e=>n.has(attributeKey(e)));if(0===s.length)continue;const r=a.resolveAttributes([i]).must.filter(e=>!p.has(attributeKey(e)));r.length>0?this.warnOnce(`${e} needs ${i} for ${s.join(", ")}, and cannot be given it without ${r.join(", ")}`):(w.push(i),u=u.filter(e=>!s.includes(e)))}const m=y.filter(e=>{const t=allowedBy([e]);return u.some(e=>t.has(attributeKey(e)))});return m.length>0&&this.warnOnce(`${e} cannot hold ${u.join(", ")}: its schema declares ${m.join(", ")}, a structural class an existing entry cannot be given`),w.length>0&&this.logger.info(`${e}: adding ${w.join(", ")}, which the change needs and its schema declares`),w}async subschemaDn(){try{const e=await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["subschemaSubentry"]},""),t=e.searchEntries[0]?.subschemaSubentry,i=Array.isArray(t)?t[0]:t;if(i)return String(i)}catch(e){this.logger.warn(`Unable to read subschemaSubentry from root DSE: ${String(e)}`)}return"cn=Subschema"}async schemaIndex(){const e=1e3*(this.config.ldap_raw_schema_cache_ttl??3600);if(this.schemaCache&&Date.now()-this.schemaCache.fetchedAt<e)return this.schemaCache.index;if(this.schemaLoading)return this.schemaLoading;this.schemaLoading=(async()=>{const e=await this.subschemaDn(),t=(await this.system.search({paged:!1,scope:"base",filter:"(objectClass=*)",attributes:["objectClasses","attributeTypes","ldapSyntaxes","matchingRules"]},e).catch(t=>{if(32===t?.code)throw new E(`Subschema entry ${e} not found`);throw t})).searchEntries[0];if(!t)throw new E(`Subschema entry ${e} not found`);const list=e=>{if(void 0===e)return[];return(Array.isArray(e)?e:[e]).map(e=>Buffer.isBuffer(e)?e.toString("utf8"):e)},i=new k(_({objectClasses:list(t.objectClasses),attributeTypes:list(t.attributeTypes),ldapSyntaxes:list(t.ldapSyntaxes),matchingRules:list(t.matchingRules)}));return this.schemaCache={index:i,fetchedAt:Date.now()},this.logger.info(`LDAP schema loaded from ${e}: ${i.schema.objectClasses.length} object classes, ${i.schema.attributeTypes.length} attribute types`),i})();try{return await this.schemaLoading}finally{this.schemaLoading=void 0}}knowAttributeNames(e){for(const t of e){if(!t||t.includes(";"))continue;const e=t.toLowerCase();this.spellings.has(e)||this.spellings.set(e,t);const i=(l(t)??t).toLowerCase(),n=this.spelledTypes.get(i);void 0===n?this.spelledTypes.set(i,t):n!==t&&this.logger.warn(`Attribute "${t}" is also named "${n}" in the configuration or a schema: plugins comparing with one name miss the other`)}}async canonicalAttributes(e){if(!e)return;const t=Object.keys(e),i=await this.canonicalNames(t);if(t.every((e,t)=>e===i[t]))return;const n=t.map(t=>e[t]);for(const i of t)delete e[i];i.forEach((t,i)=>e[t]=n[i])}async canonicalChanges(e){e&&(await this.canonicalAttributes(e.add),await this.canonicalAttributes(e.replace),Array.isArray(e.delete)?e.delete=await this.canonicalNames(e.delete):await this.canonicalAttributes(e.delete))}async canonicalNames(e){if(0===e.length)return e;let t;try{t=await this.schemaIndex(),this.schemaUnavailable=!1}catch(e){this.schemaUnavailable||this.logger.warn(`The directory schema could not be read (${String(e)}): attribute names are resolved from the standard types only, a name given as another OID is refused, and object classes are not repaired`),this.schemaUnavailable=!0}const i=new Map;return e.map(e=>{const n=this.canonicalName(e,t),s=i.get(n.toLowerCase());if(void 0!==s)throw new S(`"${s}" and "${e}" name the same attribute`);return i.set(n.toLowerCase(),e),n})}canonicalName(e,t){const[i,...n]=e.split(";"),s=i.replace(/^oid\./i,""),a=n.find(e=>"binary"!==e.toLowerCase());if(void 0!==a)throw new S(`Attribute option "${a}" of "${e}" is not accepted`);if(!/^(?:[A-Za-z][A-Za-z0-9-]*|\d+(?:\.\d+)+)$/.test(s))throw new S(`"${e}" is not an attribute description`);const r=t?.getAttributeType(s),o=t?void 0:l(s);if(!t&&!o&&/^\d+(\.\d+)+$/.test(s))throw new S(`Attribute "${e}" cannot be named by its OID here`);const c=(r?[...r.names,r.oid]:[s,...o?[o]:[]]).map(e=>this.spellings.get(e.toLowerCase())).find(Boolean)??r?.names[0]??o??s;return n.length?`${c};binary`:c}filterForCaller(e,t,i){if(!this.parent.hookBus.has("ldapsearchfilter"))return e;const one=async e=>{const[n]=await this.parent.hookBus.chain("ldapsearchfilter",[e,t,i]);return n??e};if("function"==typeof e[Symbol.asyncIterator]){const t=e;return async function*(){for await(const e of t)yield await one(e)}()}return one(e)}async search(e,t=this.base,i){i??=A();let n={...U,...e};n=await this.parent.hookBus.chain("ldapsearchopts",n),[t,n]=await this.parent.hookBus.chain("ldapsearchrequest",[t,n,i]);const s=this.cacheEnabled&&!n.paged&&"base"===n.scope,a=s?this.getCacheKey(t,n):"",r=this.cacheGeneration;if(s){const e=this.searchCache.get(a);if(L.inc({result:e?"hit":"miss"}),e)return this.logger.debug(`LDAP search cache hit: ${digestKey(a)}`),this.filterForCaller(cloneSearchResult(e),i,n)}const o=await this.acquireConnection();try{const e=n.paged?async function*(e){for await(const t of e)yield dropSelectors(t)}(o.client.searchPaginated(t,n)):C("search",()=>o.client.search(t,n)).then(dropSelectors),c=await this.parent.hookBus.chain("ldapsearchresult",e);if(s){const e=c;return r===this.cacheGeneration?(this.searchCache.set(a,cloneSearchResult(e)),this.logger.debug(`LDAP search cached: ${digestKey(a)}`)):this.logger.debug(`LDAP search not cached, a write overtook it: ${digestKey(a)}`),this.filterForCaller(e,i,n)}return n.paged?this.filterForCaller(this.wrapPaginatedSearch(c,o),i,n):this.filterForCaller(c,i,n)}finally{n.paged||this.releaseConnection(o)}}async*wrapPaginatedSearch(e,t){let i=0,n="ok";try{for(;;){const t=process.hrtime.bigint();let s;try{s=await e.next()}catch(e){throw n=D(e),e}finally{i+=$(t)}if(s.done)return;yield s.value}}finally{try{await e.return(void 0)}catch{}finally{j.observe({operation:"search",result:n},i),this.releaseConnection(t)}}}async add(e,t,n){n??=A(),e=this.setDn(e),await this.canonicalAttributes(t),t.objectClass&&0!==t.objectClass.length||!this.config.user_class||(t.objectClass=this.config.user_class);const s={};for(const[e,i]of Object.entries(t))Buffer.isBuffer(i)?s[e]=i.toString():Array.isArray(i)&&i.length>0&&Buffer.isBuffer(i[0])?s[e]=i.map(e=>e.toString()):s[e]=i;[e,t]=await this.parent.hookBus.chain("ldapaddrequest",[e,s,n]);const a=membersNamed(t),r=[];for(const[e,n]of Object.entries(t)){assertHasValue(e,n);const t=Array.isArray(n)?n:[n];r.push(new i({type:e,values:t}))}const o=await this.acquireConnection();try{this.invalidateCache(e);for(const e of a)this.invalidateCache(e);await C("add",()=>o.client.add(e,r)),this.invalidateCache(e);for(const e of a)this.invalidateCache(e);this.parent.hookBus.notify("ldapadddone",e,t,f(n))}catch(t){throw function(e,t){const i=R(t);return 68===i?withCode(new x(`Entry ${e} already exists`),i):32===i?withCode(new E(`${u(e)} does not exist`),i):20===i?ldapRefusal(`The directory refused this write to ${e}`,i,t):ldapWriteError("LDAP add error",e,t)}(e,t)}finally{this.releaseConnection(o)}return await this.parent.hookBus.notifyAndWait("ldapaddafter",e,t,f(n)),!0}async modify(e,t,i,n={}){i??=A();const s=this.opNumber(),a=[];try{return await y(s,()=>this.applyModify(e,t,i,n,s,a))}finally{this.endOperation(a,"ldapmodifyend",s)}}async applyModify(e,t,a,r,o,c){e=this.setDn(e);const l=[];if(await this.canonicalChanges(t),[e,t]=await this.parent.hookBus.chain("ldapmodifyrequest",[e,t,o,a]),t.add)for(const[e,s]of Object.entries(t.add))assertHasValue(e,s),l.push(new n({operation:"add",modification:new i({type:e,values:Array.isArray(s)?s:[s]})}));if(t.replace)for(const[e,s]of Object.entries(t.replace))l.push(new n({operation:"replace",modification:new i({type:e,values:Array.isArray(s)?s:[s]})}));if(t.delete)if(Array.isArray(t.delete))for(const e of t.delete)l.push(new n({operation:"delete",modification:new i({type:e,values:[]})}));else for(const[e,s]of Object.entries(t.delete)){const t=new n({operation:"delete",modification:new i(s?{type:e,values:Array.isArray(s)?s:[s]}:{type:e})});l.push(t)}const h=[t.add,t.replace,t.delete].flatMap(e=>Array.isArray(e)?e:Object.keys(e??{})).some(isMembershipAttribute),invalidate=()=>{if(this.invalidateCache(e),h){this.invalidateReferences(e);for(const e of[t.add,t.replace,t.delete])for(const t of membersNamed(Array.isArray(e)?{}:e))this.invalidateCache(t)}};if(0===l.length){return Object.keys(t.add||{}).length>0||Object.keys(t.replace||{}).length>0||(Array.isArray(t.delete)?t.delete.length>0:Object.keys(t.delete||{}).length>0)?this.logger.warn(`Modify on ${e} asked for changes but emitted none; nothing was written`):this.logger.debug(`Modify on ${e} had nothing to apply`),c.push(this.parent.hookBus.notifyAndWait("ldapmodifydone",e,{},o,r.context??f(a))),!1}{const h=r.permissive?[new s("1.2.840.113556.1.4.1413",{critical:!1})]:void 0,send=async()=>{const t=await this.acquireConnection();try{await C("modify",()=>t.client.modify(e,l,h))}finally{this.releaseConnection(t)}};try{invalidate();try{await send()}catch(s){if(65!==R(s))throw s;const a=await this.classRepair(e,t,r.declaredClasses);if("allowed"!==a&&0===a.length)throw s;const o="allowed"===a?void 0:new n({operation:"add",modification:new i({type:"objectClass",values:a})});o&&l.push(o),this.invalidateCache(e);try{await send()}catch(e){if(!o||20!==R(e))throw e;l.splice(l.indexOf(o),1),await send()}}return invalidate(),c.push(this.parent.hookBus.notifyAndWait("ldapmodifydone",e,t,o,r.context??f(a))),!0}catch(t){throw this.logger.warn(`Changes that failed: ${e}, ${JSON.stringify(l)}`),function(e,t){const i=R(t);return 32===i?withCode(new E(`Entry ${e} not found`),i):16===i?withCode(new E(`Entry ${e} does not hold the value to delete`),i):65!==i&&20!==i?ldapWriteError("LDAP modify error",e,t):withCode(new x(`Entry ${e} cannot hold this change: ${String(t)}`),i)}(e,t)}}}async rename(e,t,i){i??=A(),e=this.setDn(e),t=this.setDn(t);const n=h(e),s=h(t);if(1!==s.length||!n.includes(s[0]))throw new S(`${t}: an entry is renamed by one value of the attribute it is named by`);const a=this.opNumber(),r=[];try{return await y(a,()=>this.applyRename(e,t,i,r))}finally{this.endOperation(r,"ldaprenameend",a)}}async applyRename(e,t,i,n){[e,t]=await this.parent.hookBus.chain("ldaprenamerequest",[e,t,i]);const s=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),await C("rename",()=>s.client.modifyDN(e,t)),this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),n.push(this.parent.hookBus.notifyAndWait("ldaprenamedone",e,t,f(i))),!0}catch(i){throw ldapRenameError("LDAP rename error",e,t,i)}finally{this.releaseConnection(s)}}async move(e,t,i){i??=A(),e=this.setDn(e),t=this.setDn(t);const n=await this.acquireConnection();try{return this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),await C("rename",()=>n.client.modifyDN(e,t)),this.invalidateCache(e),this.invalidateCache(t),this.invalidateReferences(e),w(()=>this.parent.hookBus.notify("ldaprenamedone",e,t,f(i))),this.logger.debug(`LDAP move: ${e} -> ${t}`),!0}catch(i){throw ldapRenameError("LDAP move error",e,t,i)}finally{this.releaseConnection(n)}}async delete(e,t,i={}){t??=A();const n=(Array.isArray(e)?e:[e]).map(e=>this.setDn(e)),s=this.opNumber(),a=[];try{return await y(s,()=>this.applyDelete(n,t,i,a))}finally{this.endOperation(a,"ldapdeleteend",s)}}async applyDelete(e,t,i,n){[e]=await this.parent.hookBus.chain("ldapdeleterequest",[e,t]);const s=await this.acquireConnection();try{for(const a of e){try{this.invalidateCache(a),this.invalidateReferences(a),await C("delete",()=>s.client.del(a)),this.invalidateCache(a),this.invalidateReferences(a)}catch(e){const t=R(e);throw 32===t?withCode(new E(`Entry ${a} not found`),t):ldapError("LDAP delete error",e)}n.push(this.parent.hookBus.notifyAndWait("ldapdeletedone",a,i.context??f(t)))}return!0}finally{this.releaseConnection(s)}}setDn(e){return/=/.test(e)?/,/.test(e)||(e+=`,${this.base}`):e=`${this.config.ldap_user_main_attribute}=${d(e)},${this.base}`,e}endOperation(e,t,i){this.parent.hookBus.has(t)&&Promise.all(e).then(()=>this.parent.hookBus.notify(t,i))}opNumber(){return this.parent.operationSequence++}}export{assertModifyRequest,ldapActions as default};
@@ -1 +1 @@
1
- const t=new Set(["1.3.6.1.4.1.1466.115.121.1.5","1.3.6.1.4.1.1466.115.121.1.8","1.3.6.1.4.1.1466.115.121.1.9","1.3.6.1.4.1.1466.115.121.1.10","1.3.6.1.4.1.1466.115.121.1.28","1.3.6.1.4.1.1466.115.121.1.40","1.3.6.1.4.1.1466.115.121.1.49","1.3.6.1.1.16.1"]),e=new Set(["userpassword","jpegphoto","usercertificate","cacertificate","certificaterevocationlist","authoritiyrevocationlist","crosscertificatepair","photo","personalsignature","audio","thumbnailphoto","thumbnaillogo","objectguid","objectsid"]);function readList(t,e){if("("!==t[e])return{values:[t[e]],next:e+1};const s=[];let n=e+1;for(;n<t.length&&")"!==t[n];)"$"!==t[n]&&s.push(t[n]),n++;return{values:s,next:n+1}}function parseDefinition(t,e){const s=function(t){const e=[];let s=0;for(;s<t.length;){const n=t[s];if(" "===n||"\t"===n||"\n"===n||"\r"===n)s++;else if("("===n||")"===n)e.push(n),s++;else if("'"===n){let n="";for(s++;s<t.length&&"'"!==t[s];){if("\\"===t[s]&&s+2<t.length){const e=t.substring(s+1,s+3);if(/^[0-9a-fA-F]{2}$/.test(e)){n+=String.fromCharCode(parseInt(e,16)),s+=3;continue}}n+=t[s],s++}s++,e.push(n)}else{let n="";for(;s<t.length&&!/[\s()']/.test(t[s]);)n+=t[s],s++;e.push(n)}}return e}(t);if(s.length<2||"("!==s[0])return null;const n=s[1],i={};let o=2;for(;o<s.length&&")"!==s[o];){const t=s[o];if(o++,e.has(t)){const{values:e,next:n}=readList(s,o);i[t]=e,o=n}else if(t.startsWith("X-")){const{next:e}=readList(s,o);i[t]=[],o=e}else i[t]=[]}return{oid:n,fields:i}}const s=new Set(["NAME","DESC","SUP","EQUALITY","ORDERING","SUBSTR","SYNTAX","USAGE"]),n=new Set(["NAME","DESC","SUP","MUST","MAY"]),i=new Set(["DESC"]),o=new Set(["NAME","DESC","SUP","SYNTAX"]);function parseAttributeType(t){const e=parseDefinition(t,s);if(!e)return null;const{oid:n,fields:i}=e;let o,a=i.SYNTAX?.[0];if(a){const t=/^(.*?)\{(\d+)\}$/.exec(a);t&&(a=t[1],o=parseInt(t[2],10))}return{oid:n,names:i.NAME||[],desc:i.DESC?.[0],obsolete:"OBSOLETE"in i,sup:i.SUP?.[0],equality:i.EQUALITY?.[0],ordering:i.ORDERING?.[0],substr:i.SUBSTR?.[0],syntax:a,syntaxLength:o,singleValue:"SINGLE-VALUE"in i,collective:"COLLECTIVE"in i,noUserModification:"NO-USER-MODIFICATION"in i,usage:i.USAGE?.[0]||"userApplications"}}function parseObjectClass(t){const e=parseDefinition(t,n);if(!e)return null;const{oid:s,fields:i}=e,o="ABSTRACT"in i?"ABSTRACT":"AUXILIARY"in i?"AUXILIARY":"STRUCTURAL";return{oid:s,names:i.NAME||[],desc:i.DESC?.[0],obsolete:"OBSOLETE"in i,sup:i.SUP||[],kind:o,must:i.MUST||[],may:i.MAY||[]}}function parseSyntax(e){const s=parseDefinition(e,i);return s?{oid:s.oid,desc:s.fields.DESC?.[0],binary:t.has(s.oid)}:null}function parseMatchingRule(t){const e=parseDefinition(t,o);return e?{oid:e.oid,names:e.fields.NAME||[],desc:e.fields.DESC?.[0],syntax:e.fields.SYNTAX?.[0]}:null}function parseSchema(t){const parseAll=(t,e)=>(t||[]).map(e).filter(t=>null!==t);return{attributeTypes:parseAll(t.attributeTypes,parseAttributeType),objectClasses:parseAll(t.objectClasses,parseObjectClass),syntaxes:parseAll(t.ldapSyntaxes,parseSyntax),matchingRules:parseAll(t.matchingRules,parseMatchingRule)}}class SchemaIndex{constructor(t){this.objectClassesByName=new Map,this.attributeTypesByName=new Map,this.syntaxesByOid=new Map,this.schema=t;for(const e of t.objectClasses){this.objectClassesByName.set(e.oid.toLowerCase(),e);for(const t of e.names)this.objectClassesByName.set(t.toLowerCase(),e)}for(const e of t.attributeTypes){this.attributeTypesByName.set(e.oid.toLowerCase(),e);for(const t of e.names)this.attributeTypesByName.set(t.toLowerCase(),e)}for(const e of t.syntaxes)this.syntaxesByOid.set(e.oid,e)}getObjectClass(t){return this.objectClassesByName.get(t.toLowerCase())}getAttributeType(t){const e=t.split(";")[0];return this.attributeTypesByName.get(e.toLowerCase())}getAttributeSyntax(t){const e=new Set;let s=this.getAttributeType(t);for(;s&&!e.has(s.oid);){if(s.syntax)return s.syntax;e.add(s.oid),s=s.sup?this.getAttributeType(s.sup):void 0}}isBinaryAttribute(s){const n=s.split(";")[0].toLowerCase();if(s.split(";").slice(1).some(t=>"binary"===t.toLowerCase()))return!0;if(e.has(n))return!0;const i=this.getAttributeSyntax(n);return!!i&&t.has(i)}resolveAttributes(t){const e=new Set,s=new Set,n=new Set,walk=t=>{const i=t.toLowerCase();if(n.has(i))return;n.add(i);const o=this.getObjectClass(t);o&&(o.must.forEach(t=>e.add(t)),o.may.forEach(t=>s.add(t)),o.sup.forEach(walk))};t.forEach(walk);for(const t of e)for(const e of s)e.toLowerCase()===t.toLowerCase()&&s.delete(e);return{must:[...e],may:[...s]}}missingClasses(t,e){const s=new Set(e.map(t=>this.getObjectClass(t)?.oid??t.toLowerCase())),n=[],i=[];for(const e of t){const t=this.getObjectClass(e);t&&!s.has(t.oid)&&(s.add(t.oid),"AUXILIARY"===t.kind?n.push(t):"STRUCTURAL"===t.kind&&i.push(t.names[0]??e))}return{auxiliary:n,structural:i}}}export{SchemaIndex,parseAttributeType,parseMatchingRule,parseObjectClass,parseSchema,parseSyntax};
1
+ const e=new Set(["1.3.6.1.4.1.1466.115.121.1.5","1.3.6.1.4.1.1466.115.121.1.8","1.3.6.1.4.1.1466.115.121.1.9","1.3.6.1.4.1.1466.115.121.1.10","1.3.6.1.4.1.1466.115.121.1.28","1.3.6.1.4.1.1466.115.121.1.40","1.3.6.1.4.1.1466.115.121.1.49","1.3.6.1.1.16.1"]),t=new Set(["userpassword","jpegphoto","usercertificate","cacertificate","certificaterevocationlist","authoritiyrevocationlist","crosscertificatepair","photo","personalsignature","audio","thumbnailphoto","thumbnaillogo","objectguid","objectsid"]);function readList(e,t){if(t>=e.length||")"===e[t])return{values:[],next:t};if("("!==e[t])return{values:[e[t]],next:t+1};const s=[];let n=t+1;for(;n<e.length&&")"!==e[n];)"$"!==e[n]&&s.push(e[n]),n++;return{values:s,next:n+1}}function parseDefinition(e,t){const s=function(e){const t=[];let s=0;for(;s<e.length;){const n=e[s];if(/\s/.test(n))s++;else if("("===n||")"===n)t.push(n),s++;else if("'"===n){let n="";for(s++;s<e.length&&"'"!==e[s];){if("\\"===e[s]&&s+2<e.length){const t=e.substring(s+1,s+3);if(/^[0-9a-fA-F]{2}$/.test(t)){n+=String.fromCharCode(parseInt(t,16)),s+=3;continue}}n+=e[s],s++}s++,t.push(n)}else{let n="";for(;s<e.length&&!/[\s()']/.test(e[s]);)n+=e[s],s++;t.push(n)}}return t}(e);if(s.length<2||"("!==s[0])return null;const n=s[1],i={};let o=2;for(;o<s.length&&")"!==s[o];){const e=s[o];if(o++,t.has(e)){const{values:t,next:n}=readList(s,o);i[e]=t,o=n}else if(e.startsWith("X-")){const{next:t}=readList(s,o);i[e]=[],o=t}else i[e]=[]}return{oid:n,fields:i}}const s=new Set(["NAME","DESC","SUP","EQUALITY","ORDERING","SUBSTR","SYNTAX","USAGE"]),n=new Set(["NAME","DESC","SUP","MUST","MAY"]),i=new Set(["DESC"]),o=new Set(["NAME","DESC","SUP","SYNTAX"]);function parseAttributeType(e){const t=parseDefinition(e,s);if(!t)return null;const{oid:n,fields:i}=t;let o,a=i.SYNTAX?.[0];if(a){const e=/^(.*?)\{(\d+)\}$/.exec(a);e&&(a=e[1],o=parseInt(e[2],10))}return{oid:n,names:i.NAME||[],desc:i.DESC?.[0],obsolete:"OBSOLETE"in i,sup:i.SUP?.[0],equality:i.EQUALITY?.[0],ordering:i.ORDERING?.[0],substr:i.SUBSTR?.[0],syntax:a,syntaxLength:o,singleValue:"SINGLE-VALUE"in i,collective:"COLLECTIVE"in i,noUserModification:"NO-USER-MODIFICATION"in i,usage:i.USAGE?.[0]||"userApplications"}}function parseObjectClass(e){const t=parseDefinition(e,n);if(!t)return null;const{oid:s,fields:i}=t,o="ABSTRACT"in i?"ABSTRACT":"AUXILIARY"in i?"AUXILIARY":"STRUCTURAL";return{oid:s,names:i.NAME||[],desc:i.DESC?.[0],obsolete:"OBSOLETE"in i,sup:i.SUP||[],kind:o,must:i.MUST||[],may:i.MAY||[]}}function parseSyntax(t){const s=parseDefinition(t,i);return s?{oid:s.oid,desc:s.fields.DESC?.[0],binary:e.has(s.oid)}:null}function parseMatchingRule(e){const t=parseDefinition(e,o);return t?{oid:t.oid,names:t.fields.NAME||[],desc:t.fields.DESC?.[0],syntax:t.fields.SYNTAX?.[0]}:null}function parseSchema(e){const parseAll=(e,t)=>(e||[]).map(t).filter(e=>null!==e);return{attributeTypes:parseAll(e.attributeTypes,parseAttributeType),objectClasses:parseAll(e.objectClasses,parseObjectClass),syntaxes:parseAll(e.ldapSyntaxes,parseSyntax),matchingRules:parseAll(e.matchingRules,parseMatchingRule)}}class SchemaIndex{schema;objectClassesByName=new Map;attributeTypesByName=new Map;syntaxesByOid=new Map;constructor(e){this.schema=e;for(const t of e.objectClasses){this.objectClassesByName.set(t.oid.toLowerCase(),t);for(const e of t.names)this.objectClassesByName.set(e.toLowerCase(),t)}for(const t of e.attributeTypes){this.attributeTypesByName.set(t.oid.toLowerCase(),t);for(const e of t.names)this.attributeTypesByName.set(e.toLowerCase(),t)}for(const t of e.syntaxes)this.syntaxesByOid.set(t.oid,t)}getObjectClass(e){return this.objectClassesByName.get(e.toLowerCase())}getAttributeType(e){const t=e.split(";")[0];return this.attributeTypesByName.get(t.toLowerCase())}getAttributeSyntax(e){const t=new Set;let s=this.getAttributeType(e);for(;s&&!t.has(s.oid);){if(s.syntax)return s.syntax;t.add(s.oid),s=s.sup?this.getAttributeType(s.sup):void 0}}isBinaryAttribute(s){const n=s.split(";")[0].toLowerCase();if(s.split(";").slice(1).some(e=>"binary"===e.toLowerCase()))return!0;if(t.has(n))return!0;const i=this.getAttributeSyntax(n);return!!i&&e.has(i)}resolveAttributes(e){const t=new Set,s=new Set,n=new Set,walk=e=>{const i=e.toLowerCase();if(n.has(i))return;n.add(i);const o=this.getObjectClass(e);o&&(o.must.forEach(e=>t.add(e)),o.may.forEach(e=>s.add(e)),o.sup.forEach(walk))};e.forEach(walk);for(const e of t)for(const t of s)t.toLowerCase()===e.toLowerCase()&&s.delete(t);return{must:[...t],may:[...s]}}missingClasses(e,t){const s=new Set(t.map(e=>this.getObjectClass(e)?.oid??e.toLowerCase())),n=[],i=[];for(const t of e){const e=this.getObjectClass(t);e&&!s.has(e.oid)&&(s.add(e.oid),"AUXILIARY"===e.kind?n.push(e):"STRUCTURAL"===e.kind&&i.push(e.names[0]??t))}return{auxiliary:n,structural:i}}}export{SchemaIndex,parseAttributeType,parseMatchingRule,parseObjectClass,parseSchema,parseSyntax};
@@ -1 +1 @@
1
- class ConfigParser{constructor(e){this.config=e}parse(e=process.argv){const r={},t=this.parseCliArgs(e);for(const e of this.config){const n=this.getKeyFromCliArg(e[0]);let i=e[2],o=!0;if(void 0!==e[1]){const r=process.env[e[1]],t="array"===e[3]&&void 0!==r?splitValue(r,e[5],"plural"):[],n=void 0!==r&&("array"===e[3]&&0===t.length||("number"===e[3]||"json"===e[3])&&""===r.trim());if(n&&e[6])throw new Error(`Error in environment variable ${e[1]}: the value is empty. Set it, or leave the variable out`);if(void 0!==r&&!n)if(o=!1,"boolean"===e[3])i="true"===r.toLowerCase();else if("number"===e[3]){if(i=parseNumber(r),Number.isNaN(i))throw new Error(`Error in environment variable ${e[1]}: "${r}" is not a number`)}else if("array"===e[3])i=t;else if("json"===e[3])try{i=JSON.parse(r)}catch(r){throw new Error(`Error parsing JSON from environment variable ${e[1]}: ${r}`)}else i=r}if(t.has(e[0])){const r=t.get(e[0]);if("boolean"===e[3])i=!0;else if("number"===e[3])i=r;else if("array"===e[3])i=(o?[]:i).concat(r.flatMap(r=>splitCliValue(e[0],r,e[5],"singular"))),o=!1;else if("json"===e[3])try{i=JSON.parse(r)}catch(r){throw new Error(`Error parsing JSON from command line argument ${e[0]}: ${r}`)}else i=r;t.delete(e[0])}if("array"===e[3]&&e[4]&&t.has(e[4])){const r=t.get(e[4]);i=(o?[]:i).concat(splitCliValue(e[4],r,e[5],"plural")),t.delete(e[4])}r[n]=i}return t.forEach((e,t)=>{if(r[t])throw new Error(`Error in command line: ${t} redefined`);r[t.replace(/^-+/,"").replace(/-/g,"_")]=e}),r}parseCliArgs(e){const r=new Map;for(let t=2;t<e.length;t++){const n=e[t];if(n.startsWith("--")||n.startsWith("-")){const i=this.config.find(e=>e[0]===n);if(i&&"boolean"===i[3])r.set(n,!0);else if(i&&"number"===i[3]){if(!e[t+1]?.trim())throw new Error(`Error in command line: ${n} has an empty value. Leave it out to keep the default`);const i=parseNumber(e[t+1]);if(Number.isNaN(i))throw new Error(`Error in command line: ${n} takes a number, got "${e[t+1]}"`);r.set(n,i),t++}else if(i&&"array"===i[3]){const o=r.get(n)||[],s=e[t+1];o.push(s),r.set(n,o);const a=e[t+2];if(void 0!==a&&!a.startsWith("-"))throw new Error(`Error in command line: ${n} takes one value, got "${s}" followed by "${a}". Repeat ${n} for each value`+(i[4]?`, or give them all to ${i[4]}`:""))}else{const i=e[t+1];r.set(n,i),t++}}}return r}getKeyFromCliArg(e){return e.startsWith("--")?e.substring(2).replace(/-/g,"_"):e.startsWith("-")?e.substring(1).replace(/-/g,"_"):e}}function splitValue(e,r,t){let n;if("singular"===t)"identifiers"===r&&(n=/,/);else if("dns"===r)n=/;/;else{const t="phrases"===r?"\\r\\n":"\\s";n=new RegExp(`[${e.includes(";")?";":","}${t}]+`)}return n?e.split(n).map(e=>e.trim()).filter(e=>e.length>0):[e.trim()].filter(e=>e.length>0)}function parseNumber(e){const r=e.trim();return/^-?\d+$/.test(r)?Number(r):NaN}function splitCliValue(e,r,t,n){const i=void 0===r?[]:splitValue(r,t,n);if(0===i.length)throw new Error(`Error in command line: ${e} has an empty value. Leave it out to keep the default`);return i}function parseConfig(e,r=process.argv){return new ConfigParser(e).parse(r)}export{ConfigParser,parseConfig};
1
+ class ConfigParser{config;unknownArgs=[];get unknownOptions(){return[...this.unknownArgs]}constructor(e){this.config=e}parse(e=process.argv){const r={},t=this.parseCliArgs(e);for(const e of this.config){const n=this.getKeyFromCliArg(e[0]);let i=e[2],s=!0;if(void 0!==e[1]){const r=process.env[e[1]],t="array"===e[3]&&void 0!==r?splitValue(r,e[5],"plural"):[],n=void 0!==r&&("array"===e[3]&&0===t.length||("number"===e[3]||"json"===e[3])&&""===r.trim());if(n&&e[6])throw new Error(`Error in environment variable ${e[1]}: the value is empty. Set it, or leave the variable out`);if(void 0!==r&&!n)if(s=!1,"boolean"===e[3])i="true"===r.toLowerCase();else if("number"===e[3]){if(i=parseNumber(r),Number.isNaN(i))throw new Error(`Error in environment variable ${e[1]}: "${r}" is not a number`)}else if("array"===e[3])i=t;else if("json"===e[3])try{i=JSON.parse(r)}catch(r){throw new Error(`Error parsing JSON from environment variable ${e[1]}: ${r}`)}else i=r}if(t.has(e[0])){const r=t.get(e[0]);if("boolean"===e[3])i=!0;else if("number"===e[3])i=r;else if("array"===e[3])i=(s?[]:i).concat(r.flatMap(r=>splitCliValue(e[0],r,e[5],"singular"))),s=!1;else if("json"===e[3])try{i=JSON.parse(r)}catch(r){throw new Error(`Error parsing JSON from command line argument ${e[0]}: ${r}`)}else i=r;t.delete(e[0])}if("array"===e[3]&&e[4]&&t.has(e[4])){const r=t.get(e[4]);i=(s?[]:i).concat(splitCliValue(e[4],r,e[5],"plural")),t.delete(e[4])}r[n]=i}return this.unknownArgs=[],t.forEach((e,t)=>{if(r[t])throw new Error(`Error in command line: ${t} redefined`);this.unknownArgs.push(t),r[t.replace(/^-+/,"").replace(/-/g,"_")]=e}),r}parseCliArgs(e){const r=new Map;for(let t=2;t<e.length;t++){const n=e[t];if(n.startsWith("--")||n.startsWith("-")){const i=this.config.find(e=>e[0]===n);if(i&&"boolean"===i[3])r.set(n,!0);else if(i&&"number"===i[3]){if(!e[t+1]?.trim())throw new Error(`Error in command line: ${n} has an empty value. Leave it out to keep the default`);const i=parseNumber(e[t+1]);if(Number.isNaN(i))throw new Error(`Error in command line: ${n} takes a number, got "${e[t+1]}"`);r.set(n,i),t++}else if(i&&"array"===i[3]){const s=r.get(n)||[],o=e[t+1];s.push(o),r.set(n,s);const a=e[t+2];if(void 0!==a&&!a.startsWith("-"))throw new Error(`Error in command line: ${n} takes one value, got "${o}" followed by "${a}". Repeat ${n} for each value`+(i[4]?`, or give them all to ${i[4]}`:""))}else{const i=e[t+1];r.set(n,i),t++}}}return r}getKeyFromCliArg(e){return e.startsWith("--")?e.substring(2).replace(/-/g,"_"):e.startsWith("-")?e.substring(1).replace(/-/g,"_"):e}}function splitValue(e,r,t){let n;if("singular"===t)"identifiers"===r&&(n=/,/);else if("dns"===r)n=/;/;else{const t="phrases"===r?"\\r\\n":"\\s";n=new RegExp(`[${e.includes(";")?";":","}${t}]+`)}return n?e.split(n).map(e=>e.trim()).filter(e=>e.length>0):[e.trim()].filter(e=>e.length>0)}function parseNumber(e){const r=e.trim();return/^-?\d+$/.test(r)?Number(r):NaN}function splitCliValue(e,r,t,n){const i=void 0===r?[]:splitValue(r,t,n);if(0===i.length)throw new Error(`Error in command line: ${e} has an empty value. Leave it out to keep the default`);return i}export{ConfigParser,parseNumber,splitValue};
@@ -0,0 +1 @@
1
+ import{Value as e}from"@sinclair/typebox/value";import{BadRequestError as r}from"./errors.js";import{wantJson as s}from"./expressFormatedResponses.js";import{asyncHandler as t}from"./utils.js";const o=new WeakMap,registeredRoutes=e=>o.get(e)??[],label=(e,r)=>r&&"/"!==r?`${e}.${r.slice(1).replaceAll("/",".")}`:e,check=(r,s,t,o,a)=>{const p=o&&t&&"object"==typeof t&&!Array.isArray(t)?{...t}:void 0,n=o?e.Convert(s,t):t;if(p){const e=s.properties??{};for(const[r,s]of Object.entries(e))"integer"!==s.type||"string"!=typeof p[r]||/^[+-]?\d+$/.test(p[r])||(n[r]=p[r])}for(const t of e.Errors(s,n))a.push(`${label(r,t.path)}: ${t.message}`);return n},defineRoute=(e,a)=>{const p=t(async(e,t,o)=>{if(!1!==a.json&&!s(e,t))return;const p=[],n=a.params?check("params",a.params,e.params,!0,p):e.params,i=a.query?check("query",a.query,e.query,!0,p):e.query,d=a.body?check("body",a.body,e.body??{},!1,p):e.body;if(p.length)throw new r(`Invalid request: ${p.join("; ")}`);await a.handler({req:e,res:t,next:o,params:n,query:i,body:d})}),n=o.get(e)??[];o.has(e)||o.set(e,n),n.push({method:a.method,path:a.path,params:a.params,query:a.query,body:a.body,responses:a.responses,json:a.json,summary:a.summary,description:a.description,tags:a.tags});const i=a.middleware?Array.isArray(a.middleware)?a.middleware:[a.middleware]:[];((e,r,s,...t)=>{switch(r){case"get":return void e.get(s,...t);case"post":return void e.post(s,...t);case"put":return void e.put(s,...t);case"patch":return void e.patch(s,...t);case"delete":e.delete(s,...t)}})(e,a.method,a.path,...i,p)};export{defineRoute,registeredRoutes};
@@ -1 +1 @@
1
- import e from"fs";import{join as t,dirname as r,resolve as s,relative as a,isAbsolute as o}from"path";import{fileURLToPath as i}from"url";import n from"../config/args.js";import{transformSchemas as c}from"./utils.js";const h=t(r(i(import.meta.url)),"..","..","static","schemas"),l="ldap-rest:",isPlainObject=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function mergeSchema(e,t){if(!isPlainObject(t))return t;const r=isPlainObject(e)?{...e}:{};for(const[e,s]of Object.entries(t))null!==s?Object.defineProperty(r,e,{value:mergeSchema(Object.prototype.hasOwnProperty.call(r,e)?r[e]:void 0,s),enumerable:!0,writable:!0,configurable:!0}):delete r[e];return r}function resolveSchemaBase(e,t){if(!t.startsWith(l))return s(r(e),t);const i=s(h,t.slice(10)),n=a(h,i);if(!n||n.startsWith("..")||o(n))throw new Error(`${e}: "${t}" is not a shipped schema`);return i}class SchemaChain{constructor(e){this.options=e,this.files=[],this.realPaths=[],this.layers=[]}readError(e,t){return this.files.length?new Error(`${this.files[this.files.length-1]} extends ${e}, which cannot be read: ${t.message}`):t}add(e,t,r){if(this.realPaths.includes(t))throw new Error(`"extends" loop: ${[...this.files,e].join(" -> ")}`);this.files.push(e),this.realPaths.push(t);const s=this.options.config?c(r,this.options.config):r;let a;try{a=JSON.parse(s)}catch(t){throw new Error(`${e}: ${t.message}`)}if(!isPlainObject(a)||!("extends"in a))return void this.layers.push(a);const{extends:o,...i}=a;if("string"!=typeof o||!o)throw new Error(`${e}: "extends" must be the path of a schema`);return this.layers.push(i),resolveSchemaBase(e,o)}result(){return this.layers.reduceRight((e,t)=>mergeSchema(e,t))}}function loadSchemaFile(t,r={}){const s=new SchemaChain(r);for(let r=t;r;){let t;try{t=e.readFileSync(r,"utf8")}catch(e){throw s.readError(r,e)}r=s.add(r,e.realpathSync(r),t)}return s.result()}async function loadSchemaFileAsync(t,r={}){const s=new SchemaChain(r);for(let r=t;r;){let t;try{t=await e.promises.readFile(r,"utf8")}catch(e){throw s.readError(r,e)}r=s.add(r,await e.promises.realpath(r),t)}return s.result()}function staticName(e){const t=e.static_name,r=("string"==typeof t?t:"").trim();let s=0,a=r.length;for(;s<a&&"/"===r[s];)s++;for(;a>s&&"/"===r[a-1];)a--;return r.slice(s,a)||"static"}function schemaUrl(e,t){const r=t.indexOf("/schemas/");if(-1!==r)return`/${staticName(e)}${t.substring(r)}`}const f=[["group_schema","--group-schema"],["organization_schema","--organization-schema"],["ldap_flat_schema","--ldap-flat-schema"]];function configuredSchemaFiles(e){const t=[];for(const[r,a]of f){const o=[n.find(e=>e[0]===a)?.[2]].flat().filter(e=>"string"==typeof e&&!!e).map(e=>s(e));for(const a of[e[r]].flat())"string"==typeof a&&a&&!o.includes(s(a))&&t.push(a)}return t}export{l as SHIPPED_SCHEMA_PREFIX,configuredSchemaFiles,loadSchemaFile,loadSchemaFileAsync,mergeSchema,resolveSchemaBase,schemaUrl,h as shippedSchemasPath,staticName};
1
+ import e from"fs";import{join as t,dirname as r,resolve as s,relative as a,isAbsolute as o}from"path";import{fileURLToPath as n}from"url";import i from"../config/args.js";import{transformSchemas as c}from"./utils.js";const h=t(r(n(import.meta.url)),"..","..","static","schemas"),l="ldap-rest:",isPlainObject=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function mergeSchema(e,t){if(!isPlainObject(t))return t;const r=isPlainObject(e)?{...e}:{};for(const[e,s]of Object.entries(t))null!==s?Object.defineProperty(r,e,{value:mergeSchema(Object.hasOwn(r,e)?r[e]:void 0,s),enumerable:!0,writable:!0,configurable:!0}):delete r[e];return r}function resolveSchemaBase(e,t){if(!t.startsWith(l))return s(r(e),t);const n=s(h,t.slice(10)),i=a(h,n);if(!i||i.startsWith("..")||o(i))throw new Error(`${e}: "${t}" is not a shipped schema`);return n}class SchemaChain{options;files=[];realPaths=[];layers=[];constructor(e){this.options=e}readError(e,t){return this.files.length?new Error(`${this.files[this.files.length-1]} extends ${e}, which cannot be read: ${t.message}`):t}add(e,t,r){if(this.realPaths.includes(t))throw new Error(`"extends" loop: ${[...this.files,e].join(" -> ")}`);this.files.push(e),this.realPaths.push(t);const s=this.options.config?c(r,this.options.config):r;let a;try{a=JSON.parse(s)}catch(t){throw new Error(`${e}: ${t.message}`)}if(!isPlainObject(a)||!("extends"in a))return void this.layers.push(a);const{extends:o,...n}=a;if("string"!=typeof o||!o)throw new Error(`${e}: "extends" must be the path of a schema`);return this.layers.push(n),resolveSchemaBase(e,o)}result(){return this.layers.reduceRight((e,t)=>mergeSchema(e,t))}}function assertMaxLengths(e,t){const r=e?.attributes;if(r&&"object"==typeof r)for(const[e,s]of Object.entries(r))for(const[r,a]of[[e,s?.maxLength],[`${e}.items`,s?.items?.maxLength]])if(void 0!==a&&("number"!=typeof a||!Number.isInteger(a)||a<=0))throw new Error(`${t}: ${r}.maxLength must be a positive integer`)}function detachAttributes(e){const t=e,r=t?.attributes;r&&"object"==typeof r&&!Array.isArray(r)&&(t.attributes=Object.assign(Object.create(null),r))}function loadSchemaFile(t,r={}){const s=new SchemaChain(r);for(let r=t;r;){let t;try{t=e.readFileSync(r,"utf8")}catch(e){throw s.readError(r,e)}r=s.add(r,e.realpathSync(r),t)}const a=s.result();return assertMaxLengths(a,t),detachAttributes(a),a}async function loadSchemaFileAsync(t,r={}){const s=new SchemaChain(r);for(let r=t;r;){let t;try{t=await e.promises.readFile(r,"utf8")}catch(e){throw s.readError(r,e)}r=s.add(r,await e.promises.realpath(r),t)}const a=s.result();return assertMaxLengths(a,t),detachAttributes(a),a}function staticName(e){const t=e.static_name,r=("string"==typeof t?t:"").trim();let s=0,a=r.length;for(;s<a&&"/"===r[s];)s++;for(;a>s&&"/"===r[a-1];)a--;return r.slice(s,a)||"static"}function schemaUrl(e,t){const r=t.indexOf("/schemas/");if(-1!==r)return`/${staticName(e)}${t.substring(r)}`}const f=[["group_schema","--group-schema"],["organization_schema","--organization-schema"],["ldap_flat_schema","--ldap-flat-schema"]];function configuredSchemaFiles(e){const t=[];for(const[r,a]of f){const o=[i.find(e=>e[0]===a)?.[2]].flat().filter(e=>"string"==typeof e&&!!e).map(e=>s(e));for(const a of[e[r]].flat())"string"==typeof a&&a&&!o.includes(s(a))&&t.push(a)}return t}export{l as SHIPPED_SCHEMA_PREFIX,configuredSchemaFiles,loadSchemaFile,loadSchemaFileAsync,mergeSchema,resolveSchemaBase,schemaUrl,h as shippedSchemasPath,staticName};
@@ -1 +1 @@
1
- class Store{constructor(e){this.closed=!1,this.logger=e}health(){return Promise.resolve("ok")}static scoped(e,t){return`${e}\0${t}`}static readable(e){return e.replace("\0",":")}static encodeRecord(e,t){return`${t.deadline} ${Store.readable(e)}\n${t.value}`}static decodeRecord(e){const t=String(e),s=t.indexOf("\n"),r=s<0?t:t.slice(0,s),i=parseInt(r.split(" ")[0],10);return Number.isFinite(i)?{deadline:i,value:s<0?"":t.slice(s+1)}:null}async open(){}close(){return this.closed=!0,this.stopSweeping(),Promise.resolve()}async get(e,t){const s=Store.scoped(e,t),r=await this.load(s);return null===r?null:r.deadline<=Date.now()?(this.drop(s).catch(()=>{}),null):r.value}async set(e,t,s,r){await this.save(Store.scoped(e,t),{value:s,deadline:r})}async delete(e,t){await this.drop(Store.scoped(e,t))}startSweeping(e){if(this.sweeper||e<=0)return;const run=()=>{this.sweep().then(e=>{e>0&&this.logger.debug(`${this.name}: swept ${e} record(s)`)}).catch(e=>{this.logger.warn(`${this.name}: sweep failed: ${e instanceof Error?e.message:JSON.stringify(e)}`)})};this.sweeper=setInterval(run,1e3*e),this.sweeper.unref?.(),run()}stopSweeping(){this.sweeper&&clearInterval(this.sweeper),this.sweeper=void 0}}export{Store as default};
1
+ class Store{logger;sweeper;constructor(e){this.logger=e}health(){return Promise.resolve("ok")}static scoped(e,t){return`${e}\0${t}`}static readable(e){return e.replace("\0",":")}static encodeRecord(e,t){return`${t.deadline} ${Store.readable(e)}\n${t.value}`}static decodeRecord(e){const t=String(e),s=t.indexOf("\n"),r=s<0?t:t.slice(0,s),i=parseInt(r.split(" ")[0],10);return Number.isFinite(i)?{deadline:i,value:s<0?"":t.slice(s+1)}:null}async open(){}closed=!1;close(){return this.closed=!0,this.stopSweeping(),Promise.resolve()}async get(e,t){const s=Store.scoped(e,t),r=await this.load(s);return null===r?null:r.deadline<=Date.now()?(this.drop(s).catch(()=>{}),null):r.value}async set(e,t,s,r){await this.save(Store.scoped(e,t),{value:s,deadline:r})}async delete(e,t){await this.drop(Store.scoped(e,t))}startSweeping(e){if(this.sweeper||e<=0)return;const run=()=>{this.sweep().then(e=>{e>0&&this.logger.debug(`${this.name}: swept ${e} record(s)`)}).catch(e=>{this.logger.warn(`${this.name}: sweep failed: ${e instanceof Error?e.message:JSON.stringify(e)}`)})};this.sweeper=setInterval(run,1e3*e),this.sweeper.unref?.(),run()}stopSweeping(){this.sweeper&&clearInterval(this.sweeper),this.sweeper=void 0}}export{Store as default};
package/dist/lib/utils.js CHANGED
@@ -1 +1 @@
1
- import{BadRequestError as e}from"./errors.js";import{getLogger as n}from"./expressFormatedResponses.js";import{hookFailures as t}from"./metrics.js";import{runAsPlugin as r,outsideRequest as o}from"./requestContext.js";const i=new Map;function getCompiledRegex(e,n){const t=n?`${e}:${n}`:e;let r=i.get(t);return r||(r=new RegExp(e,n),i.set(t,r)),r}function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const describeThrown=e=>{try{return JSON.stringify(e)??String(e)}catch{return String(e)}},hookLabel=e=>{const n=hookOwner(e);return n?` in ${n.plugin} (${n.hook})`:e.name?` in ${e.name}`:""},runAsHookOwner=(e,n)=>{const t=hookOwner(e);return r(t?.plugin,t?.hook,n)},launchHooks=async(e,...r)=>{if(e)for(const i of e)i&&await runAsHookOwner(i,async()=>{try{await o(()=>i(...r))}catch(e){const r=hookLabel(i),o=hookOwner(i);t.inc({hook:o?.hook??(i.name||"anonymous"),plugin:o?.plugin??"none"}),e instanceof Error?n()?.error(`Hook error${r}`,e):n()?.error(`Hook error${r}: ${describeThrown(e)}`)}})},s=new WeakMap,recordHookOwner=(e,n,t)=>{s.set(e,{plugin:n,hook:t})},hookOwner=e=>s.get(e),isAuthzRefusal=e=>/\[authz-forbidden\]/.test(String(e?.message??"")),isRefusal=e=>403===e?.statusCode||isAuthzRefusal(e),launchHooksChained=async(e,t)=>{if(e)for(const r of e)r&&(t=await runAsHookOwner(r,async()=>{try{return await o(()=>r(t))}catch(e){if(isRefusal(e)){const t=hookOwner(r);n()?.warn(`${t?`${t.plugin} refused ${t.hook}`:`a hook no plugin registered (${r.name||"anonymous"}) refused`}: ${e.message}`)}throw e}}));return t},transformSchemas=(e,n)=>e.toString().replace(/__(\S+)__/g,(e,t)=>{if(!t||"string"!=typeof t)return e;const r=t.trim().toLowerCase();return n[r]?"object"!=typeof n[r]?n[r]:JSON.stringify(n[r]):e});function escapeLdapFilter(e){return e.replace(/\\/g,"\\5c").replace(/\*/g,"\\2a").replace(/\(/g,"\\28").replace(/\)/g,"\\29").replace(/\0/g,"\\00")}function substringSearchFilter(n,t){const r=/^[a-zA-Z][a-zA-Z0-9-]*$/,o=t.split(",").map(e=>e.trim()).filter(Boolean);if(0===o.length||!o.every(e=>r.test(e)))throw new e("Invalid LDAP attribute name");const i=escapeLdapFilter(n),s=o.map(e=>`(${e}=*${i}*)`);return 1===s.length?s[0]:`(|${s.join("")})`}function escapeDnValue(e){return e.replace(/\\/g,"\\\\").replace(/,/g,"\\,").replace(/\+/g,"\\+").replace(/"/g,'\\"').replace(/</g,"\\<").replace(/>/g,"\\>").replace(/;/g,"\\;").replace(/=/g,"\\=").replace(/\0/g,"\\00").replace(/^\s/,"\\ ").replace(/\s$/,"\\ ").replace(/^#/,"\\#")}const a=new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0});function unescapeDnValue(n){if("string"!=typeof n)throw new e("DN value must be a string");let t="",r=0,o=[];const flushBytes=()=>{if(o.length){try{t+=a.decode(new Uint8Array(o))}catch{t+=o.map(e=>String.fromCharCode(e)).join("")}o=[]}};for(;r<n.length;)if("\\"===n[r]&&r+1<n.length){const e=n[r+1];if(/[0-9a-fA-F]/.test(e)&&r+2<n.length){const e=n.substring(r+1,r+3);if(/^[0-9a-fA-F]{2}$/.test(e)){o.push(parseInt(e,16)),r+=3;continue}}flushBytes(),t+=e,r+=2}else flushBytes(),t+=n[r],r++;return flushBytes(),t}function validateDnValue(e,n){if(null==e||"string"!=typeof e)throw new Error(`${n} must be a string`);if(0===e.trim().length)throw new Error(`${n} must be a non-empty string`);if(/[\x00-\x1F\x7F]/.test(e))throw new Error(`${n} contains invalid control characters`);if(/[\u200B-\u200F\uFEFF]/.test(e))throw new Error(`${n} contains invalid invisible characters`)}function asciiSpace(e){return" "===e||e>="\t"&&e<="\r"}function trimUnescaped(e){let n=0,t=e.length;for(;n<t&&asciiSpace(e[n]);)n++;for(;t>n&&asciiSpace(e[t-1]);)t--;if(t<e.length){let r=0;for(let o=t-1;o>=n&&"\\"===e[o];o--)r++;r%2==1&&t++}return e.slice(n,t)}function parseDn(n){if("string"!=typeof n)throw new e("DN must be a string");const t=[];let r="",o=!1,i=!1;for(let e=0;e<n.length;e++){const s=n[e];o?(r+=s,o=!1):"\\"===s?(r+=s,o=!0):'"'===s&&(i||opensValue(r))?(r+=s,i=!i):i||","!==s&&";"!==s?r+=s:(t.push(trimUnescaped(r)),r="")}if(i)throw new e("DN has an unbalanced quote");return r&&t.push(trimUnescaped(r)),t}function opensValue(e){const n=e.trimEnd();if(!n.endsWith("="))return!1;let t=0;for(let e=n.length-2;e>=0&&"\\"===n[e];e--)t++;return t%2==0}function isDnSyntax(e){let n;try{n=parseDn(e)}catch{return!1}if(0===n.length)return!1;const t=/^\s*(?:[A-Za-z][A-Za-z0-9-]*|(?:OID\.)?\d+(?:\.\d+)+)\s*=/i;return n.every(e=>{if(!e)return!1;const n=[];let r="",o=!1;for(const t of e){if(o)o=!1;else if("\\"===t)o=!0;else if("+"===t){n.push(r),r="";continue}r+=t}return n.push(r),n.every(e=>t.test(e))})}function getParentDn(e){const n=parseDn(e);return n.length<=1?e:n.slice(1).join(",")}function getRdn(e){return parseDn(e)[0]||""}function rdnValue(e){const n=/^[^=]+=((?:\\.|[^,])*)/.exec(e);return n?n[1].replace(/\\(.)/g,"$1"):""}function isChildOf(e,n){try{return isDnInBranch(e,n)&&!isDnInBranch(n,e)}catch{return!1}}const c=/((?:\\[0-9a-fA-F]{2})+)|\\([^])/g;function hexByte(e){return e.toString(16).padStart(2,"0")}const l=new Map,u=new Map;function registerAttributeTypes(e){for(const{oid:n,names:t}of e){const e=(t[0]??n).toLowerCase();for(const r of[n,...t])l.set(r.toLowerCase(),e),u.set(r.toLowerCase(),t[0]??n)}}function knownAttributeName(e){return u.get(e.toLowerCase().replace(/^oid\./,""))}function rdnTypes(e){return splitRdn(parseDn(e)[0]??"").map(e=>{const n=e.indexOf("=");return-1===n?"":canonicalType(e.slice(0,n).trim())})}function canonicalType(e){const n=e.toLowerCase().replace(/^oid\./,"");return l.get(n)??n}function splitRdn(e){const n=[];let t="",r=!1,o=!1;for(const i of e){if(r)r=!1;else if("\\"===i)r=!0;else if('"'===i&&(o||opensValue(t)))o=!o;else if("+"===i&&!o){n.push(t),t="";continue}t+=i}return n.push(t),n}function normalizeDn(e){return normalizedRdns(e).join(",")}function normalizedRdns(e){return parseDn(e).map(e=>splitRdn(e).map(e=>{const n=e.indexOf("=");return-1===n?e.trim().toLowerCase():`${canonicalType(e.slice(0,n).trim())}=${function(e){let n=0,t=e.length;for(;n<t&&asciiSpace(e[n]);)n++;for(;t>n&&asciiSpace(e[t-1]);)t--;const r=e.slice(n,t);if(r.startsWith("#"))return r;let o=0;for(let e=r.length-2;e>0&&"\\"===r[e];e--)o++;r.length>=2&&r.startsWith('"')&&r.endsWith('"')&&o%2==0&&(e=r.slice(1,-1));const i=[];let s=0;const plain=e=>{for(const n of e)i.push(n)};for(const n of e.matchAll(c)){plain(e.slice(s,n.index)),s=n.index+n[0].length;const[,t,r]=n;if(void 0!==r){i.push(r);continue}const o=t.split("\\").filter(Boolean).map(e=>parseInt(e,16));try{plain(new TextDecoder("utf-8",{fatal:!0}).decode(Uint8Array.from(o)))}catch{for(const e of o)i.push(e<128?String.fromCharCode(e):`\\${hexByte(e)}`)}}s<e.length&&plain(e.slice(s));const kept=e=>3===e.length&&"\\"===e[0],a=[];let l="";for(const e of i)kept(e)?(a.push(...l.normalize("NFKC"),e),l=""):l+=e;a.push(...l.normalize("NFKC"));const u=a.filter((e,n)=>" "!==e||n>0&&" "!==a[n-1]);let f=0,p=u.length;for(;f<p&&" "===u[f];)f++;for(;p>f&&" "===u[p-1];)p--;let m="";for(let e=f;e<p;e++){const n=u[e];kept(n)?m+=n:n.charCodeAt(0)<32||127===n.charCodeAt(0)?m+=`\\${hexByte(n.charCodeAt(0))}`:',+"\\<>;='.includes(n)||e===f&&"#"===n?m+=`\\${n}`:m+=n}return m}(e.slice(n+1)).toLowerCase()}`}).sort().join("+")).filter(e=>e.length>0)}function isDummyMemberDn(e,n){if(!n||"string"!=typeof e)return!1;if(e===n)return!0;try{return normalizeDn(e)===normalizeDn(n)}catch{return!1}}function organizationLink(e){for(const n of Array.isArray(e)?e:[e]){if(null==n)continue;const e=String(n).trim();if(e.length>0)return e}}function isDnInBranch(e,n){const t=normalizedRdns(n);if(0===t.length)return!1;const r=normalizedRdns(e);if(r.length<t.length)return!1;return r.slice(r.length-t.length).every((e,n)=>e===t[n])}function movedToTrash(e,n,t){return!!e&&isDnInBranch(t,e)&&!isDnInBranch(n,e)}registerAttributeTypes([{oid:"2.5.4.0",names:["objectClass"]},{oid:"2.5.4.3",names:["cn","commonName"]},{oid:"2.5.4.4",names:["sn","surname"]},{oid:"2.5.4.5",names:["serialNumber"]},{oid:"2.5.4.6",names:["c","countryName"]},{oid:"2.5.4.7",names:["l","localityName"]},{oid:"2.5.4.8",names:["st","stateOrProvinceName"]},{oid:"2.5.4.9",names:["street","streetAddress"]},{oid:"2.5.4.10",names:["o","organizationName"]},{oid:"2.5.4.11",names:["ou","organizationalUnitName"]},{oid:"2.5.4.12",names:["title"]},{oid:"2.5.4.42",names:["givenName","gn"]},{oid:"2.5.4.43",names:["initials"]},{oid:"2.5.4.46",names:["dnQualifier"]},{oid:"2.5.4.65",names:["pseudonym"]},{oid:"0.9.2342.19200300.100.1.1",names:["uid","userid"]},{oid:"0.9.2342.19200300.100.1.3",names:["mail","rfc822Mailbox"]},{oid:"0.9.2342.19200300.100.1.25",names:["dc","domainComponent"]},{oid:"2.16.840.1.113730.3.1.241",names:["displayName"]},{oid:"2.16.840.1.113730.3.1.3",names:["employeeNumber"]},{oid:"1.3.6.1.1.1.1.0",names:["uidNumber"]},{oid:"1.3.6.1.1.1.1.1",names:["gidNumber"]}]);const asyncHandler=e=>(n,t,r)=>{Promise.resolve(e(n,t,r)).catch(r)};export{asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,getCompiledRegex,getParentDn,getRdn,hookOwner,isAuthzRefusal,isChildOf,isDnInBranch,isDnSyntax,isDummyMemberDn,knownAttributeName,launchHooks,launchHooksChained,movedToTrash,normalizeDn,organizationLink,parseDn,rdnTypes,rdnValue,recordHookOwner,registerAttributeTypes,runAsHookOwner,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue};
1
+ import{BadRequestError as e}from"./errors.js";import"./expressFormatedResponses.js";import"./metrics.js";import"./requestContext.js";const n=new Map;function getCompiledRegex(e,t){const r=t?`${e}:${t}`:e;let o=n.get(r);return o||(o=new RegExp(e,t),n.set(r,o)),o}function escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const transformSchemas=(e,n)=>e.toString().replace(/__(\S+)__/g,(e,t)=>{if(!t||"string"!=typeof t)return e;const r=t.trim().toLowerCase(),o=n;return o[r]?"object"!=typeof o[r]?o[r]:JSON.stringify(o[r]):e});function escapeLdapFilter(e){return e.replace(/\\/g,"\\5c").replace(/\*/g,"\\2a").replace(/\(/g,"\\28").replace(/\)/g,"\\29").replace(/\0/g,"\\00")}function substringSearchFilter(n,t){const r=/^[a-zA-Z][a-zA-Z0-9-]*$/,o=t.split(",").map(e=>e.trim()).filter(Boolean);if(0===o.length||!o.every(e=>r.test(e)))throw new e("Invalid LDAP attribute name");const i=escapeLdapFilter(n),s=o.map(e=>`(${e}=*${i}*)`);return 1===s.length?s[0]:`(|${s.join("")})`}function fillPattern(e,n,t){return e.split(n).join(t)}function escapeDnValue(e){let n=e.replace(/[\\,+"<>;=]/g,e=>`\\${e}`).replace(/[\0-\x1f\x7f]/g,e=>`\\${hexByte(e.charCodeAt(0))}`);return e.startsWith(" ")&&(n=`\\${n}`),e.length>1&&e.endsWith(" ")&&(n=`${n.slice(0,-1)}\\ `),e.startsWith("#")&&(n=`\\${n}`),n}const t=new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0});function unescapeDnValue(n){if("string"!=typeof n)throw new e("DN value must be a string");let t="",r=0,o=[];const flushBytes=()=>{if(o.length){for(const e of utf8Chars(o))t+="number"==typeof e?String.fromCharCode(e):e;o=[]}};for(;r<n.length;)if("\\"===n[r]&&r+1<n.length){const e=n[r+1];if(/[0-9a-fA-F]/.test(e)&&r+2<n.length){const e=n.substring(r+1,r+3);if(/^[0-9a-fA-F]{2}$/.test(e)){o.push(parseInt(e,16)),r+=3;continue}}flushBytes(),t+=e,r+=2}else flushBytes(),t+=n[r],r++;return flushBytes(),t}function validateDnValue(e,n){if(null==e||"string"!=typeof e)throw new Error(`${n} must be a string`);if(0===e.trim().length)throw new Error(`${n} must be a non-empty string`);if(/[\x00-\x1F\x7F]/.test(e))throw new Error(`${n} contains invalid control characters`);if(/[\u200B-\u200F\uFEFF]/.test(e))throw new Error(`${n} contains invalid invisible characters`)}function asciiSpace(e){return" "===e||e>="\t"&&e<="\r"}function trimUnescaped(e){let n=0,t=e.length;for(;n<t&&asciiSpace(e[n]);)n++;for(;t>n&&asciiSpace(e[t-1]);)t--;if(t<e.length){let r=0;for(let o=t-1;o>=n&&"\\"===e[o];o--)r++;r%2==1&&t++}return e.slice(n,t)}function parseDn(n){if("string"!=typeof n)throw new e("DN must be a string");const t=[];let r="",o=!1,i=!1;for(let e=0;e<n.length;e++){const s=n[e];o?(r+=s,o=!1):"\\"===s?(r+=s,o=!0):'"'===s&&(i||opensValue(r))?(r+=s,i=!i):i||","!==s&&";"!==s?r+=s:(t.push(trimUnescaped(r)),r="")}if(i)throw new e("DN has an unbalanced quote");return r&&t.push(trimUnescaped(r)),t}function opensValue(e){const n=e.trimEnd();if(!n.endsWith("="))return!1;let t=0;for(let e=n.length-2;e>=0&&"\\"===n[e];e--)t++;return t%2==0}function isDnSyntax(e){let n;try{n=parseDn(e)}catch{return!1}if(0===n.length)return!1;const t=/^\s*(?:[A-Za-z][A-Za-z0-9-]*|(?:OID\.)?\d+(?:\.\d+)+)\s*=/i;return n.every(e=>{if(!e)return!1;const n=[];let r="",o=!1;for(const t of e){if(o)o=!1;else if("\\"===t)o=!0;else if("+"===t){n.push(r),r="";continue}r+=t}return n.push(r),n.every(e=>t.test(e))})}function getParentDn(e){const n=parseDn(e);return n.length<=1?e:n.slice(1).join(",")}function getRdn(e){return parseDn(e)[0]||""}function rdnValue(e){const[n]=parseDn(e);return rdnValueOf(n)}function rdnValueOf(e){if(!e)return"";const n=e.indexOf("=");if(n<=0)return"";let t=n+1;for(;t<e.length&&asciiSpace(e[t]);)t++;return unescapeDnValue(e.slice(t))}function assertedRdnValue(e,n,t){const[r,...o]=parseDn(e),i=r?r.indexOf("="):-1;return i<=0||r.slice(0,i).trim().toLowerCase()!==n.toLowerCase()||splitRdn(r).length>1||void 0!==t&&normalizeDn(o.join(","))!==normalizeDn(t)?null:rdnValueOf(r)}function namesByDn(e,n){try{const[t]=parseDn(e),r=t?t.indexOf("="):-1;return r>0&&t.slice(0,r).trim().toLowerCase()===n.toLowerCase()}catch{return!1}}function isChildOf(e,n){try{return isDnInBranch(e,n)&&!isDnInBranch(n,e)}catch{return!1}}const r=/((?:\\[0-9a-fA-F]{2})+)|\\([^])/g;function hexByte(e){return e.toString(16).padStart(2,"0")}function utf8Chars(e){const n=[],r=Uint8Array.from(e);for(let o=0;o<e.length;){const i=utf8Length(e[o]);let s;if(i&&o+i<=e.length)try{s=t.decode(r.subarray(o,o+i))}catch{}void 0===s?n.push(e[o++]):(n.push(...s),o+=i)}return n}function utf8Length(e){return e<128?1:e>>5==6?2:e>>4==14?3:e>>3==30?4:0}const o=new Map,i=new Map;function registerAttributeTypes(e){for(const{oid:n,names:t}of e){const e=(t[0]??n).toLowerCase();for(const r of[n,...t])o.set(r.toLowerCase(),e),i.set(r.toLowerCase(),t[0]??n)}}function knownAttributeName(e){return i.get(e.toLowerCase().replace(/^oid\./,""))}function rdnTypes(e){return splitRdn(parseDn(e)[0]??"").map(e=>{const n=e.indexOf("=");return-1===n?"":canonicalType(e.slice(0,n).trim())})}function canonicalType(e){const n=e.toLowerCase().replace(/^oid\./,"");return o.get(n)??n}function splitRdn(e){const n=[];let t="",r=!1,o=!1;for(const i of e){if(r)r=!1;else if("\\"===i)r=!0;else if('"'===i&&(o||opensValue(t)))o=!o;else if("+"===i&&!o){n.push(t),t="";continue}t+=i}return n.push(t),n}function normalizeDn(e){return normalizedRdns(e).join(",")}function normalizedRdns(e){return parseDn(e).map(e=>splitRdn(e).map(e=>{const n=e.indexOf("=");return-1===n?e.trim().toLowerCase():`${canonicalType(e.slice(0,n).trim())}=${function(e){const n=trimUnescaped(e);if(/^#(?:[0-9a-fA-F]{2})+$/.test(n))return n;let t=0;for(let e=n.length-2;e>0&&"\\"===n[e];e--)t++;e=n.length>=2&&n.startsWith('"')&&n.endsWith('"')&&t%2==0?n.slice(1,-1):n;const o=[];let i=0;const plain=e=>{for(const n of e)o.push(n)};for(const n of e.matchAll(r)){plain(e.slice(i,n.index)),i=n.index+n[0].length;const[,t,r]=n;if(void 0!==r){o.push(r);continue}const s=t.split("\\").filter(Boolean).map(e=>parseInt(e,16));for(const e of utf8Chars(s))"number"==typeof e?o.push(`\\${hexByte(e)}`):plain(e)}i<e.length&&plain(e.slice(i));const kept=e=>3===e.length&&"\\"===e[0],s=[];let a="";for(const e of o)kept(e)?(s.push(...a.toLowerCase().normalize("NFKC"),e),a=""):a+=e;s.push(...a.toLowerCase().normalize("NFKC"));const c=s.filter((e,n)=>" "!==e||n>0&&" "!==s[n-1]);let l=0,u=c.length;for(;l<u&&" "===c[l];)l++;for(;u>l&&" "===c[u-1];)u--;let f="";for(let e=l;e<u;e++){const n=c[e];kept(n)?f+=n:n.charCodeAt(0)<32||127===n.charCodeAt(0)?f+=`\\${hexByte(n.charCodeAt(0))}`:',+"\\<>;='.includes(n)||e===l&&"#"===n?f+=`\\${n}`:f+=n}return f}(e.slice(n+1)).toLowerCase()}`}).sort().join("+")).filter(e=>e.length>0)}function isDummyMemberDn(e,n){if(!n||"string"!=typeof e)return!1;if(e===n)return!0;try{return normalizeDn(e)===normalizeDn(n)}catch{return!1}}function organizationLink(e){for(const n of Array.isArray(e)?e:[e]){if(null==n)continue;const e=String(n).trim();if(e.length>0)return e}}function isDnInBranch(e,n){const t=normalizedRdns(n);if(0===t.length)return!1;const r=normalizedRdns(e);if(r.length<t.length)return!1;return r.slice(r.length-t.length).every((e,n)=>e===t[n])}function movedToTrash(e,n,t){return!!e&&isDnInBranch(t,e)&&!isDnInBranch(n,e)}registerAttributeTypes([{oid:"2.5.4.0",names:["objectClass"]},{oid:"2.5.4.3",names:["cn","commonName"]},{oid:"2.5.4.4",names:["sn","surname"]},{oid:"2.5.4.5",names:["serialNumber"]},{oid:"2.5.4.6",names:["c","countryName"]},{oid:"2.5.4.7",names:["l","localityName"]},{oid:"2.5.4.8",names:["st","stateOrProvinceName"]},{oid:"2.5.4.9",names:["street","streetAddress"]},{oid:"2.5.4.10",names:["o","organizationName"]},{oid:"2.5.4.11",names:["ou","organizationalUnitName"]},{oid:"2.5.4.12",names:["title"]},{oid:"2.5.4.42",names:["givenName","gn"]},{oid:"2.5.4.43",names:["initials"]},{oid:"2.5.4.46",names:["dnQualifier"]},{oid:"2.5.4.65",names:["pseudonym"]},{oid:"0.9.2342.19200300.100.1.1",names:["uid","userid"]},{oid:"0.9.2342.19200300.100.1.3",names:["mail","rfc822Mailbox"]},{oid:"0.9.2342.19200300.100.1.25",names:["dc","domainComponent"]},{oid:"2.16.840.1.113730.3.1.241",names:["displayName"]},{oid:"2.16.840.1.113730.3.1.3",names:["employeeNumber"]},{oid:"1.3.6.1.1.1.1.0",names:["uidNumber"]},{oid:"1.3.6.1.1.1.1.1",names:["gidNumber"]}]);const asyncHandler=e=>(n,t,r)=>{Promise.resolve(e(n,t,r)).catch(r)};export{assertedRdnValue,asyncHandler,escapeDnValue,escapeLdapFilter,escapeRegex,fillPattern,getCompiledRegex,getParentDn,getRdn,isChildOf,isDnInBranch,isDnSyntax,isDummyMemberDn,knownAttributeName,movedToTrash,namesByDn,normalizeDn,organizationLink,parseDn,rdnTypes,rdnValue,registerAttributeTypes,substringSearchFilter,transformSchemas,unescapeDnValue,validateDnValue};
@@ -0,0 +1 @@
1
+ import{existsSync as r,readFileSync as t}from"fs";import{join as o,dirname as e}from"path";import{fileURLToPath as i}from"url";function readVersion(n=e(i(import.meta.url))){let a=n;for(let i=0;i<4;i++){const i=o(a,"package.json");if(r(i))try{const r=JSON.parse(t(i,"utf-8")),o="ldap-rest"===r.name?r.version?.trim():"";if(o)return o}catch{}a=e(a)}}const n=readVersion();export{n as SERVER_VERSION,readVersion};
@@ -1 +1 @@
1
- import{inspect as e}from"util";import r from"winston";import{currentLogContext as o}from"../lib/requestContext.js";const t={levels:{error:0,warn:1,notice:2,info:3,debug:4}},n=["console","file"],l=r.format(e=>{const r=o();return r?(r.requestId&&(e.requestId=r.requestId),!r.plugin||void 0!==e.plugin&&e.plugin!==r.plugin||(e.plugin=r.plugin,e.op??(e.op=r.op)),e):e}),buildLogger=o=>{const i=o.logger||"console";if(!n.includes(i))throw new Error(`Unknown logger "${String(i)}": --logger takes ${n.join(" or ")}`);if("file"===i&&!o.log_file)throw new Error("--logger file needs a file: set --log-file (or DM_LOG_FILE)");return r.createLogger({levels:t.levels,level:o.log_level,format:r.format.combine(r.format.timestamp(),l(),r.format.printf(({timestamp:r,level:o,message:t,requestId:n,plugin:l,op:i,error:s})=>`${r} [${o}]${"string"==typeof n?` [${n}]`:""}${"string"==typeof l?` [${l}${"string"==typeof i?` ${i}`:""}]`:""}: ${t}${void 0===s?"":` (${"string"==typeof s?s:s instanceof Error?String(s):e(s,{breakLength:1/0})})`}`)),transports:["console"===i?new r.transports.Console({stderrLevels:["error","warn"],format:r.format.json()}):new r.transports.File({filename:o.log_file})]})};export{buildLogger};
1
+ import{inspect as e}from"util";import r from"winston";import{currentLogContext as o}from"../lib/requestContext.js";const t={levels:{error:0,warn:1,notice:2,info:3,debug:4}},n=["console","file"],l=r.format(e=>{const r=o();return r?(r.requestId&&(e.requestId=r.requestId),!r.plugin||void 0!==e.plugin&&e.plugin!==r.plugin||(e.plugin=r.plugin,e.op??=r.op),e):e}),buildLogger=o=>{const i=o.logger||"console";if(!n.includes(i))throw new Error(`Unknown logger "${String(i)}": --logger takes ${n.join(" or ")}`);if("file"===i&&!o.log_file)throw new Error("--logger file needs a file: set --log-file (or DM_LOG_FILE)");return r.createLogger({levels:t.levels,level:o.log_level,format:r.format.combine(r.format.timestamp(),l(),r.format.printf(({timestamp:r,level:o,message:t,requestId:n,plugin:l,op:i,error:s})=>`${r} [${o}]${"string"==typeof n?` [${n}]`:""}${"string"==typeof l?` [${l}${"string"==typeof i?` ${i}`:""}]`:""}: ${t}${void 0===s?"":` (${"string"==typeof s?s:s instanceof Error?String(s):e(s,{breakLength:1/0})})`}`)),transports:["console"===i?new r.transports.Console({stderrLevels:["error","warn"],format:r.format.json()}):new r.transports.File({filename:o.log_file})]})};export{buildLogger};
@@ -0,0 +1 @@
1
+ import{defineOptions as _,opt as t,readOptions as e}from"../../config/options.js";const n=_({auth_token:t.array({env:"DM_AUTH_TOKENS",default:[],plural:"auth-tokens"})}),a=_({auth_fake_user:t.string({env:"DM_AUTH_FAKE_USER",default:""})}),r=_({auth_totp:t.array({env:"DM_AUTH_TOTP",default:[],plural:"auth-totps",kind:"phrases"}),auth_totp_window:t.number({env:"DM_AUTH_TOTP_WINDOW",default:1}),auth_totp_step:t.number({env:"DM_AUTH_TOTP_STEP",default:30,min:1})}),i=_({auth_hmac:t.array({env:"DM_AUTH_HMAC",default:[],plural:"auth-hmacs",kind:"phrases"}),auth_hmac_window:t.number({env:"DM_AUTH_HMAC_WINDOW",default:12e4,min:1})}),u=_({llng_ini:t.string({env:"DM_LLNG_INI",default:"/etc/lemonldap-ng/lemonldap-ng.ini"}),llng_username_header:t.string({env:"DM_LLNG_USERNAME_HEADER",default:""})}),l=_({authz_per_route:t.array({env:"DM_AUTHZ_PER_ROUTES",default:[],plural:"authz-per-routes"})}),d=_({authz_per_branch_config:t.json({env:"DM_AUTHZ_PER_BRANCH_CONFIG",default:{default:{read:!0,write:!1,delete:!1}}}),authz_per_branch_cache_ttl:t.number({env:"DM_AUTHZ_PER_BRANCH_CACHE_TTL",default:60,min:1}),ldap_group_member_attribute:t.string({env:"DM_LDAP_GROUP_MEMBER_ATTRIBUTE",default:"member"})}),s=_({authz_dynamic_bypass:t.array({env:"DM_AUTHZ_DYNAMIC_BYPASS",default:[],kind:"identifiers"}),authz_dynamic_base:t.string({env:"DM_AUTHZ_DYNAMIC_BASE",default:""}),authz_dynamic_cache_ttl:t.number({env:"DM_AUTHZ_DYNAMIC_CACHE_TTL",default:60,min:1}),authz_dynamic_token_attribute:t.string({env:"DM_AUTHZ_DYNAMIC_TOKEN_ATTRIBUTE",default:"userPassword"}),authz_dynamic_config_attribute:t.string({env:"DM_AUTHZ_DYNAMIC_CONFIG_ATTRIBUTE",default:"description"}),authz_dynamic_tenant_attribute:t.string({env:"DM_AUTHZ_DYNAMIC_TENANT_ATTRIBUTE",default:"cn"}),authz_dynamic_reload_endpoint:t.boolean({env:"DM_AUTHZ_DYNAMIC_RELOAD_ENDPOINT",default:!1})}),o=_({authz_local_admin_attribute:t.string({env:"DM_AUTHZ_LOCAL_ADMIN_ATTRIBUTE",default:"twakeLocalAdminLink"}),authz_global_branch:t.array({env:"DM_AUTHZ_GLOBAL_BRANCHES",default:[],plural:"authz-global-branches",kind:"dns"}),authz_global_write_branch:t.array({env:"DM_AUTHZ_GLOBAL_WRITE_BRANCHES",default:[],plural:"authz-global-write-branches",kind:"dns"}),authz_global_organization:t.string({env:"DM_AUTHZ_GLOBAL_ORGANIZATION",default:""})}),T=_({oidc_server:t.string({env:"DM_OIDC_SERVER",default:""}),oidc_username_claim:t.string({env:"DM_OIDC_USERNAME_CLAIM",default:"sub"}),oidc_client_id:t.string({env:"DM_OIDC_CLIENT_ID",default:""}),oidc_client_secret:t.string({env:"DM_OIDC_CLIENT_SECRET",default:""}),base_url:t.string({env:"DM_BASE_URL",default:""})}),A=_({rate_limit_window_ms:t.number({env:"DM_RATE_LIMIT_WINDOW_MS",default:9e5,min:1}),rate_limit_max:t.number({env:"DM_RATE_LIMIT_MAX",default:100,min:1})}),c=_({crowdsec_url:t.string({env:"DM_CROWDSEC_URL",default:"http://localhost:8080/v1/decisions"}),crowdsec_api_key:t.string({env:"DM_CROWDSEC_API_KEY",default:""}),crowdsec_cache_ttl:t.number({env:"DM_CROWDSEC_CACHE_TTL",default:60,min:1})}),f=_({trusted_proxy:t.array({env:"DM_TRUSTED_PROXIES",default:[],plural:"trusted-proxies",kind:"identifiers"}),trusted_proxy_auth_header:t.string({env:"DM_TRUSTED_PROXY_AUTH_HEADER",default:"Auth-User"})});function totpOptionsOf(_){return e(r,_)}function hmacOptionsOf(_){return e(i,_)}function perBranchOptionsOf(_){return e(d,_)}function dynamicOptionsOf(_){return e(s,_)}function rateLimitOptionsOf(_){return e(A,_)}function crowdsecOptionsOf(_){return e(c,_)}export{c as crowdsecOptions,crowdsecOptionsOf,s as dynamicOptions,dynamicOptionsOf,a as fakeOptions,i as hmacOptions,hmacOptionsOf,o as linid1Options,u as llngOptions,T as openidconnectOptions,d as perBranchOptions,perBranchOptionsOf,l as perRouteOptions,A as rateLimitOptions,rateLimitOptionsOf,n as tokenOptions,r as totpOptions,totpOptionsOf,f as trustedProxyOptions};
@@ -1 +1 @@
1
- import t from"../../lib/auth/base.js";import{runTogether as e}from"../../lib/authz/composition.js";import{ForbiddenError as i}from"../../lib/errors.js";import{unauthorized as s,ok as n}from"../../lib/expressFormatedResponses.js";import{getParentDn as r,asyncHandler as a}from"../../lib/utils.js";import{verifyLdapPassword as o}from"./authzDynamicHash.js";function asString(t){if(null!=t){if(Array.isArray(t)){const e=t[0];return null==e?void 0:Buffer.isBuffer(e)?e.toString():String(e)}return Buffer.isBuffer(t)?t.toString():String(t)}}function isAtOrUnder(t,e){const i=t.toLowerCase().trim(),s=e.toLowerCase().trim();return!(!i||!s)&&(i===s||i.endsWith(","+s))}class AuthzDynamic extends t{constructor(t){if(super(t),this.name="authzDynamic",this.roles=["auth","authz","configurable"],this.tokens=[],this.lastLoad=0,this.lastFailure=0,this.hooks={ldapsearchrequest:([t,e,i])=>{const s=this.activeToken(i);return s?(this.requireBranchPermission(s,t,"read"),[t,e,i]):[t,e,i]},ldapaddrequest:([t,e,i])=>{const s=this.activeToken(i);return s?(this.requireBranchPermission(s,r(t),"write"),[t,e,i]):[t,e,i]},ldapmodifyrequest:([t,e,i,s])=>{const n=this.activeToken(s);return n?(this.requireBranchPermission(n,r(t),"write"),[t,e,i,s]):[t,e,i,s]},ldapdeleterequest:([t,e])=>{const i=this.activeToken(e);if(!i)return[t,e];const s=Array.isArray(t)?t:[t];for(const t of s)this.requireBranchPermission(i,r(t),"delete");return[t,e]},ldaprenamerequest:([t,e,i])=>{const s=this.activeToken(i);return s?(this.requireBranchPermission(s,r(t),"read"),this.requireBranchPermission(s,r(e),"write"),[t,e,i]):[t,e,i]}},this.base=this.config.authz_dynamic_base||"",this.cacheTtlMs=1e3*(this.config.authz_dynamic_cache_ttl||60),this.failureBackoffMs=Math.max(Math.floor(this.cacheTtlMs/4),5e3),this.tokenAttr=this.config.authz_dynamic_token_attribute||"userPassword",this.configAttr=this.config.authz_dynamic_config_attribute||"description",this.tenantAttr=this.config.authz_dynamic_tenant_attribute||"cn",this.reloadEndpoint=Boolean(this.config.authz_dynamic_reload_endpoint),!this.base)throw new Error("authzDynamic requires --authz-dynamic-base (the LDAP branch holding token entries)")}api(t){if(super.api(t),this.reloadEndpoint){const e=this.config.api_prefix||"/api";t.post(`${e}/v1/authz-dynamic/reload`,a(async(t,e)=>{t.authzToken?(await this.reload(),n(e,{success:!0,tokens:this.tokens.length})):s(e)})),this.logger.info(`authzDynamic reload endpoint registered at ${e}/v1/authz-dynamic/reload`)}}async reload(){try{const t=Array.from(new Set(["dn","cn",this.tokenAttr,this.configAttr,this.tenantAttr])),e=await this.server.ldap.system.search({filter:"(objectClass=*)",scope:"sub",paged:!1,attributes:t},this.base),i=[];for(const t of e.searchEntries||[]){const e="string"==typeof t.dn?t.dn:String(t.dn),s=asString(t.cn)||"",n=asString(t[this.tokenAttr])||"";if(!n)continue;let r,a=[];const o=asString(t[this.configAttr]);if(o)try{const t=JSON.parse(o);t&&"string"==typeof t.tenant&&t.tenant&&(r=t.tenant),t&&Array.isArray(t.bases)&&(a=t.bases.filter(t=>t&&"string"==typeof t.dn&&t.dn.length>0).map(t=>({dn:t.dn,read:Boolean(t.read),write:Boolean(t.write),delete:Boolean(t.delete)})))}catch(t){this.logger.warn(`authzDynamic: invalid JSON in ${this.configAttr} of ${e}: ${String(t)}`)}const h="cn"===this.tenantAttr?s:asString(t[this.tenantAttr])||s,u=r||h;i.push({dn:e,cn:s,tenant:u,hash:n,bases:a})}this.tokens=i,this.lastLoad=Date.now(),this.lastFailure=0,this.logger.info(`authzDynamic: loaded ${i.length} token(s) from ${this.base}`)}catch(t){this.lastFailure=Date.now(),this.logger.error(`authzDynamic: failed to load tokens from ${this.base}: ${String(t)}`)}}async ensureFresh(){const t=Date.now();if(!(this.lastLoad>0&&t-this.lastLoad<this.cacheTtlMs||this.lastFailure>0&&t-this.lastFailure<this.failureBackoffMs))return this.loading||(this.loading=this.reload().finally(()=>{this.loading=void 0})),this.loading}bypassReason(t){const e=this.config.authz_dynamic_bypass||[];if(0===e.length)return;const i=t;return e.includes("trusted-proxy")&&!0===i.trustedProxy&&i.proxyAuthUser?`trusted-proxy vouching for ${i.proxyAuthUser}`:t.user?e.includes("any-authenticated")?"any-authenticated":e.includes(t.user)?`identity ${t.user}`:void 0:void 0}identitySource(){return"req.user and req.userName: the token's tenant"}afterLoad(){super.afterLoad();const t=Object.values(this.server.loadedPlugins).filter(t=>t!==this&&t.roles?.includes("auth")&&e(this,{name:t.name,pathPrefixes:t.pathPrefixes||[]})).map(t=>t.name);if(0===t.length)return;const i=this.config.authz_dynamic_bypass||[];this.logger.warn(`authzDynamic: ${t.join(", ")} authenticate the same paths as this plugin, so a request can arrive already identified. It is `+(0===i.length?"checked against the ACLs of the token it carries, and refused without one — set --authz-dynamic-bypass to let an identity through without a token.":`let through without a token when it matches ${i.join(", ")} (--authz-dynamic-bypass), and checked against its token ACLs otherwise.`))}afterChain(t,e,i){const n=t;if(!n.authzDynamicPending)return i();n.authzDynamicPending=!1;const r=this.bypassReason(n);if(r)return this.logger.info(`authzDynamic: request allowed past the token ACLs (${r})`),i();this.logger.warn(n.authzDynamicPendingReason||"authzDynamic: no token on a request no bypass covers"),s(e)}refuseOrDefer(t,e,i,n){const r=this.bypassReason(t);if(r)return this.logger.info(`authzDynamic: request allowed past the token ACLs (${r})`),void this.passThrough(t,i);if(!t.user&&(this.config.authz_dynamic_bypass||[]).length>0){const e=t;return e.authzDynamicPending=!0,e.authzDynamicPendingReason=n,void this.passThrough(t,i)}this.logger.warn(n),s(e)}authMethod(t,e,i){const n=this.bypassReason(t);if(n)return this.logger.info(`authzDynamic: request allowed past the token ACLs (${n})`),void this.passThrough(t,i);this.ensureFresh().then(()=>{const s=t.headers.authorization;if(!s||!/^Bearer\s+/.test(s))return void this.refuseOrDefer(t,e,i,"authzDynamic: missing or invalid Authorization header");const n=s.slice(s.indexOf(" ")+1).trim();if(!n)return void this.refuseOrDefer(t,e,i,"authzDynamic: empty bearer token");let r;for(const t of this.tokens)if(o(n,t.hash)){r=t;break}if(!r){const s=n.length>8?`${n.substring(0,8)}...`:"***";return void this.refuseOrDefer(t,e,i,`authzDynamic: unauthorized token ${s}`)}t.user||this.publishIdentity(t,r.tenant),t.authzToken=r,i()}).catch(t=>{this.logger.error(`authzDynamic: authMethod error: ${String(t)}`),s(e)})}activeToken(t){return t?.authzToken}requireBranchPermission(t,e,s){for(const i of t.bases)if(isAtOrUnder(e,i.dn)&&i[s])return;throw this.logger.debug(`authzDynamic: token "${t.tenant}" denied ${s} on ${e}`),new i(`[authz-forbidden] Token does not have ${s} permission on this branch`)}getConfigApiData(){return{enabled:!0,base:this.base,cacheTtlSeconds:this.cacheTtlMs/1e3,tokenCount:this.tokens.length,reloadEndpoint:this.reloadEndpoint?`${this.config.api_prefix||"/api"}/v1/authz-dynamic/reload`:void 0,tokenAttribute:this.tokenAttr,configAttribute:this.configAttr,tenantAttribute:this.tenantAttr}}_tokens(){return this.tokens}}export{AuthzDynamic as default};
1
+ import"../../abstract/plugin.js";import t from"../../lib/auth/base.js";import{runTogether as e}from"../../lib/authz/composition.js";import{AuthzRefusedError as i}from"../../lib/errors.js";import{unauthorized as n,ok as s}from"../../lib/expressFormatedResponses.js";import{getParentDn as a}from"../../lib/utils.js";import{dynamicOptions as r}from"./auth.options.js";import{verifyLdapPassword as o}from"./authzDynamicHash.js";import{defineRoute as h}from"../../lib/route.js";function asString(t){if(null!=t){if(Array.isArray(t)){const e=t[0];return null==e?void 0:Buffer.isBuffer(e)?e.toString():String(e)}return Buffer.isBuffer(t)?t.toString():String(t)}}function isAtOrUnder(t,e){const i=t.toLowerCase().trim(),n=e.toLowerCase().trim();return!(!i||!n)&&(i===n||i.endsWith(","+n))}class AuthzDynamic extends t{static optionDefinitions=r;name="authzDynamic";roles=["auth","authz","configurable"];tokens=[];lastLoad=0;lastFailure=0;cacheTtlMs;failureBackoffMs;base;tokenAttr;configAttr;tenantAttr;reloadEndpoint;loading;constructor(t){super(t);const e=this.options(r);if(this.base=e.authz_dynamic_base,this.cacheTtlMs=1e3*e.authz_dynamic_cache_ttl,this.failureBackoffMs=Math.max(Math.floor(this.cacheTtlMs/4),5e3),this.tokenAttr=e.authz_dynamic_token_attribute,this.configAttr=e.authz_dynamic_config_attribute,this.tenantAttr=e.authz_dynamic_tenant_attribute,this.reloadEndpoint=e.authz_dynamic_reload_endpoint,!this.base)throw new Error("authzDynamic requires --authz-dynamic-base (the LDAP branch holding token entries)")}api(t){if(super.api(t),this.reloadEndpoint){const e=this.config.api_prefix||"/api";h(t,{method:"post",path:`${e}/v1/authz-dynamic/reload`,handler:async({req:t,res:e})=>{t.authzToken?(await this.reload(),s(e,{success:!0,tokens:this.tokens.length})):n(e)}}),this.logger.info(`authzDynamic reload endpoint registered at ${e}/v1/authz-dynamic/reload`)}}async reload(){try{const t=Array.from(new Set(["dn","cn",this.tokenAttr,this.configAttr,this.tenantAttr])),e=await this.server.ldap.system.search({filter:"(objectClass=*)",scope:"sub",paged:!1,attributes:t},this.base),i=[];for(const t of e.searchEntries||[]){const e="string"==typeof t.dn?t.dn:String(t.dn),n=asString(t.cn)||"",s=asString(t[this.tokenAttr])||"";if(!s)continue;let a,r=[];const o=asString(t[this.configAttr]);if(o)try{const t=JSON.parse(o);t&&"string"==typeof t.tenant&&t.tenant&&(a=t.tenant),t&&Array.isArray(t.bases)&&(r=t.bases.filter(t=>t&&"string"==typeof t.dn&&t.dn.length>0).map(t=>({dn:t.dn,read:Boolean(t.read),write:Boolean(t.write),delete:Boolean(t.delete)})))}catch(t){this.logger.warn(`authzDynamic: invalid JSON in ${this.configAttr} of ${e}: ${String(t)}`)}const h="cn"===this.tenantAttr?n:asString(t[this.tenantAttr])||n,u=a||h;i.push({dn:e,cn:n,tenant:u,hash:s,bases:r})}this.tokens=i,this.lastLoad=Date.now(),this.lastFailure=0,this.logger.info(`authzDynamic: loaded ${i.length} token(s) from ${this.base}`)}catch(t){this.lastFailure=Date.now(),this.logger.error(`authzDynamic: failed to load tokens from ${this.base}: ${String(t)}`)}}async ensureFresh(){const t=Date.now();if(!(this.lastLoad>0&&t-this.lastLoad<this.cacheTtlMs||this.lastFailure>0&&t-this.lastFailure<this.failureBackoffMs))return this.loading||(this.loading=this.reload().finally(()=>{this.loading=void 0})),this.loading}bypassReason(t,e){if(0===e.length)return;const i=t;return e.includes("trusted-proxy")&&!0===i.trustedProxy&&i.proxyAuthUser?`trusted-proxy vouching for ${i.proxyAuthUser}`:t.user?e.includes("any-authenticated")?"any-authenticated":e.includes(t.user)?`identity ${t.user}`:void 0:void 0}identitySource(){return"req.user and req.userName: the token's tenant"}afterLoad(){super.afterLoad();const t=Object.values(this.server.loadedPlugins).filter(t=>t!==this&&t.roles?.includes("auth")&&e(this,{name:t.name,pathPrefixes:t.pathPrefixes||[]})).map(t=>t.name);if(0===t.length)return;const i=this.options(r).authz_dynamic_bypass;this.logger.warn(`authzDynamic: ${t.join(", ")} authenticate the same paths as this plugin, so a request can arrive already identified. It is `+(0===i.length?"checked against the ACLs of the token it carries, and refused without one — set --authz-dynamic-bypass to let an identity through without a token.":`let through without a token when it matches ${i.join(", ")} (--authz-dynamic-bypass), and checked against its token ACLs otherwise.`))}afterChain(t,e,i){const s=t;if(!s.authzDynamicPending)return i();s.authzDynamicPending=!1;const a=s.authzDynamicBypass??this.options(r).authz_dynamic_bypass,o=this.bypassReason(s,a);if(o)return this.logger.info(`authzDynamic: request allowed past the token ACLs (${o})`),i();this.logger.warn(s.authzDynamicPendingReason||"authzDynamic: no token on a request no bypass covers"),n(e)}refuseOrDefer(t,e,i,s,a){const r=this.bypassReason(t,a);if(r)return this.logger.info(`authzDynamic: request allowed past the token ACLs (${r})`),void this.passThrough(t,i);if(!t.user&&a.length>0){const e=t;return e.authzDynamicPending=!0,e.authzDynamicPendingReason=s,e.authzDynamicBypass=a,void this.passThrough(t,i)}this.logger.warn(s),n(e)}authMethod(t,e,i){const s=this.options(r).authz_dynamic_bypass,a=this.bypassReason(t,s);if(a)return this.logger.info(`authzDynamic: request allowed past the token ACLs (${a})`),void this.passThrough(t,i);this.ensureFresh().then(()=>{const n=t.headers.authorization;if(!n||!/^Bearer\s+/.test(n))return void this.refuseOrDefer(t,e,i,"authzDynamic: missing or invalid Authorization header",s);const a=n.slice(n.indexOf(" ")+1).trim();if(!a)return void this.refuseOrDefer(t,e,i,"authzDynamic: empty bearer token",s);let r;for(const t of this.tokens)if(o(a,t.hash)){r=t;break}if(!r){const n=a.length>8?`${a.substring(0,8)}...`:"***";return void this.refuseOrDefer(t,e,i,`authzDynamic: unauthorized token ${n}`,s)}t.user||this.publishIdentity(t,r.tenant),t.authzToken=r,i()}).catch(t=>{this.logger.error(`authzDynamic: authMethod error: ${String(t)}`),n(e)})}hooks={ldapsearchrequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,t,"read"),[t,e,i]):[t,e,i]},ldapaddrequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,a(t),"write"),[t,e,i]):[t,e,i]},ldapmodifyrequest:([t,e,i,n])=>{const s=this.activeToken(n);return s?(this.requireBranchPermission(s,a(t),"write"),[t,e,i,n]):[t,e,i,n]},ldapdeleterequest:([t,e])=>{const i=this.activeToken(e);if(!i)return[t,e];const n=Array.isArray(t)?t:[t];for(const t of n)this.requireBranchPermission(i,a(t),"delete");return[t,e]},ldaprenamerequest:([t,e,i])=>{const n=this.activeToken(i);return n?(this.requireBranchPermission(n,a(t),"read"),this.requireBranchPermission(n,a(e),"write"),[t,e,i]):[t,e,i]}};activeToken(t){return t?.authzToken}requireBranchPermission(t,e,n){for(const i of t.bases)if(isAtOrUnder(e,i.dn)&&i[n])return;throw this.logger.debug(`authzDynamic: token "${t.tenant}" denied ${n} on ${e}`),new i(`Token does not have ${n} permission on this branch`)}getConfigApiData(){return{enabled:!0,base:this.base,cacheTtlSeconds:this.cacheTtlMs/1e3,tokenCount:this.tokens.length,reloadEndpoint:this.reloadEndpoint?`${this.config.api_prefix||"/api"}/v1/authz-dynamic/reload`:void 0,tokenAttribute:this.tokenAttr,configAttribute:this.configAttr,tenantAttribute:this.tenantAttr}}_tokens(){return this.tokens}}export{AuthzDynamic as default};
@@ -1 +1 @@
1
- import e,{mostSpecificBranchRule as t}from"../../lib/authz/base.js";import{isDnInBranch as i,escapeLdapFilter as s}from"../../lib/utils.js";class AuthzLinid1 extends e{constructor(e){super(e),this.name="authzLinid1",this.permissionsCache=new Map,this.cacheTTL=3e5;const t=this.config.authz_local_admin_attribute||"twakeLocalAdminLink";this.logger.info(`AuthzLinid1: Authorization based on ${t} attribute enabled`);const s=this.globalOrganization(),r=s&&this.globalBranches().find(e=>i(s,e));if(r)throw new Error(`AuthzLinid1: --authz-global-organization ${s} lies in the global branch ${r}`);const n=s&&this.globalWriteBranches().find(e=>i(s,e));if(n)throw new Error(`AuthzLinid1: --authz-global-organization ${s} lies in the global write branch ${n}`);for(const e of this.globalWriteBranches()){const t=this.globalBranches().find(t=>i(e,t)||i(t,e));if(t)throw new Error(`AuthzLinid1: the global branch ${t} and the global write branch ${e} overlap`)}}async mayLinkAnyDomain(e){if(await super.mayLinkAnyDomain(e))return!0;const t=this.globalOrganization();return!!t&&!0===(await this.getUserPermissions(e,t)).write}globalOrganization(){return this.config.authz_global_organization||this.config.ldap_top_organization||void 0}filtersAttachedEntries(){return!0}globalBranches(){return this.config.authz_global_branch??[]}globalWriteBranches(){return this.config.authz_global_write_branch??[]}isGlobalWrite(e){return this.globalWriteBranches().some(t=>i(e,t))}sourcePermission(e){return this.isGlobalWrite(e)?"delete":"read"}async resolveUser(e){return this.getUserDn(e)}async getUserDn(e){return this.findUserDn(e)}async getUserPermissions(e,t){const s=this.globalOrganization();if(this.globalBranches().some(e=>i(t,e)))return s&&!i(s,t)?this.getUserPermissions(e,s):{read:!1,write:!1,delete:!1};if(this.isGlobalWrite(t)){const r=s&&!i(s,t)?await this.getUserPermissions(e,s):{read:!1,write:!1,delete:!1};return{read:r.read||(await this.getAuthorizedBranches(e)).length>0,write:r.write,delete:r.delete}}return this.ownPermissions(e,t)}async ownPermissions(e,t){const i=Date.now(),s=this.permissionsCache.get(e);if(s&&i-s.timestamp<this.cacheTTL){const e=this.findBranchPermissions(s.branches,t);if(e)return e}await this.refreshUserPermissions(e);const r=this.permissionsCache.get(e);if(r){const e=this.findBranchPermissions(r.branches,t);if(e)return e}return{read:!1,write:!1,delete:!1}}async refreshUserPermissions(e){const t=new Map;try{const i=`(${this.config.authz_local_admin_attribute||"twakeLocalAdminLink"}=${s(e)})`,r=this.config.ldap_top_organization||this.config.ldap_base||"",n=await this.server.ldap.system.search({paged:!1,filter:i,attributes:["dn"],scope:"sub"},r);if(n.searchEntries)for(const e of n.searchEntries){const i="string"==typeof e.dn?e.dn:String(e.dn);t.set(i,{read:!0,write:!0,delete:!0})}}catch(t){this.logger.error(`Failed to refresh permissions for user ${e}: ${t}`)}this.permissionsCache.set(e,{branches:t,timestamp:Date.now()})}async getAuthorizedBranches(e){const t=Date.now(),i=this.permissionsCache.get(e);(!i||t-i.timestamp>=this.cacheTTL)&&await this.refreshUserPermissions(e);const s=this.permissionsCache.get(e);return s?Array.from(s.branches.keys()):[]}ruleBranches(e){return this.getAuthorizedBranches(e)}findBranchPermissions(e,i){return t(e.entries(),i)}}export{AuthzLinid1 as default};
1
+ import t,{mostSpecificBranchRule as e}from"../../lib/authz/base.js";import{isDnInBranch as i,escapeLdapFilter as s}from"../../lib/utils.js";import{linid1Options as r}from"./auth.options.js";class AuthzLinid1 extends t{static optionDefinitions=r;name="authzLinid1";permissionsCache=new Map;constructor(t){super(t),this.cacheTTL=3e5;const e=this.options(r).authz_local_admin_attribute;this.logger.info(`AuthzLinid1: Authorization based on ${e} attribute enabled`);const s=this.globalOrganization(),n=s&&this.globalBranches().find(t=>i(s,t));if(n)throw new Error(`AuthzLinid1: --authz-global-organization ${s} lies in the global branch ${n}`);const a=s&&this.globalWriteBranches().find(t=>i(s,t));if(a)throw new Error(`AuthzLinid1: --authz-global-organization ${s} lies in the global write branch ${a}`);for(const t of this.globalWriteBranches()){const e=this.globalBranches().find(e=>i(t,e)||i(e,t));if(e)throw new Error(`AuthzLinid1: the global branch ${e} and the global write branch ${t} overlap`)}}async mayLinkAnyDomain(t){if(await super.mayLinkAnyDomain(t))return!0;const e=this.globalOrganization();return!!e&&!0===(await this.getUserPermissions(t,e)).write}globalOrganization(){return this.options(r).authz_global_organization||this.config.ldap_top_organization||void 0}filtersAttachedEntries(){return!0}globalBranches(){return this.options(r).authz_global_branch}globalWriteBranches(){return this.options(r).authz_global_write_branch}isGlobalWrite(t){return this.globalWriteBranches().some(e=>i(t,e))}sourcePermission(t){return this.isGlobalWrite(t)?"delete":"read"}async resolveUser(t){return this.getUserDn(t)}async getUserDn(t){return this.findUserDn(t)}async getUserPermissions(t,e){const s=this.globalOrganization();if(this.globalBranches().some(t=>i(e,t)))return s&&!i(s,e)?this.getUserPermissions(t,s):{read:!1,write:!1,delete:!1};if(this.isGlobalWrite(e)){const r=s&&!i(s,e)?await this.getUserPermissions(t,s):{read:!1,write:!1,delete:!1};return{read:r.read||(await this.getAuthorizedBranches(t)).length>0,write:r.write,delete:r.delete}}return this.ownPermissions(t,e)}async ownPermissions(t,e){const i=Date.now(),s=this.permissionsCache.get(t);if(s&&i-s.timestamp<this.cacheTTL){const t=this.findBranchPermissions(s.branches,e);if(t)return t}await this.refreshUserPermissions(t);const r=this.permissionsCache.get(t);if(r){const t=this.findBranchPermissions(r.branches,e);if(t)return t}return{read:!1,write:!1,delete:!1}}async refreshUserPermissions(t){const e=new Map;try{const i=`(${this.options(r).authz_local_admin_attribute}=${s(t)})`,n=this.config.ldap_top_organization||this.config.ldap_base||"",a=await this.server.ldap.system.search({paged:!1,filter:i,attributes:["dn"],scope:"sub"},n);if(a.searchEntries)for(const t of a.searchEntries){const i="string"==typeof t.dn?t.dn:String(t.dn);e.set(i,{read:!0,write:!0,delete:!0})}}catch(e){this.logger.error(`Failed to refresh permissions for user ${t}: ${e}`)}this.permissionsCache.set(t,{branches:e,timestamp:Date.now()})}async getAuthorizedBranches(t){const e=Date.now(),i=this.permissionsCache.get(t);(!i||e-i.timestamp>=this.cacheTTL)&&await this.refreshUserPermissions(t);const s=this.permissionsCache.get(t);return s?Array.from(s.branches.keys()):[]}ruleBranches(t){return this.getAuthorizedBranches(t)}findBranchPermissions(t,i){return e(t.entries(),i)}}export{AuthzLinid1 as default};
@@ -1 +1 @@
1
- import e,{mostSpecificBranchRule as t}from"../../lib/authz/base.js";import{AmbiguousIdentityError as s}from"../../lib/errors.js";import{normalizeDn as r,escapeLdapFilter as o}from"../../lib/utils.js";import{warnUnmatchedRuleKeys as i}from"../../lib/auth/base.js";class AuthzPerBranch extends e{constructor(e){super(e),this.name="authzPerBranch",this.groupCache=new Map,this.pendingGroups=new Map,this.groupGeneration=0,this.cacheTTL=1e3*(this.config.authz_per_branch_cache_ttl||60),this.authConfig=this.config.authz_per_branch_config,this.authConfig&&this.logger.info("Authorization config loaded");const t=this.hooks,forget=()=>this.forgetGroups();this.hooks={...t,ldapadddone:forget,ldapmodifydone:([,e])=>{this.touchesMembership(e)&&forget()},ldapdeletedone:()=>{t.ldapdeletedone(),forget()},ldaprenamedone:()=>{t.ldaprenamedone(),forget()}}}touchesMembership(e){const t=[this.config.ldap_group_member_attribute||"member",this.config.ldap_user_main_attribute||"uid"].map(e=>e.toLowerCase());return[...Object.keys(e.add??{}),...Object.keys(e.replace??{}),...Array.isArray(e.delete)?e.delete:Object.keys(e.delete??{})].some(e=>t.includes(e.split(";")[0].toLowerCase()))}forgetGroups(){this.groupGeneration++,this.groupCache.clear(),this.pendingGroups.clear()}resolveUser(e){return Promise.resolve(e)}afterLoad(){i(Object.keys(this.authConfig?.users??{}),this.server.loadedPlugins,this.name,this.logger)}shouldSkipAuthorization(e){return super.shouldSkipAuthorization(e)||!this.authConfig}async getUserPermissions(e,t){if(!this.authConfig)return{read:!0,write:!0,delete:!0};const s=[];if(this.authConfig.users?.[e]){const r=this.findBranchPermissions(this.authConfig.users[e],t);r&&s.push(r)}const r=await this.getUserGroups(e);for(const e of r){const r=this.groupRules(e);if(r){const e=this.findBranchPermissions(r,t);e&&s.push(e)}}const decide=e=>{const t=s.map(t=>t[e]).filter(e=>void 0!==e);return t.length>0?t.some(Boolean):this.authConfig?.default?.[e]??!1};return{read:decide("read"),write:decide("write"),delete:decide("delete")}}async getAuthorizedBranches(e){return this.getAuthorizedBranchesForPermission(e,"read")}async ruleBranches(e){if(!this.authConfig)return[];const t=Object.keys(this.authConfig.users?.[e]??{});for(const s of await this.getUserGroups(e))t.push(...Object.keys(this.groupRules(s)??{}));return[...new Set(t)]}async getAuthorizedBranchesForPermission(e,t){if(!this.authConfig)return[];const s=[];if(this.authConfig.users?.[e])for(const[r,o]of Object.entries(this.authConfig.users[e]))o[t]&&s.push(r);const r=await this.getUserGroups(e);for(const e of r){const r=this.groupRules(e);if(r)for(const[e,o]of Object.entries(r))o[t]&&!s.includes(e)&&s.push(e)}return s}findBranchPermissions(e,s){return t(Object.entries(e),s)}groupRules(e){const t=this.authConfig?.groups;if(!t)return;if(this.groupRulesIndex?.source!==t){const e=new Map;for(const[s,o]of Object.entries(t)){let t;try{t=r(s)}catch{this.logger.warn(`${this.name}: group "${s}" is not a valid DN, compared as text`),t=s.toLowerCase()}e.has(t)&&this.logger.warn(`${this.name}: several groups entries name ${t}; only the last one applies`),e.set(t,o)}this.groupRulesIndex={source:t,rules:e}}let s;try{s=r(e)}catch{s=e.toLowerCase()}return this.groupRulesIndex.rules.get(s)}async getUserGroups(e){const t=Date.now(),s=this.groupCache.get(e);if(s&&t-s.timestamp<this.cacheTTL)return s.groups;const r=this.pendingGroups.get(e);if(r)return r;const o=this.groupGeneration,i=this.lookupGroups(e).then(t=>(o===this.groupGeneration&&this.groupCache.set(e,{groups:t,timestamp:Date.now()}),t)).finally(()=>{this.pendingGroups.get(e)===i&&this.pendingGroups.delete(e)});return this.pendingGroups.set(e,i),i}async lookupGroups(e){let t;try{t=await this.findUserDn(e)}catch(t){if(!(t instanceof s))throw t;return this.logger.warn(`${this.name}: ${e} names ${t.count} directory entries, so no group rule applies to them`),[]}if(!t)return[];const r=`(${this.config.ldap_group_member_attribute||"member"}=${o(t)})`,i=await this.server.ldap.system.search({paged:!1,filter:r,attributes:["dn"]},this.server.ldap.base),n=[];for(const e of i.searchEntries??[])e.dn&&n.push("string"==typeof e.dn?e.dn:String(e.dn));return n}}export{AuthzPerBranch as default};
1
+ import e,{mostSpecificBranchRule as t}from"../../lib/authz/base.js";import{AmbiguousIdentityError as s}from"../../lib/errors.js";import{normalizeDn as r,escapeLdapFilter as o}from"../../lib/utils.js";import{warnUnmatchedRuleKeys as i}from"../../lib/auth/base.js";import{perBranchOptions as n}from"./auth.options.js";class AuthzPerBranch extends e{static optionDefinitions=n;name="authzPerBranch";authConfig;groupCache=new Map;pendingGroups=new Map;groupGeneration=0;groupRulesIndex;declaredByBase;constructor(e){super(e),this.declaredByBase={...this.hooks};const t=this.options(n);this.cacheTTL=1e3*t.authz_per_branch_cache_ttl,this.authConfig=t.authz_per_branch_config,this.authConfig&&this.logger.info("Authorization config loaded");const s=this.declaredByBase.ldapdeletedone,r=this.declaredByBase.ldaprenamedone,forget=()=>this.forgetGroups();this.hooks={...this.declaredByBase,ldapadddone:forget,ldapmodifydone:(e,t)=>{this.touchesMembership(t)&&forget()},ldapdeletedone:(e,t)=>(forget(),s?.(e,t)),ldaprenamedone:(e,t,s)=>(forget(),r?.(e,t,s))}}get memberAttribute(){return this.options(n).ldap_group_member_attribute}touchesMembership(e){const t=[this.memberAttribute,this.config.ldap_user_main_attribute||"uid"].map(e=>e.toLowerCase());return[...Object.keys(e.add??{}),...Object.keys(e.replace??{}),...Array.isArray(e.delete)?e.delete:Object.keys(e.delete??{})].some(e=>t.includes(e.split(";")[0].toLowerCase()))}forgetGroups(){this.groupGeneration++,this.groupCache.clear(),this.pendingGroups.clear()}resolveUser(e){return Promise.resolve(e)}afterLoad(){i(Object.keys(this.authConfig?.users??{}),this.server.loadedPlugins,this.name,this.logger)}shouldSkipAuthorization(e){return super.shouldSkipAuthorization(e)||!this.authConfig}async getUserPermissions(e,t){if(!this.authConfig)return{read:!0,write:!0,delete:!0};const s=[];if(this.authConfig.users?.[e]){const r=this.findBranchPermissions(this.authConfig.users[e],t);r&&s.push(r)}const r=await this.getUserGroups(e);for(const e of r){const r=this.groupRules(e);if(r){const e=this.findBranchPermissions(r,t);e&&s.push(e)}}const decide=e=>{const t=s.map(t=>t[e]).filter(e=>void 0!==e);return t.length>0?t.some(Boolean):this.authConfig?.default?.[e]??!1};return{read:decide("read"),write:decide("write"),delete:decide("delete")}}async getAuthorizedBranches(e){return this.getAuthorizedBranchesForPermission(e,"read")}async ruleBranches(e){if(!this.authConfig)return[];const t=Object.keys(this.authConfig.users?.[e]??{});for(const s of await this.getUserGroups(e))t.push(...Object.keys(this.groupRules(s)??{}));return[...new Set(t)]}async getAuthorizedBranchesForPermission(e,t){if(!this.authConfig)return[];const s=[];if(this.authConfig.users?.[e])for(const[r,o]of Object.entries(this.authConfig.users[e]))o[t]&&s.push(r);const r=await this.getUserGroups(e);for(const e of r){const r=this.groupRules(e);if(r)for(const[e,o]of Object.entries(r))o[t]&&!s.includes(e)&&s.push(e)}return s}findBranchPermissions(e,s){return t(Object.entries(e),s)}groupRules(e){const t=this.authConfig?.groups;if(!t)return;if(this.groupRulesIndex?.source!==t){const e=new Map;for(const[s,o]of Object.entries(t)){let t;try{t=r(s)}catch{this.logger.warn(`${this.name}: group "${s}" is not a valid DN, compared as text`),t=s.toLowerCase()}e.has(t)&&this.logger.warn(`${this.name}: several groups entries name ${t}; only the last one applies`),e.set(t,o)}this.groupRulesIndex={source:t,rules:e}}let s;try{s=r(e)}catch{s=e.toLowerCase()}return this.groupRulesIndex.rules.get(s)}async getUserGroups(e){const t=Date.now(),s=this.groupCache.get(e);if(s&&t-s.timestamp<this.cacheTTL)return s.groups;const r=this.pendingGroups.get(e);if(r)return r;const o=this.groupGeneration,i=this.lookupGroups(e).then(t=>(o===this.groupGeneration&&this.groupCache.set(e,{groups:t,timestamp:Date.now()}),t)).finally(()=>{this.pendingGroups.get(e)===i&&this.pendingGroups.delete(e)});return this.pendingGroups.set(e,i),i}async lookupGroups(e){let t;try{t=await this.findUserDn(e)}catch(t){if(!(t instanceof s))throw t;return this.logger.warn(`${this.name}: ${e} names ${t.count} directory entries, so no group rule applies to them`),[]}if(!t)return[];const r=`(${this.memberAttribute}=${o(t)})`,i=await this.server.ldap.system.search({paged:!1,filter:r,attributes:["dn"]},this.server.ldap.base),n=[];for(const e of i.searchEntries??[])e.dn&&n.push("string"==typeof e.dn?e.dn:String(e.dn));return n}}export{AuthzPerBranch as default};
@@ -1 +1 @@
1
- import e from"../../abstract/plugin.js";import{forbidden as t}from"../../lib/expressFormatedResponses.js";import{assertIdentityMode as i,warnUnmatchedRuleKeys as r,identityFor as s}from"../../lib/auth/base.js";import{authzFor as n,servesRequest as o}from"../../lib/authz/composition.js";const a=/^[\w/.\-+*]*$/;function globToRegex(e){if(!a.test(e))throw new Error(`Invalid glob pattern: "${e}" — only [a-zA-Z0-9_/.+*-] are allowed`);const t=e.replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/\\\*\\\*/g,".*").replace(/\\\*/g,"[^/]*");return new RegExp(`^${t}$`)}class AuthzPerRoute extends e{constructor(...e){super(...e),this.name="authzPerRoute",this.roles=["authz"],this.rules=new Map,this.fallbackWarned=!1,this.unidentifiedRoutes=new Set,this.unidentifiedWarnings=0,i(this.config,this.constructor.name),n(this.config,this.constructor.name);const t=this.config.authz_per_route??[];for(const e of t)this.parseEntry(e);const r=[...this.rules.entries()].map(([e,t])=>`${e}: ${t.some(e=>"wildcard"===e.kind)?"full access":`${t.length} rule${1!==t.length?"s":""}`}`).join(", ");this.logger.info(`authzPerRoute: ${this.rules.size} user${1!==this.rules.size?"s":""} configured${this.rules.size>0?` (${r})`:""}`)}parseEntry(e){const t=e.split(":").map(e=>e.trim());if(t.length<2)return void this.logger.warn(`authzPerRoute: ignoring invalid rule entry: ${e}`);const i=t[0];if(i)if(2!==t.length||"*"!==t[1]){if(t.length>=3){const r=t[1].toUpperCase(),s=new Set(["GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS","*"]);if(!r||!s.has(r))return void this.logger.warn(`authzPerRoute: ignoring rule with invalid method "${t[1]}" in entry: ${e}`);const n=t.slice(2).join(":").trim();if(!n)return void this.logger.warn(`authzPerRoute: ignoring rule with empty path pattern in entry: ${e}`);let o;try{o=globToRegex(n)}catch{return void this.logger.warn(`authzPerRoute: ignoring entry with invalid glob "${n}" in rule: ${e}`)}return void this.addRule(i,{kind:"method-path",method:r,pathRe:o})}this.logger.warn(`authzPerRoute: ignoring invalid rule entry: ${e}`)}else this.addRule(i,{kind:"wildcard"});else this.logger.warn(`authzPerRoute: ignoring rule with empty user: ${e}`)}addRule(e,t){const i=this.rules.get(e);i?i.push(t):this.rules.set(e,[t])}matches(e,t,i){for(const r of e){if("wildcard"===r.kind)return!0;if(("*"===r.method||r.method===t.toUpperCase())&&r.pathRe.test(i))return!0}return!1}afterLoad(){r([...this.rules.keys()],this.server.loadedPlugins,this.name,this.logger)}api(e){e.use((e,i,r)=>{const{value:n,fellBack:a}=s(e,this.config);if(a&&!this.fallbackWarned&&(this.fallbackWarned=!0,this.logger.warn(`${this.name}: --authz-identity asks for req.userName and the authenticator published none, so rules are matched against req.user instead`)),!n){const t=`${e.method} ${e.path}`,i=`${this.name}: ${t} carries no identity, so no route rule applies to it`;return!this.unidentifiedRoutes.has(t)&&this.unidentifiedWarnings<AuthzPerRoute.UNIDENTIFIED_MAX?(this.unidentifiedWarnings++,this.unidentifiedRoutes.add(t),this.logger.warn(i)):this.logger.debug(i),r()}if(!o(e,this.config))return this.logger.debug(`${this.name}: ${e.method} ${e.path} authenticated by ${(e.authenticators??[]).join(", ")}, which this plugin does not serve`),r();const h=this.rules.get(n);return h?this.matches(h,e.method,e.path)?r():(this.logger.warn(`authzPerRoute: user '${n}' denied for ${e.method} ${e.path}`),t(i)):(this.logger.warn(`authzPerRoute: user '${n}' denied for ${e.method} ${e.path} (no rules configured)`),t(i))})}}AuthzPerRoute.UNIDENTIFIED_MAX=1e3;export{AuthzPerRoute as default,globToRegex};
1
+ import e from"../../abstract/plugin.js";import{forbidden as t}from"../../lib/expressFormatedResponses.js";import{assertIdentityMode as i,warnUnmatchedRuleKeys as r,identityFor as n}from"../../lib/auth/base.js";import{authzFor as s,servesRequest as o}from"../../lib/authz/composition.js";import{perRouteOptions as a}from"./auth.options.js";const h=/^[\w/.\-+*]*$/;function globToRegex(e){if(!h.test(e))throw new Error(`Invalid glob pattern: "${e}" — only [a-zA-Z0-9_/.+*-] are allowed`);const t=e.replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/\\\*\\\*/g,".*").replace(/\\\*/g,"[^/]*");return new RegExp(`^${t}$`)}class AuthzPerRoute extends e{static optionDefinitions=a;name="authzPerRoute";roles=["authz"];rules=new Map;fallbackWarned=!1;unidentifiedRoutes=new Set;unidentifiedWarnings=0;static UNIDENTIFIED_MAX=1e3;constructor(...e){super(...e),i(this.config,this.constructor.name),s(this.config,this.constructor.name);const t=this.options(a).authz_per_route;for(const e of t)this.parseEntry(e);const r=[...this.rules.entries()].map(([e,t])=>`${e}: ${t.some(e=>"wildcard"===e.kind)?"full access":`${t.length} rule${1!==t.length?"s":""}`}`).join(", ");this.logger.info(`authzPerRoute: ${this.rules.size} user${1!==this.rules.size?"s":""} configured${this.rules.size>0?` (${r})`:""}`)}parseEntry(e){const t=e.split(":").map(e=>e.trim());if(t.length<2)return void this.logger.warn(`authzPerRoute: ignoring invalid rule entry: ${e}`);const i=t[0];if(i)if(2!==t.length||"*"!==t[1]){if(t.length>=3){const r=t[1].toUpperCase(),n=new Set(["GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS","*"]);if(!r||!n.has(r))return void this.logger.warn(`authzPerRoute: ignoring rule with invalid method "${t[1]}" in entry: ${e}`);const s=t.slice(2).join(":").trim();if(!s)return void this.logger.warn(`authzPerRoute: ignoring rule with empty path pattern in entry: ${e}`);let o;try{o=globToRegex(s)}catch{return void this.logger.warn(`authzPerRoute: ignoring entry with invalid glob "${s}" in rule: ${e}`)}return void this.addRule(i,{kind:"method-path",method:r,pathRe:o})}this.logger.warn(`authzPerRoute: ignoring invalid rule entry: ${e}`)}else this.addRule(i,{kind:"wildcard"});else this.logger.warn(`authzPerRoute: ignoring rule with empty user: ${e}`)}addRule(e,t){const i=this.rules.get(e);i?i.push(t):this.rules.set(e,[t])}matches(e,t,i){for(const r of e){if("wildcard"===r.kind)return!0;if(("*"===r.method||r.method===t.toUpperCase())&&r.pathRe.test(i))return!0}return!1}afterLoad(){r([...this.rules.keys()],this.server.loadedPlugins,this.name,this.logger)}api(e){e.use((e,i,r)=>{const{value:s,fellBack:a}=n(e,this.config);if(a&&!this.fallbackWarned&&(this.fallbackWarned=!0,this.logger.warn(`${this.name}: --authz-identity asks for req.userName and the authenticator published none, so rules are matched against req.user instead`)),!s){const t=`${e.method} ${e.path}`,i=`${this.name}: ${t} carries no identity, so no route rule applies to it`;return!this.unidentifiedRoutes.has(t)&&this.unidentifiedWarnings<AuthzPerRoute.UNIDENTIFIED_MAX?(this.unidentifiedWarnings++,this.unidentifiedRoutes.add(t),this.logger.warn(i)):this.logger.debug(i),r()}if(!o(e,this.config))return this.logger.debug(`${this.name}: ${e.method} ${e.path} authenticated by ${(e.authenticators??[]).join(", ")}, which this plugin does not serve`),r();const h=this.rules.get(s);return h?this.matches(h,e.method,e.path)?r():(this.logger.warn(`authzPerRoute: user '${s}' denied for ${e.method} ${e.path}`),t(i)):(this.logger.warn(`authzPerRoute: user '${s}' denied for ${e.method} ${e.path} (no rules configured)`),t(i))})}}export{AuthzPerRoute as default,globToRegex};
@@ -1 +1 @@
1
- import e from"../../abstract/plugin.js";import{identityFor as s}from"../../lib/auth/base.js";import{servesRequest as t,overlap as i,EVERYONE as a,authzFor as r}from"../../lib/authz/composition.js";import{asyncHandler as o}from"../../lib/utils.js";import{UnauthorizedError as n}from"../../lib/errors.js";import{roleAttribute as c}from"../../config/schema.js";class AuthzScope extends e{constructor(){super(...arguments),this.name="authzScope",this.roles=["api"]}api(e){e.get(`${this.config.api_prefix}/v1/authz/scope`,o(async(e,s)=>this.scope(e,s)))}static canDescribe(e){if(!e.roles?.includes("authz"))return!1;const s=e;return"function"==typeof s.resolveUser&&"function"==typeof s.getAuthorizedBranches&&"function"==typeof s.getUserPermissions}describers(){return Object.values(this.server.loadedPlugins).filter(e=>AuthzScope.canDescribe(e))}static judges(e,s){return e.roles?.includes("auth")?Boolean(s.authenticators?.includes(e.name)):Boolean(s.user)&&t(s,e.config)}assertComposition(){const e=this.config.authz_scope_source;if(!e)return;const s=this.describers().map(e=>e.name);if(!s.includes(e))throw new Error(`${this.name}: --authz-scope-source names ${e}, which is not a loaded plugin able to describe a scope`+(s.length>0?` (loaded: ${s.join(", ")})`:""))}afterLoad(){if(this.config.authz_scope_source)return;const e=this.describers(),s=this.server.authenticators,population=e=>r(e.config,e.name)??a;for(let t=0;t<e.length;t++)for(let a=t+1;a<e.length;a++){const r=e[t],o=e[a];if(i(population(r),population(o),s))return void this.logger.warn(`${this.name}: ${r.name} and ${o.name} can both describe the scope of one caller, and ${r.name} answers because it loaded first. Set --authz-scope-source to choose; the answer lists both under \`sources\``)}}entities(){const e=(this.config.ldap_organization_link_attribute||"").toLowerCase(),linkable=s=>Boolean(e)&&(!s?.attributes||Object.keys(s.attributes).some(s=>s.toLowerCase()===e)),s=[],t=this.server.loadedPlugins.ldapFlatGeneric;for(const e of t?.instances||[])s.push({name:e.pluralName,base:e.base,linkable:linkable(e.schema)});const i=this.server.loadedPlugins.ldapGroups;return i?.base&&s.push({name:"groups",base:i.base,linkable:linkable(i.schema)}),this.server.loadedPlugins.ldapOrganizations&&this.config.ldap_top_organization&&s.push({name:"organizations",base:this.config.ldap_top_organization,linkable:!1}),s}async scope(e,t){const i=this.describers();if(Object.values(this.server.loadedPlugins).some(e=>e.roles?.includes("authz"))&&!e.user)throw new n("No authenticated user");const a=Object.values(this.server.loadedPlugins).filter(s=>s.roles?.includes("authz")&&AuthzScope.judges(s,e)),r=i.filter(e=>a.includes(e)),o=this.config.authz_scope_source,c=r.find(e=>e.name===o)??r[0],u=a.map(e=>e.name);if(!c)return 0===a.length?void t.json({user:e.user??null,unrestricted:!0,described:!0,source:null,sources:[],branches:[],linksAnyDomain:!0,...this.passwordPolicies(!0),entities:this.entities().map(({name:e,base:s})=>({name:e,base:s,create:!0}))}):void t.json({user:e.user??null,unrestricted:!1,described:!1,source:null,sources:u,branches:[],linksAnyDomain:!1,...this.passwordPolicies(!1),entities:[]});const l=s(e,c.config).value,h=await c.resolveUser(l);if(!h)throw new n(`Unknown user ${l}`);const d=await c.getAuthorizedBranches(h),p=[],f=new Map,permissionsOn=async e=>{let s=f.get(e);return s||(s=await c.getUserPermissions(h,e),f.set(e,s)),s};for(const e of d){const s=await permissionsOn(e);p.push({dn:e,...s,...await this.branchLabel(e)})}const g=d.some(e=>!0===f.get(e)?.write),m=this.config.ldap_top_organization,b=[];for(const{name:e,base:s,linkable:t}of this.entities()){const i=m&&s===m?!0===(await permissionsOn(m)).write:t?g:!0===(await permissionsOn(s)).write;b.push({name:e,base:s,create:i})}t.json({user:h,unrestricted:!1,described:!0,source:c.name,sources:u,branches:p,transit:c.transitBranch?.()??null,linksAnyDomain:!0===await(c.mayLinkAnyDomain?.(h)),...this.passwordPolicies(await this.readsPolicies(c,h)),entities:b})}policyPlugin(){return this.server.loadedPlugins.ldapPasswordPolicy}passwordPolicies(e){return this.policyPlugin()?{passwordPolicies:e}:{}}async readsPolicies(e,s){const t=this.policyPlugin();return!(!t||!e.searchRefusal)&&void 0===await e.searchRefusal(s,t.policyBase())}async branchLabel(e){const s=this.server.loadedPlugins.ldapOrganizations,t=c(s?.schema,"organizationPath")||this.config.ldap_organization_path_attribute;try{const s=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:["ou","o","cn",...t?[t]:[]]},e),i=s?.searchEntries?.[0];if(!i)return{};const first=e=>{const s=Array.isArray(e)?e[0]:e;return"string"==typeof s?s||void 0:Buffer.isBuffer(s)&&s.toString("utf8")||void 0};return{name:first(i.ou)||first(i.o)||first(i.cn),path:t?first(i[t]):void 0}}catch(e){return{}}}}export{AuthzScope as default};
1
+ import{Type as e}from"@sinclair/typebox";import s from"../../abstract/plugin.js";import{identityFor as t}from"../../lib/auth/base.js";import{servesRequest as i,overlap as n,EVERYONE as o,authzFor as a}from"../../lib/authz/composition.js";import{UnauthorizedError as r}from"../../lib/errors.js";import{roleAttribute as c}from"../../config/schema.js";import{defineRoute as l}from"../../lib/route.js";const u=e.Object({user:e.Union([e.String(),e.Null()]),unrestricted:e.Boolean(),described:e.Boolean(),source:e.Union([e.String(),e.Null()]),sources:e.Array(e.String()),branches:e.Array(e.Object({dn:e.String(),read:e.Optional(e.Boolean()),write:e.Optional(e.Boolean()),delete:e.Optional(e.Boolean()),name:e.Optional(e.String()),path:e.Optional(e.String())})),transit:e.Optional(e.Union([e.String(),e.Null()])),linksAnyDomain:e.Boolean(),passwordPolicies:e.Optional(e.Boolean()),entities:e.Array(e.Object({name:e.String(),base:e.String(),create:e.Boolean()}))});class AuthzScope extends s{name="authzScope";roles=["api"];api(e){l(e,{method:"get",path:`${this.config.api_prefix}/v1/authz/scope`,responses:{200:u},handler:async({req:e,res:s})=>this.scope(e,s)})}static canDescribe(e){if(!e.roles?.includes("authz"))return!1;const s=e;return"function"==typeof s.resolveUser&&"function"==typeof s.getAuthorizedBranches&&"function"==typeof s.getUserPermissions}describers(){return Object.values(this.server.loadedPlugins).filter(e=>AuthzScope.canDescribe(e))}static judges(e,s){return e.roles?.includes("auth")?Boolean(s.authenticators?.includes(e.name)):Boolean(s.user)&&i(s,e.config)}assertComposition(){const e=this.config.authz_scope_source;if(!e)return;const s=this.describers().map(e=>e.name);if(!s.includes(e))throw new Error(`${this.name}: --authz-scope-source names ${e}, which is not a loaded plugin able to describe a scope`+(s.length>0?` (loaded: ${s.join(", ")})`:""))}afterLoad(){if(this.config.authz_scope_source)return;const e=this.describers(),s=this.server.authenticators,population=e=>a(e.config,e.name)??o;for(let t=0;t<e.length;t++)for(let i=t+1;i<e.length;i++){const o=e[t],a=e[i];if(n(population(o),population(a),s))return void this.logger.warn(`${this.name}: ${o.name} and ${a.name} can both describe the scope of one caller, and ${o.name} answers because it loaded first. Set --authz-scope-source to choose; the answer lists both under \`sources\``)}}entities(){const e=(this.config.ldap_organization_link_attribute||"").toLowerCase(),linkable=s=>Boolean(e)&&(!s?.attributes||Object.keys(s.attributes).some(s=>s.toLowerCase()===e)),s=[],t=this.server.loadedPlugins.ldapFlatGeneric;for(const e of t?.instances||[])s.push({name:e.pluralName,base:e.base,linkable:linkable(e.schema)});const i=this.server.loadedPlugins.ldapGroups;return i?.base&&s.push({name:"groups",base:i.base,linkable:linkable(i.schema)}),this.server.loadedPlugins.ldapOrganizations&&this.config.ldap_top_organization&&s.push({name:"organizations",base:this.config.ldap_top_organization,linkable:!1}),s}async scope(e,s){const i=this.describers();if(Object.values(this.server.loadedPlugins).some(e=>e.roles?.includes("authz"))&&!e.user)throw new r("No authenticated user");const n=Object.values(this.server.loadedPlugins).filter(s=>s.roles?.includes("authz")&&AuthzScope.judges(s,e)),o=i.filter(e=>n.includes(e)),a=this.config.authz_scope_source,c=o.find(e=>e.name===a)??o[0],l=n.map(e=>e.name);if(!c)return 0===n.length?void s.json({user:e.user??null,unrestricted:!0,described:!0,source:null,sources:[],branches:[],linksAnyDomain:!0,...this.passwordPolicies(!0),entities:this.entities().map(({name:e,base:s})=>({name:e,base:s,create:!0}))}):void s.json({user:e.user??null,unrestricted:!1,described:!1,source:null,sources:l,branches:[],linksAnyDomain:!1,...this.passwordPolicies(!1),entities:[]});const u=t(e,c.config).value,h=await c.resolveUser(u);if(!h)throw new r(`Unknown user ${u}`);const d=await c.getAuthorizedBranches(h),p=[],g=new Map,permissionsOn=async e=>{let s=g.get(e);return s||(s=await c.getUserPermissions(h,e),g.set(e,s)),s};for(const e of d){const s=await permissionsOn(e);p.push({dn:e,...s,...await this.branchLabel(e)})}const f=d.some(e=>!0===g.get(e)?.write),m=this.config.ldap_top_organization,b=[];for(const{name:e,base:s,linkable:t}of this.entities()){const i=m&&s===m?!0===(await permissionsOn(m)).write:t?f:!0===(await permissionsOn(s)).write;b.push({name:e,base:s,create:i})}s.json({user:h,unrestricted:!1,described:!0,source:c.name,sources:l,branches:p,transit:c.transitBranch?.()??null,linksAnyDomain:!0===await(c.mayLinkAnyDomain?.(h)),...this.passwordPolicies(await this.readsPolicies(c,h)),entities:b})}policyPlugin(){return this.server.loadedPlugins.ldapPasswordPolicy}passwordPolicies(e){return this.policyPlugin()?{passwordPolicies:e}:{}}async readsPolicies(e,s){const t=this.policyPlugin();return!(!t||!e.searchRefusal)&&void 0===await e.searchRefusal(s,t.policyBase())}async branchLabel(e){const s=this.server.loadedPlugins.ldapOrganizations,t=c(s?.schema,"organizationPath")||this.config.ldap_organization_path_attribute;try{const s=await this.server.ldap.system.search({paged:!1,scope:"base",attributes:["ou","o","cn",...t?[t]:[]]},e),i=s?.searchEntries?.[0];if(!i)return{};const first=e=>{const s=Array.isArray(e)?e[0]:e;return"string"==typeof s?s||void 0:Buffer.isBuffer(s)&&s.toString("utf8")||void 0};return{name:first(i.ou)||first(i.o)||first(i.cn),path:t?first(i[t]):void 0}}catch(e){return{}}}}export{u as ScopeResponse,AuthzScope as default};
@@ -1 +1 @@
1
- import e from"../../abstract/plugin.js";class CrowdSec extends e{constructor(...e){if(super(...e),this.name="crowdsec",this.roles=["protect"],this.cache=new Map,this.apiUrl=this.config.crowdsec_url||"http://localhost:8080/v1/decisions",this.apiKey=this.config.crowdsec_api_key,this.cacheTtl=this.config.crowdsec_cache_ttl||60,!this.apiKey)throw new Error("CrowdSec plugin requires crowdsec_api_key configuration");this.logger.info(`CrowdSec integration enabled with API at ${this.apiUrl} (cache TTL: ${this.cacheTtl}s)`)}api(e){e.use(async(e,r,t)=>{const s=this.getClientIp(e);try{if(await this.checkIpBanned(s))return this.logger.warn(`Blocked request from CrowdSec-banned IP: ${s}`),r.status(403).json({error:"Access denied",reason:"IP address is banned by security policies"});t()}catch(e){this.logger.error(`CrowdSec API error for IP ${s}: ${e instanceof Error?e.message:String(e)}`),t()}})}async checkIpBanned(e){const r=this.cache.get(e),t=Date.now();if(r&&t<r.expiresAt)return this.logger.debug(`CrowdSec cache hit for IP ${e}: ${r.isBanned}`),r.isBanned;const s=`${this.apiUrl}?ip=${encodeURIComponent(e)}`;this.logger.debug(`Querying CrowdSec API: ${s}`);const i=await this.fetch(s,{headers:{"X-Api-Key":this.apiKey}});if(!i.ok)throw new Error(`CrowdSec API returned status ${i.status}: ${i.statusText}`);let o;try{const e=await i.text();o="null"===e||""===e?null:JSON.parse(e)}catch(e){throw this.logger.error(`Failed to parse CrowdSec API response: ${e instanceof Error?e.message:String(e)}`),new Error("Invalid JSON response from CrowdSec API")}let c=!1;return null!==o&&Array.isArray(o)&&(c=o.some(e=>"ban"===e.type)),this.cache.set(e,{isBanned:c,expiresAt:t+1e3*this.cacheTtl}),this.logger.debug(`CrowdSec decision for IP ${e}: ${c?"BANNED":"ALLOWED"}`),c}getClientIp(e){const r=e.headers["x-forwarded-for"];if(r){return(Array.isArray(r)?r[0]:r).split(",")[0].trim()}return e.socket.remoteAddress||"unknown"}}export{CrowdSec as default};
1
+ import e from"../../abstract/plugin.js";import{crowdsecOptions as r}from"./auth.options.js";class CrowdSec extends e{static optionDefinitions=r;name="crowdsec";roles=["protect"];apiUrl;apiKey;cacheTtl;cache=new Map;constructor(...e){super(...e);const t=this.options(r);if(this.apiUrl=t.crowdsec_url,this.apiKey=t.crowdsec_api_key,this.cacheTtl=t.crowdsec_cache_ttl,!this.apiKey)throw new Error("CrowdSec plugin requires crowdsec_api_key configuration");this.logger.info(`CrowdSec integration enabled with API at ${this.apiUrl} (cache TTL: ${this.cacheTtl}s)`)}api(e){e.use(async(e,r,t)=>{const s=this.getClientIp(e);try{if(await this.checkIpBanned(s))return this.logger.warn(`Blocked request from CrowdSec-banned IP: ${s}`),r.status(403).json({error:"Access denied",reason:"IP address is banned by security policies"});t()}catch(e){this.logger.error(`CrowdSec API error for IP ${s}: ${e instanceof Error?e.message:String(e)}`),t()}})}async checkIpBanned(e){const r=this.cache.get(e),t=Date.now();if(r&&t<r.expiresAt)return this.logger.debug(`CrowdSec cache hit for IP ${e}: ${r.isBanned}`),r.isBanned;const s=`${this.apiUrl}?ip=${encodeURIComponent(e)}`;this.logger.debug(`Querying CrowdSec API: ${s}`);const o=await this.fetch(s,{headers:{"X-Api-Key":this.apiKey}});if(!o.ok)throw new Error(`CrowdSec API returned status ${o.status}: ${o.statusText}`);let i;try{const e=await o.text();i="null"===e||""===e?null:JSON.parse(e)}catch(e){throw this.logger.error(`Failed to parse CrowdSec API response: ${e instanceof Error?e.message:String(e)}`),new Error("Invalid JSON response from CrowdSec API")}let c=!1;return null!==i&&Array.isArray(i)&&(c=i.some(e=>"ban"===e.type)),this.cache.set(e,{isBanned:c,expiresAt:t+1e3*this.cacheTtl}),this.logger.debug(`CrowdSec decision for IP ${e}: ${c?"BANNED":"ALLOWED"}`),c}getClientIp(e){const r=e.headers["x-forwarded-for"];if(r){return(Array.isArray(r)?r[0]:r).split(",")[0].trim()}return e.socket.remoteAddress||"unknown"}}export{CrowdSec as default};
@@ -1 +1 @@
1
- import e from"../../lib/auth/base.js";class AuthFake extends e{constructor(...e){if(super(...e),this.name="authFake",this.roles=["auth"],"production"===process.env.NODE_ENV)throw new Error(`${this.name}: refused with NODE_ENV=production — it serves every request without authentication`);if(this.user=(this.config.auth_fake_user||"").trim(),!this.user)throw new Error(`${this.name}: --auth-fake-user is required`)}knownIdentities(){return[this.user]}identitySource(){return"req.user and req.userName: --auth-fake-user"}afterLoad(){super.afterLoad(),this.logger.warn(`${this.name}: every request is served as "${this.user}", without authentication`)}authMethod(e,t,r){this.publishIdentity(e,this.user),r()}}export{AuthFake as default};
1
+ import e from"../../lib/auth/base.js";import{fakeOptions as t}from"./auth.options.js";class AuthFake extends e{static optionDefinitions=t;name="authFake";roles=["auth"];user;constructor(...e){if(super(...e),"production"===process.env.NODE_ENV)throw new Error(`${this.name}: refused with NODE_ENV=production — it serves every request without authentication`);if(this.user=this.options(t).auth_fake_user.trim(),!this.user)throw new Error(`${this.name}: --auth-fake-user is required`)}knownIdentities(){return[this.user]}identitySource(){return"req.user and req.userName: --auth-fake-user"}afterLoad(){super.afterLoad(),this.logger.warn(`${this.name}: every request is served as "${this.user}", without authentication`)}authMethod(e,t,r){this.publishIdentity(e,this.user),r()}}export{AuthFake as default};
@@ -1 +1 @@
1
- import{createHmac as e,createHash as t,timingSafeEqual as r}from"crypto";import{unauthorized as i}from"../../lib/expressFormatedResponses.js";import{rawBodyOf as s}from"../../lib/rawBody.js";import n from"../../lib/auth/base.js";class AuthHmac extends n{knownIdentities(){return[...this.services.values()].map(e=>e.name)}identitySource(){return"req.user and req.userName: the service name given in --auth-hmac"}constructor(...e){super(...e),this.name="authHmac",this.roles=["auth"],this.services=new Map;const t=this.config.auth_hmac;this.timeWindow=this.config.auth_hmac_window??12e4,t&&Array.isArray(t)&&t.forEach((e,t)=>{const r=e.split(":"),i=r[0].trim(),s=(r[1]??"").trim(),n=r.slice(2).join(":").trim();if(r.length<3||!i||!s||!n)throw new Error(`Invalid --auth-hmac entry at index ${t}: expected "service-id:secret:name", none of them empty; entries are separated by \`,\`, \`;\` or newlines`);s.length<32&&this.logger.warn(`HMAC secret for service "${i}" is too short (minimum 32 characters recommended)`),r.length>4&&this.logger.warn(`HMAC entry ${t} for service "${i}" has ${r.length} fields: if several services were given, separate them with \`,\` or \`;\``),this.services.set(i,{id:i,secret:s,name:n})}),0===this.services.size?this.logger.warn("No valid HMAC services configured"):(this.logger.info(`HMAC authentication initialized with ${this.services.size} service(s): ${Array.from(this.services.keys()).join(", ")}`),this.logger.info(`Time window for replay protection: ${this.timeWindow}ms`))}authMethod(e,t,r){const s=e.headers.authorization;if(!s||!s.startsWith("HMAC-SHA256 "))return this.logger.warn("Missing or invalid Authorization header (expected HMAC-SHA256)"),i(t);const n=s.substring(12).split(":");if(3!==n.length)return this.logger.warn("Invalid HMAC authorization format (expected service-id:timestamp:signature)"),i(t);const[a,o,h]=n,c=this.services.get(a);if(!c)return this.logger.warn(`Unknown service ID: ${a}`),i(t);const u=parseInt(o,10);if(isNaN(u)||u<=0)return this.logger.warn(`Invalid timestamp: ${o}`),i(t);const g=Date.now(),m=Math.abs(g-u);if(m>this.timeWindow)return this.logger.warn(`Timestamp outside allowed window: ${m}ms (max: ${this.timeWindow}ms) for service ${a}`),i(t);const l=this.calculateBodyHash(e);if(void 0===l)return this.logger.warn(`Refusing ${e.method} ${e.originalUrl||e.url} from service ${a}: its body (${e.headers["content-type"]||"no Content-Type"}) cannot be checked against the signature`),i(t);const d=`${e.method.toUpperCase()}|${e.originalUrl||e.url}|${u}|${l}`,f=this.calculateHmac(c.secret,d);if(!this.secureCompare(h,f))return this.logger.warn(`Invalid signature for service ${a} (${c.name}). Expected: ${f.substring(0,8)}..., Got: ${h.substring(0,8)}...`),this.logger.debug(`Signing string: ${d}`),i(t);this.logger.debug(`HMAC authentication successful for service: ${a} (${c.name})`),this.publishIdentity(e,c.name),r()}calculateHmac(t,r){const i=e("sha256",t);return i.update(r),i.digest("hex")}calculateBodyHash(e){const r=e.method.toUpperCase(),i=s(e),n=parseInt(e.headers["content-length"]??"",10),a=!i&&(void 0!==e.headers["transfer-encoding"]||n>0);if("GET"===r||"DELETE"===r||"HEAD"===r)return i?.bytes.length||a?void 0:"";if(!a){if(!i?.bytes.length)return"";if("utf-8"===i.encoding)return t("sha256").update(i.bytes).digest("hex")}}secureCompare(e,t){if(e.length!==t.length)return!1;try{const i=Buffer.from(e,"utf8"),s=Buffer.from(t,"utf8");return r(i,s)}catch{return!1}}}export{AuthHmac as default};
1
+ import{createHmac as e,createHash as t,timingSafeEqual as i}from"crypto";import{unauthorized as r}from"../../lib/expressFormatedResponses.js";import{rawBodyOf as s}from"../../lib/rawBody.js";import n from"../../lib/auth/base.js";import{hmacOptions as o}from"./auth.options.js";class AuthHmac extends n{static optionDefinitions=o;knownIdentities(){return[...this.services.values()].map(e=>e.name)}identitySource(){return"req.user and req.userName: the service name given in --auth-hmac"}name="authHmac";roles=["auth"];services=new Map;timeWindow;constructor(...e){super(...e);const t=this.options(o),i=t.auth_hmac;this.timeWindow=t.auth_hmac_window,i.forEach((e,t)=>{const i=e.split(":"),r=i[0].trim(),s=(i[1]??"").trim(),n=i.slice(2).join(":").trim();if(i.length<3||!r||!s||!n)throw new Error(`Invalid --auth-hmac entry at index ${t}: expected "service-id:secret:name", none of them empty; entries are separated by \`,\`, \`;\` or newlines`);s.length<32&&this.logger.warn(`HMAC secret for service "${r}" is too short (minimum 32 characters recommended)`),i.length>4&&this.logger.warn(`HMAC entry ${t} for service "${r}" has ${i.length} fields: if several services were given, separate them with \`,\` or \`;\``),this.services.set(r,{id:r,secret:s,name:n})}),0===this.services.size?this.logger.warn("No valid HMAC services configured"):(this.logger.info(`HMAC authentication initialized with ${this.services.size} service(s): ${Array.from(this.services.keys()).join(", ")}`),this.logger.info(`Time window for replay protection: ${this.timeWindow}ms`))}authMethod(e,t,i){const s=e.headers.authorization;if(!s||!s.startsWith("HMAC-SHA256 "))return this.logger.warn("Missing or invalid Authorization header (expected HMAC-SHA256)"),r(t);const n=s.substring(12).split(":");if(3!==n.length)return this.logger.warn("Invalid HMAC authorization format (expected service-id:timestamp:signature)"),r(t);const[o,a,h]=n,c=this.services.get(o);if(!c)return this.logger.warn(`Unknown service ID: ${o}`),r(t);const u=parseInt(a,10);if(isNaN(u)||u<=0)return this.logger.warn(`Invalid timestamp: ${a}`),r(t);const m=Date.now(),l=Math.abs(m-u);if(l>this.timeWindow)return this.logger.warn(`Timestamp outside allowed window: ${l}ms (max: ${this.timeWindow}ms) for service ${o}`),r(t);const d=this.calculateBodyHash(e);if(void 0===d)return this.logger.warn(`Refusing ${e.method} ${e.originalUrl||e.url} from service ${o}: its body (${e.headers["content-type"]||"no Content-Type"}) cannot be checked against the signature`),r(t);const g=`${e.method.toUpperCase()}|${e.originalUrl||e.url}|${u}|${d}`,f=this.calculateHmac(c.secret,g);if(!this.secureCompare(h,f))return this.logger.warn(`Invalid signature for service ${o} (${c.name}). Expected: ${f.substring(0,8)}..., Got: ${h.substring(0,8)}...`),this.logger.debug(`Signing string: ${g}`),r(t);this.logger.debug(`HMAC authentication successful for service: ${o} (${c.name})`),this.publishIdentity(e,c.name),i()}calculateHmac(t,i){const r=e("sha256",t);return r.update(i),r.digest("hex")}calculateBodyHash(e){const i=e.method.toUpperCase(),r=s(e),n=parseInt(e.headers["content-length"]??"",10),o=!r&&(void 0!==e.headers["transfer-encoding"]||n>0);if("GET"===i||"DELETE"===i||"HEAD"===i)return r?.bytes.length||o?void 0:"";if(!o){if(!r?.bytes.length)return"";if("utf-8"===r.encoding)return t("sha256").update(r.bytes).digest("hex")}}secureCompare(e,t){if(e.length!==t.length)return!1;try{const r=Buffer.from(e,"utf8"),s=Buffer.from(t,"utf8");return i(r,s)}catch{return!1}}}export{AuthHmac as default};
@@ -1 +1 @@
1
- import e from"../../lib/auth/base.js";const t=new WeakMap;class AuthLLNG extends e{constructor(){super(...arguments),this.name="authLemonldapNg",this.roles=["auth"]}async api(e){let n;try{n=await this.loadHandler()}catch(e){throw new Error(`${this.name}: requires the optional dependency "lemonldap-ng-handler", which is not installed. Install it to use this plugin, or remove it from the configuration. (${e instanceof Error?e.message:String(e)})`)}const r=this.config.llng_ini??"",a=t.get(n);if(void 0!==a&&a!==r)throw new Error(`${this.name}: lemonldap-ng-handler is already initialized from "${a}"; it keeps its state at the module level, so a second instance of this plugin cannot also use "${r}". Load one instance of this plugin, or point every instance at the same lemonldap-ng.ini.`);try{await n.init({configStorage:{confFile:r}})}catch(e){throw new Error(`${this.name}: cannot initialize the LemonLDAP::NG handler from ${r}. Check that the file exists, that its [configuration] section is reachable and that [node-handler] lists this server in nodeVhosts. (${e instanceof Error?e.message:String(e)})`)}t.set(n,r),this.handler=n,super.api(e)}async loadHandler(){return import("lemonldap-ng-handler")}static dropForgedIdentity(e,t){const n=t??AuthLLNG.USER_HEADER;for(const t of Object.keys(e.headers))t.toLowerCase()===n&&delete e.headers[t]}static headerValue(e,t){for(const[n,r]of Object.entries(e.headers))if(n.toLowerCase()===t&&"string"==typeof r)return r}static vouchedIdentity(e){return AuthLLNG.headerValue(e,AuthLLNG.USER_HEADER)}identitySource(){const e=this.config.llng_username_header||"";return"req.user: LemonLDAP::NG's whatToTrace, via Lm-Remote-User; "+(e?`req.userName: the ${e} header LLNG exports`:"req.userName: the same value, until --llng-username-header names an exported header")}authMethod(e,t,n){if(!this.handler)throw new Error(`${this.name}: lemonldap-ng-handler is not loaded`);const r=(this.config.llng_username_header||"").toLowerCase();AuthLLNG.dropForgedIdentity(e),r&&AuthLLNG.dropForgedIdentity(e,r),this.handler.run(e,t,()=>{const a=AuthLLNG.vouchedIdentity(e);if(!a)return this.logger.warn(`${this.name}: the handler passed a request on without naming a user, which a \`skip\` rule does. Refusing rather than serving it with no identity`),void t.status(401).json({error:"Unauthorized"});const i=r?AuthLLNG.headerValue(e,r):void 0;this.publishIdentity(e,a,i),n()})}}AuthLLNG.USER_HEADER="lm-remote-user";export{AuthLLNG as default};
1
+ import e from"../../lib/auth/base.js";import{llngOptions as t}from"./auth.options.js";const n=new WeakMap;class AuthLLNG extends e{static optionDefinitions=t;name="authLemonldapNg";roles=["auth"];handler;async api(e){let r;try{r=await this.loadHandler()}catch(e){throw new Error(`${this.name}: requires the optional dependency "lemonldap-ng-handler", which is not installed. Install it to use this plugin, or remove it from the configuration. (${e instanceof Error?e.message:String(e)})`)}const a=this.options(t).llng_ini,o=n.get(r);if(void 0!==o&&o!==a)throw new Error(`${this.name}: lemonldap-ng-handler is already initialized from "${o}"; it keeps its state at the module level, so a second instance of this plugin cannot also use "${a}". Load one instance of this plugin, or point every instance at the same lemonldap-ng.ini.`);try{await r.init({configStorage:{confFile:a}})}catch(e){throw new Error(`${this.name}: cannot initialize the LemonLDAP::NG handler from ${a}. Check that the file exists, that its [configuration] section is reachable and that [node-handler] lists this server in nodeVhosts. (${e instanceof Error?e.message:String(e)})`)}n.set(r,a),this.handler=r,super.api(e)}async loadHandler(){return import("lemonldap-ng-handler")}static USER_HEADER="lm-remote-user";static dropForgedIdentity(e,t){const n=t??AuthLLNG.USER_HEADER;for(const t of Object.keys(e.headers))t.toLowerCase()===n&&delete e.headers[t]}static headerValue(e,t){for(const[n,r]of Object.entries(e.headers))if(n.toLowerCase()===t&&"string"==typeof r)return r}static vouchedIdentity(e){return AuthLLNG.headerValue(e,AuthLLNG.USER_HEADER)}identitySource(){const e=this.options(t).llng_username_header;return"req.user: LemonLDAP::NG's whatToTrace, via Lm-Remote-User; "+(e?`req.userName: the ${e} header LLNG exports`:"req.userName: the same value, until --llng-username-header names an exported header")}authMethod(e,n,r){if(!this.handler)throw new Error(`${this.name}: lemonldap-ng-handler is not loaded`);const a=this.options(t).llng_username_header.toLowerCase();AuthLLNG.dropForgedIdentity(e),a&&AuthLLNG.dropForgedIdentity(e,a),this.handler.run(e,n,()=>{const t=AuthLLNG.vouchedIdentity(e);if(!t)return this.logger.warn(`${this.name}: the handler passed a request on without naming a user, which a \`skip\` rule does. Refusing rather than serving it with no identity`),void n.status(401).json({error:"Unauthorized"});const o=a?AuthLLNG.headerValue(e,a):void 0;this.publishIdentity(e,t,o),r()})}}export{AuthLLNG as default};
@@ -1 +1 @@
1
- import{randomUUID as e}from"crypto";import o from"express-openid-connect";import s from"../../lib/auth/base.js";import{launchHooksChained as t,runAsHookOwner as i}from"../../lib/utils.js";import{serverError as n,unauthorized as r}from"../../lib/expressFormatedResponses.js";const{auth:a,requiresAuth:c}=o,d=["/login","/logout","/callback","/backchannel-logout"],h=/^end_session_endpoint must be configured/,u=/^\/logout\/?$/i,l="dmSession",sessionClaims=e=>{const{oidc:o,appSession:s}=e,t=o?.idTokenClaims;if(!t)return;const i=s?.[l];return"string"==typeof i?{...t,session:i}:t},subjectOf=e=>{try{const o=JSON.parse(Buffer.from(String(e).split(".")[1],"base64url").toString());return"string"==typeof o.sub?o.sub:void 0}catch{return}},walkSubscribers=async(e,o)=>{let s;for(const t of e??[])if(t)try{await i(t,()=>t(o))}catch(e){s??(s=e instanceof Error?e:new Error(String(e)))}if(s)throw s};class OpenIDConnect extends s{constructor(e){super(e),this.name="openidconnect",this.configApiKey="openidconnect",this.roles=["auth","configurable"],this.warnedNoEndSession=!1,this.warnedNoRecord=!1,this.warnedUnnamed=!1;for(const e of["oidc_server","oidc_client_id","oidc_client_secret","base_url"])if(!this.config[e])throw new Error(`Missing config parameter ${e}`)}buildConfig(){return{authRequired:!1,issuerBaseURL:this.config.oidc_server,clientID:this.config.oidc_client_id,secret:this.config.oidc_client_secret,baseURL:this.config.base_url,clientSecret:this.config.oidc_client_secret,authorizationParams:{response_type:"code",scope:"openid profile email"},idpLogout:!0,routes:{logout:!1},afterCallback:async(e,o,s)=>({...s,[l]:await this.sessionId(e,subjectOf(s.id_token))}),backchannelLogout:{onLogoutToken:async e=>{this.server.hooks.oidclogouttoken?await walkSubscribers(this.server.hooks.oidclogouttoken,e):this.logger.warn("openidconnect: a logout token arrived but no Back-Channel Logout plugin is loaded, so nothing records it")},onLogin:async e=>{if(!this.server.hooks.oidclogin)return;const o=e.oidc?.idTokenClaims;if(o)try{await walkSubscribers(this.server.hooks.oidclogin,o)}catch(o){const s=e.appSession;if(s)for(const e of Object.keys(s))delete s[e];throw o}},isLoggedOut:e=>this.loggedOut(e)}}}identitySource(){const e=this.config.oidc_username_claim||"sub";return`req.user: the OIDC sub, an opaque provider identifier; req.userName: the "${e}" claim`+("sub"===e?" — the same value, until --oidc-username-claim names another":"")}getConfigApiData(){return{enabled:!0,endpoints:{logout:"/logout"}}}get pathPrefixes(){const e=super.pathPrefixes;return 0===e.length?e:[...e,...d]}api(e){this.router=a(this.buildConfig()),super.api(e)}authMethod(e,o,s){if(!this.router)return n(o,new Error(`${this.name}: api() has not built the router`));this.router(e,o,t=>{if(t&&u.test(e.path)&&this.noEndSession(t))return o.redirect(this.config.base_url);if(t)return n(o,t);if(("GET"===e.method||"HEAD"===e.method)&&u.test(e.path))return void this.endHere(e).then(()=>o.oidc.logout({logoutParams:{post_logout_redirect_uri:void 0}}));const guard=()=>{c()(e,o,t=>{if(t)return r(o);const i=e.oidc.user,n=this.config.oidc_username_claim||"sub",a=i[n];"sub"!==n&&"string"!=typeof a&&this.logger.warn(`${this.name}: no "${n}" claim on this session, so req.userName falls back to the sub. Check --oidc-username-claim against the scopes the provider is asked for`),this.publishIdentity(e,String(i.sub),"string"==typeof a?a:String(i.sub)),s()})},i=this.renewExpired(e);i?i.then(guard):guard()})}renewExpired(e){const o=e.oidc,s=o.accessToken;if(!s?.isExpired())return;const end=()=>{e.appSession=void 0};if(o.refreshToken)return s.refresh().then(()=>{},e=>{this.logger.info(`${this.name}: the provider refused to renew an access token, so the session ends: ${String(e)}`),end()});end()}async loggedOut(e){if(!this.server.hooks.oidcsessionvalid)return!1;const o=sessionClaims(e),[,s]=await t(this.server.hooks.oidcsessionvalid,[o,!0]);return!s}async sessionId(o,s){const{oidc:t,appSession:i}=o,n=i?.[l];return"string"==typeof n&&void 0!==s&&t?.isAuthenticated()&&t.idTokenClaims?.sub===s&&!await this.loggedOut(o).catch(()=>!0)?n:e()}async endHere(e){if(!e.oidc?.isAuthenticated())return;const o=sessionClaims(e);if(!o)return;const{iss:s,sid:t,session:i}=o,n=i?{iss:s,session:i}:t?{iss:s,sid:t}:void 0;if(n)if(this.server.hooks.oidclogouttoken)try{await walkSubscribers(this.server.hooks.oidclogouttoken,n)}catch(e){this.logger.error(`${this.name}: /logout could not record the session it ends, so a copy of its cookie stays valid until its tokens expire: ${String(e)}`)}else this.warnedNoRecord||(this.warnedNoRecord=!0,this.logger.warn(`${this.name}: no Back-Channel Logout plugin is loaded, so /logout drops the browser's cookie and nothing else: a copy of it stays valid until its tokens expire. Load core/bcl to end the session here as well`));else this.warnedUnnamed||(this.warnedUnnamed=!0,this.logger.warn(`${this.name}: /logout ended a session nothing names — one established before this version, from a provider that sends no sid — so a copy of its cookie stays valid until its tokens expire`))}noEndSession(e){return!!(e instanceof Error&&h.test(e.message))&&(this.warnedNoEndSession||(this.warnedNoEndSession=!0,this.logger.warn(`${this.name}: the provider publishes no end_session_endpoint, so /logout ends the session here only, and the provider may log the caller back in`)),!0)}}export{OpenIDConnect as default};
1
+ import{randomUUID as e}from"crypto";import o from"express-openid-connect";import s from"../../lib/auth/base.js";import{serverError as t,unauthorized as i}from"../../lib/expressFormatedResponses.js";import{openidconnectOptions as n}from"./auth.options.js";const{auth:r,requiresAuth:a}=o,c=["/login","/logout","/callback","/backchannel-logout"],d=/end_session_endpoint/,u=/^\/logout\/?$/i,h="dmSession",sessionClaims=e=>{const{oidc:o,appSession:s}=e,t=o?.idTokenClaims;if(!t)return;const i=s?.[h];return"string"==typeof i?{...t,session:i}:t},subjectOf=e=>{try{const o=JSON.parse(Buffer.from(String(e).split(".")[1],"base64url").toString());return"string"==typeof o.sub?o.sub:void 0}catch{return}};class OpenIDConnect extends s{static optionDefinitions=n;name="openidconnect";configApiKey="openidconnect";roles=["auth","configurable"];router;warnedNoEndSession=!1;warnedNoRecord=!1;warnedUnnamed=!1;constructor(e){super(e);for(const e of["oidc_server","oidc_client_id","oidc_client_secret","base_url"])if(!this.config[e])throw new Error(`Missing config parameter ${e}`)}buildConfig(){const e=this.options(n);return{authRequired:!1,issuerBaseURL:e.oidc_server,clientID:e.oidc_client_id,secret:e.oidc_client_secret,baseURL:e.base_url,clientSecret:e.oidc_client_secret,authorizationParams:{response_type:"code",scope:"openid profile email"},idpLogout:!0,routes:{logout:!1},afterCallback:async(e,o,s)=>({...s,[h]:await this.sessionId(e,subjectOf(s.id_token))}),backchannelLogout:{onLogoutToken:async e=>{this.server.hookBus.has("oidclogouttoken")?await this.hookBus.notifyStrict("oidclogouttoken",e):this.logger.warn("openidconnect: a logout token arrived but no Back-Channel Logout plugin is loaded, so nothing records it")},onLogin:async e=>{if(!this.server.hookBus.has("oidclogin"))return;const o=e.oidc?.idTokenClaims;if(o)try{await this.hookBus.notifyStrict("oidclogin",o)}catch(o){const s=e.appSession;if(s)for(const e of Object.keys(s))delete s[e];throw o}},isLoggedOut:e=>this.loggedOut(e)}}}identitySource(){const e=this.options(n).oidc_username_claim;return`req.user: the OIDC sub, an opaque provider identifier; req.userName: the "${e}" claim`+("sub"===e?" — the same value, until --oidc-username-claim names another":"")}getConfigApiData(){return{enabled:!0,endpoints:{logout:"/logout"}}}get pathPrefixes(){const e=super.pathPrefixes;return 0===e.length?e:[...e,...c]}api(e){this.router=r(this.buildConfig()),super.api(e)}authMethod(e,o,s){if(!this.router)return t(o,new Error(`${this.name}: api() has not built the router`));this.router(e,o,r=>{if(r&&u.test(e.path)&&this.noEndSession(r))return o.redirect(this.options(n).base_url);if(r)return t(o,r);if(("GET"===e.method||"HEAD"===e.method)&&u.test(e.path))return void this.endHere(e).then(()=>o.oidc.logout({logoutParams:{post_logout_redirect_uri:void 0}}));const guard=()=>{a()(e,o,t=>{if(t)return i(o);const r=e.oidc.user,a=this.options(n).oidc_username_claim,c=r[a];"sub"!==a&&"string"!=typeof c&&this.logger.warn(`${this.name}: no "${a}" claim on this session, so req.userName falls back to the sub. Check --oidc-username-claim against the scopes the provider is asked for`),this.publishIdentity(e,String(r.sub),"string"==typeof c?c:String(r.sub)),s()})},c=this.renewExpired(e);c?c.then(guard):guard()})}renewExpired(e){const o=e.oidc,s=o.accessToken;if(!s?.isExpired())return;const end=()=>{e.appSession=void 0};if(o.refreshToken)return s.refresh().then(()=>{},e=>{this.logger.info(`${this.name}: the provider refused to renew an access token, so the session ends: ${String(e)}`),end()});end()}async loggedOut(e){if(!this.server.hookBus.has("oidcsessionvalid"))return!1;const o=sessionClaims(e),[,s]=await this.hookBus.chain("oidcsessionvalid",[o,!0]);return!s}async sessionId(o,s){const{oidc:t,appSession:i}=o,n=i?.[h];return"string"==typeof n&&void 0!==s&&t?.isAuthenticated()&&t.idTokenClaims?.sub===s&&!await this.loggedOut(o).catch(()=>!0)?n:e()}async endHere(e){if(!e.oidc?.isAuthenticated())return;const o=sessionClaims(e);if(!o)return;const{iss:s,sid:t,session:i}=o,n=i?{iss:s,session:i}:t?{iss:s,sid:t}:void 0;if(n)if(this.server.hookBus.has("oidclogouttoken"))try{await this.hookBus.notifyStrict("oidclogouttoken",n)}catch(e){this.logger.error(`${this.name}: /logout could not record the session it ends, so a copy of its cookie stays valid until its tokens expire: ${String(e)}`)}else this.warnedNoRecord||(this.warnedNoRecord=!0,this.logger.warn(`${this.name}: no Back-Channel Logout plugin is loaded, so /logout drops the browser's cookie and nothing else: a copy of it stays valid until its tokens expire. Load core/bcl to end the session here as well`));else this.warnedUnnamed||(this.warnedUnnamed=!0,this.logger.warn(`${this.name}: /logout ended a session nothing names — one established before this version, from a provider that sends no sid — so a copy of its cookie stays valid until its tokens expire`))}noEndSession(e){return!!(e instanceof Error&&d.test(e.message))&&(this.warnedNoEndSession||(this.warnedNoEndSession=!0,this.logger.warn(`${this.name}: the provider publishes no end_session_endpoint, so /logout ends the session here only, and the provider may log the caller back in`)),!0)}}export{OpenIDConnect as default};