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