rucio 35.7.0__py3-none-any.whl

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.

Potentially problematic release.


This version of rucio might be problematic. Click here for more details.

Files changed (493) hide show
  1. rucio/__init__.py +17 -0
  2. rucio/alembicrevision.py +15 -0
  3. rucio/client/__init__.py +15 -0
  4. rucio/client/accountclient.py +433 -0
  5. rucio/client/accountlimitclient.py +183 -0
  6. rucio/client/baseclient.py +974 -0
  7. rucio/client/client.py +76 -0
  8. rucio/client/configclient.py +126 -0
  9. rucio/client/credentialclient.py +59 -0
  10. rucio/client/didclient.py +866 -0
  11. rucio/client/diracclient.py +56 -0
  12. rucio/client/downloadclient.py +1785 -0
  13. rucio/client/exportclient.py +44 -0
  14. rucio/client/fileclient.py +50 -0
  15. rucio/client/importclient.py +42 -0
  16. rucio/client/lifetimeclient.py +90 -0
  17. rucio/client/lockclient.py +109 -0
  18. rucio/client/metaconventionsclient.py +140 -0
  19. rucio/client/pingclient.py +44 -0
  20. rucio/client/replicaclient.py +454 -0
  21. rucio/client/requestclient.py +125 -0
  22. rucio/client/rseclient.py +746 -0
  23. rucio/client/ruleclient.py +294 -0
  24. rucio/client/scopeclient.py +90 -0
  25. rucio/client/subscriptionclient.py +173 -0
  26. rucio/client/touchclient.py +82 -0
  27. rucio/client/uploadclient.py +955 -0
  28. rucio/common/__init__.py +13 -0
  29. rucio/common/cache.py +74 -0
  30. rucio/common/config.py +801 -0
  31. rucio/common/constants.py +159 -0
  32. rucio/common/constraints.py +17 -0
  33. rucio/common/didtype.py +189 -0
  34. rucio/common/dumper/__init__.py +335 -0
  35. rucio/common/dumper/consistency.py +452 -0
  36. rucio/common/dumper/data_models.py +318 -0
  37. rucio/common/dumper/path_parsing.py +64 -0
  38. rucio/common/exception.py +1151 -0
  39. rucio/common/extra.py +36 -0
  40. rucio/common/logging.py +420 -0
  41. rucio/common/pcache.py +1408 -0
  42. rucio/common/plugins.py +153 -0
  43. rucio/common/policy.py +84 -0
  44. rucio/common/schema/__init__.py +150 -0
  45. rucio/common/schema/atlas.py +413 -0
  46. rucio/common/schema/belleii.py +408 -0
  47. rucio/common/schema/domatpc.py +401 -0
  48. rucio/common/schema/escape.py +426 -0
  49. rucio/common/schema/generic.py +433 -0
  50. rucio/common/schema/generic_multi_vo.py +412 -0
  51. rucio/common/schema/icecube.py +406 -0
  52. rucio/common/stomp_utils.py +159 -0
  53. rucio/common/stopwatch.py +55 -0
  54. rucio/common/test_rucio_server.py +148 -0
  55. rucio/common/types.py +403 -0
  56. rucio/common/utils.py +2238 -0
  57. rucio/core/__init__.py +13 -0
  58. rucio/core/account.py +496 -0
  59. rucio/core/account_counter.py +236 -0
  60. rucio/core/account_limit.py +423 -0
  61. rucio/core/authentication.py +620 -0
  62. rucio/core/config.py +456 -0
  63. rucio/core/credential.py +225 -0
  64. rucio/core/did.py +3000 -0
  65. rucio/core/did_meta_plugins/__init__.py +252 -0
  66. rucio/core/did_meta_plugins/did_column_meta.py +331 -0
  67. rucio/core/did_meta_plugins/did_meta_plugin_interface.py +165 -0
  68. rucio/core/did_meta_plugins/filter_engine.py +613 -0
  69. rucio/core/did_meta_plugins/json_meta.py +240 -0
  70. rucio/core/did_meta_plugins/mongo_meta.py +216 -0
  71. rucio/core/did_meta_plugins/postgres_meta.py +316 -0
  72. rucio/core/dirac.py +237 -0
  73. rucio/core/distance.py +187 -0
  74. rucio/core/exporter.py +59 -0
  75. rucio/core/heartbeat.py +363 -0
  76. rucio/core/identity.py +300 -0
  77. rucio/core/importer.py +259 -0
  78. rucio/core/lifetime_exception.py +377 -0
  79. rucio/core/lock.py +576 -0
  80. rucio/core/message.py +282 -0
  81. rucio/core/meta_conventions.py +203 -0
  82. rucio/core/monitor.py +447 -0
  83. rucio/core/naming_convention.py +195 -0
  84. rucio/core/nongrid_trace.py +136 -0
  85. rucio/core/oidc.py +1461 -0
  86. rucio/core/permission/__init__.py +119 -0
  87. rucio/core/permission/atlas.py +1348 -0
  88. rucio/core/permission/belleii.py +1077 -0
  89. rucio/core/permission/escape.py +1078 -0
  90. rucio/core/permission/generic.py +1130 -0
  91. rucio/core/permission/generic_multi_vo.py +1150 -0
  92. rucio/core/quarantined_replica.py +223 -0
  93. rucio/core/replica.py +4158 -0
  94. rucio/core/replica_sorter.py +366 -0
  95. rucio/core/request.py +3089 -0
  96. rucio/core/rse.py +1875 -0
  97. rucio/core/rse_counter.py +186 -0
  98. rucio/core/rse_expression_parser.py +459 -0
  99. rucio/core/rse_selector.py +302 -0
  100. rucio/core/rule.py +4483 -0
  101. rucio/core/rule_grouping.py +1618 -0
  102. rucio/core/scope.py +180 -0
  103. rucio/core/subscription.py +364 -0
  104. rucio/core/topology.py +490 -0
  105. rucio/core/trace.py +375 -0
  106. rucio/core/transfer.py +1517 -0
  107. rucio/core/vo.py +169 -0
  108. rucio/core/volatile_replica.py +150 -0
  109. rucio/daemons/__init__.py +13 -0
  110. rucio/daemons/abacus/__init__.py +13 -0
  111. rucio/daemons/abacus/account.py +116 -0
  112. rucio/daemons/abacus/collection_replica.py +124 -0
  113. rucio/daemons/abacus/rse.py +117 -0
  114. rucio/daemons/atropos/__init__.py +13 -0
  115. rucio/daemons/atropos/atropos.py +242 -0
  116. rucio/daemons/auditor/__init__.py +289 -0
  117. rucio/daemons/auditor/hdfs.py +97 -0
  118. rucio/daemons/auditor/srmdumps.py +355 -0
  119. rucio/daemons/automatix/__init__.py +13 -0
  120. rucio/daemons/automatix/automatix.py +293 -0
  121. rucio/daemons/badreplicas/__init__.py +13 -0
  122. rucio/daemons/badreplicas/minos.py +322 -0
  123. rucio/daemons/badreplicas/minos_temporary_expiration.py +171 -0
  124. rucio/daemons/badreplicas/necromancer.py +196 -0
  125. rucio/daemons/bb8/__init__.py +13 -0
  126. rucio/daemons/bb8/bb8.py +353 -0
  127. rucio/daemons/bb8/common.py +759 -0
  128. rucio/daemons/bb8/nuclei_background_rebalance.py +153 -0
  129. rucio/daemons/bb8/t2_background_rebalance.py +153 -0
  130. rucio/daemons/c3po/__init__.py +13 -0
  131. rucio/daemons/c3po/algorithms/__init__.py +13 -0
  132. rucio/daemons/c3po/algorithms/simple.py +134 -0
  133. rucio/daemons/c3po/algorithms/t2_free_space.py +128 -0
  134. rucio/daemons/c3po/algorithms/t2_free_space_only_pop.py +130 -0
  135. rucio/daemons/c3po/algorithms/t2_free_space_only_pop_with_network.py +294 -0
  136. rucio/daemons/c3po/c3po.py +371 -0
  137. rucio/daemons/c3po/collectors/__init__.py +13 -0
  138. rucio/daemons/c3po/collectors/agis.py +108 -0
  139. rucio/daemons/c3po/collectors/free_space.py +81 -0
  140. rucio/daemons/c3po/collectors/jedi_did.py +57 -0
  141. rucio/daemons/c3po/collectors/mock_did.py +51 -0
  142. rucio/daemons/c3po/collectors/network_metrics.py +71 -0
  143. rucio/daemons/c3po/collectors/workload.py +112 -0
  144. rucio/daemons/c3po/utils/__init__.py +13 -0
  145. rucio/daemons/c3po/utils/dataset_cache.py +50 -0
  146. rucio/daemons/c3po/utils/expiring_dataset_cache.py +56 -0
  147. rucio/daemons/c3po/utils/expiring_list.py +62 -0
  148. rucio/daemons/c3po/utils/popularity.py +85 -0
  149. rucio/daemons/c3po/utils/timeseries.py +89 -0
  150. rucio/daemons/cache/__init__.py +13 -0
  151. rucio/daemons/cache/consumer.py +197 -0
  152. rucio/daemons/common.py +415 -0
  153. rucio/daemons/conveyor/__init__.py +13 -0
  154. rucio/daemons/conveyor/common.py +562 -0
  155. rucio/daemons/conveyor/finisher.py +529 -0
  156. rucio/daemons/conveyor/poller.py +404 -0
  157. rucio/daemons/conveyor/preparer.py +205 -0
  158. rucio/daemons/conveyor/receiver.py +249 -0
  159. rucio/daemons/conveyor/stager.py +132 -0
  160. rucio/daemons/conveyor/submitter.py +403 -0
  161. rucio/daemons/conveyor/throttler.py +532 -0
  162. rucio/daemons/follower/__init__.py +13 -0
  163. rucio/daemons/follower/follower.py +101 -0
  164. rucio/daemons/hermes/__init__.py +13 -0
  165. rucio/daemons/hermes/hermes.py +774 -0
  166. rucio/daemons/judge/__init__.py +13 -0
  167. rucio/daemons/judge/cleaner.py +159 -0
  168. rucio/daemons/judge/evaluator.py +185 -0
  169. rucio/daemons/judge/injector.py +162 -0
  170. rucio/daemons/judge/repairer.py +154 -0
  171. rucio/daemons/oauthmanager/__init__.py +13 -0
  172. rucio/daemons/oauthmanager/oauthmanager.py +198 -0
  173. rucio/daemons/reaper/__init__.py +13 -0
  174. rucio/daemons/reaper/dark_reaper.py +278 -0
  175. rucio/daemons/reaper/reaper.py +743 -0
  176. rucio/daemons/replicarecoverer/__init__.py +13 -0
  177. rucio/daemons/replicarecoverer/suspicious_replica_recoverer.py +626 -0
  178. rucio/daemons/rsedecommissioner/__init__.py +13 -0
  179. rucio/daemons/rsedecommissioner/config.py +81 -0
  180. rucio/daemons/rsedecommissioner/profiles/__init__.py +24 -0
  181. rucio/daemons/rsedecommissioner/profiles/atlas.py +60 -0
  182. rucio/daemons/rsedecommissioner/profiles/generic.py +451 -0
  183. rucio/daemons/rsedecommissioner/profiles/types.py +92 -0
  184. rucio/daemons/rsedecommissioner/rse_decommissioner.py +280 -0
  185. rucio/daemons/storage/__init__.py +13 -0
  186. rucio/daemons/storage/consistency/__init__.py +13 -0
  187. rucio/daemons/storage/consistency/actions.py +846 -0
  188. rucio/daemons/tracer/__init__.py +13 -0
  189. rucio/daemons/tracer/kronos.py +536 -0
  190. rucio/daemons/transmogrifier/__init__.py +13 -0
  191. rucio/daemons/transmogrifier/transmogrifier.py +762 -0
  192. rucio/daemons/undertaker/__init__.py +13 -0
  193. rucio/daemons/undertaker/undertaker.py +137 -0
  194. rucio/db/__init__.py +13 -0
  195. rucio/db/sqla/__init__.py +52 -0
  196. rucio/db/sqla/constants.py +201 -0
  197. rucio/db/sqla/migrate_repo/__init__.py +13 -0
  198. rucio/db/sqla/migrate_repo/env.py +110 -0
  199. rucio/db/sqla/migrate_repo/versions/01eaf73ab656_add_new_rule_notification_state_progress.py +70 -0
  200. rucio/db/sqla/migrate_repo/versions/0437a40dbfd1_add_eol_at_in_rules.py +47 -0
  201. rucio/db/sqla/migrate_repo/versions/0f1adb7a599a_create_transfer_hops_table.py +59 -0
  202. rucio/db/sqla/migrate_repo/versions/102efcf145f4_added_stuck_at_column_to_rules.py +43 -0
  203. rucio/db/sqla/migrate_repo/versions/13d4f70c66a9_introduce_transfer_limits.py +91 -0
  204. rucio/db/sqla/migrate_repo/versions/140fef722e91_cleanup_distances_table.py +76 -0
  205. rucio/db/sqla/migrate_repo/versions/14ec5aeb64cf_add_request_external_host.py +43 -0
  206. rucio/db/sqla/migrate_repo/versions/156fb5b5a14_add_request_type_to_requests_idx.py +50 -0
  207. rucio/db/sqla/migrate_repo/versions/1677d4d803c8_split_rse_availability_into_multiple.py +68 -0
  208. rucio/db/sqla/migrate_repo/versions/16a0aca82e12_create_index_on_table_replicas_path.py +40 -0
  209. rucio/db/sqla/migrate_repo/versions/1803333ac20f_adding_provenance_and_phys_group.py +45 -0
  210. rucio/db/sqla/migrate_repo/versions/1a29d6a9504c_add_didtype_chck_to_requests.py +60 -0
  211. rucio/db/sqla/migrate_repo/versions/1a80adff031a_create_index_on_rules_hist_recent.py +40 -0
  212. rucio/db/sqla/migrate_repo/versions/1c45d9730ca6_increase_identity_length.py +140 -0
  213. rucio/db/sqla/migrate_repo/versions/1d1215494e95_add_quarantined_replicas_table.py +73 -0
  214. rucio/db/sqla/migrate_repo/versions/1d96f484df21_asynchronous_rules_and_rule_approval.py +74 -0
  215. rucio/db/sqla/migrate_repo/versions/1f46c5f240ac_add_bytes_column_to_bad_replicas.py +43 -0
  216. rucio/db/sqla/migrate_repo/versions/1fc15ab60d43_add_message_history_table.py +50 -0
  217. rucio/db/sqla/migrate_repo/versions/2190e703eb6e_move_rse_settings_to_rse_attributes.py +134 -0
  218. rucio/db/sqla/migrate_repo/versions/21d6b9dc9961_add_mismatch_scheme_state_to_requests.py +64 -0
  219. rucio/db/sqla/migrate_repo/versions/22cf51430c78_add_availability_column_to_table_rses.py +39 -0
  220. rucio/db/sqla/migrate_repo/versions/22d887e4ec0a_create_sources_table.py +64 -0
  221. rucio/db/sqla/migrate_repo/versions/25821a8a45a3_remove_unique_constraint_on_requests.py +51 -0
  222. rucio/db/sqla/migrate_repo/versions/25fc855625cf_added_unique_constraint_to_rules.py +41 -0
  223. rucio/db/sqla/migrate_repo/versions/269fee20dee9_add_repair_cnt_to_locks.py +43 -0
  224. rucio/db/sqla/migrate_repo/versions/271a46ea6244_add_ignore_availability_column_to_rules.py +44 -0
  225. rucio/db/sqla/migrate_repo/versions/277b5fbb41d3_switch_heartbeats_executable.py +53 -0
  226. rucio/db/sqla/migrate_repo/versions/27e3a68927fb_remove_replicas_tombstone_and_replicas_.py +38 -0
  227. rucio/db/sqla/migrate_repo/versions/2854cd9e168_added_rule_id_column.py +47 -0
  228. rucio/db/sqla/migrate_repo/versions/295289b5a800_processed_by_and__at_in_requests.py +45 -0
  229. rucio/db/sqla/migrate_repo/versions/2962ece31cf4_add_nbaccesses_column_in_the_did_table.py +45 -0
  230. rucio/db/sqla/migrate_repo/versions/2af3291ec4c_added_replicas_history_table.py +57 -0
  231. rucio/db/sqla/migrate_repo/versions/2b69addda658_add_columns_for_third_party_copy_read_.py +45 -0
  232. rucio/db/sqla/migrate_repo/versions/2b8e7bcb4783_add_config_table.py +69 -0
  233. rucio/db/sqla/migrate_repo/versions/2ba5229cb54c_add_submitted_at_to_requests_table.py +43 -0
  234. rucio/db/sqla/migrate_repo/versions/2cbee484dcf9_added_column_volume_to_rse_transfer_.py +42 -0
  235. rucio/db/sqla/migrate_repo/versions/2edee4a83846_add_source_to_requests_and_requests_.py +47 -0
  236. rucio/db/sqla/migrate_repo/versions/2eef46be23d4_change_tokens_pk.py +46 -0
  237. rucio/db/sqla/migrate_repo/versions/2f648fc909f3_index_in_rule_history_on_scope_name.py +40 -0
  238. rucio/db/sqla/migrate_repo/versions/3082b8cef557_add_naming_convention_table_and_closed_.py +67 -0
  239. rucio/db/sqla/migrate_repo/versions/30fa38b6434e_add_index_on_service_column_in_the_message_table.py +44 -0
  240. rucio/db/sqla/migrate_repo/versions/3152492b110b_added_staging_area_column.py +77 -0
  241. rucio/db/sqla/migrate_repo/versions/32c7d2783f7e_create_bad_replicas_table.py +60 -0
  242. rucio/db/sqla/migrate_repo/versions/3345511706b8_replicas_table_pk_definition_is_in_.py +72 -0
  243. rucio/db/sqla/migrate_repo/versions/35ef10d1e11b_change_index_on_table_requests.py +42 -0
  244. rucio/db/sqla/migrate_repo/versions/379a19b5332d_create_rse_limits_table.py +65 -0
  245. rucio/db/sqla/migrate_repo/versions/384b96aa0f60_created_rule_history_tables.py +133 -0
  246. rucio/db/sqla/migrate_repo/versions/3ac1660a1a72_extend_distance_table.py +55 -0
  247. rucio/db/sqla/migrate_repo/versions/3ad36e2268b0_create_collection_replicas_updates_table.py +76 -0
  248. rucio/db/sqla/migrate_repo/versions/3c9df354071b_extend_waiting_request_state.py +60 -0
  249. rucio/db/sqla/migrate_repo/versions/3d9813fab443_add_a_new_state_lost_in_badfilesstatus.py +44 -0
  250. rucio/db/sqla/migrate_repo/versions/40ad39ce3160_add_transferred_at_to_requests_table.py +43 -0
  251. rucio/db/sqla/migrate_repo/versions/4207be2fd914_add_notification_column_to_rules.py +64 -0
  252. rucio/db/sqla/migrate_repo/versions/42db2617c364_create_index_on_requests_external_id.py +40 -0
  253. rucio/db/sqla/migrate_repo/versions/436827b13f82_added_column_activity_to_table_requests.py +43 -0
  254. rucio/db/sqla/migrate_repo/versions/44278720f774_update_requests_typ_sta_upd_idx_index.py +44 -0
  255. rucio/db/sqla/migrate_repo/versions/45378a1e76a8_create_collection_replica_table.py +78 -0
  256. rucio/db/sqla/migrate_repo/versions/469d262be19_removing_created_at_index.py +41 -0
  257. rucio/db/sqla/migrate_repo/versions/4783c1f49cb4_create_distance_table.py +59 -0
  258. rucio/db/sqla/migrate_repo/versions/49a21b4d4357_create_index_on_table_tokens.py +44 -0
  259. rucio/db/sqla/migrate_repo/versions/4a2cbedda8b9_add_source_replica_expression_column_to_.py +43 -0
  260. rucio/db/sqla/migrate_repo/versions/4a7182d9578b_added_bytes_length_accessed_at_columns.py +49 -0
  261. rucio/db/sqla/migrate_repo/versions/4bab9edd01fc_create_index_on_requests_rule_id.py +40 -0
  262. rucio/db/sqla/migrate_repo/versions/4c3a4acfe006_new_attr_account_table.py +63 -0
  263. rucio/db/sqla/migrate_repo/versions/4cf0a2e127d4_adding_transient_metadata.py +43 -0
  264. rucio/db/sqla/migrate_repo/versions/4df2c5ddabc0_remove_temporary_dids.py +55 -0
  265. rucio/db/sqla/migrate_repo/versions/50280c53117c_add_qos_class_to_rse.py +45 -0
  266. rucio/db/sqla/migrate_repo/versions/52153819589c_add_rse_id_to_replicas_table.py +43 -0
  267. rucio/db/sqla/migrate_repo/versions/52fd9f4916fa_added_activity_to_rules.py +43 -0
  268. rucio/db/sqla/migrate_repo/versions/53b479c3cb0f_fix_did_meta_table_missing_updated_at_.py +45 -0
  269. rucio/db/sqla/migrate_repo/versions/5673b4b6e843_add_wfms_metadata_to_rule_tables.py +47 -0
  270. rucio/db/sqla/migrate_repo/versions/575767d9f89_added_source_history_table.py +58 -0
  271. rucio/db/sqla/migrate_repo/versions/58bff7008037_add_started_at_to_requests.py +45 -0
  272. rucio/db/sqla/migrate_repo/versions/58c8b78301ab_rename_callback_to_message.py +106 -0
  273. rucio/db/sqla/migrate_repo/versions/5f139f77382a_added_child_rule_id_column.py +55 -0
  274. rucio/db/sqla/migrate_repo/versions/688ef1840840_adding_did_meta_table.py +50 -0
  275. rucio/db/sqla/migrate_repo/versions/6e572a9bfbf3_add_new_split_container_column_to_rules.py +47 -0
  276. rucio/db/sqla/migrate_repo/versions/70587619328_add_comment_column_for_subscriptions.py +43 -0
  277. rucio/db/sqla/migrate_repo/versions/739064d31565_remove_history_table_pks.py +41 -0
  278. rucio/db/sqla/migrate_repo/versions/7541902bf173_add_didsfollowed_and_followevents_table.py +91 -0
  279. rucio/db/sqla/migrate_repo/versions/7ec22226cdbf_new_replica_state_for_temporary_.py +72 -0
  280. rucio/db/sqla/migrate_repo/versions/810a41685bc1_added_columns_rse_transfer_limits.py +49 -0
  281. rucio/db/sqla/migrate_repo/versions/83f991c63a93_correct_rse_expression_length.py +43 -0
  282. rucio/db/sqla/migrate_repo/versions/8523998e2e76_increase_size_of_extended_attributes_.py +43 -0
  283. rucio/db/sqla/migrate_repo/versions/8ea9122275b1_adding_missing_function_based_indices.py +53 -0
  284. rucio/db/sqla/migrate_repo/versions/90f47792bb76_add_clob_payload_to_messages.py +45 -0
  285. rucio/db/sqla/migrate_repo/versions/914b8f02df38_new_table_for_lifetime_model_exceptions.py +68 -0
  286. rucio/db/sqla/migrate_repo/versions/94a5961ddbf2_add_estimator_columns.py +45 -0
  287. rucio/db/sqla/migrate_repo/versions/9a1b149a2044_add_saml_identity_type.py +94 -0
  288. rucio/db/sqla/migrate_repo/versions/9a45bc4ea66d_add_vp_table.py +54 -0
  289. rucio/db/sqla/migrate_repo/versions/9eb936a81eb1_true_is_true.py +72 -0
  290. rucio/db/sqla/migrate_repo/versions/a08fa8de1545_transfer_stats_table.py +55 -0
  291. rucio/db/sqla/migrate_repo/versions/a118956323f8_added_vo_table_and_vo_col_to_rse.py +76 -0
  292. rucio/db/sqla/migrate_repo/versions/a193a275255c_add_status_column_in_messages.py +47 -0
  293. rucio/db/sqla/migrate_repo/versions/a5f6f6e928a7_1_7_0.py +121 -0
  294. rucio/db/sqla/migrate_repo/versions/a616581ee47_added_columns_to_table_requests.py +59 -0
  295. rucio/db/sqla/migrate_repo/versions/a6eb23955c28_state_idx_non_functional.py +52 -0
  296. rucio/db/sqla/migrate_repo/versions/a74275a1ad30_added_global_quota_table.py +54 -0
  297. rucio/db/sqla/migrate_repo/versions/a93e4e47bda_heartbeats.py +64 -0
  298. rucio/db/sqla/migrate_repo/versions/ae2a56fcc89_added_comment_column_to_rules.py +49 -0
  299. rucio/db/sqla/migrate_repo/versions/b0070f3695c8_add_deletedidmeta_table.py +57 -0
  300. rucio/db/sqla/migrate_repo/versions/b4293a99f344_added_column_identity_to_table_tokens.py +43 -0
  301. rucio/db/sqla/migrate_repo/versions/b5493606bbf5_fix_primary_key_for_subscription_history.py +41 -0
  302. rucio/db/sqla/migrate_repo/versions/b7d287de34fd_removal_of_replicastate_source.py +91 -0
  303. rucio/db/sqla/migrate_repo/versions/b818052fa670_add_index_to_quarantined_replicas.py +40 -0
  304. rucio/db/sqla/migrate_repo/versions/b8caac94d7f0_add_comments_column_for_subscriptions_.py +43 -0
  305. rucio/db/sqla/migrate_repo/versions/b96a1c7e1cc4_new_bad_pfns_table_and_bad_replicas_.py +143 -0
  306. rucio/db/sqla/migrate_repo/versions/bb695f45c04_extend_request_state.py +76 -0
  307. rucio/db/sqla/migrate_repo/versions/bc68e9946deb_add_staging_timestamps_to_request.py +50 -0
  308. rucio/db/sqla/migrate_repo/versions/bf3baa1c1474_correct_pk_and_idx_for_history_tables.py +72 -0
  309. rucio/db/sqla/migrate_repo/versions/c0937668555f_add_qos_policy_map_table.py +55 -0
  310. rucio/db/sqla/migrate_repo/versions/c129ccdb2d5_add_lumiblocknr_to_dids.py +43 -0
  311. rucio/db/sqla/migrate_repo/versions/ccdbcd48206e_add_did_type_column_index_on_did_meta_.py +65 -0
  312. rucio/db/sqla/migrate_repo/versions/cebad904c4dd_new_payload_column_for_heartbeats.py +47 -0
  313. rucio/db/sqla/migrate_repo/versions/d1189a09c6e0_oauth2_0_and_jwt_feature_support_adding_.py +146 -0
  314. rucio/db/sqla/migrate_repo/versions/d23453595260_extend_request_state_for_preparer.py +104 -0
  315. rucio/db/sqla/migrate_repo/versions/d6dceb1de2d_added_purge_column_to_rules.py +44 -0
  316. rucio/db/sqla/migrate_repo/versions/d6e2c3b2cf26_remove_third_party_copy_column_from_rse.py +43 -0
  317. rucio/db/sqla/migrate_repo/versions/d91002c5841_new_account_limits_table.py +103 -0
  318. rucio/db/sqla/migrate_repo/versions/e138c364ebd0_extending_columns_for_filter_and_.py +49 -0
  319. rucio/db/sqla/migrate_repo/versions/e59300c8b179_support_for_archive.py +104 -0
  320. rucio/db/sqla/migrate_repo/versions/f1b14a8c2ac1_postgres_use_check_constraints.py +29 -0
  321. rucio/db/sqla/migrate_repo/versions/f41ffe206f37_oracle_global_temporary_tables.py +74 -0
  322. rucio/db/sqla/migrate_repo/versions/f85a2962b021_adding_transfertool_column_to_requests_.py +47 -0
  323. rucio/db/sqla/migrate_repo/versions/fa7a7d78b602_increase_refresh_token_size.py +43 -0
  324. rucio/db/sqla/migrate_repo/versions/fb28a95fe288_add_replicas_rse_id_tombstone_idx.py +37 -0
  325. rucio/db/sqla/migrate_repo/versions/fe1a65b176c9_set_third_party_copy_read_and_write_.py +43 -0
  326. rucio/db/sqla/migrate_repo/versions/fe8ea2fa9788_added_third_party_copy_column_to_rse_.py +43 -0
  327. rucio/db/sqla/models.py +1740 -0
  328. rucio/db/sqla/sautils.py +55 -0
  329. rucio/db/sqla/session.py +498 -0
  330. rucio/db/sqla/types.py +206 -0
  331. rucio/db/sqla/util.py +543 -0
  332. rucio/gateway/__init__.py +13 -0
  333. rucio/gateway/account.py +339 -0
  334. rucio/gateway/account_limit.py +286 -0
  335. rucio/gateway/authentication.py +375 -0
  336. rucio/gateway/config.py +217 -0
  337. rucio/gateway/credential.py +71 -0
  338. rucio/gateway/did.py +970 -0
  339. rucio/gateway/dirac.py +81 -0
  340. rucio/gateway/exporter.py +59 -0
  341. rucio/gateway/heartbeat.py +74 -0
  342. rucio/gateway/identity.py +204 -0
  343. rucio/gateway/importer.py +45 -0
  344. rucio/gateway/lifetime_exception.py +120 -0
  345. rucio/gateway/lock.py +153 -0
  346. rucio/gateway/meta_conventions.py +87 -0
  347. rucio/gateway/permission.py +71 -0
  348. rucio/gateway/quarantined_replica.py +78 -0
  349. rucio/gateway/replica.py +529 -0
  350. rucio/gateway/request.py +321 -0
  351. rucio/gateway/rse.py +600 -0
  352. rucio/gateway/rule.py +417 -0
  353. rucio/gateway/scope.py +99 -0
  354. rucio/gateway/subscription.py +277 -0
  355. rucio/gateway/vo.py +122 -0
  356. rucio/rse/__init__.py +96 -0
  357. rucio/rse/protocols/__init__.py +13 -0
  358. rucio/rse/protocols/bittorrent.py +184 -0
  359. rucio/rse/protocols/cache.py +122 -0
  360. rucio/rse/protocols/dummy.py +111 -0
  361. rucio/rse/protocols/gfal.py +703 -0
  362. rucio/rse/protocols/globus.py +243 -0
  363. rucio/rse/protocols/gsiftp.py +92 -0
  364. rucio/rse/protocols/http_cache.py +82 -0
  365. rucio/rse/protocols/mock.py +123 -0
  366. rucio/rse/protocols/ngarc.py +209 -0
  367. rucio/rse/protocols/posix.py +250 -0
  368. rucio/rse/protocols/protocol.py +594 -0
  369. rucio/rse/protocols/rclone.py +364 -0
  370. rucio/rse/protocols/rfio.py +136 -0
  371. rucio/rse/protocols/srm.py +338 -0
  372. rucio/rse/protocols/ssh.py +413 -0
  373. rucio/rse/protocols/storm.py +206 -0
  374. rucio/rse/protocols/webdav.py +550 -0
  375. rucio/rse/protocols/xrootd.py +301 -0
  376. rucio/rse/rsemanager.py +764 -0
  377. rucio/tests/__init__.py +13 -0
  378. rucio/tests/common.py +270 -0
  379. rucio/tests/common_server.py +132 -0
  380. rucio/transfertool/__init__.py +13 -0
  381. rucio/transfertool/bittorrent.py +199 -0
  382. rucio/transfertool/bittorrent_driver.py +52 -0
  383. rucio/transfertool/bittorrent_driver_qbittorrent.py +133 -0
  384. rucio/transfertool/fts3.py +1596 -0
  385. rucio/transfertool/fts3_plugins.py +152 -0
  386. rucio/transfertool/globus.py +201 -0
  387. rucio/transfertool/globus_library.py +181 -0
  388. rucio/transfertool/mock.py +90 -0
  389. rucio/transfertool/transfertool.py +221 -0
  390. rucio/vcsversion.py +11 -0
  391. rucio/version.py +38 -0
  392. rucio/web/__init__.py +13 -0
  393. rucio/web/rest/__init__.py +13 -0
  394. rucio/web/rest/flaskapi/__init__.py +13 -0
  395. rucio/web/rest/flaskapi/authenticated_bp.py +27 -0
  396. rucio/web/rest/flaskapi/v1/__init__.py +13 -0
  397. rucio/web/rest/flaskapi/v1/accountlimits.py +236 -0
  398. rucio/web/rest/flaskapi/v1/accounts.py +1089 -0
  399. rucio/web/rest/flaskapi/v1/archives.py +102 -0
  400. rucio/web/rest/flaskapi/v1/auth.py +1644 -0
  401. rucio/web/rest/flaskapi/v1/common.py +426 -0
  402. rucio/web/rest/flaskapi/v1/config.py +304 -0
  403. rucio/web/rest/flaskapi/v1/credentials.py +212 -0
  404. rucio/web/rest/flaskapi/v1/dids.py +2334 -0
  405. rucio/web/rest/flaskapi/v1/dirac.py +116 -0
  406. rucio/web/rest/flaskapi/v1/export.py +75 -0
  407. rucio/web/rest/flaskapi/v1/heartbeats.py +127 -0
  408. rucio/web/rest/flaskapi/v1/identities.py +261 -0
  409. rucio/web/rest/flaskapi/v1/import.py +132 -0
  410. rucio/web/rest/flaskapi/v1/lifetime_exceptions.py +312 -0
  411. rucio/web/rest/flaskapi/v1/locks.py +358 -0
  412. rucio/web/rest/flaskapi/v1/main.py +91 -0
  413. rucio/web/rest/flaskapi/v1/meta_conventions.py +241 -0
  414. rucio/web/rest/flaskapi/v1/metrics.py +36 -0
  415. rucio/web/rest/flaskapi/v1/nongrid_traces.py +97 -0
  416. rucio/web/rest/flaskapi/v1/ping.py +88 -0
  417. rucio/web/rest/flaskapi/v1/redirect.py +365 -0
  418. rucio/web/rest/flaskapi/v1/replicas.py +1890 -0
  419. rucio/web/rest/flaskapi/v1/requests.py +998 -0
  420. rucio/web/rest/flaskapi/v1/rses.py +2239 -0
  421. rucio/web/rest/flaskapi/v1/rules.py +854 -0
  422. rucio/web/rest/flaskapi/v1/scopes.py +159 -0
  423. rucio/web/rest/flaskapi/v1/subscriptions.py +650 -0
  424. rucio/web/rest/flaskapi/v1/templates/auth_crash.html +80 -0
  425. rucio/web/rest/flaskapi/v1/templates/auth_granted.html +82 -0
  426. rucio/web/rest/flaskapi/v1/traces.py +100 -0
  427. rucio/web/rest/flaskapi/v1/types.py +20 -0
  428. rucio/web/rest/flaskapi/v1/vos.py +278 -0
  429. rucio/web/rest/main.py +18 -0
  430. rucio/web/rest/metrics.py +27 -0
  431. rucio/web/rest/ping.py +27 -0
  432. rucio-35.7.0.data/data/rucio/etc/alembic.ini.template +71 -0
  433. rucio-35.7.0.data/data/rucio/etc/alembic_offline.ini.template +74 -0
  434. rucio-35.7.0.data/data/rucio/etc/globus-config.yml.template +5 -0
  435. rucio-35.7.0.data/data/rucio/etc/ldap.cfg.template +30 -0
  436. rucio-35.7.0.data/data/rucio/etc/mail_templates/rule_approval_request.tmpl +38 -0
  437. rucio-35.7.0.data/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +4 -0
  438. rucio-35.7.0.data/data/rucio/etc/mail_templates/rule_approved_user.tmpl +17 -0
  439. rucio-35.7.0.data/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +6 -0
  440. rucio-35.7.0.data/data/rucio/etc/mail_templates/rule_denied_user.tmpl +17 -0
  441. rucio-35.7.0.data/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +19 -0
  442. rucio-35.7.0.data/data/rucio/etc/rse-accounts.cfg.template +25 -0
  443. rucio-35.7.0.data/data/rucio/etc/rucio.cfg.atlas.client.template +42 -0
  444. rucio-35.7.0.data/data/rucio/etc/rucio.cfg.template +257 -0
  445. rucio-35.7.0.data/data/rucio/etc/rucio_multi_vo.cfg.template +234 -0
  446. rucio-35.7.0.data/data/rucio/requirements.server.txt +268 -0
  447. rucio-35.7.0.data/data/rucio/tools/bootstrap.py +34 -0
  448. rucio-35.7.0.data/data/rucio/tools/merge_rucio_configs.py +144 -0
  449. rucio-35.7.0.data/data/rucio/tools/reset_database.py +40 -0
  450. rucio-35.7.0.data/scripts/rucio +2542 -0
  451. rucio-35.7.0.data/scripts/rucio-abacus-account +74 -0
  452. rucio-35.7.0.data/scripts/rucio-abacus-collection-replica +46 -0
  453. rucio-35.7.0.data/scripts/rucio-abacus-rse +78 -0
  454. rucio-35.7.0.data/scripts/rucio-admin +2447 -0
  455. rucio-35.7.0.data/scripts/rucio-atropos +60 -0
  456. rucio-35.7.0.data/scripts/rucio-auditor +205 -0
  457. rucio-35.7.0.data/scripts/rucio-automatix +50 -0
  458. rucio-35.7.0.data/scripts/rucio-bb8 +57 -0
  459. rucio-35.7.0.data/scripts/rucio-c3po +85 -0
  460. rucio-35.7.0.data/scripts/rucio-cache-client +134 -0
  461. rucio-35.7.0.data/scripts/rucio-cache-consumer +42 -0
  462. rucio-35.7.0.data/scripts/rucio-conveyor-finisher +58 -0
  463. rucio-35.7.0.data/scripts/rucio-conveyor-poller +66 -0
  464. rucio-35.7.0.data/scripts/rucio-conveyor-preparer +37 -0
  465. rucio-35.7.0.data/scripts/rucio-conveyor-receiver +43 -0
  466. rucio-35.7.0.data/scripts/rucio-conveyor-stager +76 -0
  467. rucio-35.7.0.data/scripts/rucio-conveyor-submitter +139 -0
  468. rucio-35.7.0.data/scripts/rucio-conveyor-throttler +104 -0
  469. rucio-35.7.0.data/scripts/rucio-dark-reaper +53 -0
  470. rucio-35.7.0.data/scripts/rucio-dumper +160 -0
  471. rucio-35.7.0.data/scripts/rucio-follower +44 -0
  472. rucio-35.7.0.data/scripts/rucio-hermes +54 -0
  473. rucio-35.7.0.data/scripts/rucio-judge-cleaner +89 -0
  474. rucio-35.7.0.data/scripts/rucio-judge-evaluator +137 -0
  475. rucio-35.7.0.data/scripts/rucio-judge-injector +44 -0
  476. rucio-35.7.0.data/scripts/rucio-judge-repairer +44 -0
  477. rucio-35.7.0.data/scripts/rucio-kronos +43 -0
  478. rucio-35.7.0.data/scripts/rucio-minos +53 -0
  479. rucio-35.7.0.data/scripts/rucio-minos-temporary-expiration +50 -0
  480. rucio-35.7.0.data/scripts/rucio-necromancer +120 -0
  481. rucio-35.7.0.data/scripts/rucio-oauth-manager +63 -0
  482. rucio-35.7.0.data/scripts/rucio-reaper +83 -0
  483. rucio-35.7.0.data/scripts/rucio-replica-recoverer +248 -0
  484. rucio-35.7.0.data/scripts/rucio-rse-decommissioner +66 -0
  485. rucio-35.7.0.data/scripts/rucio-storage-consistency-actions +74 -0
  486. rucio-35.7.0.data/scripts/rucio-transmogrifier +77 -0
  487. rucio-35.7.0.data/scripts/rucio-undertaker +76 -0
  488. rucio-35.7.0.dist-info/METADATA +72 -0
  489. rucio-35.7.0.dist-info/RECORD +493 -0
  490. rucio-35.7.0.dist-info/WHEEL +5 -0
  491. rucio-35.7.0.dist-info/licenses/AUTHORS.rst +97 -0
  492. rucio-35.7.0.dist-info/licenses/LICENSE +201 -0
  493. rucio-35.7.0.dist-info/top_level.txt +1 -0
rucio/core/oidc.py ADDED
@@ -0,0 +1,1461 @@
1
+ # Copyright European Organization for Nuclear Research (CERN) since 2012
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import hashlib
16
+ import json
17
+ import logging
18
+ import subprocess
19
+ import traceback
20
+ from datetime import datetime, timedelta
21
+ from math import floor
22
+ from secrets import choice
23
+ from typing import TYPE_CHECKING, Any, Final, Optional, Union
24
+ from urllib.parse import parse_qs, urljoin, urlparse
25
+
26
+ import requests
27
+ from dogpile.cache.api import NoValue
28
+ from jwkest.jws import JWS
29
+ from jwkest.jwt import JWT
30
+ from oic import rndstr
31
+ from oic.oauth2.message import CCAccessTokenRequest
32
+ from oic.oic import REQUEST2ENDPOINT, Client, Grant, Token
33
+ from oic.oic.message import AccessTokenResponse, AuthorizationResponse, Message, RegistrationResponse
34
+ from oic.utils import time_util
35
+ from oic.utils.authn.client import CLIENT_AUTHN_METHOD
36
+ from sqlalchemy import delete, select, update
37
+ from sqlalchemy.sql.expression import true
38
+
39
+ from rucio.common import types
40
+ from rucio.common.cache import make_region_memcached
41
+ from rucio.common.config import config_get, config_get_int
42
+ from rucio.common.exception import CannotAuthenticate, CannotAuthorize, RucioException
43
+ from rucio.common.stopwatch import Stopwatch
44
+ from rucio.common.utils import all_oidc_req_claims_present, build_url, val_to_space_sep_str
45
+ from rucio.core.account import account_exists
46
+ from rucio.core.identity import exist_identity_account, get_default_account
47
+ from rucio.core.monitor import MetricManager
48
+ from rucio.db.sqla import filter_thread_work, models
49
+ from rucio.db.sqla.constants import IdentityType
50
+ from rucio.db.sqla.session import read_session, transactional_session
51
+
52
+ if TYPE_CHECKING:
53
+ from sqlalchemy.orm import Session
54
+
55
+ # The WLCG Common JWT Profile dictates that the lifetime of access and ID tokens
56
+ # should range from five minutes to six hours.
57
+ TOKEN_MIN_LIFETIME: Final = config_get_int('oidc', 'token_min_lifetime', default=300)
58
+ TOKEN_MAX_LIFETIME: Final = config_get_int('oidc', 'token_max_lifetime', default=21600)
59
+
60
+ REGION: Final = make_region_memcached(expiration_time=TOKEN_MAX_LIFETIME)
61
+ METRICS = MetricManager(module=__name__)
62
+
63
+ # worokaround for a bug in pyoidc (as of Dec 2019)
64
+ REQUEST2ENDPOINT['CCAccessTokenRequest'] = 'token_endpoint'
65
+
66
+ # private/protected file containing Rucio Client secrets known to the Identity Provider as well
67
+ IDPSECRETS = config_get('oidc', 'idpsecrets', False)
68
+ ADMIN_ISSUER_ID = config_get('oidc', 'admin_issuer', False)
69
+ EXPECTED_OIDC_AUDIENCE = config_get('oidc', 'expected_audience', False, 'rucio')
70
+ EXPECTED_OIDC_SCOPE = config_get('oidc', 'expected_scope', False, 'openid profile')
71
+ EXCHANGE_GRANT_TYPE = config_get('oidc', 'exchange_grant_type', False, 'urn:ietf:params:oauth:grant-type:token-exchange')
72
+ REFRESH_LIFETIME_H = config_get_int('oidc', 'default_jwt_refresh_lifetime', False, 96)
73
+
74
+ # Allow 2 mins of leeway in case Rucio and IdP server clocks are not perfectly synchronized
75
+ # this affects the token issued time (a token could be issued in the future if IdP clock is ahead)
76
+ LEEWAY_SECS = 120
77
+
78
+
79
+ # TO-DO permission layer: if scope == 'wlcg.groups'
80
+ # --> check 'profile' info (requested profile scope)
81
+
82
+
83
+ @METRICS.time_it
84
+ def _token_cache_get(
85
+ key: str,
86
+ min_lifetime: int = TOKEN_MIN_LIFETIME,
87
+ ) -> Optional[str]:
88
+ """Retrieve a token from the cache.
89
+
90
+ Return ``None`` if the cache backend did not return a value, the value is
91
+ not a valid JWT, or the token has a remaining lifetime less than
92
+ ``min_lifetime`` seconds.
93
+ """
94
+ value = REGION.get(key)
95
+ if isinstance(value, NoValue):
96
+ METRICS.counter('token_cache.miss').inc()
97
+ return None
98
+
99
+ if isinstance(value, str):
100
+ try:
101
+ payload = JWT().unpack(value).payload()
102
+ except Exception:
103
+ METRICS.counter('token_cache.invalid').inc()
104
+ return None
105
+ else:
106
+ METRICS.counter('token_cache.invalid').inc()
107
+ return None
108
+
109
+ now = datetime.utcnow().timestamp()
110
+ expiration = payload.get('exp', 0) # type: ignore
111
+ if now + min_lifetime > expiration:
112
+ METRICS.counter('token_cache.expired').inc()
113
+ return None
114
+
115
+ METRICS.counter('token_cache.hit').inc()
116
+ return value
117
+
118
+
119
+ def _token_cache_set(key: str, value: str) -> None:
120
+ """Store a token in the cache."""
121
+ REGION.set(key, value)
122
+
123
+
124
+ def request_token(audience: str, scope: str, use_cache: bool = True) -> Optional[str]:
125
+ """Request a token from the provider.
126
+
127
+ Return ``None`` if the configuration was not loaded properly or the request
128
+ was unsuccessful.
129
+ """
130
+ if not all([OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_PROVIDER_ENDPOINT]):
131
+ if OIDC_CONFIGURATION_RUN or not __load_oidc_configuration():
132
+ return None
133
+
134
+ key = hashlib.md5(f'audience={audience};scope={scope}'.encode()).hexdigest()
135
+
136
+ if use_cache and (token := _token_cache_get(key)):
137
+ return token
138
+
139
+ try:
140
+ response = requests.post(url=OIDC_PROVIDER_ENDPOINT,
141
+ auth=(OIDC_CLIENT_ID, OIDC_CLIENT_SECRET),
142
+ data={'grant_type': 'client_credentials',
143
+ 'audience': audience,
144
+ 'scope': scope})
145
+ response.raise_for_status()
146
+ payload = response.json()
147
+ token = payload['access_token']
148
+ except Exception:
149
+ logging.debug('Failed to procure a token', exc_info=True)
150
+ return None
151
+
152
+ if use_cache:
153
+ _token_cache_set(key, token)
154
+
155
+ return token
156
+
157
+
158
+ def __get_rucio_oidc_clients(keytimeout: int = 43200) -> tuple[dict, dict]:
159
+ """
160
+ Creates a Rucio OIDC Client instances per Identity Provider (IdP)
161
+ according to etc/idpsecrets.json configuration file.
162
+ Clients have to be pre-registered with the respective IdP with the appropriate settings:
163
+ allowed to request refresh tokens which have lifetime set in their unverified header,
164
+ allowed to request token exchange, immediate refresh tokens expiration after first use)
165
+
166
+ :returns: Dictionary of {'https://issuer_1/': <Rucio OIDC Client_1 instance>,
167
+ 'https://issuer_2/': <Rucio OIDC Client_2 instance>,}.
168
+ In case of trouble, Exception is raised.
169
+ """
170
+ clients = {}
171
+ admin_clients = {}
172
+ try:
173
+ with open(IDPSECRETS) as client_secret_file:
174
+ client_secrets = json.load(client_secret_file)
175
+ except:
176
+ return (clients, admin_clients)
177
+ for iss in client_secrets:
178
+ try:
179
+ client_secret = client_secrets[iss]
180
+ issuer = client_secret["issuer"]
181
+ client = Client(client_authn_method=CLIENT_AUTHN_METHOD)
182
+ # general parameter discovery about the Identity Provider via issuers URL
183
+ client.provider_config(issuer)
184
+ # storing client specific parameters into the client itself
185
+ client_reg = RegistrationResponse(**client_secret)
186
+ client.store_registration_info(client_reg)
187
+ # setting public_key cache timeout to 'keytimeout' seconds
188
+ keybundles = client.keyjar.issuer_keys[client.issuer]
189
+ for keybundle in keybundles:
190
+ keybundle.cache_time = keytimeout
191
+ clients[issuer] = client
192
+ # doing the same to store a Rucio Admin client
193
+ # which has client credential flow allowed
194
+ client_secret = client_secrets[iss]["SCIM"]
195
+ client = Client(client_authn_method=CLIENT_AUTHN_METHOD)
196
+ client.provider_config(issuer)
197
+ client_reg = RegistrationResponse(**client_secret)
198
+ client.store_registration_info(client_reg)
199
+ admin_clients[issuer] = client
200
+ except Exception as error:
201
+ raise RucioException(error.args) from error
202
+ return (clients, admin_clients)
203
+
204
+
205
+ # global variables to represent the IdP clients
206
+ OIDC_CLIENTS = {}
207
+ OIDC_ADMIN_CLIENTS = {}
208
+ # New-style token support.
209
+ OIDC_CLIENT_ID = ''
210
+ OIDC_CLIENT_SECRET = ''
211
+ OIDC_PROVIDER_ENDPOINT = ''
212
+ OIDC_CONFIGURATION_RUN = False
213
+
214
+
215
+ def __initialize_oidc_clients() -> None:
216
+ """
217
+ Initialising Rucio OIDC Clients
218
+ """
219
+
220
+ try:
221
+ ALL_OIDC_CLIENTS = __get_rucio_oidc_clients()
222
+ global OIDC_CLIENTS
223
+ global OIDC_ADMIN_CLIENTS
224
+ OIDC_CLIENTS = ALL_OIDC_CLIENTS[0]
225
+ OIDC_ADMIN_CLIENTS = ALL_OIDC_CLIENTS[1]
226
+ except Exception as error:
227
+ logging.debug("OIDC clients not properly loaded: %s", error)
228
+ pass
229
+
230
+
231
+ def __load_oidc_configuration() -> bool:
232
+ """Load the configuration for the new-style token support."""
233
+ global OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_PROVIDER_ENDPOINT, OIDC_CONFIGURATION_RUN
234
+
235
+ OIDC_CONFIGURATION_RUN = True
236
+
237
+ if not IDPSECRETS:
238
+ logging.error('Configuration option "idpsecrets" in section "oidc" is not set')
239
+ return False
240
+ if not ADMIN_ISSUER_ID:
241
+ logging.error('Configuration option "admin_issuer" in section "oidc" is not set')
242
+ return False
243
+
244
+ try:
245
+ with open(IDPSECRETS) as f:
246
+ data = json.load(f)
247
+ OIDC_CLIENT_ID = data[ADMIN_ISSUER_ID]['client_id']
248
+ OIDC_CLIENT_SECRET = data[ADMIN_ISSUER_ID]['client_secret']
249
+ issuer = data[ADMIN_ISSUER_ID]['issuer']
250
+ except Exception:
251
+ logging.error('Failed to parse configuration file "%s"', IDPSECRETS,
252
+ exc_info=True)
253
+ return False
254
+ try:
255
+ oidc_discover_url = urljoin(issuer, '.well-known/openid-configuration')
256
+ response = requests.get(oidc_discover_url)
257
+ response.raise_for_status()
258
+ payload = response.json()
259
+ OIDC_PROVIDER_ENDPOINT = payload['token_endpoint']
260
+ except (requests.HTTPError, requests.JSONDecodeError, KeyError):
261
+ logging.error('Failed to discover token endpoint', exc_info=True)
262
+ return False
263
+
264
+ return True
265
+
266
+
267
+ def __get_init_oidc_client(token_object: models.Token = None, token_type: str = None, **kwargs) -> dict[Any, Any]:
268
+ """
269
+ Get an OIDC client object, (re-)initialised with parameters corresponding
270
+ to authorization flows used to get a token. For special cases - token refresh,
271
+ token exchange - these parameters are being mocked as pyoidc library
272
+ has to develop these areas. Initialisation can be made either by kwargs
273
+ (for a authorization code flow e.g.) or via kwargs (for token exchange or token refresh).
274
+
275
+ :param session_state: state value of the first authorization request
276
+ :param token_object: DB token token to be included in a Grant for
277
+ the token exchange or token refresh mechanisms
278
+ :param token_type: e.g. "subject_token" for token exchange or "refresh_token"
279
+ :param kwargs: optional strings which contain expected oauth session parameters:
280
+ issuer_id/issuer, redirect_uri, redirect_to, state, nonce, code,
281
+ scope, audience,
282
+
283
+ :returns: if first_init == True: dict {'client': oidc client object, 'request': auth_url}
284
+ for all other cases return oidc client object. If anything goes wrong, exception is thrown.
285
+ """
286
+
287
+ if not OIDC_CLIENTS:
288
+ # retry once loading OIDC clients
289
+ __initialize_oidc_clients()
290
+ if not OIDC_CLIENTS:
291
+ raise CannotAuthenticate(traceback.format_exc())
292
+
293
+ try:
294
+
295
+ auth_args = {"grant_types": ["authorization_code"],
296
+ "response_type": "code",
297
+ "state": kwargs.get('state', rndstr()),
298
+ "nonce": kwargs.get('nonce', rndstr())}
299
+ auth_args["scope"] = token_object.oidc_scope if token_object else kwargs.get('scope', " ")
300
+ auth_args["audience"] = token_object.audience if token_object else kwargs.get('audience', " ")
301
+
302
+ if token_object:
303
+ issuer = token_object.identity.split(", ")[1].split("=")[1]
304
+ oidc_client = OIDC_CLIENTS[issuer]
305
+ auth_args["client_id"] = oidc_client.client_id
306
+ token = ''
307
+ if not token_type:
308
+ token_type = kwargs.get('token_type', None)
309
+ if token_type == 'subject_token': # noqa: S105
310
+ token = token_object.token
311
+ # do not remove - even though None, oic expects this key to exist
312
+ auth_args["redirect_uri"] = None
313
+ if token_type == 'refresh_token': # noqa: S105
314
+ token = token_object.refresh_token
315
+ # do not remove - even though None, oic expects this key to exist
316
+ auth_args["redirect_uri"] = None
317
+ if token_type and token:
318
+ oidc_client.grant[auth_args['state']] = Grant()
319
+ oidc_client.grant[auth_args['state']].grant_expiration_time = time_util.utc_time_sans_frac() + 300
320
+ resp = AccessTokenResponse()
321
+ resp[token_type] = token
322
+ oidc_client.grant[auth_args['state']].tokens.append(Token(resp))
323
+ else:
324
+ secrets, client_secret = {}, {}
325
+ try:
326
+ with open(IDPSECRETS) as client_secret_file:
327
+ secrets = json.load(client_secret_file)
328
+ except Exception as error:
329
+ raise CannotAuthenticate("Rucio server is missing information from the idpsecrets.json file.") from error
330
+ if 'issuer_id' in kwargs:
331
+ client_secret = secrets[kwargs.get('issuer_id', ADMIN_ISSUER_ID)]
332
+ elif 'issuer' in kwargs:
333
+ client_secret = next((secrets[i] for i in secrets if 'issuer' in secrets[i] and # NOQA: W504
334
+ kwargs.get('issuer') in secrets[i]['issuer']), None)
335
+ redirect_url = kwargs.get('redirect_uri', None)
336
+ if not redirect_url:
337
+ redirect_to = kwargs.get("redirect_to", "auth/oidc_token")
338
+ redirect_urls = [u for u in client_secret["redirect_uris"] if redirect_to in u]
339
+ redirect_url = choice(redirect_urls)
340
+ if not redirect_url:
341
+ raise CannotAuthenticate("Could not pick any redirect URL(s) from the ones defined "
342
+ + "in Rucio OIDC Client configuration file.") # NOQA: W503
343
+ auth_args["redirect_uri"] = redirect_url
344
+ oidc_client = OIDC_CLIENTS[client_secret["issuer"]]
345
+ auth_args["client_id"] = oidc_client.client_id
346
+
347
+ if kwargs.get('first_init', False):
348
+ auth_url = build_url(oidc_client.authorization_endpoint, params=auth_args)
349
+ return {'redirect': redirect_url, 'auth_url': auth_url}
350
+
351
+ oidc_client.construct_AuthorizationRequest(request_args=auth_args)
352
+ # parsing the authorization query string by the Rucio OIDC Client (creates a Grant)
353
+ oidc_client.parse_response(AuthorizationResponse,
354
+ info='code=' + kwargs.get('code', rndstr()) + '&state=' + auth_args['state'],
355
+ sformat="urlencoded")
356
+ return {'client': oidc_client, 'state': auth_args['state']}
357
+ except Exception as error:
358
+ raise CannotAuthenticate(traceback.format_exc()) from error
359
+
360
+
361
+ @transactional_session
362
+ def get_auth_oidc(account: str, *, session: "Session", **kwargs) -> str:
363
+ """
364
+ Assembles the authorization request of the Rucio Client tailored to the Rucio user
365
+ & Identity Provider. Saves authentication session parameters in the oauth_requests
366
+ DB table (for later use-cases). This information is saved for the token lifetime
367
+ of a token to allow token exchange and refresh.
368
+ Returns authorization URL as a string or a redirection url to
369
+ be used in user's browser for authentication.
370
+
371
+ :param account: Rucio Account identifier as a string.
372
+ :param auth_scope: space separated list of scope names. Scope parameter
373
+ defines which user's info the user allows to provide
374
+ to the Rucio Client.
375
+ :param audience: audience for which tokens are requested (EXPECTED_OIDC_AUDIENCE is the default)
376
+ :param auto: If True, the function will return authorization URL to the Rucio Client
377
+ which will log-in with user's IdP credentials automatically.
378
+ Also it will instruct the IdP to return an AuthZ code to another Rucio REST
379
+ endpoint /oidc_token. If False, the function will return a URL
380
+ to be used by the user in the browser in order to authenticate via IdP
381
+ (which will then return with AuthZ code to /oidc_code REST endpoint).
382
+ :param polling: If True, '_polling' string will be appended to the access_msg
383
+ in the DB oauth_requests table to inform the authorization stage
384
+ that the Rucio Client is polling the server for a token
385
+ (and no fetchcode needs to be returned at the end).
386
+ :param refresh_lifetime: specifies how long the OAuth daemon should
387
+ be refreshing this token. Default is 96 hours.
388
+ :param ip: IP address of the client as a string.
389
+ :param session: The database session in use.
390
+
391
+ :returns: User & Rucio OIDC Client specific Authorization or Redirection URL as a string
392
+ OR a redirection url to be used in user's browser for authentication.
393
+ """
394
+ # TO-DO - implement a check if that account already has a valid
395
+ # token with the required scope and audience and return such token !
396
+ auth_scope = kwargs.get('auth_scope', EXPECTED_OIDC_SCOPE)
397
+ if not auth_scope:
398
+ auth_scope = EXPECTED_OIDC_SCOPE
399
+ audience = kwargs.get('audience', EXPECTED_OIDC_AUDIENCE)
400
+ if not audience:
401
+ audience = EXPECTED_OIDC_AUDIENCE
402
+ # checking that minimal audience and scope requirements (required by Rucio) are satisfied !
403
+ if not all_oidc_req_claims_present(auth_scope, audience, EXPECTED_OIDC_SCOPE, EXPECTED_OIDC_AUDIENCE):
404
+ raise CannotAuthenticate("Requirements of scope and audience do not satisfy minimal requirements of the Rucio server.")
405
+ issuer_id = kwargs.get('issuer', ADMIN_ISSUER_ID)
406
+ if not issuer_id:
407
+ issuer_id = ADMIN_ISSUER_ID
408
+ auto = kwargs.get('auto', False)
409
+ polling = kwargs.get('polling', False)
410
+ refresh_lifetime = kwargs.get('refresh_lifetime', REFRESH_LIFETIME_H)
411
+ ip = kwargs.get('ip', None)
412
+ webhome = kwargs.get('webhome', None)
413
+ # For webui a mock account will be used here and default account
414
+ # will be assigned to the identity during get_token_oidc
415
+ if account.external == 'webui':
416
+ pass
417
+ else:
418
+ # Make sure the account exists
419
+ if not account_exists(account, session=session):
420
+ logging.debug("Account %s does not exist.", account)
421
+ return None
422
+
423
+ try:
424
+ stopwatch = Stopwatch()
425
+ # redirect_url needs to be specified & one of those defined
426
+ # in the Rucio OIDC Client configuration
427
+ redirect_to = "auth/oidc_code"
428
+ if auto:
429
+ redirect_to = "auth/oidc_token"
430
+ # random strings in order to keep track of responses to outstanding requests (state)
431
+ # and to associate a client session with an ID Token and to mitigate replay attacks (nonce).
432
+ state, nonce = rndstr(50), rndstr(50)
433
+ # in the following statement we retrieve the authorization endpoint
434
+ # from the client of the issuer and build url
435
+ oidc_dict = __get_init_oidc_client(issuer_id=issuer_id, redirect_to=redirect_to,
436
+ state=state, nonce=nonce,
437
+ scope=auth_scope, audience=audience, first_init=True)
438
+ auth_url = oidc_dict['auth_url']
439
+ redirect_url = oidc_dict['redirect']
440
+ # redirect code is put in access_msg and returned to the user (if auto=False)
441
+ access_msg = None
442
+ if not auto:
443
+ access_msg = rndstr(23)
444
+ if polling:
445
+ access_msg += '_polling'
446
+ if auto and webhome:
447
+ access_msg = str(webhome)
448
+ # Making sure refresh_lifetime is an integer or None.
449
+ if refresh_lifetime:
450
+ refresh_lifetime = int(refresh_lifetime)
451
+ # Specifying temporarily 5 min lifetime for the authentication session.
452
+ expired_at = datetime.utcnow() + timedelta(seconds=300)
453
+ # saving session parameters into the Rucio DB
454
+ oauth_session_params = models.OAuthRequest(account=account,
455
+ state=state,
456
+ nonce=nonce,
457
+ access_msg=access_msg,
458
+ redirect_msg=auth_url,
459
+ expired_at=expired_at,
460
+ refresh_lifetime=refresh_lifetime,
461
+ ip=ip)
462
+ oauth_session_params.save(session=session)
463
+ # If user selected authentication via web browser, a redirection
464
+ # URL is returned instead of the direct URL pointing to the IdP.
465
+ if not auto:
466
+ # the following takes into account deployments where the base url of the rucio server is
467
+ # not equivalent to the network location, e.g. if the server is proxied
468
+ auth_server = urlparse(redirect_url)
469
+ auth_url = build_url('https://' + auth_server.netloc, path='{}auth/oidc_redirect'.format(
470
+ auth_server.path.split('auth/')[0].lstrip('/')), params=access_msg)
471
+
472
+ METRICS.timer('IdP_authentication.request').observe(stopwatch.elapsed)
473
+ return auth_url
474
+
475
+ except Exception as error:
476
+ raise CannotAuthenticate(traceback.format_exc()) from error
477
+
478
+
479
+ @transactional_session
480
+ def get_token_oidc(
481
+ auth_query_string: str,
482
+ ip: Optional[str] = None,
483
+ *,
484
+ session: "Session"
485
+ ) -> Optional[dict[str, Optional[Union[str, bool]]]]:
486
+ """
487
+ After Rucio User got redirected to Rucio /auth/oidc_token (or /auth/oidc_code)
488
+ REST endpoints with authz code and session state encoded within the URL.
489
+ These parameters are used to eventually gets user's info and tokens from IdP.
490
+
491
+ :param auth_query_string: IdP redirection URL query string (AuthZ code & user session state).
492
+ :param ip: IP address of the client as a string.
493
+ :param session: The database session in use.
494
+
495
+ :returns: One of the following tuples: ("fetchcode", <code>); ("token", <token>);
496
+ ("polling", True); The result depends on the authentication strategy being used
497
+ (no auto, auto, polling).
498
+ """
499
+ try:
500
+ stopwatch = Stopwatch()
501
+ parsed_authquery = parse_qs(auth_query_string)
502
+ state = parsed_authquery["state"][0]
503
+ code = parsed_authquery["code"][0]
504
+ # getting oauth request params from the oauth_requests DB Table
505
+ query = select(
506
+ models.OAuthRequest
507
+ ).where(
508
+ models.OAuthRequest.state == state
509
+ )
510
+ oauth_req_params = session.execute(query).scalar()
511
+ if oauth_req_params is None:
512
+ raise CannotAuthenticate("User related Rucio OIDC session could not keep "
513
+ + "track of responses from outstanding requests.") # NOQA: W503
514
+ req_url = urlparse(oauth_req_params.redirect_msg or '')
515
+ issuer = req_url.scheme + "://" + req_url.netloc
516
+ req_params = parse_qs(req_url.query)
517
+ client_params = {}
518
+ for key in list(req_params):
519
+ client_params[key] = val_to_space_sep_str(req_params[key])
520
+
521
+ oidc_client = __get_init_oidc_client(issuer=issuer, code=code, **client_params)['client']
522
+ METRICS.counter(name='IdP_authentication.code_granted').inc()
523
+ # exchange access code for a access token
524
+ oidc_tokens = oidc_client.do_access_token_request(state=state,
525
+ request_args={"code": code},
526
+ authn_method="client_secret_basic",
527
+ skew=LEEWAY_SECS)
528
+ if 'error' in oidc_tokens:
529
+ raise CannotAuthorize(oidc_tokens['error'])
530
+ # mitigate replay attacks
531
+ nonce = oauth_req_params.nonce
532
+ if oidc_tokens['id_token']['nonce'] != nonce:
533
+ raise CannotAuthenticate("ID token could not be associated with the Rucio OIDC Client"
534
+ + " session. This points to possible replay attack !") # NOQA: W503
535
+
536
+ # starting to fill dictionary with parameters for token DB row
537
+ jwt_row_dict, extra_dict = {}, {}
538
+ jwt_row_dict['identity'] = oidc_identity_string(oidc_tokens['id_token']['sub'],
539
+ oidc_tokens['id_token']['iss'])
540
+ jwt_row_dict['account'] = oauth_req_params.account
541
+
542
+ if jwt_row_dict['account'].external == 'webui':
543
+ try:
544
+ jwt_row_dict['account'] = get_default_account(jwt_row_dict['identity'], IdentityType.OIDC, True, session=session)
545
+ except Exception:
546
+ return {'webhome': None, 'token': None}
547
+
548
+ # check if given account has the identity registered
549
+ if not exist_identity_account(jwt_row_dict['identity'], IdentityType.OIDC, jwt_row_dict['account'], session=session):
550
+ raise CannotAuthenticate("OIDC identity '%s' of the '%s' account is unknown to Rucio."
551
+ % (jwt_row_dict['identity'], str(jwt_row_dict['account'])))
552
+ METRICS.counter(name='success').inc()
553
+ # get access token expiry timestamp
554
+ jwt_row_dict['lifetime'] = datetime.utcnow() + timedelta(seconds=oidc_tokens['expires_in'])
555
+ # get audience and scope info from the token
556
+ if 'scope' in oidc_tokens and 'audience' in oidc_tokens:
557
+ jwt_row_dict['authz_scope'] = val_to_space_sep_str(oidc_tokens['scope'])
558
+ jwt_row_dict['audience'] = val_to_space_sep_str(oidc_tokens['audience'])
559
+ elif 'access_token' in oidc_tokens:
560
+ try:
561
+ values = __get_keyvalues_from_claims(oidc_tokens['access_token'], ['scope', 'aud'])
562
+ jwt_row_dict['authz_scope'] = values['scope']
563
+ jwt_row_dict['audience'] = values['aud']
564
+ except Exception:
565
+ # we assume the Identity Provider did not do the right job here
566
+ jwt_row_dict['authz_scope'] = None
567
+ jwt_row_dict['audience'] = None
568
+ # groups = oidc_tokens['id_token']['groups']
569
+ # nothing done with group info for the moment - TO-DO !
570
+ # collect extra token DB row parameters
571
+ extra_dict = {}
572
+ extra_dict['ip'] = ip
573
+ extra_dict['state'] = state
574
+ # In case user requested to grant Rucio a refresh token,
575
+ # this token will get saved in the DB and an automatic refresh
576
+ # for a specified period of time will be initiated (done by the Rucio daemon).
577
+ if 'refresh_token' in oidc_tokens:
578
+ extra_dict['refresh_token'] = oidc_tokens['refresh_token']
579
+ extra_dict['refresh'] = True
580
+ extra_dict['refresh_lifetime'] = REFRESH_LIFETIME_H
581
+ try:
582
+ if oauth_req_params.refresh_lifetime is not None:
583
+ extra_dict['refresh_lifetime'] = int(oauth_req_params.refresh_lifetime)
584
+ except Exception:
585
+ pass
586
+ try:
587
+ values = __get_keyvalues_from_claims(oidc_tokens['refresh_token'], ['exp'])
588
+ exp = values['exp']
589
+ extra_dict['refresh_expired_at'] = datetime.utcfromtimestamp(float(exp))
590
+ except Exception:
591
+ # 4 day expiry period by default
592
+ extra_dict['refresh_expired_at'] = datetime.utcnow() + timedelta(hours=REFRESH_LIFETIME_H)
593
+
594
+ new_token = __save_validated_token(oidc_tokens['access_token'], jwt_row_dict, extra_dict=extra_dict, session=session)
595
+ METRICS.counter(name='IdP_authorization.access_token.saved').inc()
596
+ if 'refresh_token' in oidc_tokens:
597
+ METRICS.counter(name='IdP_authorization.refresh_token.saved').inc()
598
+ # In case authentication via browser was requested,
599
+ # we save the token in the oauth_requests table
600
+ if oauth_req_params.access_msg:
601
+ # If Rucio Client waits for a fetchcode, we save the token under this code in the DB.
602
+ if 'http' not in oauth_req_params.access_msg:
603
+ if '_polling' not in oauth_req_params.access_msg:
604
+ fetchcode = rndstr(50)
605
+ query = update(
606
+ models.OAuthRequest
607
+ ).where(
608
+ models.OAuthRequest.state == state
609
+ ).values({
610
+ models.OAuthRequest.access_msg: fetchcode,
611
+ models.OAuthRequest.redirect_msg: new_token['token']
612
+ })
613
+ # If Rucio Client was requested to poll the Rucio Auth server
614
+ # for a token automatically, we save the token under a access_msg.
615
+ else:
616
+ query = update(
617
+ models.OAuthRequest
618
+ ).where(
619
+ models.OAuthRequest.state == state
620
+ ).values({
621
+ models.OAuthRequest.access_msg: oauth_req_params.access_msg,
622
+ models.OAuthRequest.redirect_msg: new_token['token']
623
+ })
624
+ session.execute(query)
625
+ session.commit()
626
+ METRICS.timer('IdP_authorization').observe(stopwatch.elapsed)
627
+ if '_polling' in oauth_req_params.access_msg:
628
+ return {'polling': True}
629
+ elif 'http' in oauth_req_params.access_msg:
630
+ return {'webhome': oauth_req_params.access_msg, 'token': new_token}
631
+ else:
632
+ return {'fetchcode': fetchcode}
633
+ else:
634
+ METRICS.timer('IdP_authorization').observe(stopwatch.elapsed)
635
+ return {'token': new_token}
636
+
637
+ except Exception:
638
+ # TO-DO catch different exceptions - InvalidGrant etc. ...
639
+ METRICS.counter(name='IdP_authorization.access_token.exception').inc()
640
+ logging.debug(traceback.format_exc())
641
+ return None
642
+ # raise CannotAuthenticate(traceback.format_exc())
643
+
644
+
645
+ @transactional_session
646
+ def __get_admin_token_oidc(account: types.InternalAccount, req_scope, req_audience, issuer, *, session: "Session"):
647
+ """
648
+ Get a token for Rucio application to act on behalf of itself.
649
+ client_credential flow is used for this purpose.
650
+ No refresh token is expected to be used.
651
+
652
+ :param account: the Rucio Admin account name to be used (InternalAccount object expected)
653
+ :param req_scope: the audience requested for the Rucio client's token
654
+ :param req_audience: the scope requested for the Rucio client's token
655
+ :param issuer: the Identity Provider nickname or the Rucio instance in use
656
+ :param session: The database session in use.
657
+ :returns: A dict with token and expires_at entries.
658
+ """
659
+
660
+ if not OIDC_ADMIN_CLIENTS:
661
+ # retry once loading OIDC clients
662
+ __initialize_oidc_clients()
663
+ if not OIDC_ADMIN_CLIENTS:
664
+ raise CannotAuthenticate(traceback.format_exc())
665
+
666
+ try:
667
+
668
+ oidc_client = OIDC_ADMIN_CLIENTS[issuer]
669
+ args = {"client_id": oidc_client.client_id,
670
+ "client_secret": oidc_client.client_secret,
671
+ "grant_type": "client_credentials",
672
+ "scope": req_scope,
673
+ "audience": req_audience}
674
+ # in the future should use oauth2 pyoidc client (base) instead
675
+ oidc_tokens = oidc_client.do_any(request=CCAccessTokenRequest,
676
+ request_args=args,
677
+ response=AccessTokenResponse)
678
+ if 'error' in oidc_tokens:
679
+ raise CannotAuthorize(oidc_tokens['error'])
680
+ METRICS.counter(name='IdP_authentication.rucio_admin_token_granted').inc()
681
+ # save the access token in the Rucio DB
682
+ if 'access_token' in oidc_tokens:
683
+ validate_dict = __get_rucio_jwt_dict(oidc_tokens['access_token'], account=account, session=session)
684
+ if validate_dict:
685
+ METRICS.counter(name='IdP_authentication.success').inc()
686
+ new_token = __save_validated_token(oidc_tokens['access_token'], validate_dict, extra_dict={}, session=session)
687
+ METRICS.counter(name='IdP_authentication.access_token.saved').inc()
688
+ return new_token
689
+ else:
690
+ logging.debug("Rucio could not get a valid admin token from the Identity Provider.")
691
+ return None
692
+ else:
693
+ logging.debug("Rucio could not get its admin access token from the Identity Provider.")
694
+ return None
695
+
696
+ except Exception:
697
+ # TO-DO catch different exceptions - InvalidGrant etc. ...
698
+ METRICS.counter(name='IdP_authorization.access_token.exception').inc()
699
+ logging.debug(traceback.format_exc())
700
+ return None
701
+ # raise CannotAuthenticate(traceback.format_exc())
702
+
703
+
704
+ @read_session
705
+ def __get_admin_account_for_issuer(*, session: "Session"):
706
+ """ Gets admin account for the IdP issuer
707
+ :returns : dictionary { 'issuer_1': (account, identity), ... }
708
+ """
709
+
710
+ if not OIDC_ADMIN_CLIENTS:
711
+ # retry once loading OIDC clients
712
+ __initialize_oidc_clients()
713
+ if not OIDC_ADMIN_CLIENTS:
714
+ raise CannotAuthenticate(traceback.format_exc())
715
+
716
+ issuer_account_dict = {}
717
+ for issuer in OIDC_ADMIN_CLIENTS:
718
+ admin_identity = oidc_identity_string(OIDC_ADMIN_CLIENTS[issuer].client_id, issuer)
719
+ query = select(
720
+ models.IdentityAccountAssociation.account
721
+ ).where(
722
+ models.IdentityAccountAssociation.identity_type == IdentityType.OIDC,
723
+ models.IdentityAccountAssociation.identity == admin_identity
724
+ )
725
+ admin_account = session.execute(query).scalar()
726
+ issuer_account_dict[issuer] = (admin_account, admin_identity)
727
+ return issuer_account_dict
728
+
729
+
730
+ @transactional_session
731
+ def get_token_for_account_operation(account: str, req_audience: str = None, req_scope: str = None, admin: bool = False, *, session: "Session"):
732
+ """
733
+ Looks-up a JWT token with the required scope and audience claims with the account OIDC issuer.
734
+ If tokens are found, and none contains the requested audience and scope a new token is requested
735
+ (via token exchange or client credential grants in case admin = True)
736
+ :param account: Rucio account name in order to lookup the issuer and corresponding valid tokens
737
+ :param req_audience: audience required to be present in the token (e.g. 'fts:atlas')
738
+ :param req_scope: scope requested to be present in the token (e.g. fts:submit-transfer)
739
+ :param admin: If True tokens will be requested for the Rucio admin root account,
740
+ preferably with the same issuer as the requesting account OIDC identity
741
+ :param session: DB session in use
742
+
743
+ :return: token dictionary or None, throws an exception in case of problems
744
+ """
745
+ try:
746
+ if not req_scope:
747
+ req_scope = EXPECTED_OIDC_SCOPE
748
+ if not req_audience:
749
+ req_audience = EXPECTED_OIDC_AUDIENCE
750
+
751
+ # get all identities for the corresponding account
752
+ query = select(
753
+ models.IdentityAccountAssociation.identity
754
+ ).where(
755
+ models.IdentityAccountAssociation.identity_type == IdentityType.OIDC,
756
+ models.IdentityAccountAssociation.account == account
757
+ )
758
+ identities = session.execute(query).scalars().all()
759
+ # get all active/valid OIDC tokens
760
+ query = select(
761
+ models.Token
762
+ ).where(
763
+ models.Token.identity.in_(identities),
764
+ models.Token.account == account,
765
+ models.Token.expired_at > datetime.utcnow()
766
+ ).with_for_update(
767
+ skip_locked=True
768
+ )
769
+ account_tokens = session.execute(query).scalars().all()
770
+
771
+ # for Rucio Admin account we ask IdP for a token via client_credential grant
772
+ # for each user account OIDC identity there is an OIDC issuer that must be, by construction,
773
+ # supported by Rucio server (have OIDC admin client registered as well)
774
+ # that is why we take the issuer of the account identity that has an active/valid token
775
+ # and look for admin account identity which has this issuer assigned
776
+ # requester should always have at least one active subject token unless it is root
777
+ # this is why we first discover if the requester is root or not
778
+ get_token_for_adminacc = False
779
+ admin_identity = None
780
+ admin_issuer = None
781
+ admin_iss_acc_idt_dict = __get_admin_account_for_issuer(session=session)
782
+
783
+ # check if preferred issuer exists - if multiple present last one is taken
784
+ preferred_issuer = None
785
+ for token in account_tokens:
786
+ preferred_issuer = token.identity.split(", ")[1].split("=")[1]
787
+ # loop through all OIDC identities registered for the account of the requester
788
+ for identity in identities:
789
+ issuer = identity.split(", ")[1].split("=")[1]
790
+ # compare the account of the requester with the account of the admin
791
+ if account == admin_iss_acc_idt_dict[issuer][0]:
792
+ # take first matching case which means root is requesting OIDC authentication
793
+ admin_identity = admin_iss_acc_idt_dict[issuer][1]
794
+ if preferred_issuer and preferred_issuer != issuer:
795
+ continue
796
+ else:
797
+ admin_issuer = issuer
798
+ get_token_for_adminacc = True
799
+ break
800
+
801
+ # Rucio admin account requesting OIDC token
802
+ if get_token_for_adminacc:
803
+ # openid scope is not supported for client_credentials auth flow - removing it if being asked for
804
+ if 'openid' in req_scope:
805
+ req_scope = req_scope.replace("openid", "").strip()
806
+ # checking if there is not already a token to use
807
+ query = select(
808
+ models.Token
809
+ ).where(
810
+ models.Token.account == account,
811
+ models.Token.expired_at > datetime.utcnow()
812
+ )
813
+ admin_account_tokens = session.execute(query).scalars().all()
814
+ for admin_token in admin_account_tokens:
815
+ if hasattr(admin_token, 'audience') and hasattr(admin_token, 'oidc_scope') and\
816
+ all_oidc_req_claims_present(admin_token.oidc_scope, admin_token.audience, req_scope, req_audience):
817
+ return token_dictionary(admin_token)
818
+ # if not found request a new one
819
+ new_admin_token = __get_admin_token_oidc(account, req_scope, req_audience, admin_issuer, session=session)
820
+ return new_admin_token
821
+
822
+ # Rucio server requests Rucio user to be represented by Rucio admin OIDC identity
823
+ if admin and not get_token_for_adminacc:
824
+ # we require any other account than admin to have valid OIDC token in the Rucio DB
825
+ if not account_tokens:
826
+ logging.debug("No valid token exists for account %s.", account)
827
+ return None
828
+ # we also require that these tokens at least one has the Rucio scopes and audiences
829
+ valid_subject_token_exists = False
830
+ for account_token in account_tokens:
831
+ if all_oidc_req_claims_present(account_token.oidc_scope, account_token.audience, EXPECTED_OIDC_SCOPE, EXPECTED_OIDC_AUDIENCE):
832
+ valid_subject_token_exists = True
833
+ if not valid_subject_token_exists:
834
+ logging.debug("No valid audience/scope exists for account %s token.", account)
835
+ return None
836
+ # openid scope is not supported for client_credentials auth flow - removing it if being asked for
837
+ if 'openid' in req_scope:
838
+ req_scope = req_scope.replace("openid", "").strip()
839
+
840
+ admin_account = None
841
+ for account_token in account_tokens:
842
+ # for each valid account token in the DB we need to check if a valid root token does not exist with the required
843
+ # scope and audience
844
+ admin_issuer = account_token.identity.split(", ")[1].split("=")[1]
845
+ # assuming the requesting account is using Rucio supported IdPs, we check if any token of this admin identity
846
+ # has already a token with the requested scopes and audiences
847
+ admin_acc_idt_tuple = admin_iss_acc_idt_dict[admin_issuer]
848
+ admin_account = admin_acc_idt_tuple[0]
849
+ admin_identity = admin_acc_idt_tuple[1]
850
+ query = select(
851
+ models.Token
852
+ ).where(
853
+ models.Token.identity == admin_identity,
854
+ models.Token.account == admin_account,
855
+ models.Token.expired_at > datetime.utcnow()
856
+ )
857
+ admin_account_tokens = session.execute(query).scalars().all()
858
+ for admin_token in admin_account_tokens:
859
+ if hasattr(admin_token, 'audience') and hasattr(admin_token, 'oidc_scope') and\
860
+ all_oidc_req_claims_present(admin_token.oidc_scope, admin_token.audience, req_scope, req_audience):
861
+ return token_dictionary(admin_token)
862
+ # if no admin token existing was found for the issuer of the valid user token
863
+ # we request a new one
864
+ new_admin_token = __get_admin_token_oidc(admin_account, req_scope, req_audience, admin_issuer, session=session)
865
+ return new_admin_token
866
+ # Rucio server requests exchange token for a Rucio user
867
+ if not admin and not get_token_for_adminacc:
868
+ # we require any other account than admin to have valid OIDC token in the Rucio DB
869
+ if not account_tokens:
870
+ logging.debug("No valid token exists for account %s.", account)
871
+ return None
872
+ # we also require that these tokens at least one has the Rucio scopes and audiences
873
+ valid_subject_token_exists = False
874
+ for account_token in account_tokens:
875
+ if all_oidc_req_claims_present(account_token.oidc_scope, account_token.audience, EXPECTED_OIDC_SCOPE, EXPECTED_OIDC_AUDIENCE):
876
+ valid_subject_token_exists = True
877
+ if not valid_subject_token_exists:
878
+ logging.debug("No valid audience/scope exists for account %s token.", account)
879
+ return None
880
+ subject_token = None
881
+ for token in account_tokens:
882
+ if hasattr(token, 'audience') and hasattr(token, 'oidc_scope'):
883
+ if all_oidc_req_claims_present(token.oidc_scope, token.audience, req_scope, req_audience):
884
+ return token_dictionary(token)
885
+ # from available tokens select preferentially the one which are being refreshed
886
+ if hasattr(token, 'oidc_scope') and ('offline_access' in str(token['oidc_scope'])):
887
+ subject_token = token
888
+ # if not proceed with token exchange
889
+ if not subject_token:
890
+ subject_token = choice(account_tokens)
891
+ exchanged_token = __exchange_token_oidc(subject_token,
892
+ scope=req_scope,
893
+ audience=req_audience,
894
+ identity=subject_token.identity,
895
+ refresh_lifetime=subject_token.refresh_lifetime,
896
+ account=account,
897
+ session=session)
898
+ return exchanged_token
899
+ logging.debug("No token could be returned for account operation for account %s.", account)
900
+ return None
901
+ except Exception:
902
+ # raise CannotAuthorize(traceback.format_exc(), type(account), account)
903
+ logging.debug(traceback.format_exc())
904
+ return None
905
+
906
+
907
+ @METRICS.time_it
908
+ @transactional_session
909
+ def __exchange_token_oidc(subject_token_object: models.Token, *, session: "Session", **kwargs):
910
+ """
911
+ Exchanged an access_token for a new one with different scope &/ audience
912
+ providing that the scope specified is registered with IdP for the Rucio OIDC Client
913
+ and the Rucio user has this scope linked to the subject token presented
914
+ for the token exchange.
915
+
916
+ :param subject_token_object: DB subject token to be exchanged
917
+ :param kwargs: 'scope', 'audience', 'grant_type', 'ip' and 'account' doing the exchange
918
+ :param session: The database session in use.
919
+
920
+ :returns: A dict with token and expires_at entries.
921
+ """
922
+ grant_type = kwargs.get('grant_type', EXCHANGE_GRANT_TYPE)
923
+ jwt_row_dict, extra_dict = {}, {}
924
+ jwt_row_dict['account'] = kwargs.get('account', '')
925
+ jwt_row_dict['authz_scope'] = kwargs.get('scope', '')
926
+ jwt_row_dict['audience'] = kwargs.get('audience', '')
927
+ jwt_row_dict['identity'] = kwargs.get('identity', '')
928
+ extra_dict['ip'] = kwargs.get('ip', None)
929
+
930
+ # if subject token has offline access scope but *no* refresh token in the DB
931
+ # (happens when user presents subject token acquired from other sources then Rucio CLI mechanism),
932
+ # add offline_access scope to the token exchange request !
933
+ if 'offline_access' in str(subject_token_object.oidc_scope) and not subject_token_object.refresh_token:
934
+ jwt_row_dict['authz_scope'] += ' offline_access'
935
+ if not grant_type:
936
+ grant_type = EXCHANGE_GRANT_TYPE
937
+ try:
938
+ oidc_dict = __get_init_oidc_client(token_object=subject_token_object, token_type="subject_token") # noqa: S106
939
+ oidc_client = oidc_dict['client']
940
+ args = {"subject_token": subject_token_object.token,
941
+ "scope": jwt_row_dict['authz_scope'],
942
+ "audience": jwt_row_dict['audience'],
943
+ "grant_type": grant_type}
944
+ # exchange , access token for a new one
945
+ oidc_token_response = oidc_dict['client'].do_any(Message,
946
+ endpoint=oidc_client.provider_info["token_endpoint"],
947
+ state=oidc_dict['state'],
948
+ request_args=args,
949
+ authn_method="client_secret_basic")
950
+ oidc_tokens = oidc_token_response.json()
951
+ if 'error' in oidc_tokens:
952
+ raise CannotAuthorize(oidc_tokens['error'])
953
+ # get audience and scope information
954
+ if 'scope' in oidc_tokens and 'audience' in oidc_tokens:
955
+ jwt_row_dict['authz_scope'] = val_to_space_sep_str(oidc_tokens['scope'])
956
+ jwt_row_dict['audience'] = val_to_space_sep_str(oidc_tokens['audience'])
957
+ elif 'access_token' in oidc_tokens:
958
+ values = __get_keyvalues_from_claims(oidc_tokens['access_token'], ['scope', 'aud'])
959
+ jwt_row_dict['authz_scope'] = values['scope']
960
+ jwt_row_dict['audience'] = values['aud']
961
+ jwt_row_dict['lifetime'] = datetime.utcnow() + timedelta(seconds=oidc_tokens['expires_in'])
962
+ if 'refresh_token' in oidc_tokens:
963
+ extra_dict['refresh_token'] = oidc_tokens['refresh_token']
964
+ extra_dict['refresh'] = True
965
+ extra_dict['refresh_lifetime'] = kwargs.get('refresh_lifetime', REFRESH_LIFETIME_H)
966
+ if extra_dict['refresh_lifetime'] is None:
967
+ extra_dict['refresh_lifetime'] = REFRESH_LIFETIME_H
968
+ try:
969
+ values = __get_keyvalues_from_claims(oidc_tokens['refresh_token'], ['exp'])
970
+ extra_dict['refresh_expired_at'] = datetime.utcfromtimestamp(float(values['exp']))
971
+ except Exception:
972
+ # 4 day expiry period by default
973
+ extra_dict['refresh_expired_at'] = datetime.utcnow() + timedelta(hours=REFRESH_LIFETIME_H)
974
+
975
+ new_token = __save_validated_token(oidc_tokens['access_token'], jwt_row_dict, extra_dict=extra_dict, session=session)
976
+ METRICS.counter(name='IdP_authorization.access_token.saved').inc()
977
+ if 'refresh_token' in oidc_tokens:
978
+ METRICS.counter(name='IdP_authorization.refresh_token.saved').inc()
979
+ return new_token
980
+
981
+ except Exception:
982
+ # raise CannotAuthorize(traceback.format_exc())
983
+ logging.debug(traceback.format_exc())
984
+ return None
985
+
986
+
987
+ @transactional_session
988
+ def __change_refresh_state(token: str, refresh: bool = False, *, session: "Session"):
989
+ """
990
+ Changes token refresh state to True/False.
991
+
992
+ :param token: the access token for which the refresh value should be changed.
993
+ """
994
+ try:
995
+ query = update(
996
+ models.Token
997
+ ).where(
998
+ models.Token.token == token
999
+ )
1000
+ if refresh:
1001
+ # update refresh column for a token to True
1002
+ query = query.values({
1003
+ models.Token.refresh: True
1004
+ })
1005
+ else:
1006
+ query = query.values({
1007
+ models.Token.refresh: False,
1008
+ models.Token.refresh_expired_at: datetime.utcnow()
1009
+ })
1010
+ session.execute(query)
1011
+ except Exception as error:
1012
+ raise RucioException(error.args) from error
1013
+
1014
+
1015
+ @transactional_session
1016
+ def refresh_cli_auth_token(token_string: str, account: str, *, session: "Session") -> Optional[tuple[str, int]]:
1017
+ """
1018
+ Checks if there is active refresh token and if so returns
1019
+ either active token with expiration timestamp or requests a new
1020
+ refresh and returns new access token.
1021
+ :param token_string: token string
1022
+ :param account: Rucio account for which token refresh should be considered
1023
+
1024
+ :return: tuple of (access token, expiration epoch), None otherswise
1025
+ """
1026
+ # only validated tokens are in the DB, check presence of token_string
1027
+ query = select(
1028
+ models.Token
1029
+ ).where(
1030
+ models.Token.token == token_string,
1031
+ models.Token.account == account,
1032
+ models.Token.expired_at > datetime.utcnow()
1033
+ ).with_for_update(
1034
+ skip_locked=True
1035
+ )
1036
+ account_token = session.execute(query).scalar()
1037
+
1038
+ # if token does not exist in the DB, return None
1039
+ if account_token is None:
1040
+ logging.debug("No valid token exists for account %s.", account)
1041
+ return None
1042
+
1043
+ # protection (!) no further action should be made
1044
+ # for token_string without refresh_token in the DB !
1045
+ if account_token.refresh_token is None:
1046
+ logging.debug("No refresh token exists for account %s.", account)
1047
+ return None
1048
+
1049
+ # if the token exists, check if it was refreshed already, if not, refresh it
1050
+ if account_token.refresh:
1051
+ # protection (!) returning the same token if the token_string
1052
+ # is a result of a refresh which happened in the last 5 min
1053
+ datetime_min_ago = datetime.utcnow() - timedelta(seconds=300)
1054
+ if account_token.updated_at > datetime_min_ago:
1055
+ epoch_exp = int(floor((account_token.expired_at - datetime(1970, 1, 1)).total_seconds()))
1056
+ new_token_string = account_token.token
1057
+ return new_token_string, epoch_exp
1058
+
1059
+ # asking for a refresh of this token
1060
+ new_token = __refresh_token_oidc(account_token, session=session)
1061
+ new_token_string = new_token['token']
1062
+ epoch_exp = int(floor((new_token['expires_at'] - datetime(1970, 1, 1)).total_seconds()))
1063
+ return new_token_string, epoch_exp
1064
+
1065
+ else:
1066
+ # find account token with the same scope,
1067
+ # audience and has a valid refresh token
1068
+ query = select(
1069
+ models.Token
1070
+ ).where(
1071
+ models.Token.refresh == true(),
1072
+ models.Token.refresh_expired_at > datetime.utcnow(),
1073
+ models.Token.account == account,
1074
+ models.Token.expired_at > datetime.utcnow()
1075
+ ).with_for_update(
1076
+ skip_locked=True
1077
+ )
1078
+ new_token = session.execute(query).scalar()
1079
+ if new_token is None:
1080
+ return None
1081
+
1082
+ # if the new_token has same audience and scopes as the original
1083
+ # account_token --> return this token and exp timestamp to the user
1084
+ if all_oidc_req_claims_present(new_token.oidc_scope, new_token.audience,
1085
+ account_token.oidc_scope, account_token.audience):
1086
+ epoch_exp = int(floor((new_token.expired_at - datetime(1970, 1, 1)).total_seconds()))
1087
+ new_token_string = new_token.token
1088
+ return new_token_string, epoch_exp
1089
+ # if scopes and audience are not the same, return None
1090
+ logging.debug("No token could be returned for refresh operation for account %s.", account)
1091
+ return None
1092
+
1093
+
1094
+ @transactional_session
1095
+ def refresh_jwt_tokens(total_workers: int, worker_number: int, refreshrate: int = 3600, limit: int = 1000, *, session: "Session"):
1096
+ """
1097
+ Refreshes tokens which expired or will expire before (now + refreshrate)
1098
+ next run of this function and which have valid refresh token.
1099
+
1100
+ :param total_workers: Number of total workers.
1101
+ :param worker_number: id of the executing worker.
1102
+ :param limit: Maximum number of tokens to refresh per call.
1103
+ :param session: Database session in use.
1104
+
1105
+ :return: numper of tokens refreshed
1106
+ """
1107
+ nrefreshed = 0
1108
+ try:
1109
+ # get tokens for refresh that expire in the next <refreshrate> seconds
1110
+ expiration_future = datetime.utcnow() + timedelta(seconds=refreshrate)
1111
+ query = select(
1112
+ models.Token
1113
+ ).where(
1114
+ models.Token.refresh == true(),
1115
+ models.Token.refresh_expired_at > datetime.utcnow(),
1116
+ models.Token.expired_at < expiration_future
1117
+ ).order_by(
1118
+ models.Token.expired_at
1119
+ )
1120
+ query = filter_thread_work(session=session, query=query, total_threads=total_workers, thread_id=worker_number, hash_variable='token')
1121
+ # limiting the number of tokens for refresh
1122
+ query = query.limit(limit)
1123
+ # Oracle does not support chaining order_by(), limit(), and
1124
+ # with_for_update(). Use a nested query to overcome this.
1125
+ if session.bind.dialect.name == 'oracle':
1126
+ query = select(
1127
+ models.Token
1128
+ ).where(
1129
+ models.Token.token.in_(query.with_only_columns(models.Token.token))
1130
+ ).with_for_update(
1131
+ skip_locked=True
1132
+ )
1133
+ else:
1134
+ query = query.with_for_update(skip_locked=True)
1135
+ filtered_tokens = session.execute(query).scalars().all()
1136
+
1137
+ # refreshing these tokens
1138
+ for token in filtered_tokens:
1139
+ new_token = __refresh_token_oidc(token, session=session)
1140
+ if new_token:
1141
+ nrefreshed += 1
1142
+
1143
+ except Exception as error:
1144
+ raise RucioException(error.args) from error
1145
+
1146
+ return nrefreshed
1147
+
1148
+
1149
+ @METRICS.time_it
1150
+ @transactional_session
1151
+ def __refresh_token_oidc(token_object: models.Token, *, session: "Session"):
1152
+ """
1153
+ Requests new access and refresh tokens from the Identity Provider.
1154
+ Assumption: The Identity Provider issues refresh tokens for one time use only and
1155
+ with a limited lifetime. The refresh tokens are invalidated no matter which of these
1156
+ situations happens first.
1157
+
1158
+ :param token_object: Rucio models.Token DB row object
1159
+
1160
+ :returns: A dict with token and expires_at entries if all went OK, None if
1161
+ refresh was not possible due to token invalidity or refresh lifetime
1162
+ constraints. Otherwise, throws an an Exception.
1163
+ """
1164
+ try:
1165
+ jwt_row_dict, extra_dict = {}, {}
1166
+ jwt_row_dict['account'] = token_object.account
1167
+ jwt_row_dict['identity'] = token_object.identity
1168
+ extra_dict['refresh_start'] = datetime.utcnow()
1169
+ # check if refresh token started in the past already
1170
+ if hasattr(token_object, 'refresh_start'):
1171
+ if token_object.refresh_start:
1172
+ extra_dict['refresh_start'] = token_object.refresh_start
1173
+ # check if refresh lifetime is set for the token
1174
+ extra_dict['refresh_lifetime'] = REFRESH_LIFETIME_H
1175
+ if token_object.refresh_lifetime:
1176
+ extra_dict['refresh_lifetime'] = token_object.refresh_lifetime
1177
+ # if the token has been refreshed for time exceeding
1178
+ # the refresh_lifetime, the attempt will be aborted and refresh stopped
1179
+ if datetime.utcnow() - extra_dict['refresh_start'] > timedelta(hours=extra_dict['refresh_lifetime']):
1180
+ __change_refresh_state(token_object.token, refresh=False, session=session)
1181
+ return None
1182
+ oidc_dict = __get_init_oidc_client(token_object=token_object, token_type="refresh_token") # noqa: S106
1183
+ oidc_client = oidc_dict['client']
1184
+ # getting a new refreshed set of tokens
1185
+ state = oidc_dict['state']
1186
+ oidc_tokens = oidc_client.do_access_token_refresh(state=state, skew=LEEWAY_SECS)
1187
+ if 'error' in oidc_tokens:
1188
+ raise CannotAuthorize(oidc_tokens['error'])
1189
+ METRICS.counter(name='IdP_authorization.refresh_token.refreshed').inc()
1190
+ # get audience and scope information
1191
+ if 'scope' in oidc_tokens and 'audience' in oidc_tokens:
1192
+ jwt_row_dict['authz_scope'] = val_to_space_sep_str(oidc_tokens['scope'])
1193
+ jwt_row_dict['audience'] = val_to_space_sep_str(oidc_tokens['audience'])
1194
+ elif 'access_token' in oidc_tokens:
1195
+ values = __get_keyvalues_from_claims(oidc_tokens['access_token'], ['scope', 'aud'])
1196
+ jwt_row_dict['authz_scope'] = values['scope']
1197
+ jwt_row_dict['audience'] = values['aud']
1198
+ # save new access and refresh tokens in the DB
1199
+ if 'refresh_token' in oidc_tokens and 'access_token' in oidc_tokens:
1200
+ # aborting refresh of the original token
1201
+ # (keeping it in place until it expires)
1202
+ __change_refresh_state(token_object.token, refresh=False, session=session)
1203
+
1204
+ # get access token expiry timestamp
1205
+ jwt_row_dict['lifetime'] = datetime.utcnow() + timedelta(seconds=oidc_tokens['expires_in'])
1206
+ extra_dict['refresh'] = True
1207
+ extra_dict['refresh_token'] = oidc_tokens['refresh_token']
1208
+ try:
1209
+ values = __get_keyvalues_from_claims(oidc_tokens['refresh_token'], ['exp'])
1210
+ extra_dict['refresh_expired_at'] = datetime.utcfromtimestamp(float(values['exp']))
1211
+ except Exception:
1212
+ # 4 day expiry period by default
1213
+ extra_dict['refresh_expired_at'] = datetime.utcnow() + timedelta(hours=REFRESH_LIFETIME_H)
1214
+ new_token = __save_validated_token(oidc_tokens['access_token'], jwt_row_dict, extra_dict=extra_dict, session=session)
1215
+ METRICS.counter(name='IdP_authorization.access_token.saved').inc()
1216
+ METRICS.counter(name='IdP_authorization.refresh_token.saved').inc()
1217
+ else:
1218
+ raise CannotAuthorize("OIDC identity '%s' of the '%s' account is did not " % (token_object.identity, token_object.account)
1219
+ + "succeed requesting a new access and refresh tokens.") # NOQA: W503
1220
+ return new_token
1221
+
1222
+ except Exception as error:
1223
+ METRICS.counter(name='IdP_authorization.refresh_token.exception').inc()
1224
+ raise CannotAuthorize(traceback.format_exc()) from error
1225
+
1226
+
1227
+ @transactional_session
1228
+ def delete_expired_oauthrequests(total_workers: int, worker_number: int, limit: int = 1000, *, session: "Session"):
1229
+ """
1230
+ Delete expired OAuth request parameters.
1231
+
1232
+ :param total_workers: Number of total workers.
1233
+ :param worker_number: id of the executing worker.
1234
+ :param limit: Maximum number of oauth request session parameters to delete.
1235
+ :param session: Database session in use.
1236
+
1237
+ :returns: number of deleted rows
1238
+ """
1239
+
1240
+ try:
1241
+ # get expired OAuth request parameters
1242
+ query = select(
1243
+ models.OAuthRequest.state
1244
+ ).where(
1245
+ models.OAuthRequest.expired_at < datetime.utcnow()
1246
+ ).order_by(
1247
+ models.OAuthRequest.expired_at
1248
+ )
1249
+ query = filter_thread_work(session=session, query=query, total_threads=total_workers, thread_id=worker_number, hash_variable='state')
1250
+ # limiting the number of oauth requests deleted at once
1251
+ query = query.limit(limit)
1252
+ # Oracle does not support chaining order_by(), limit(), and
1253
+ # with_for_update(). Use a nested query to overcome this.
1254
+ if session.bind.dialect.name == 'oracle':
1255
+ query = select(
1256
+ models.OAuthRequest.state
1257
+ ).where(
1258
+ models.OAuthRequest.state.in_(query)
1259
+ ).with_for_update(
1260
+ skip_locked=True
1261
+ )
1262
+ else:
1263
+ query = query.with_for_update(skip_locked=True)
1264
+
1265
+ ndeleted = 0
1266
+ for states in session.execute(query).scalars().partitions(10):
1267
+ query = delete(
1268
+ models.OAuthRequest
1269
+ ).where(
1270
+ models.OAuthRequest.state.in_(states)
1271
+ )
1272
+ ndeleted += session.execute(query).rowcount
1273
+ return ndeleted
1274
+ except Exception as error:
1275
+ raise RucioException(error.args) from error
1276
+
1277
+
1278
+ def __get_keyvalues_from_claims(token: str, keys=None):
1279
+ """
1280
+ Extracting claims from token, e.g. scope and audience.
1281
+ :param token: the JWT to be unpacked
1282
+ :param key: list of key names to extract from the token claims
1283
+
1284
+ :returns: The list of unicode values under the key, throws an exception otherwise.
1285
+ """
1286
+ resdict = {}
1287
+ try:
1288
+ claims = JWT().unpack(token).payload()
1289
+ if not keys:
1290
+ keys = claims.keys()
1291
+ for key in keys:
1292
+ value = ''
1293
+ if key in claims:
1294
+ value = val_to_space_sep_str(claims[key]) # type: ignore
1295
+ resdict[key] = value
1296
+ return resdict
1297
+ except Exception as error:
1298
+ raise CannotAuthenticate(traceback.format_exc()) from error
1299
+
1300
+
1301
+ @read_session
1302
+ def __get_rucio_jwt_dict(jwt: str, account=None, *, session: "Session"):
1303
+ """
1304
+ Get a Rucio token dictionary from token claims.
1305
+ Check token expiration and find default Rucio
1306
+ account for token identity.
1307
+ :param jwt: JSON Web Token to be inspected
1308
+ :param session: DB session in use
1309
+
1310
+ :returns: Rucio token dictionary, None otherwise
1311
+ """
1312
+ try:
1313
+ # getting token paylod
1314
+ token_payload = __get_keyvalues_from_claims(jwt)
1315
+ identity_string = oidc_identity_string(token_payload['sub'], token_payload['iss'])
1316
+ expiry_date = datetime.utcfromtimestamp(float(token_payload['exp']))
1317
+ if expiry_date < datetime.utcnow(): # check if expired
1318
+ logging.debug("Token has already expired since: %s", str(expiry_date))
1319
+ return None
1320
+ scope = None
1321
+ audience = None
1322
+ if 'scope' in token_payload:
1323
+ scope = val_to_space_sep_str(token_payload['scope'])
1324
+ if 'aud' in token_payload:
1325
+ audience = val_to_space_sep_str(token_payload['aud'])
1326
+ if not account:
1327
+ # this assumes token has been previously looked up in DB
1328
+ # before to be sure that we do not have the right account already in the DB !
1329
+ account = get_default_account(identity_string, IdentityType.OIDC, True, session=session)
1330
+ else:
1331
+ if not exist_identity_account(identity_string, IdentityType.OIDC, account, session=session):
1332
+ logging.debug("No OIDC identity exists for account: %s", str(account))
1333
+ return None
1334
+ value = {'account': account,
1335
+ 'identity': identity_string,
1336
+ 'lifetime': expiry_date,
1337
+ 'audience': audience,
1338
+ 'authz_scope': scope}
1339
+ return value
1340
+ except Exception:
1341
+ logging.debug(traceback.format_exc())
1342
+ return None
1343
+
1344
+
1345
+ @transactional_session
1346
+ def __save_validated_token(token, valid_dict, extra_dict=None, *, session: "Session"):
1347
+ """
1348
+ Save JWT token to the Rucio DB.
1349
+
1350
+ :param token: Authentication token as a variable-length string.
1351
+ :param valid_dict: Validation Rucio dictionary as the output
1352
+ of the __get_rucio_jwt_dict function
1353
+ :raises RucioException: on any error
1354
+ :returns: A dict with token and expires_at entries.
1355
+ """
1356
+ try:
1357
+ if not extra_dict:
1358
+ extra_dict = {}
1359
+ new_token = models.Token(account=valid_dict.get('account', None),
1360
+ token=token,
1361
+ oidc_scope=valid_dict.get('authz_scope', None),
1362
+ expired_at=valid_dict.get('lifetime', None),
1363
+ audience=valid_dict.get('audience', None),
1364
+ identity=valid_dict.get('identity', None),
1365
+ refresh=extra_dict.get('refresh', False),
1366
+ refresh_token=extra_dict.get('refresh_token', None),
1367
+ refresh_expired_at=extra_dict.get('refresh_expired_at', None),
1368
+ refresh_lifetime=extra_dict.get('refresh_lifetime', None),
1369
+ refresh_start=extra_dict.get('refresh_start', None),
1370
+ ip=extra_dict.get('ip', None))
1371
+ new_token.save(session=session)
1372
+
1373
+ return token_dictionary(new_token)
1374
+
1375
+ except Exception as error:
1376
+ raise RucioException(error.args) from error
1377
+
1378
+
1379
+ @transactional_session
1380
+ def validate_jwt(json_web_token: str, *, session: "Session") -> dict[str, Any]:
1381
+ """
1382
+ Verifies signature and validity of a JSON Web Token.
1383
+ Gets the issuer public keys from the oidc_client
1384
+ and verifies the validity of the token.
1385
+ Used only for external tokens, not known to Rucio DB.
1386
+
1387
+ :param json_web_token: the JWT string to verify
1388
+
1389
+ :returns: dictionary { account: <account name>,
1390
+ identity: <identity>,
1391
+ lifetime: <token lifetime>,
1392
+ audience: <audience>,
1393
+ authz_scope: <authz_scope> }
1394
+ if successful.
1395
+ :raises: CannotAuthenticate if unsuccessful
1396
+ """
1397
+
1398
+ if not OIDC_CLIENTS:
1399
+ # retry once loading OIDC clients
1400
+ __initialize_oidc_clients()
1401
+ if not OIDC_CLIENTS:
1402
+ raise CannotAuthenticate(traceback.format_exc())
1403
+
1404
+ try:
1405
+
1406
+ # getting issuer from the token payload
1407
+ token_dict: Optional[dict[str, Any]] = __get_rucio_jwt_dict(json_web_token, session=session)
1408
+ if not token_dict:
1409
+ raise CannotAuthenticate(traceback.format_exc())
1410
+ issuer = token_dict['identity'].split(", ")[1].split("=")[1]
1411
+ oidc_client = OIDC_CLIENTS[issuer]
1412
+ issuer_keys = oidc_client.keyjar.get_issuer_keys(issuer)
1413
+ JWS().verify_compact(json_web_token, issuer_keys)
1414
+ # if there is no audience and scope information,
1415
+ # try to get it from IdP introspection endpoint
1416
+ # TO-BE-REMOVED - once all IdPs support scope and audience in token claims !!!
1417
+ if not token_dict['authz_scope'] or not token_dict['audience']:
1418
+ clprocess = subprocess.Popen(['curl', '-s', '-L', '-u', '%s:%s' # noqa: S607
1419
+ % (oidc_client.client_id, oidc_client.client_secret),
1420
+ '-d', 'token=%s' % (json_web_token),
1421
+ oidc_client.introspection_endpoint],
1422
+ shell=False, stdout=subprocess.PIPE)
1423
+ inspect_claims = json.loads(clprocess.communicate()[0])
1424
+ try:
1425
+ token_dict['audience'] = inspect_claims['aud']
1426
+ token_dict['authz_scope'] = inspect_claims['scope']
1427
+ except:
1428
+ pass
1429
+ METRICS.counter(name='JSONWebToken.valid').inc()
1430
+ # if token is valid and coming from known issuer --> check aud and scope and save it if unknown
1431
+ if token_dict['authz_scope'] and token_dict['audience']:
1432
+ if all_oidc_req_claims_present(token_dict['authz_scope'], token_dict['audience'], EXPECTED_OIDC_SCOPE, EXPECTED_OIDC_AUDIENCE):
1433
+ # save the token in Rucio DB giving the permission to use it for Rucio operations
1434
+ __save_validated_token(json_web_token, token_dict, session=session)
1435
+ else:
1436
+ logging.debug("Token audience [%s] or scope [%s] verification failed.", token_dict['audience'], token_dict['authz_scope'])
1437
+ raise CannotAuthenticate(traceback.format_exc())
1438
+ else:
1439
+ logging.debug("Token audience or scope not present.")
1440
+ raise CannotAuthenticate(traceback.format_exc())
1441
+ METRICS.counter(name='JSONWebToken.saved').inc()
1442
+ return token_dict
1443
+ except Exception:
1444
+ METRICS.counter(name='JSONWebToken.invalid').inc()
1445
+ logging.debug(traceback.format_exc())
1446
+ raise CannotAuthenticate(traceback.format_exc())
1447
+
1448
+
1449
+ def oidc_identity_string(sub: str, iss: str):
1450
+ """
1451
+ Transform IdP sub claim and issuers url into users identity string.
1452
+ :param sub: users SUB claim from the Identity Provider
1453
+ :param iss: issuer (IdP) https url
1454
+
1455
+ :returns: OIDC identity string "SUB=<usersid>, ISS=https://iam-test.ch/"
1456
+ """
1457
+ return 'SUB=' + str(sub) + ', ISS=' + str(iss)
1458
+
1459
+
1460
+ def token_dictionary(token: models.Token):
1461
+ return {'token': token.token, 'expires_at': token.expired_at}