From e2779111dce9a5db4de850a0ecc785373597b70b Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 27 Jul 2021 12:36:07 -0500 Subject: [PATCH 001/211] feat(passport): initial ideas for passport POST support --- .secrets.baseline | 107 ++++++++++++++--------------- fence/blueprints/data/blueprint.py | 18 ++++- fence/blueprints/data/indexd.py | 1 + fence/config-default.yaml | 7 +- 4 files changed, 76 insertions(+), 57 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index c4b3abd42..f69ea81a1 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1,11 +1,11 @@ { - "generated_at": "2021-06-10T21:56:24Z", + "version": "1.1.0", "plugins_used": [ { - "name": "AWSKeyDetector" + "name": "ArtifactoryDetector" }, { - "name": "ArtifactoryDetector" + "name": "AWSKeyDetector" }, { "name": "Base64HighEntropyString", @@ -31,8 +31,8 @@ "name": "JwtTokenDetector" }, { - "keyword_exclude": null, - "name": "KeywordDetector" + "name": "KeywordDetector", + "keyword_exclude": "" }, { "name": "MailchimpDetector" @@ -53,6 +53,52 @@ "name": "TwilioKeyDetector" } ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".secrets.baseline" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + }, + { + "path": "detect_secrets.filters.regex.should_exclude_file", + "pattern": [ + "poetry.lock" + ] + } + ], "results": { "fence/blueprints/storage_creds/google.py": [ { @@ -79,13 +125,6 @@ "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", "is_verified": false, "line_number": 31 - }, - { - "type": "Secret Keyword", - "filename": "fence/config-default.yaml", - "hashed_secret": "5d07e1b80e448a213b392049888111e1779a52db", - "is_verified": false, - "line_number": 554 } ], "fence/local_settings.example.py": [ @@ -221,47 +260,5 @@ } ] }, - "version": "1.1.0", - "filters_used": [ - { - "path": "detect_secrets.filters.allowlist.is_line_allowlisted" - }, - { - "path": "detect_secrets.filters.heuristic.is_sequential_string" - }, - { - "path": "detect_secrets.filters.heuristic.is_potential_uuid" - }, - { - "path": "detect_secrets.filters.heuristic.is_likely_id_string" - }, - { - "path": "detect_secrets.filters.heuristic.is_templated_secret" - }, - { - "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" - }, - { - "path": "detect_secrets.filters.heuristic.is_indirect_reference" - }, - { - "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", - "min_level": 2 - }, - { - "path": "detect_secrets.filters.regex.should_exclude_file", - "pattern": [ - "poetry.lock" - ] - }, - { - "path": "detect_secrets.filters.heuristic.is_lock_file" - }, - { - "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" - }, - { - "path": "detect_secrets.filters.heuristic.is_swagger_file" - } - ] + "generated_at": "2021-07-27T17:35:56Z" } diff --git a/fence/blueprints/data/blueprint.py b/fence/blueprints/data/blueprint.py index c4aac4327..a3805bc08 100644 --- a/fence/blueprints/data/blueprint.py +++ b/fence/blueprints/data/blueprint.py @@ -9,6 +9,7 @@ IndexedFile, get_signed_url_for_file, ) +from fence.config import config from fence.errors import Forbidden, InternalError, UserError, Forbidden from fence.resources.audit.utils import enable_audit_logging from fence.utils import get_valid_expiration @@ -292,12 +293,27 @@ def upload_file(file_id): return flask.jsonify(result) -@blueprint.route("/download/", methods=["GET"]) +@blueprint.route("/download/", methods=["GET", "POST"]) @enable_audit_logging def download_file(file_id): """ Get a presigned url to download a file given by file_id. """ + if request.method == "POST": + passport = flask.request.get_json(force=True, silent=True).get( + config["GA4GH_DRS_POSTED_PASSPORT_FIELD"] + ) + + """ TODO + check cache + if not cache: + validate passport + parse visas + validate visas + update user table with visas + parse visa contents + """ + result = get_signed_url_for_file("download", file_id) if not "redirect" in flask.request.args or not "url" in result: return flask.jsonify(result) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index f6d98b904..ee35be09f 100644 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -1143,6 +1143,7 @@ def _get_user_info(sub_type=str): populated information about an anonymous user. By default, cast `sub` to str. Use `sub_type` to override this behavior. """ + # TODO Update to support POSTed passport try: set_current_token( validate_request(scope={"user"}, audience=config.get("BASE_URL")) diff --git a/fence/config-default.yaml b/fence/config-default.yaml index 382f1520b..300f1322e 100644 --- a/fence/config-default.yaml +++ b/fence/config-default.yaml @@ -417,6 +417,11 @@ TOKEN_PROJECTS_CUTOFF: 10 # If set to true, will generate an new access token each time when a browser session update happens RENEW_ACCESS_TOKEN_BEFORE_EXPIRATION: false +# The JSON field the GA4GH Passport is in when a request is POST-ed to DRS +# We use the same field name for POSTs to /data/download for consistency +GA4GH_DRS_POSTED_PASSPORT_FIELD: "auth" + + ######################################################################################## # OPTIONAL CONFIGURATIONS # ######################################################################################## @@ -846,7 +851,7 @@ SERVICE_ACCOUNT_LIMIT: 6 # Global sync visas during login # None(Default): Allow per client i.e. a fence client can pick whether or not to sync their visas during login with parse_visas param in /authorization endpoint -# True: Parse for all clients i.e. a fence client will always sync their visas during login +# True: Parse for all clients i.e. a fence client will always sync their visas during login # False: Parse for no clients i.e. a fence client will not be able to sync visas during login even with parse_visas param GLOBAL_PARSE_VISAS_ON_LOGIN: # Settings for usersync with visas From 70a7ec4b3bca1c5f92e592e09d43ba97e557f3aa Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 27 Jul 2021 15:38:54 -0500 Subject: [PATCH 002/211] fix(passport): correctly get request --- fence/blueprints/data/blueprint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fence/blueprints/data/blueprint.py b/fence/blueprints/data/blueprint.py index a3805bc08..662291b11 100644 --- a/fence/blueprints/data/blueprint.py +++ b/fence/blueprints/data/blueprint.py @@ -299,7 +299,7 @@ def download_file(file_id): """ Get a presigned url to download a file given by file_id. """ - if request.method == "POST": + if flask.request.method == "POST": passport = flask.request.get_json(force=True, silent=True).get( config["GA4GH_DRS_POSTED_PASSPORT_FIELD"] ) From e37a9089c1ed3eee9f1c3b36f3a7d996024cb936 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 27 Jul 2021 17:04:09 -0500 Subject: [PATCH 003/211] feat(utils): function to do a simple db -> mem cache --- fence/utils.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/fence/utils.py b/fence/utils.py index 8dc949f14..e510a0f74 100644 --- a/fence/utils.py +++ b/fence/utils.py @@ -246,8 +246,8 @@ def send_email(from_email, to_emails, subject, text, smtp_domain): "smtp_hostname": "smtp.mailgun.org", "default_login": "postmaster@mailgun.planx-pla.net", "api_url": "https://api.mailgun.net/v3/mailgun.planx-pla.net", - "smtp_password": "password", - "api_key": "api key" + "smtp_password": "password", # pragma: allowlist secret + "api_key": "api key" # pragma: allowlist secret } Returns: @@ -364,6 +364,35 @@ def _is_status(code): return False +def get_from_cache(item_id, memory_cache, db_cache_table, db_cache_table_id_field="id"): + """ + Attempt to get a cached item and store in memory cache from db if necessary. + + NOTE: This requires custom implementation for putting items in the db cache table. + """ + # try to retrieve from local in-memory cache + rv, expires_at = memory_cache.get(item_id, (None, 0)) + if expires_at > expiry: + return rv + + # try to retrieve from database cache + if hasattr(flask.current_app, "db"): # we don't have db in startup + with flask.current_app.db.session as session: + cache = ( + session.query(db_cache_table) + .filter( + getattr(db_cache_table, db_cache_table_id_field, None) == item_id + ) + .first() + ) + if cache and cache.expires_at and cache.expires_at > expiry: + rv = dict(cache) + + # store in memory cache + memory_cache[item_id] = rv, cache.expires_at + return rv + + # Default settings to control usage of backoff library. DEFAULT_BACKOFF_SETTINGS = { "on_backoff": log_backoff_retry, From c6821c89f9ec9ddf3bdc5f6c6682a1375dc61b5b Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 31 Aug 2021 13:45:54 -0500 Subject: [PATCH 004/211] feat(passports): more skeleton code for passports for authN/Z --- fence/blueprints/data/blueprint.py | 17 ++---- fence/blueprints/data/indexd.py | 94 +++++++++++++++++++++++------- fence/config-default.yaml | 2 +- fence/resources/ga4gh/passports.py | 94 ++++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 34 deletions(-) create mode 100644 fence/resources/ga4gh/passports.py diff --git a/fence/blueprints/data/blueprint.py b/fence/blueprints/data/blueprint.py index 662291b11..00f8d554d 100644 --- a/fence/blueprints/data/blueprint.py +++ b/fence/blueprints/data/blueprint.py @@ -299,22 +299,15 @@ def download_file(file_id): """ Get a presigned url to download a file given by file_id. """ + ga4gh_passports = None if flask.request.method == "POST": - passport = flask.request.get_json(force=True, silent=True).get( + ga4gh_passports = flask.request.get_json(force=True, silent=True).get( config["GA4GH_DRS_POSTED_PASSPORT_FIELD"] ) - """ TODO - check cache - if not cache: - validate passport - parse visas - validate visas - update user table with visas - parse visa contents - """ - - result = get_signed_url_for_file("download", file_id) + result = get_signed_url_for_file( + "download", file_id, ga4gh_passports=ga4gh_passports + ) if not "redirect" in flask.request.args or not "url" in result: return flask.jsonify(result) return flask.redirect(result["url"]) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index ee35be09f..3b1e21adc 100644 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -53,7 +53,7 @@ ANONYMOUS_USERNAME = "anonymous" -def get_signed_url_for_file(action, file_id, file_name=None): +def get_signed_url_for_file(action, file_id, file_name=None, ga4gh_passports=None): requested_protocol = flask.request.args.get("protocol", None) r_pays_project = flask.request.args.get("userProject", None) @@ -64,6 +64,12 @@ def get_signed_url_for_file(action, file_id, file_name=None): if no_force_sign_param and no_force_sign_param.lower() == "true": force_signed_url = False + user_ids_from_passports = None + if ga4gh_passports: + user_ids_from_passports = get_gen3_user_ids_from_ga4gh_passports( + ga4gh_passports + ) + # add the user details to `flask.g.audit_data` first, so they are # included in the audit log if `IndexedFile(file_id)` raises a 404 user_info = _get_user_info(sub_type=int) @@ -88,6 +94,7 @@ def get_signed_url_for_file(action, file_id, file_name=None): force_signed_url=force_signed_url, r_pays_project=r_pays_project, file_name=file_name, + user_ids_from_passports=user_ids_from_passports, ) # increment counter for gen3-metrics @@ -367,13 +374,17 @@ def get_signed_url( force_signed_url=True, r_pays_project=None, file_name=None, + user_ids_from_passports=None, ): if self.index_document.get("authz"): action_to_permission = { "upload": "write-storage", "download": "read-storage", } - if not self.check_authz(action_to_permission[action]): + if not self.check_authz( + action_to_permission[action], + user_ids_from_passports=user_ids_from_passports, + ): raise Unauthorized( f"Either you weren't logged in or you don't have " f"{action_to_permission[action]} permission " @@ -386,7 +397,9 @@ def get_signed_url( ) # don't check the authorization if the file is public # (downloading public files with no auth is fine) - if not self.public_acl and not self.check_authorization(action): + if not self.public_acl and not self.check_authorization( + action, user_ids_from_passports=user_ids_from_passports + ): raise Unauthorized( f"You don't have access permission on this file: {self.file_id}" ) @@ -449,7 +462,7 @@ def set_acls(self): else: raise Unauthorized("This file is not accessible") - def check_authz(self, action): + def check_authz(self, action, user_ids_from_passports=None): if not self.index_document.get("authz"): raise ValueError("index record missing `authz`") @@ -457,20 +470,33 @@ def check_authz(self, action): f"authz check can user {action} on {self.index_document['authz']} for fence?" ) - try: - token = get_jwt() - except Unauthorized: - # get_jwt raises an Unauthorized error when user is anonymous (no - # availble token), so to allow anonymous users possible access to - # public data, we still make the request to Arborist - token = None - - return flask.current_app.arborist.auth_request( - jwt=token, - service="fence", - methods=action, - resources=self.index_document["authz"], - ) + # handle multiple GA4GH passports as a means of authn/z + if user_ids_from_passports: + for user_id in user_ids_from_passports: + authorized = flask.current_app.arborist.auth_request( + user_id=user_id, + service="fence", + methods=action, + resources=self.index_document["authz"], + ) + # if any passport provides access, user is authorized + if authorized: + return authorized + else: + try: + token = get_jwt() + except Unauthorized: + # get_jwt raises an Unauthorized error when user is anonymous (no + # availble token), so to allow anonymous users possible access to + # public data, we still make the request to Arborist + token = None + + return flask.current_app.arborist.auth_request( + jwt=token, + service="fence", + methods=action, + resources=self.index_document["authz"], + ) @cached_property def metadata(self): @@ -489,7 +515,7 @@ def public_authz(self): return "/open" in self.index_document.get("authz", []) @login_required({"data"}) - def check_authorization(self, action): + def check_authorization(self, action, user_ids_from_passports=None): # if we have a data file upload without corresponding metadata, the record can # have just the `uploader` field and no ACLs. in this just check that the # current user's username matches the uploader field @@ -504,8 +530,29 @@ def check_authorization(self, action): ) return self.index_document.get("uploader") == username - given_acls = set(filter_auth_ids(action, flask.g.user.project_access)) - return len(self.set_acls & given_acls) > 0 + # handle multiple GA4GH passports as a means of authn/z + project_accesses = [] + if user_ids_from_passports: + for user_id in user_ids_from_passports: + new_project_access = _get_project_access_for_user_id(user_id) + if new_project_access: + project_accesses.append(new_project_access) + + if not project_accesses: + # if we didn't get anything from passports, assume old JWT, get from flask context + project_accesses.append(flask.g.user.project_access) + + has_access = False + for project_access in project_accesses: + given_acls = set(filter_auth_ids(action, project_access)) + has_access = len(self.set_acls & given_acls) > 0 + + # if any of the project_access information results in a success, + # this user has access + if has_access: + break + + return has_access @login_required({"data"}) def delete_files(self, urls=None, delete_all=True): @@ -1181,3 +1228,8 @@ def filter_auth_ids(action, list_auth_ids): if checked_permission in values: authorized_dbgaps.append(key) return authorized_dbgaps + + +def _get_project_access_for_user_id(user_id): + # TODO + return {} diff --git a/fence/config-default.yaml b/fence/config-default.yaml index 300f1322e..dba735098 100644 --- a/fence/config-default.yaml +++ b/fence/config-default.yaml @@ -419,7 +419,7 @@ RENEW_ACCESS_TOKEN_BEFORE_EXPIRATION: false # The JSON field the GA4GH Passport is in when a request is POST-ed to DRS # We use the same field name for POSTs to /data/download for consistency -GA4GH_DRS_POSTED_PASSPORT_FIELD: "auth" +GA4GH_DRS_POSTED_PASSPORT_FIELD: "passports" ######################################################################################## diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py new file mode 100644 index 000000000..75b4b691f --- /dev/null +++ b/fence/resources/ga4gh/passports.py @@ -0,0 +1,94 @@ +def get_gen3_user_ids_from_ga4gh_passports(passports): + user_ids_from_passports = [] + + was_cached = False + raw_visas = [] + for passport in passports: + try: + # TODO check cache + cached_user_ids = get_gen3_user_ids_for_passport_from_cache(passport) + + if cached_user_ids: + # existence in the cache means that this passport was validated + # previously + user_ids_from_passports.extend(cached_user_ids) + was_cached = True + continue + + # below function also validates passport (or raises exception) + raw_visas.extend(get_unvalidated_visas_from_valid_passport(passport)) + except Exception as exc: + logger.warning(f"invalid passport provided, ignoring. Error: {exc}") + continue + + for raw_visa in raw_visas: + try: + # below function also validates visa (or raises exception) and + # extracts the subject id + subject_id, issuer = get_sub_iss_from_visa(raw_visa) + + # query idp user table + gen3_user = get_or_create_gen3_user_from_sub_iss(subject_id, issuer) + user_ids_from_passports.append(gen3_user.id) + + except Exception as exc: + logger.warning(f"invalid visa provided, ignoring. Error: {exc}") + continue + + # NOTE: does not validate, assumes validation occurs above. + sync_visa_authorization(raw_visa) + + if not was_cached: + put_gen3_user_ids_for_passport_into_cache(passport, user_ids_from_passports) + + return users_from_passports + + +def get_gen3_user_ids_for_passport_from_cache(passport): + cached_user_ids = [] + # TODO + return cached_user_ids + + +def get_unvalidated_visas_from_valid_passport(passport): + # validate passport, return visas + return [] + + +def is_raw_visa_valid(raw_visa): + # check signature + # is a type we recognize? + return False + + +def get_sub_iss_from_visa(raw_visa): + if not is_valid_visa(raw_visa): + raise Exception() + + subject_id = None + issuer = None + + # TODO + + return subject_id, issuer + + +def sync_valid_visa_authorization(visa): + # DOES NOT VALIDATE VISA + + # syncs authz to backend + + pass + + +def get_or_create_gen3_user_from_sub_iss(subject_id, issuer): + # TODO query idp user table, not there, create user and add row + return None + + +def sync_visa_authorization(raw_visa): + pass + + +def put_gen3_user_ids_for_passport_into_cache(passport, user_ids_from_passports): + pass From 3935fbe7b03d42c7cff9e751c6d6bac068cd83e9 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 27 Jul 2021 12:36:07 -0500 Subject: [PATCH 005/211] feat(passport): initial ideas for passport POST support --- .secrets.baseline | 17 ++++++++++------- fence/blueprints/data/blueprint.py | 20 ++++++++++++++++++-- fence/blueprints/data/indexd.py | 1 + fence/config-default.yaml | 5 +++++ 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 38d639ca4..29673c0d2 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -66,6 +66,10 @@ { "path": "detect_secrets.filters.allowlist.is_line_allowlisted" }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".secrets.baseline" + }, { "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", "min_level": 2 @@ -96,6 +100,12 @@ }, { "path": "detect_secrets.filters.heuristic.is_templated_secret" + }, + { + "path": "detect_secrets.filters.regex.should_exclude_file", + "pattern": [ + "poetry.lock" + ] } ], "results": { @@ -115,13 +125,6 @@ "hashed_secret": "98c144f5ecbb4dbe575147a39698b6be1a5649dd", "is_verified": false, "line_number": 66 - }, - { - "type": "Secret Keyword", - "filename": "fence/blueprints/storage_creds/other.py", - "hashed_secret": "98c144f5ecbb4dbe575147a39698b6be1a5649dd", - "is_verified": false, - "line_number": 66 } ], "fence/config-default.yaml": [ diff --git a/fence/blueprints/data/blueprint.py b/fence/blueprints/data/blueprint.py index 8b10e54ea..7ed7300ae 100755 --- a/fence/blueprints/data/blueprint.py +++ b/fence/blueprints/data/blueprint.py @@ -9,7 +9,8 @@ IndexedFile, get_signed_url_for_file, ) -from fence.errors import Forbidden, InternalError, UserError +from fence.config import config +from fence.errors import Forbidden, InternalError, UserError, Forbidden from fence.resources.audit.utils import enable_audit_logging from fence.utils import get_valid_expiration @@ -296,12 +297,27 @@ def upload_file(file_id): return flask.jsonify(result) -@blueprint.route("/download/", methods=["GET"]) +@blueprint.route("/download/", methods=["GET", "POST"]) @enable_audit_logging def download_file(file_id): """ Get a presigned url to download a file given by file_id. """ + if request.method == "POST": + passport = flask.request.get_json(force=True, silent=True).get( + config["GA4GH_DRS_POSTED_PASSPORT_FIELD"] + ) + + """ TODO + check cache + if not cache: + validate passport + parse visas + validate visas + update user table with visas + parse visa contents + """ + result = get_signed_url_for_file("download", file_id) if not "redirect" in flask.request.args or not "url" in result: return flask.jsonify(result) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index 9237a2e95..cddcd08ae 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -1385,6 +1385,7 @@ def _get_user_info(sub_type=str): populated information about an anonymous user. By default, cast `sub` to str. Use `sub_type` to override this behavior. """ + # TODO Update to support POSTed passport try: set_current_token( validate_request(scope={"user"}, audience=config.get("BASE_URL")) diff --git a/fence/config-default.yaml b/fence/config-default.yaml index 8c8e70c20..224016b77 100755 --- a/fence/config-default.yaml +++ b/fence/config-default.yaml @@ -423,6 +423,11 @@ RENEW_ACCESS_TOKEN_BEFORE_EXPIRATION: false # The maximum lifetime of a Gen3 passport in seconds GEN3_PASSPORT_EXPIRES_IN: 43200 +# The JSON field the GA4GH Passport is in when a request is POST-ed to DRS +# We use the same field name for POSTs to /data/download for consistency +GA4GH_DRS_POSTED_PASSPORT_FIELD: "auth" + + ######################################################################################## # OPTIONAL CONFIGURATIONS # ######################################################################################## From 7c29a1deb4ea74c75823ec3270b5348cc36c7f7d Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 27 Jul 2021 15:38:54 -0500 Subject: [PATCH 006/211] fix(passport): correctly get request --- fence/blueprints/data/blueprint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fence/blueprints/data/blueprint.py b/fence/blueprints/data/blueprint.py index 7ed7300ae..052f97395 100755 --- a/fence/blueprints/data/blueprint.py +++ b/fence/blueprints/data/blueprint.py @@ -303,7 +303,7 @@ def download_file(file_id): """ Get a presigned url to download a file given by file_id. """ - if request.method == "POST": + if flask.request.method == "POST": passport = flask.request.get_json(force=True, silent=True).get( config["GA4GH_DRS_POSTED_PASSPORT_FIELD"] ) From 30a816c0825160b36b79f9d26c04cbf30db5a0f3 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 27 Jul 2021 17:04:09 -0500 Subject: [PATCH 007/211] feat(utils): function to do a simple db -> mem cache --- fence/utils.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/fence/utils.py b/fence/utils.py index 8dc949f14..e510a0f74 100644 --- a/fence/utils.py +++ b/fence/utils.py @@ -246,8 +246,8 @@ def send_email(from_email, to_emails, subject, text, smtp_domain): "smtp_hostname": "smtp.mailgun.org", "default_login": "postmaster@mailgun.planx-pla.net", "api_url": "https://api.mailgun.net/v3/mailgun.planx-pla.net", - "smtp_password": "password", - "api_key": "api key" + "smtp_password": "password", # pragma: allowlist secret + "api_key": "api key" # pragma: allowlist secret } Returns: @@ -364,6 +364,35 @@ def _is_status(code): return False +def get_from_cache(item_id, memory_cache, db_cache_table, db_cache_table_id_field="id"): + """ + Attempt to get a cached item and store in memory cache from db if necessary. + + NOTE: This requires custom implementation for putting items in the db cache table. + """ + # try to retrieve from local in-memory cache + rv, expires_at = memory_cache.get(item_id, (None, 0)) + if expires_at > expiry: + return rv + + # try to retrieve from database cache + if hasattr(flask.current_app, "db"): # we don't have db in startup + with flask.current_app.db.session as session: + cache = ( + session.query(db_cache_table) + .filter( + getattr(db_cache_table, db_cache_table_id_field, None) == item_id + ) + .first() + ) + if cache and cache.expires_at and cache.expires_at > expiry: + rv = dict(cache) + + # store in memory cache + memory_cache[item_id] = rv, cache.expires_at + return rv + + # Default settings to control usage of backoff library. DEFAULT_BACKOFF_SETTINGS = { "on_backoff": log_backoff_retry, From 326ca968d9acd5c0821754e835f9c7d91b82b7d0 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 31 Aug 2021 13:45:54 -0500 Subject: [PATCH 008/211] feat(passports): more skeleton code for passports for authN/Z --- fence/blueprints/data/blueprint.py | 17 ++---- fence/blueprints/data/indexd.py | 96 +++++++++++++++++++++++------- fence/config-default.yaml | 2 +- fence/resources/ga4gh/passports.py | 94 +++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 35 deletions(-) create mode 100644 fence/resources/ga4gh/passports.py diff --git a/fence/blueprints/data/blueprint.py b/fence/blueprints/data/blueprint.py index 052f97395..2f9004089 100755 --- a/fence/blueprints/data/blueprint.py +++ b/fence/blueprints/data/blueprint.py @@ -303,22 +303,15 @@ def download_file(file_id): """ Get a presigned url to download a file given by file_id. """ + ga4gh_passports = None if flask.request.method == "POST": - passport = flask.request.get_json(force=True, silent=True).get( + ga4gh_passports = flask.request.get_json(force=True, silent=True).get( config["GA4GH_DRS_POSTED_PASSPORT_FIELD"] ) - """ TODO - check cache - if not cache: - validate passport - parse visas - validate visas - update user table with visas - parse visa contents - """ - - result = get_signed_url_for_file("download", file_id) + result = get_signed_url_for_file( + "download", file_id, ga4gh_passports=ga4gh_passports + ) if not "redirect" in flask.request.args or not "url" in result: return flask.jsonify(result) return flask.redirect(result["url"]) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index cddcd08ae..3e8b192f1 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -62,9 +62,9 @@ ANONYMOUS_USERNAME = "anonymous" -def get_signed_url_for_file(action, file_id, file_name=None, requested_protocol=None): +def get_signed_url_for_file(action, file_id, file_name=None, requested_protocol=None, ga4gh_passports=None): requested_protocol = requested_protocol or flask.request.args.get("protocol", None) - r_pays_project = flask.request.args.get("userProject", None) + r_pays_project = flask.request.args.get("userProject", None, requested_protocol=None) # default to signing the url even if it's a public object # this will work so long as we're provided a user token @@ -73,6 +73,12 @@ def get_signed_url_for_file(action, file_id, file_name=None, requested_protocol= if no_force_sign_param and no_force_sign_param.lower() == "true": force_signed_url = False + user_ids_from_passports = None + if ga4gh_passports: + user_ids_from_passports = get_gen3_user_ids_from_ga4gh_passports( + ga4gh_passports + ) + # add the user details to `flask.g.audit_data` first, so they are # included in the audit log if `IndexedFile(file_id)` raises a 404 user_info = _get_user_info(sub_type=int) @@ -97,6 +103,7 @@ def get_signed_url_for_file(action, file_id, file_name=None, requested_protocol= force_signed_url=force_signed_url, r_pays_project=r_pays_project, file_name=file_name, + user_ids_from_passports=user_ids_from_passports, ) # increment counter for gen3-metrics @@ -394,13 +401,17 @@ def get_signed_url( force_signed_url=True, r_pays_project=None, file_name=None, + user_ids_from_passports=None, ): if self.index_document.get("authz"): action_to_permission = { "upload": "write-storage", "download": "read-storage", } - if not self.check_authz(action_to_permission[action]): + if not self.check_authz( + action_to_permission[action], + user_ids_from_passports=user_ids_from_passports, + ): raise Unauthorized( f"Either you weren't logged in or you don't have " f"{action_to_permission[action]} permission " @@ -413,7 +424,9 @@ def get_signed_url( ) # don't check the authorization if the file is public # (downloading public files with no auth is fine) - if not self.public_acl and not self.check_authorization(action): + if not self.public_acl and not self.check_authorization( + action, user_ids_from_passports=user_ids_from_passports + ): raise Unauthorized( f"You don't have access permission on this file: {self.file_id}" ) @@ -476,7 +489,7 @@ def set_acls(self): else: raise Unauthorized("This file is not accessible") - def check_authz(self, action): + def check_authz(self, action, user_ids_from_passports=None): if not self.index_document.get("authz"): raise ValueError("index record missing `authz`") @@ -484,20 +497,33 @@ def check_authz(self, action): f"authz check can user {action} on {self.index_document['authz']} for fence?" ) - try: - token = get_jwt() - except Unauthorized: - # get_jwt raises an Unauthorized error when user is anonymous (no - # availble token), so to allow anonymous users possible access to - # public data, we still make the request to Arborist - token = None - - return flask.current_app.arborist.auth_request( - jwt=token, - service="fence", - methods=action, - resources=self.index_document["authz"], - ) + # handle multiple GA4GH passports as a means of authn/z + if user_ids_from_passports: + for user_id in user_ids_from_passports: + authorized = flask.current_app.arborist.auth_request( + user_id=user_id, + service="fence", + methods=action, + resources=self.index_document["authz"], + ) + # if any passport provides access, user is authorized + if authorized: + return authorized + else: + try: + token = get_jwt() + except Unauthorized: + # get_jwt raises an Unauthorized error when user is anonymous (no + # availble token), so to allow anonymous users possible access to + # public data, we still make the request to Arborist + token = None + + return flask.current_app.arborist.auth_request( + jwt=token, + service="fence", + methods=action, + resources=self.index_document["authz"], + ) @cached_property def metadata(self): @@ -519,7 +545,7 @@ def public_authz(self): return "/open" in self.index_document.get("authz", []) @login_required({"data"}) - def check_authorization(self, action): + def check_authorization(self, action, user_ids_from_passports=None): # if we have a data file upload without corresponding metadata, the record can # have just the `uploader` field and no ACLs. in this just check that the # current user's username matches the uploader field @@ -534,8 +560,29 @@ def check_authorization(self, action): ) return self.index_document.get("uploader") == username - given_acls = set(filter_auth_ids(action, flask.g.user.project_access)) - return len(self.set_acls & given_acls) > 0 + # handle multiple GA4GH passports as a means of authn/z + project_accesses = [] + if user_ids_from_passports: + for user_id in user_ids_from_passports: + new_project_access = _get_project_access_for_user_id(user_id) + if new_project_access: + project_accesses.append(new_project_access) + + if not project_accesses: + # if we didn't get anything from passports, assume old JWT, get from flask context + project_accesses.append(flask.g.user.project_access) + + has_access = False + for project_access in project_accesses: + given_acls = set(filter_auth_ids(action, project_access)) + has_access = len(self.set_acls & given_acls) > 0 + + # if any of the project_access information results in a success, + # this user has access + if has_access: + break + + return has_access @login_required({"data"}) def delete_files(self, urls=None, delete_all=True): @@ -1423,3 +1470,8 @@ def filter_auth_ids(action, list_auth_ids): if checked_permission in values: authorized_dbgaps.append(key) return authorized_dbgaps + + +def _get_project_access_for_user_id(user_id): + # TODO + return {} diff --git a/fence/config-default.yaml b/fence/config-default.yaml index 224016b77..117c40abe 100755 --- a/fence/config-default.yaml +++ b/fence/config-default.yaml @@ -425,7 +425,7 @@ GEN3_PASSPORT_EXPIRES_IN: 43200 # The JSON field the GA4GH Passport is in when a request is POST-ed to DRS # We use the same field name for POSTs to /data/download for consistency -GA4GH_DRS_POSTED_PASSPORT_FIELD: "auth" +GA4GH_DRS_POSTED_PASSPORT_FIELD: "passports" ######################################################################################## diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py new file mode 100644 index 000000000..75b4b691f --- /dev/null +++ b/fence/resources/ga4gh/passports.py @@ -0,0 +1,94 @@ +def get_gen3_user_ids_from_ga4gh_passports(passports): + user_ids_from_passports = [] + + was_cached = False + raw_visas = [] + for passport in passports: + try: + # TODO check cache + cached_user_ids = get_gen3_user_ids_for_passport_from_cache(passport) + + if cached_user_ids: + # existence in the cache means that this passport was validated + # previously + user_ids_from_passports.extend(cached_user_ids) + was_cached = True + continue + + # below function also validates passport (or raises exception) + raw_visas.extend(get_unvalidated_visas_from_valid_passport(passport)) + except Exception as exc: + logger.warning(f"invalid passport provided, ignoring. Error: {exc}") + continue + + for raw_visa in raw_visas: + try: + # below function also validates visa (or raises exception) and + # extracts the subject id + subject_id, issuer = get_sub_iss_from_visa(raw_visa) + + # query idp user table + gen3_user = get_or_create_gen3_user_from_sub_iss(subject_id, issuer) + user_ids_from_passports.append(gen3_user.id) + + except Exception as exc: + logger.warning(f"invalid visa provided, ignoring. Error: {exc}") + continue + + # NOTE: does not validate, assumes validation occurs above. + sync_visa_authorization(raw_visa) + + if not was_cached: + put_gen3_user_ids_for_passport_into_cache(passport, user_ids_from_passports) + + return users_from_passports + + +def get_gen3_user_ids_for_passport_from_cache(passport): + cached_user_ids = [] + # TODO + return cached_user_ids + + +def get_unvalidated_visas_from_valid_passport(passport): + # validate passport, return visas + return [] + + +def is_raw_visa_valid(raw_visa): + # check signature + # is a type we recognize? + return False + + +def get_sub_iss_from_visa(raw_visa): + if not is_valid_visa(raw_visa): + raise Exception() + + subject_id = None + issuer = None + + # TODO + + return subject_id, issuer + + +def sync_valid_visa_authorization(visa): + # DOES NOT VALIDATE VISA + + # syncs authz to backend + + pass + + +def get_or_create_gen3_user_from_sub_iss(subject_id, issuer): + # TODO query idp user table, not there, create user and add row + return None + + +def sync_visa_authorization(raw_visa): + pass + + +def put_gen3_user_ids_for_passport_into_cache(passport, user_ids_from_passports): + pass From 4fb730381ab76b7f23544f64ef91101a8fa47eac Mon Sep 17 00:00:00 2001 From: BinamB Date: Wed, 22 Sep 2021 13:16:57 -0500 Subject: [PATCH 009/211] resolve comments --- fence/blueprints/data/indexd.py | 1 + fence/blueprints/ga4gh.py | 1 + fence/blueprints/login/ras.py | 2 +- fence/resources/openid/idp_oauth2.py | 5 ++++- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index 6a336dc66..bae9be202 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -69,6 +69,7 @@ def get_signed_url_for_file( r_pays_project = flask.request.args.get("userProject", None) # default to signing the url even if it's a public object # this will work so long as we're provided a user token + force_signed_url = True no_force_sign_param = flask.request.args.get("no_force_sign") if no_force_sign_param and no_force_sign_param.lower() == "true": diff --git a/fence/blueprints/ga4gh.py b/fence/blueprints/ga4gh.py index 60f3afe47..c02ba4f7a 100644 --- a/fence/blueprints/ga4gh.py +++ b/fence/blueprints/ga4gh.py @@ -20,6 +20,7 @@ def get_ga4gh_signed_url(object_id, access_id): if not access_id: raise UserError("Access ID/Protocol is required.") + result = get_signed_url_for_file( "download", object_id, requested_protocol=access_id ) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index b348be78f..dfc0630b9 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -149,7 +149,7 @@ def post_login(self, user=None, token_result=None): try: # map user to idp flask.current_app.ras_client.map_user_idp_info( - user, userinfo.get("sub"), "ras" + user, userinfo.get("sub"), IdentityProvider.ras ) except Exception as e: logger.error("Could not store user and idp info: {}".format(e)) diff --git a/fence/resources/openid/idp_oauth2.py b/fence/resources/openid/idp_oauth2.py index f793ae2b4..ecfcf88b1 100644 --- a/fence/resources/openid/idp_oauth2.py +++ b/fence/resources/openid/idp_oauth2.py @@ -3,7 +3,7 @@ from jose import jwt import requests import time -from fence.errors import AuthError +from fence.errors import AuthError, UserError from fence.models import UpstreamRefreshToken, IdPUser, IdentityProvider from flask_sqlalchemy_session import current_session @@ -197,6 +197,9 @@ def map_user_idp_info(self, user, idp_sub, provider, extra_info=None): .filter(IdentityProvider.name == provider) .first() ) + if not idp: + raise UserError("IdP: {} not implemented".format(provider)) + idp_id = idp.id user_id = user.id From 73b5a720104e9ac250f93542745619cd94b00e0f Mon Sep 17 00:00:00 2001 From: BinamB Date: Thu, 23 Sep 2021 11:26:33 -0500 Subject: [PATCH 010/211] handle no idp --- fence/blueprints/data/indexd.py | 2 +- fence/blueprints/login/base.py | 37 ++++++++++++++++++++++++++++ fence/blueprints/login/ras.py | 4 +-- fence/resources/openid/idp_oauth2.py | 30 ---------------------- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index bae9be202..ebae2f2fa 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -67,9 +67,9 @@ def get_signed_url_for_file( ): requested_protocol = requested_protocol or flask.request.args.get("protocol", None) r_pays_project = flask.request.args.get("userProject", None) + # default to signing the url even if it's a public object # this will work so long as we're provided a user token - force_signed_url = True no_force_sign_param = flask.request.args.get("no_force_sign") if no_force_sign_param and no_force_sign_param.lower() == "true": diff --git a/fence/blueprints/login/base.py b/fence/blueprints/login/base.py index 61ce1ca44..eaab01438 100644 --- a/fence/blueprints/login/base.py +++ b/fence/blueprints/login/base.py @@ -6,6 +6,7 @@ from fence.blueprints.login.redirect import validate_redirect from fence.config import config from fence.errors import UserError +from fence.models import IdentityProvider, IdPUser class DefaultOAuth2Login(Resource): @@ -110,6 +111,42 @@ def get(self): def post_login(self, user=None, token_result=None): prepare_login_log(self.idp_name) + def map_user_idp_info(self, user, idp_sub, provider, extra_info=None): + """ + Map user to idp. + Args: + user (User): User object + idp_sub (str): sub provided by the IdP + provider (str): name of the Identity Provider as seen on db + extra_info (dict): any info sent by the IdP that could be useful + """ + idp = ( + current_session.query(IdentityProvider) + .filter(IdentityProvider.name == provider) + .first() + ) + if not idp: + # this is for cases where we might receive a passport that we haven't seen before. + # add to db if we haven't seen this before. + idp = IdentityProvider( + name=provider, description="IdP from foreign Passport" + ) + current_session.add(idp) + current_session.commit() + + idp_id = idp.id + user_id = user.id + + user_to_idp = IdPUser( + sub=idp_sub, + fk_to_idp=idp_id, + fk_to_User=user_id, + extra_info=extra_info, + ) + + current_session.add(user_to_idp) + current_session.commit() + def prepare_login_log(idp_name): flask.g.audit_data = { diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index dfc0630b9..8e81724cc 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -148,9 +148,7 @@ def post_login(self, user=None, token_result=None): if not user.idp_to_users: try: # map user to idp - flask.current_app.ras_client.map_user_idp_info( - user, userinfo.get("sub"), IdentityProvider.ras - ) + self.map_user_idp_info(user, userinfo.get("sub"), IdentityProvider.ras) except Exception as e: logger.error("Could not store user and idp info: {}".format(e)) diff --git a/fence/resources/openid/idp_oauth2.py b/fence/resources/openid/idp_oauth2.py index ecfcf88b1..a7bb8423a 100644 --- a/fence/resources/openid/idp_oauth2.py +++ b/fence/resources/openid/idp_oauth2.py @@ -182,33 +182,3 @@ def store_refresh_token( current_db_session = db_session.object_session(upstream_refresh_token) current_db_session.add(upstream_refresh_token) db_session.commit() - - def map_user_idp_info(self, user, idp_sub, provider, extra_info=None): - """ - Map user to idp. - Args: - user (User): User object - idp_sub (str): sub provided by the IdP - provider (str): name of the Identity Provider as seen on db - extra_info (dict): any info sent by the IdP that could be useful - """ - idp = ( - current_session.query(IdentityProvider) - .filter(IdentityProvider.name == provider) - .first() - ) - if not idp: - raise UserError("IdP: {} not implemented".format(provider)) - - idp_id = idp.id - user_id = user.id - - user_to_idp = IdPUser( - sub=idp_sub, - fk_to_idp=idp_id, - fk_to_User=user_id, - extra_info=extra_info, - ) - - current_session.add(user_to_idp) - current_session.commit() From 7d8483a44d9da9a8adf8181f7ed546d0596990e0 Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 24 Sep 2021 13:23:20 -0500 Subject: [PATCH 011/211] handle idp not found --- fence/blueprints/login/base.py | 4 +++- fence/blueprints/login/ras.py | 6 +++++- tests/test_audit_service.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/fence/blueprints/login/base.py b/fence/blueprints/login/base.py index eaab01438..4af1dfc75 100644 --- a/fence/blueprints/login/base.py +++ b/fence/blueprints/login/base.py @@ -111,7 +111,9 @@ def get(self): def post_login(self, user=None, token_result=None): prepare_login_log(self.idp_name) - def map_user_idp_info(self, user, idp_sub, provider, extra_info=None): + def map_user_idp_info( + self, user, idp_sub, provider, current_session, extra_info=None + ): """ Map user to idp. Args: diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index 8e81724cc..5a1bb966a 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -118,6 +118,7 @@ def post_login(self, user=None, token_result=None): ) current_session.add(visa) current_session.commit() + current_session.close() # Store refresh token in db assert "refresh_token" in flask.g.tokens, "No refresh_token in user tokens" @@ -148,8 +149,11 @@ def post_login(self, user=None, token_result=None): if not user.idp_to_users: try: # map user to idp - self.map_user_idp_info(user, userinfo.get("sub"), IdentityProvider.ras) + self.map_user_idp_info( + user, userinfo.get("sub"), IdentityProvider.ras, current_session + ) except Exception as e: + current_session.rollback() logger.error("Could not store user and idp info: {}".format(e)) global_parse_visas_on_login = config["GLOBAL_PARSE_VISAS_ON_LOGIN"] diff --git a/tests/test_audit_service.py b/tests/test_audit_service.py index f7d6ac60c..15923c6e9 100644 --- a/tests/test_audit_service.py +++ b/tests/test_audit_service.py @@ -444,7 +444,7 @@ def test_login_log_login_endpoint( get_user_id_value = {"username": username} endpoint = "callback" # these should be populated by a /login/ call that we're skipping: - flask.g.userinfo = {} + flask.g.userinfo = {"sub": "testSub123"} flask.g.tokens = { "refresh_token": jwt_string, "id_token": jwt_string, From 9dbc73c758c07c89de0a52a12eeeaca1f5df51b7 Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 24 Sep 2021 13:28:14 -0500 Subject: [PATCH 012/211] unused imports --- fence/resources/openid/idp_oauth2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fence/resources/openid/idp_oauth2.py b/fence/resources/openid/idp_oauth2.py index a7bb8423a..ae0e3ec8b 100644 --- a/fence/resources/openid/idp_oauth2.py +++ b/fence/resources/openid/idp_oauth2.py @@ -3,8 +3,8 @@ from jose import jwt import requests import time -from fence.errors import AuthError, UserError -from fence.models import UpstreamRefreshToken, IdPUser, IdentityProvider +from fence.errors import AuthError +from fence.models import UpstreamRefreshToken from flask_sqlalchemy_session import current_session From 2a1ca84b08826e717da4dca0e94be26941c0fcb0 Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 24 Sep 2021 13:43:39 -0500 Subject: [PATCH 013/211] black + fix session --- fence/blueprints/login/ras.py | 1 - fence/resources/openid/idp_oauth2.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index 5a1bb966a..290ae5ba6 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -118,7 +118,6 @@ def post_login(self, user=None, token_result=None): ) current_session.add(visa) current_session.commit() - current_session.close() # Store refresh token in db assert "refresh_token" in flask.g.tokens, "No refresh_token in user tokens" diff --git a/fence/resources/openid/idp_oauth2.py b/fence/resources/openid/idp_oauth2.py index ae0e3ec8b..0a1931ee0 100644 --- a/fence/resources/openid/idp_oauth2.py +++ b/fence/resources/openid/idp_oauth2.py @@ -4,7 +4,7 @@ import requests import time from fence.errors import AuthError -from fence.models import UpstreamRefreshToken +from fence.models import UpstreamRefreshToken from flask_sqlalchemy_session import current_session From 2ea9e5c116126e777f335941c457eaa0120a4a58 Mon Sep 17 00:00:00 2001 From: BinamB Date: Mon, 27 Sep 2021 14:22:07 -0500 Subject: [PATCH 014/211] move functions to ga4gh --- fence/resources/ga4gh/passports.py | 164 +++++++++++++++++++++++++++ fence/resources/openid/ras_oauth2.py | 141 +---------------------- 2 files changed, 167 insertions(+), 138 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 75b4b691f..36994c04e 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -1,3 +1,17 @@ +import base64 + +from authutils.errors import JWTError +from authutils.token.core import get_iss, get_keys_url, get_kid, validate_jwt +from authutils.token.keys import get_public_key_for_token +from cdislogging import get_logger +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization + +from fence.config import config + +logger = get_logger(__name__, log_level="debug") + def get_gen3_user_ids_from_ga4gh_passports(passports): user_ids_from_passports = [] @@ -92,3 +106,153 @@ def sync_visa_authorization(raw_visa): def put_gen3_user_ids_for_passport_into_cache(passport, user_ids_from_passports): pass + +def validate_single_passport(encoded_passport, pkey_cache=None): + decoded_passport = {} + passport_issuer, passport_kid = None, None + + if not pkey_cache: + pkey_cache = {} + + try: + passport_issuer = get_iss(encoded_passport) + passport_kid = get_kid(encoded_passport) + except Exception as e: + logger.error( + "Could not get issuer or kid from passport: {}. Discarding passport.".format( + e + ) + ) + + public_key = pkey_cache.get(passport_issuer, {}).get(passport_kid) + if not public_key: + try: + logger.info("Fetching public key from flask app...") + public_key = get_public_key_for_token( + encoded_passport, attempt_refresh=True + ) + except Exception as e: + logger.info( + "Could not fetch public key from flask app to validate passport: {}. Trying to fetch from source.".format( + e + ) + ) + try: + logger.info("Trying to Fetch public keys from JWKs url...") + public_key = refresh_cronjob_pkey_cache( + passport_issuer, passport_kid, pkey_cache + ) + except Exception as e: + logger.info( + "Could not fetch public key from JWKs key url: {}".format(e) + ) + if not public_key: + logger.error( + "Could not fetch public key to validate visa: Successfully fetched " + "issuer's keys but did not find the visa's key id among them. Discarding visa." + ) + try: + decoded_passport = validate_jwt( + encoded_passport, + public_key, + aud=None, + scope={"openid"}, + issuers=config.get("GA4GH_VISA_ISSUER_ALLOWLIST", []), + options={ + "require_iat": True, + "require_exp": True, + }, + ) + except Exception as e: + logger.error( + "Passport failed validation: {}. Discarding passport.".format(e) + ) + return decoded_passport.get("ga4gh_passport_v1", []) + + + +def validate_multiple_passports(passports): + """ + Validate multuple passports being sent to fence through POST /ga4gh/drs/v1/objects//access/ endpoints + + Args: + passports(list): list of encoded passports + """ + pass + +def validate_multiple_visas(visas): + pass + + +def refresh_cronjob_pkey_cache(issuer, kid, pkey_cache): + """ + Update app public key cache for a specific Passport Visa issuer + + Args: + issuer(str): Passport Visa issuer. Can be found under `issuer` in a Passport or a Visa + kid(str): Passsport Visa kid. Can be found in the header of an encoded Passport or encoded Visa + pkey_cache (dict): app cache of public keys_dir + + Return: + dict: public key for given issuer + """ + jwks_url = get_keys_url(issuer) + try: + jwt_public_keys = httpx.get(jwks_url).json()["keys"] + except Exception as e: + raise JWTError( + "Could not get public key to validate Passport/Visa: Could not fetch keys from JWKs url: {}".format( + e + ) + ) + + issuer_public_keys = {} + try: + for key in jwt_public_keys: + if "kty" in key and key["kty"] == "RSA": + logger.debug( + "Serializing RSA public key (kid: {}) to PEM format.".format( + key["kid"] + ) + ) + # Decode public numbers https://tools.ietf.org/html/rfc7518#section-6.3.1 + n_padded_bytes = base64.urlsafe_b64decode( + key["n"] + "=" * (4 - len(key["n"]) % 4) + ) + e_padded_bytes = base64.urlsafe_b64decode( + key["e"] + "=" * (4 - len(key["e"]) % 4) + ) + n = int.from_bytes(n_padded_bytes, "big", signed=False) + e = int.from_bytes(e_padded_bytes, "big", signed=False) + # Serialize and encode public key--PyJWT decode/validation requires PEM + rsa_public_key = rsa.RSAPublicNumbers(e, n).public_key( + default_backend() + ) + public_bytes = rsa_public_key.public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + # Cache the encoded key by issuer + issuer_public_keys[key["kid"]] = public_bytes + else: + logger.debug( + "Key type (kty) is not 'RSA'; assuming PEM format. " + "Skipping key serialization. (kid: {})".format(key[0]) + ) + issuer_public_keys[key[0]] = key[1] + + pkey_cache.update({issuer: issuer_public_keys}) + logger.info( + "Refreshed cronjob pkey cache for Passport/Visa issuer {}".format( + issuer + ) + ) + except Exception as e: + logger.error( + "Could not refresh cronjob pkey cache for issuer {}: " + "Something went wrong during serialization: {}. Discarding Passport/Visa.".format( + issuer, e + ) + ) + + return pkey_cache.get(issuer, {}).get(kid) \ No newline at end of file diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 58120ad83..bb0b999da 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -8,13 +8,10 @@ from authutils.errors import JWTError from authutils.token.core import get_iss, get_keys_url, get_kid, validate_jwt -from authutils.token.keys import get_public_key_for_token -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import rsa from fence.config import config from fence.models import GA4GHVisaV1 +from fence.resources.ga4gh.passports import validate_single_passport from fence.utils import DEFAULT_BACKOFF_SETTINGS from .idp_oauth2 import Oauth2ClientBase @@ -78,7 +75,7 @@ def get_userinfo(self, token): def get_encoded_visas_v11_userinfo(self, userinfo, pkey_cache=None): """ - Return encoded visas after extracting and validating passport from userinfo respoonse + Return encoded visas after extracting and validating passport from userinfo response Args: userinfo (dict): userinfo response @@ -87,67 +84,8 @@ def get_encoded_visas_v11_userinfo(self, userinfo, pkey_cache=None): Return: list: list of encoded GA4GH visas """ - decoded_passport = {} encoded_passport = userinfo.get("passport_jwt_v11") - passport_issuer, passport_kid = None, None - - if not pkey_cache: - pkey_cache = {} - - try: - passport_issuer = get_iss(encoded_passport) - passport_kid = get_kid(encoded_passport) - except Exception as e: - self.logger.error( - "Could not get issuer or kid from passport: {}. Discarding passport.".format( - e - ) - ) - - public_key = pkey_cache.get(passport_issuer, {}).get(passport_kid) - if not public_key: - try: - self.logger.info("Fetching public key from flask app...") - public_key = get_public_key_for_token( - encoded_passport, attempt_refresh=True - ) - except Exception as e: - self.logger.info( - "Could not fetch public key from flask app to validate passport: {}. Trying to fetch from source.".format( - e - ) - ) - try: - self.logger.info("Trying to Fetch public keys from JWKs url...") - public_key = self.refresh_cronjob_pkey_cache( - passport_issuer, passport_kid, pkey_cache - ) - except Exception as e: - self.logger.info( - "Could not fetch public key from JWKs key url: {}".format(e) - ) - if not public_key: - self.logger.error( - "Could not fetch public key to validate visa: Successfully fetched " - "issuer's keys but did not find the visa's key id among them. Discarding visa." - ) - try: - decoded_passport = validate_jwt( - encoded_passport, - public_key, - aud=None, - scope={"openid"}, - issuers=config.get("GA4GH_VISA_ISSUER_ALLOWLIST", []), - options={ - "require_iat": True, - "require_exp": True, - }, - ) - except Exception as e: - self.logger.error( - "Passport failed validation: {}. Discarding passport.".format(e) - ) - return decoded_passport.get("ga4gh_passport_v1", []) + return validate_single_passport(encoded_passport, pkey_cache) def get_user_id(self, code): @@ -209,79 +147,6 @@ def get_user_id(self, code): return {"username": username, "email": userinfo.get("email")} - def refresh_cronjob_pkey_cache(self, issuer, kid, pkey_cache): - """ - Update app public key cache for a specific Passport Visa issuer - - Args: - issuer(str): Passport Visa issuer. Can be found under `issuer` in a Passport or a Visa - kid(str): Passsport Visa kid. Can be found in the header of an encoded Passport or encoded Visa - pkey_cache (dict): app cache of public keys_dir - - Return: - dict: public key for given issuer - """ - jwks_url = get_keys_url(issuer) - try: - jwt_public_keys = httpx.get(jwks_url).json()["keys"] - except Exception as e: - raise JWTError( - "Could not get public key to validate Passport/Visa: Could not fetch keys from JWKs url: {}".format( - e - ) - ) - - issuer_public_keys = {} - try: - for key in jwt_public_keys: - if "kty" in key and key["kty"] == "RSA": - self.logger.debug( - "Serializing RSA public key (kid: {}) to PEM format.".format( - key["kid"] - ) - ) - # Decode public numbers https://tools.ietf.org/html/rfc7518#section-6.3.1 - n_padded_bytes = base64.urlsafe_b64decode( - key["n"] + "=" * (4 - len(key["n"]) % 4) - ) - e_padded_bytes = base64.urlsafe_b64decode( - key["e"] + "=" * (4 - len(key["e"]) % 4) - ) - n = int.from_bytes(n_padded_bytes, "big", signed=False) - e = int.from_bytes(e_padded_bytes, "big", signed=False) - # Serialize and encode public key--PyJWT decode/validation requires PEM - rsa_public_key = rsa.RSAPublicNumbers(e, n).public_key( - default_backend() - ) - public_bytes = rsa_public_key.public_bytes( - serialization.Encoding.PEM, - serialization.PublicFormat.SubjectPublicKeyInfo, - ) - # Cache the encoded key by issuer - issuer_public_keys[key["kid"]] = public_bytes - else: - self.logger.debug( - "Key type (kty) is not 'RSA'; assuming PEM format. " - "Skipping key serialization. (kid: {})".format(key[0]) - ) - issuer_public_keys[key[0]] = key[1] - - pkey_cache.update({issuer: issuer_public_keys}) - self.logger.info( - "Refreshed cronjob pkey cache for Passport/Visa issuer {}".format( - issuer - ) - ) - except Exception as e: - self.logger.error( - "Could not refresh cronjob pkey cache for issuer {}: " - "Something went wrong during serialization: {}. Discarding Passport/Visa.".format( - issuer, e - ) - ) - - return pkey_cache.get(issuer, {}).get(kid) - @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) def update_user_visas(self, user, pkey_cache, db_session=current_session): """ From 6e36b6e40f07da2c0e030fe938b7830dd76976a8 Mon Sep 17 00:00:00 2001 From: BinamB Date: Mon, 27 Sep 2021 14:22:17 -0500 Subject: [PATCH 015/211] fix function --- fence/resources/openid/ras_oauth2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index bb0b999da..b4d12cc39 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -11,7 +11,7 @@ from fence.config import config from fence.models import GA4GHVisaV1 -from fence.resources.ga4gh.passports import validate_single_passport +from fence.resources.ga4gh.passports import validate_single_passport, refresh_cronjob_pkey_cache from fence.utils import DEFAULT_BACKOFF_SETTINGS from .idp_oauth2 import Oauth2ClientBase @@ -189,7 +189,7 @@ def update_user_visas(self, user, pkey_cache, db_session=current_session): public_key = pkey_cache.get(visa_issuer, {}).get(visa_kid) if not public_key: try: - public_key = self.refresh_cronjob_pkey_cache( + public_key = refresh_cronjob_pkey_cache( visa_issuer, visa_kid, pkey_cache ) except Exception as e: From 22d05a96fc0f5a0a12d60808304474010f7e48fa Mon Sep 17 00:00:00 2001 From: BinamB Date: Mon, 27 Sep 2021 14:45:14 -0500 Subject: [PATCH 016/211] fix imports --- fence/resources/ga4gh/passports.py | 1 + fence/resources/openid/ras_oauth2.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 36994c04e..1812fe841 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -1,4 +1,5 @@ import base64 +import httpx from authutils.errors import JWTError from authutils.token.core import get_iss, get_keys_url, get_kid, validate_jwt diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index b4d12cc39..0143f77a2 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -1,7 +1,5 @@ import backoff -import base64 import flask -import httpx import requests from flask_sqlalchemy_session import current_session from jose import jwt as jose_jwt From 8fa29caa9bbd16bbc2966d9203166dddaef4995a Mon Sep 17 00:00:00 2001 From: BinamB Date: Tue, 28 Sep 2021 13:42:53 -0500 Subject: [PATCH 017/211] cleanup --- fence/blueprints/ga4gh.py | 5 +- fence/resources/ga4gh/passports.py | 140 ++++++++++++--------------- fence/resources/openid/ras_oauth2.py | 7 +- 3 files changed, 73 insertions(+), 79 deletions(-) diff --git a/fence/blueprints/ga4gh.py b/fence/blueprints/ga4gh.py index c02ba4f7a..5d3b9eeb5 100644 --- a/fence/blueprints/ga4gh.py +++ b/fence/blueprints/ga4gh.py @@ -18,10 +18,13 @@ methods=["GET", "POST"], ) def get_ga4gh_signed_url(object_id, access_id): + + passports = flask.request.args.get("passports") + if not access_id: raise UserError("Access ID/Protocol is required.") result = get_signed_url_for_file( - "download", object_id, requested_protocol=access_id + "download", object_id, requested_protocol=access_id, ga4gh_passports=passports, ) return flask.jsonify(result) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 1812fe841..d0be7caf8 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -13,6 +13,7 @@ logger = get_logger(__name__, log_level="debug") + def get_gen3_user_ids_from_ga4gh_passports(passports): user_ids_from_passports = [] @@ -65,59 +66,26 @@ def get_gen3_user_ids_for_passport_from_cache(passport): return cached_user_ids -def get_unvalidated_visas_from_valid_passport(passport): - # validate passport, return visas - return [] - - -def is_raw_visa_valid(raw_visa): - # check signature - # is a type we recognize? - return False - - -def get_sub_iss_from_visa(raw_visa): - if not is_valid_visa(raw_visa): - raise Exception() - - subject_id = None - issuer = None - - # TODO - - return subject_id, issuer - - -def sync_valid_visa_authorization(visa): - # DOES NOT VALIDATE VISA - - # syncs authz to backend - - pass - - -def get_or_create_gen3_user_from_sub_iss(subject_id, issuer): - # TODO query idp user table, not there, create user and add row - return None - - -def sync_visa_authorization(raw_visa): - pass - +def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): + """ + Return encoded visas after extracting and validating encoded passport -def put_gen3_user_ids_for_passport_into_cache(passport, user_ids_from_passports): - pass + Args: + encoded_passport (string): encoded ga4gh passport + pkey_cache (dict): app cache of public keys_dir -def validate_single_passport(encoded_passport, pkey_cache=None): + Return: + list: list of encoded GA4GH visas + """ decoded_passport = {} passport_issuer, passport_kid = None, None if not pkey_cache: pkey_cache = {} - + try: - passport_issuer = get_iss(encoded_passport) - passport_kid = get_kid(encoded_passport) + passport_issuer = get_iss(passport) + passport_kid = get_kid(passport) except Exception as e: logger.error( "Could not get issuer or kid from passport: {}. Discarding passport.".format( @@ -125,28 +93,28 @@ def validate_single_passport(encoded_passport, pkey_cache=None): ) ) - public_key = pkey_cache.get(passport_issuer, {}).get(passport_kid) + public_key = pkey_cache.get(passport_issuer, {}).get(passport_kid) if not public_key: + try: + logger.info("Fetching public key from flask app...") + public_key = get_public_key_for_token( + passport, attempt_refresh=True + ) + except Exception as e: + logger.info( + "Could not fetch public key from flask app to validate passport: {}. Trying to fetch from source.".format( + e + ) + ) try: - logger.info("Fetching public key from flask app...") - public_key = get_public_key_for_token( - encoded_passport, attempt_refresh=True + logger.info("Trying to Fetch public keys from JWKs url...") + public_key = refresh_cronjob_pkey_cache( + passport_issuer, passport_kid, pkey_cache ) except Exception as e: logger.info( - "Could not fetch public key from flask app to validate passport: {}. Trying to fetch from source.".format( - e - ) + "Could not fetch public key from JWKs key url: {}".format(e) ) - try: - logger.info("Trying to Fetch public keys from JWKs url...") - public_key = refresh_cronjob_pkey_cache( - passport_issuer, passport_kid, pkey_cache - ) - except Exception as e: - logger.info( - "Could not fetch public key from JWKs key url: {}".format(e) - ) if not public_key: logger.error( "Could not fetch public key to validate visa: Successfully fetched " @@ -154,7 +122,7 @@ def validate_single_passport(encoded_passport, pkey_cache=None): ) try: decoded_passport = validate_jwt( - encoded_passport, + passport, public_key, aud=None, scope={"openid"}, @@ -165,26 +133,48 @@ def validate_single_passport(encoded_passport, pkey_cache=None): }, ) except Exception as e: - logger.error( - "Passport failed validation: {}. Discarding passport.".format(e) - ) + logger.error("Passport failed validation: {}. Discarding passport.".format(e)) return decoded_passport.get("ga4gh_passport_v1", []) +def is_raw_visa_valid(raw_visa): + # check signature + # is a type we recognize? + return False + -def validate_multiple_passports(passports): - """ - Validate multuple passports being sent to fence through POST /ga4gh/drs/v1/objects//access/ endpoints +def get_sub_iss_from_visa(raw_visa): + if not is_valid_visa(raw_visa): + raise Exception() + + subject_id = None + issuer = None + + # TODO + + return subject_id, issuer + + +def sync_valid_visa_authorization(visa): + # DOES NOT VALIDATE VISA + + # syncs authz to backend - Args: - passports(list): list of encoded passports - """ pass -def validate_multiple_visas(visas): + +def get_or_create_gen3_user_from_sub_iss(subject_id, issuer): + # TODO query idp user table, not there, create user and add row + return None + + +def sync_visa_authorization(raw_visa): pass +def put_gen3_user_ids_for_passport_into_cache(passport, user_ids_from_passports): + pass + def refresh_cronjob_pkey_cache(issuer, kid, pkey_cache): """ Update app public key cache for a specific Passport Visa issuer @@ -244,9 +234,7 @@ def refresh_cronjob_pkey_cache(issuer, kid, pkey_cache): pkey_cache.update({issuer: issuer_public_keys}) logger.info( - "Refreshed cronjob pkey cache for Passport/Visa issuer {}".format( - issuer - ) + "Refreshed cronjob pkey cache for Passport/Visa issuer {}".format(issuer) ) except Exception as e: logger.error( @@ -256,4 +244,4 @@ def refresh_cronjob_pkey_cache(issuer, kid, pkey_cache): ) ) - return pkey_cache.get(issuer, {}).get(kid) \ No newline at end of file + return pkey_cache.get(issuer, {}).get(kid) diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 0143f77a2..51e6e3ed2 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -9,7 +9,10 @@ from fence.config import config from fence.models import GA4GHVisaV1 -from fence.resources.ga4gh.passports import validate_single_passport, refresh_cronjob_pkey_cache +from fence.resources.ga4gh.passports import ( + get_unvalidated_visas_from_valid_passport, + refresh_cronjob_pkey_cache, +) from fence.utils import DEFAULT_BACKOFF_SETTINGS from .idp_oauth2 import Oauth2ClientBase @@ -83,7 +86,7 @@ def get_encoded_visas_v11_userinfo(self, userinfo, pkey_cache=None): list: list of encoded GA4GH visas """ encoded_passport = userinfo.get("passport_jwt_v11") - return validate_single_passport(encoded_passport, pkey_cache) + return get_unvalidated_visas_from_valid_passport(encoded_passport, pkey_cache) def get_user_id(self, code): From 7bd1436a0397165fcc7034b7e424fb09d2fde784 Mon Sep 17 00:00:00 2001 From: BinamB Date: Wed, 29 Sep 2021 11:15:27 -0500 Subject: [PATCH 018/211] resolve review comments --- fence/blueprints/login/base.py | 6 +++--- fence/blueprints/login/ras.py | 15 +++++---------- fence/models.py | 2 +- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/fence/blueprints/login/base.py b/fence/blueprints/login/base.py index 4af1dfc75..107f96cec 100644 --- a/fence/blueprints/login/base.py +++ b/fence/blueprints/login/base.py @@ -6,7 +6,7 @@ from fence.blueprints.login.redirect import validate_redirect from fence.config import config from fence.errors import UserError -from fence.models import IdentityProvider, IdPUser +from fence.models import IdentityProvider, IdPToUser class DefaultOAuth2Login(Resource): @@ -139,8 +139,8 @@ def map_user_idp_info( idp_id = idp.id user_id = user.id - user_to_idp = IdPUser( - sub=idp_sub, + user_to_idp = IdPToUser( + sub=idp_sub + provider, fk_to_idp=idp_id, fk_to_User=user_id, extra_info=extra_info, diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index 290ae5ba6..c6f5e00a2 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -104,6 +104,11 @@ def post_login(self, user=None, token_result=None): # Also require 'sub' claim (see note above about pyjwt and the options arg). if "sub" not in decoded_visa: raise JWTError("Visa is missing the 'sub' claim.") + if not user.idp_to_users: + # map user to idp + self.map_user_idp_info( + user, userinfo.get("sub"), IdentityProvider.ras, current_session + ) except Exception as e: logger.error("Visa failed validation: {}. Discarding visa.".format(e)) continue @@ -145,16 +150,6 @@ def post_login(self, user=None, token_result=None): user=user, refresh_token=refresh_token, expires=expires + issued_time ) - if not user.idp_to_users: - try: - # map user to idp - self.map_user_idp_info( - user, userinfo.get("sub"), IdentityProvider.ras, current_session - ) - except Exception as e: - current_session.rollback() - logger.error("Could not store user and idp info: {}".format(e)) - global_parse_visas_on_login = config["GLOBAL_PARSE_VISAS_ON_LOGIN"] usersync = config.get("USERSYNC", {}) sync_from_visas = usersync.get("sync_from_visas", False) diff --git a/fence/models.py b/fence/models.py index ec2eb8e32..7a57c2d4a 100644 --- a/fence/models.py +++ b/fence/models.py @@ -592,7 +592,7 @@ class UpstreamRefreshToken(Base): expires = Column(BigInteger, nullable=False) -class IdPUser(Base): +class IdPToUser(Base): # IdP & IdP sub mapping to Gen3 User sub __tablename__ = "idp_to_user" From 5083956293bb027832ec8c7d4cb3f027f0649a82 Mon Sep 17 00:00:00 2001 From: BinamB Date: Wed, 29 Sep 2021 16:14:00 -0500 Subject: [PATCH 019/211] unit tests --- fence/blueprints/login/base.py | 3 +- tests/ga4gh/test_ga4gh.py | 5 ++++ tests/ras/test_ras.py | 54 ++++++++++++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 tests/ga4gh/test_ga4gh.py diff --git a/fence/blueprints/login/base.py b/fence/blueprints/login/base.py index 107f96cec..fe2dbd17d 100644 --- a/fence/blueprints/login/base.py +++ b/fence/blueprints/login/base.py @@ -116,6 +116,7 @@ def map_user_idp_info( ): """ Map user to idp. + NOTE: Only do this if and only if the passport has been validated. Args: user (User): User object idp_sub (str): sub provided by the IdP @@ -140,7 +141,7 @@ def map_user_idp_info( user_id = user.id user_to_idp = IdPToUser( - sub=idp_sub + provider, + sub="{}_{}".format(provider, idp_sub), fk_to_idp=idp_id, fk_to_User=user_id, extra_info=extra_info, diff --git a/tests/ga4gh/test_ga4gh.py b/tests/ga4gh/test_ga4gh.py new file mode 100644 index 000000000..6aecfd643 --- /dev/null +++ b/tests/ga4gh/test_ga4gh.py @@ -0,0 +1,5 @@ +from fence.blueprints.login.base import DefaultOAuth2Login, DefaultOAuth2Callback + + +def test_map_user_idp_info(): + oauth2callback = DefaultOAuth2Callback("mock_idp") diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index 3adad6197..2a33229c6 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -7,10 +7,16 @@ from cdislogging import get_logger +from fence.blueprints.login.ras import RASCallback from fence.config import config -from fence.models import User, UpstreamRefreshToken, GA4GHVisaV1 +from fence.models import ( + User, + UpstreamRefreshToken, + GA4GHVisaV1, + IdentityProvider, + IdPToUser, +) from fence.resources.openid.ras_oauth2 import RASOauth2Client as RASClient -from fence.config import config from tests.dbgap_sync.conftest import add_visa_manually from fence.job.visa_update_cronjob import Visa_Token_Update @@ -635,3 +641,47 @@ def test_visa_update_cronjob( for visa in query_visas: assert visa.ga4gh_visa == encoded_visa + + +def test_map_user_idp_info_for_ras(db_session): + """ + test regular flow where user and idp already exists in database and map it to the idp_to_user table + """ + + ras_callback = RASCallback() + test_user = add_test_user(db_session) + user_sub = "sub12345" + provider = IdentityProvider.ras + + query_idp_to_user = db_session.query(IdPToUser).all() + assert len(query_idp_to_user) == 0 + + ras_callback.map_user_idp_info(test_user, user_sub, provider, db_session) + + query_idp_to_user = db_session.query(IdPToUser).first() + assert query_idp_to_user.sub == "{}_{}".format(provider, user_sub) + assert str(query_idp_to_user.fk_to_User) == str(test_user.id) + + +def test_map_idp_info_for_unknown_idp(db_session): + """ + Test flow where user exists in database but idp does not + """ + ras_callback = RASCallback() + test_user = add_test_user(db_session) + user_sub = "sub12345" + provider = "new_idp" + + query_idp_to_user = db_session.query(IdPToUser).all() + assert len(query_idp_to_user) == 0 + + query_idp = db_session.query(IdentityProvider).all() + assert len(query_idp) == 0 + + ras_callback.map_user_idp_info(test_user, user_sub, provider, db_session) + + query_idp_to_user = db_session.query(IdPToUser).first() + query_idp = db_session.query(IdentityProvider).all() + assert len(query_idp) == 1 + assert query_idp[0].id == query_idp_to_user.fk_to_idp + assert query_idp[0].name == provider From e6d893abd70cc0053d326063a2332ec54398fd5e Mon Sep 17 00:00:00 2001 From: BinamB Date: Thu, 30 Sep 2021 11:12:12 -0500 Subject: [PATCH 020/211] make primary key --- fence/blueprints/login/base.py | 2 +- fence/models.py | 5 ++++- tests/ga4gh/test_ga4gh.py | 5 ----- tests/ras/test_ras.py | 11 ++++++----- 4 files changed, 11 insertions(+), 12 deletions(-) delete mode 100644 tests/ga4gh/test_ga4gh.py diff --git a/fence/blueprints/login/base.py b/fence/blueprints/login/base.py index fe2dbd17d..a65abf3e2 100644 --- a/fence/blueprints/login/base.py +++ b/fence/blueprints/login/base.py @@ -141,7 +141,7 @@ def map_user_idp_info( user_id = user.id user_to_idp = IdPToUser( - sub="{}_{}".format(provider, idp_sub), + sub=idp_sub, fk_to_idp=idp_id, fk_to_User=user_id, extra_info=extra_info, diff --git a/fence/models.py b/fence/models.py index 7a57c2d4a..6270d7055 100644 --- a/fence/models.py +++ b/fence/models.py @@ -600,7 +600,10 @@ class IdPToUser(Base): sub = Column(String(), primary_key=True) fk_to_idp = Column( - Integer, ForeignKey(IdentityProvider.id, ondelete="CASCADE"), nullable=False + Integer, + ForeignKey(IdentityProvider.id, ondelete="CASCADE"), + nullable=False, + primary_key=True, ) # foreign key for identity_provider table idp = relationship( "IdentityProvider", diff --git a/tests/ga4gh/test_ga4gh.py b/tests/ga4gh/test_ga4gh.py deleted file mode 100644 index 6aecfd643..000000000 --- a/tests/ga4gh/test_ga4gh.py +++ /dev/null @@ -1,5 +0,0 @@ -from fence.blueprints.login.base import DefaultOAuth2Login, DefaultOAuth2Callback - - -def test_map_user_idp_info(): - oauth2callback = DefaultOAuth2Callback("mock_idp") diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index 2a33229c6..2f964b25a 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -659,7 +659,7 @@ def test_map_user_idp_info_for_ras(db_session): ras_callback.map_user_idp_info(test_user, user_sub, provider, db_session) query_idp_to_user = db_session.query(IdPToUser).first() - assert query_idp_to_user.sub == "{}_{}".format(provider, user_sub) + assert query_idp_to_user.sub == user_sub assert str(query_idp_to_user.fk_to_User) == str(test_user.id) @@ -676,12 +676,13 @@ def test_map_idp_info_for_unknown_idp(db_session): assert len(query_idp_to_user) == 0 query_idp = db_session.query(IdentityProvider).all() - assert len(query_idp) == 0 + n_idp = len(query_idp) ras_callback.map_user_idp_info(test_user, user_sub, provider, db_session) query_idp_to_user = db_session.query(IdPToUser).first() + assert query_idp_to_user.sub == user_sub + assert str(query_idp_to_user.fk_to_User) == str(test_user.id) + query_idp = db_session.query(IdentityProvider).all() - assert len(query_idp) == 1 - assert query_idp[0].id == query_idp_to_user.fk_to_idp - assert query_idp[0].name == provider + assert len(query_idp) == n_idp + 1 From 4dea68c070a2a8a867ad83a884616ffabae2886c Mon Sep 17 00:00:00 2001 From: BinamB Date: Mon, 4 Oct 2021 12:30:33 -0500 Subject: [PATCH 021/211] add global sub --- fence/blueprints/ga4gh.py | 3 +++ fence/blueprints/login/ras.py | 2 ++ fence/job/visa_update_cronjob.py | 2 ++ fence/resources/ga4gh/passports.py | 4 +++ fence/resources/openid/ras_oauth2.py | 2 ++ tests/test_drs.py | 40 ++++++++++++++++++++++++++++ 6 files changed, 53 insertions(+) diff --git a/fence/blueprints/ga4gh.py b/fence/blueprints/ga4gh.py index 5d3b9eeb5..7d78a9212 100644 --- a/fence/blueprints/ga4gh.py +++ b/fence/blueprints/ga4gh.py @@ -1,4 +1,5 @@ import flask +from flask import request from fence.errors import UserError from fence.blueprints.data.indexd import ( @@ -20,6 +21,8 @@ def get_ga4gh_signed_url(object_id, access_id): passports = flask.request.args.get("passports") + if passports: + return UserError("Passports not supported yet") if not access_id: raise UserError("Access ID/Protocol is required.") diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index c8df791e1..2b146ddc1 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -19,6 +19,8 @@ logger = get_logger(__name__) +GLOBAL_USER_SUB_FROM_PASSPORT = [] + class RASLogin(DefaultOAuth2Login): def __init__(self): diff --git a/fence/job/visa_update_cronjob.py b/fence/job/visa_update_cronjob.py index cff3bc58f..ce0fd76a8 100644 --- a/fence/job/visa_update_cronjob.py +++ b/fence/job/visa_update_cronjob.py @@ -17,6 +17,8 @@ logger = get_logger(__name__, log_level="debug") +GLOBAL_USER_SUB_FROM_PASSPORT = [] + class Visa_Token_Update(object): def __init__( diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index d0be7caf8..4468e47c2 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -13,8 +13,10 @@ logger = get_logger(__name__, log_level="debug") +GLOBAL_USER_SUB_FROM_PASSPORT = [] def get_gen3_user_ids_from_ga4gh_passports(passports): + user_ids_from_passports = [] was_cached = False @@ -134,6 +136,8 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): ) except Exception as e: logger.error("Passport failed validation: {}. Discarding passport.".format(e)) + + GLOBAL_USER_SUB_FROM_PASSPORT.append(decoded_passport.get("sub")) return decoded_passport.get("ga4gh_passport_v1", []) diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 51e6e3ed2..07c837c19 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -16,6 +16,8 @@ from fence.utils import DEFAULT_BACKOFF_SETTINGS from .idp_oauth2 import Oauth2ClientBase +GLOBAL_USER_SUB_FROM_PASSPORT = [] + class RASOauth2Client(Oauth2ClientBase): """ diff --git a/tests/test_drs.py b/tests/test_drs.py index 2769784de..c5dc888d0 100644 --- a/tests/test_drs.py +++ b/tests/test_drs.py @@ -230,3 +230,43 @@ def test_get_presigned_url_with_query_params( headers=user, ) assert res.status_code == 200 + + +@responses.activate +@pytest.mark.parametrize("indexd_client", ["s3", "gs"], indirect=True) +def test_get_presigned_url_with_query_params_post( + client, + user_client, + indexd_client, + kid, + rsa_private_key, + google_proxy_group, + primary_google_service_account, + cloud_manager, + google_signed_url, +): + access_id = indexd_client["indexed_file_location"] + test_guid = "1" + user = { + "Authorization": "Bearer " + + jwt.encode( + utils.authorized_download_context_claims( + user_client.username, user_client.user_id + ), + key=rsa_private_key, + headers={"kid": kid}, + algorithm="RS256", + ).decode("utf-8") + } + data = get_doc() + data["did"] = "dg.TEST/ed8f4658-6acd-4f96-9dd8-3709890c959e" + did = "dg.TEST%2Fed8f4658-6acd-4f96-9dd8-3709890c959e" + + res = client.post( + "/ga4gh/drs/v1/objects/" + + did + + "/access/" + + access_id, + data=json.dumps({"passports": "eyghnsapodkasdas;dksa;das;fjoingoiahfpi"}) + ) + assert res.status_code == 200 From 16cd3baf816a02756ad005ad0c2cd9f8aacc5b23 Mon Sep 17 00:00:00 2001 From: BinamB Date: Wed, 6 Oct 2021 10:47:49 -0500 Subject: [PATCH 022/211] close off endpoint --- fence/blueprints/ga4gh.py | 10 ++++++---- fence/resources/ga4gh/passports.py | 8 ++++---- tests/test_drs.py | 12 ++++++------ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/fence/blueprints/ga4gh.py b/fence/blueprints/ga4gh.py index 7d78a9212..dcc982640 100644 --- a/fence/blueprints/ga4gh.py +++ b/fence/blueprints/ga4gh.py @@ -20,14 +20,16 @@ ) def get_ga4gh_signed_url(object_id, access_id): - passports = flask.request.args.get("passports") - if passports: - return UserError("Passports not supported yet") + if flask.request.method == "POST": + passports = flask.request.form.get("passports") + raise UserError("Passports not supported yet") if not access_id: raise UserError("Access ID/Protocol is required.") result = get_signed_url_for_file( - "download", object_id, requested_protocol=access_id, ga4gh_passports=passports, + "download", + object_id, + requested_protocol=access_id, ) return flask.jsonify(result) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 4468e47c2..59d5d7a7d 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -15,6 +15,7 @@ GLOBAL_USER_SUB_FROM_PASSPORT = [] + def get_gen3_user_ids_from_ga4gh_passports(passports): user_ids_from_passports = [] @@ -99,9 +100,7 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): if not public_key: try: logger.info("Fetching public key from flask app...") - public_key = get_public_key_for_token( - passport, attempt_refresh=True - ) + public_key = get_public_key_for_token(passport, attempt_refresh=True) except Exception as e: logger.info( "Could not fetch public key from flask app to validate passport: {}. Trying to fetch from source.".format( @@ -136,7 +135,7 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): ) except Exception as e: logger.error("Passport failed validation: {}. Discarding passport.".format(e)) - + GLOBAL_USER_SUB_FROM_PASSPORT.append(decoded_passport.get("sub")) return decoded_passport.get("ga4gh_passport_v1", []) @@ -179,6 +178,7 @@ def sync_visa_authorization(raw_visa): def put_gen3_user_ids_for_passport_into_cache(passport, user_ids_from_passports): pass + def refresh_cronjob_pkey_cache(issuer, kid, pkey_cache): """ Update app public key cache for a specific Passport Visa issuer diff --git a/tests/test_drs.py b/tests/test_drs.py index c5dc888d0..e43206440 100644 --- a/tests/test_drs.py +++ b/tests/test_drs.py @@ -245,6 +245,9 @@ def test_get_presigned_url_with_query_params_post( cloud_manager, google_signed_url, ): + """ + Temporary test for checking if we return 400 when we try to POST drs endpoint + """ access_id = indexd_client["indexed_file_location"] test_guid = "1" user = { @@ -263,10 +266,7 @@ def test_get_presigned_url_with_query_params_post( did = "dg.TEST%2Fed8f4658-6acd-4f96-9dd8-3709890c959e" res = client.post( - "/ga4gh/drs/v1/objects/" - + did - + "/access/" - + access_id, - data=json.dumps({"passports": "eyghnsapodkasdas;dksa;das;fjoingoiahfpi"}) + "/ga4gh/drs/v1/objects/" + did + "/access/" + access_id, + data=json.dumps({"passports": "eyghnsapodkasdas;dksa;das;fjoingoiahfpi"}), ) - assert res.status_code == 200 + assert res.status_code == 400 From 56e2832ebca4dce76f5f0b4d7d276b81159788e6 Mon Sep 17 00:00:00 2001 From: BinamB Date: Wed, 6 Oct 2021 11:10:53 -0500 Subject: [PATCH 023/211] fix test name --- tests/test_drs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_drs.py b/tests/test_drs.py index e43206440..9db052613 100644 --- a/tests/test_drs.py +++ b/tests/test_drs.py @@ -234,7 +234,7 @@ def test_get_presigned_url_with_query_params( @responses.activate @pytest.mark.parametrize("indexd_client", ["s3", "gs"], indirect=True) -def test_get_presigned_url_with_query_params_post( +def test_get_presigned_url_with_query_params_post_400( client, user_client, indexd_client, From 9687644f25d3597b1f7f7f95dd936df35179b78e Mon Sep 17 00:00:00 2001 From: BinamB Date: Wed, 6 Oct 2021 11:41:35 -0500 Subject: [PATCH 024/211] add test --- fence/blueprints/login/ras.py | 2 -- fence/job/visa_update_cronjob.py | 2 -- fence/resources/openid/ras_oauth2.py | 2 -- tests/ras/test_ras.py | 2 ++ 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index 2b146ddc1..c8df791e1 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -19,8 +19,6 @@ logger = get_logger(__name__) -GLOBAL_USER_SUB_FROM_PASSPORT = [] - class RASLogin(DefaultOAuth2Login): def __init__(self): diff --git a/fence/job/visa_update_cronjob.py b/fence/job/visa_update_cronjob.py index ce0fd76a8..cff3bc58f 100644 --- a/fence/job/visa_update_cronjob.py +++ b/fence/job/visa_update_cronjob.py @@ -17,8 +17,6 @@ logger = get_logger(__name__, log_level="debug") -GLOBAL_USER_SUB_FROM_PASSPORT = [] - class Visa_Token_Update(object): def __init__( diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 07c837c19..51e6e3ed2 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -16,8 +16,6 @@ from fence.utils import DEFAULT_BACKOFF_SETTINGS from .idp_oauth2 import Oauth2ClientBase -GLOBAL_USER_SUB_FROM_PASSPORT = [] - class RASOauth2Client(Oauth2ClientBase): """ diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index 3adad6197..ae49d79ef 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -11,6 +11,7 @@ from fence.models import User, UpstreamRefreshToken, GA4GHVisaV1 from fence.resources.openid.ras_oauth2 import RASOauth2Client as RASClient from fence.config import config +from fence.resources.ga4gh.passports import GLOBAL_USER_SUB_FROM_PASSPORT from tests.dbgap_sync.conftest import add_visa_manually from fence.job.visa_update_cronjob import Visa_Token_Update @@ -176,6 +177,7 @@ def test_update_visa_token( query_visa = db_session.query(GA4GHVisaV1).first() assert query_visa.ga4gh_visa assert query_visa.ga4gh_visa == encoded_visa + assert "abcde12345aspdij" in GLOBAL_USER_SUB_FROM_PASSPORT @mock.patch("fence.resources.openid.ras_oauth2.RASOauth2Client.get_userinfo") From e20a6a683858c28185faf4698ebf6ae0a6da7b3b Mon Sep 17 00:00:00 2001 From: BinamB Date: Wed, 6 Oct 2021 15:15:39 -0500 Subject: [PATCH 025/211] remove sub list --- fence/blueprints/ga4gh.py | 4 --- fence/resources/ga4gh/passports.py | 9 +++---- tests/ras/test_ras.py | 2 -- tests/test_drs.py | 39 ------------------------------ 4 files changed, 3 insertions(+), 51 deletions(-) diff --git a/fence/blueprints/ga4gh.py b/fence/blueprints/ga4gh.py index dcc982640..7e1f86569 100644 --- a/fence/blueprints/ga4gh.py +++ b/fence/blueprints/ga4gh.py @@ -20,10 +20,6 @@ ) def get_ga4gh_signed_url(object_id, access_id): - if flask.request.method == "POST": - passports = flask.request.form.get("passports") - raise UserError("Passports not supported yet") - if not access_id: raise UserError("Access ID/Protocol is required.") diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 59d5d7a7d..f6160b07e 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -11,9 +11,7 @@ from fence.config import config -logger = get_logger(__name__, log_level="debug") - -GLOBAL_USER_SUB_FROM_PASSPORT = [] +logger = get_logger(__name__) def get_gen3_user_ids_from_ga4gh_passports(passports): @@ -74,7 +72,7 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): Return encoded visas after extracting and validating encoded passport Args: - encoded_passport (string): encoded ga4gh passport + passport (string): encoded ga4gh passport pkey_cache (dict): app cache of public keys_dir Return: @@ -103,7 +101,7 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): public_key = get_public_key_for_token(passport, attempt_refresh=True) except Exception as e: logger.info( - "Could not fetch public key from flask app to validate passport: {}. Trying to fetch from source.".format( + "Could not fetch public key from flask app to validate passport: {}. Trying to fetch from source.".format( e ) ) @@ -136,7 +134,6 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): except Exception as e: logger.error("Passport failed validation: {}. Discarding passport.".format(e)) - GLOBAL_USER_SUB_FROM_PASSPORT.append(decoded_passport.get("sub")) return decoded_passport.get("ga4gh_passport_v1", []) diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index 360356a59..c815a5f59 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -18,7 +18,6 @@ ) from fence.resources.openid.ras_oauth2 import RASOauth2Client as RASClient from fence.config import config -from fence.resources.ga4gh.passports import GLOBAL_USER_SUB_FROM_PASSPORT from tests.dbgap_sync.conftest import add_visa_manually from fence.job.visa_update_cronjob import Visa_Token_Update @@ -184,7 +183,6 @@ def test_update_visa_token( query_visa = db_session.query(GA4GHVisaV1).first() assert query_visa.ga4gh_visa assert query_visa.ga4gh_visa == encoded_visa - assert "abcde12345aspdij" in GLOBAL_USER_SUB_FROM_PASSPORT @mock.patch("fence.resources.openid.ras_oauth2.RASOauth2Client.get_userinfo") diff --git a/tests/test_drs.py b/tests/test_drs.py index 9db052613..aa9e7bba7 100644 --- a/tests/test_drs.py +++ b/tests/test_drs.py @@ -231,42 +231,3 @@ def test_get_presigned_url_with_query_params( ) assert res.status_code == 200 - -@responses.activate -@pytest.mark.parametrize("indexd_client", ["s3", "gs"], indirect=True) -def test_get_presigned_url_with_query_params_post_400( - client, - user_client, - indexd_client, - kid, - rsa_private_key, - google_proxy_group, - primary_google_service_account, - cloud_manager, - google_signed_url, -): - """ - Temporary test for checking if we return 400 when we try to POST drs endpoint - """ - access_id = indexd_client["indexed_file_location"] - test_guid = "1" - user = { - "Authorization": "Bearer " - + jwt.encode( - utils.authorized_download_context_claims( - user_client.username, user_client.user_id - ), - key=rsa_private_key, - headers={"kid": kid}, - algorithm="RS256", - ).decode("utf-8") - } - data = get_doc() - data["did"] = "dg.TEST/ed8f4658-6acd-4f96-9dd8-3709890c959e" - did = "dg.TEST%2Fed8f4658-6acd-4f96-9dd8-3709890c959e" - - res = client.post( - "/ga4gh/drs/v1/objects/" + did + "/access/" + access_id, - data=json.dumps({"passports": "eyghnsapodkasdas;dksa;das;fjoingoiahfpi"}), - ) - assert res.status_code == 400 From c487c3b9ff8849bc18af6a359bf77668c7019781 Mon Sep 17 00:00:00 2001 From: BinamB Date: Wed, 6 Oct 2021 15:15:47 -0500 Subject: [PATCH 026/211] black --- tests/test_drs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_drs.py b/tests/test_drs.py index aa9e7bba7..2769784de 100644 --- a/tests/test_drs.py +++ b/tests/test_drs.py @@ -230,4 +230,3 @@ def test_get_presigned_url_with_query_params( headers=user, ) assert res.status_code == 200 - From cba40e50bf1f2acfab4765969276678025fda0cd Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Thu, 7 Oct 2021 12:49:03 -0500 Subject: [PATCH 027/211] feat(storage_expiration): add expires to db for Google access, create cronjob logic, add support for passing expiration --- .secrets.baseline | 4 +- bin/fence_create.py | 4 + fence/models.py | 11 ++ fence/resources/google/utils.py | 3 +- fence/resources/storage/__init__.py | 21 ++- fence/scripting/fence_create.py | 52 ++++++ fence/sync/sync_users.py | 29 +++- tests/scripting/test_fence-create.py | 232 +++++++++++++++++++++++++-- 8 files changed, 328 insertions(+), 28 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index c7a49e349..42233f77a 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -152,7 +152,7 @@ "filename": "fence/resources/google/utils.py", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 277 + "line_number": 125 } ], "fence/utils.py": [ @@ -263,5 +263,5 @@ } ] }, - "generated_at": "2021-08-18T02:36:18Z" + "generated_at": "2021-10-07T17:46:11Z" } diff --git a/bin/fence_create.py b/bin/fence_create.py index 1c32f1541..da4a5126a 100755 --- a/bin/fence_create.py +++ b/bin/fence_create.py @@ -17,6 +17,7 @@ create_sample_data, delete_client_action, delete_users, + delete_expired_google_access, google_init, list_client_action, link_external_bucket, @@ -147,6 +148,7 @@ def parse_arguments(): subparsers.add_parser("expired-service-account-delete") subparsers.add_parser("bucket-access-group-verify") + subparsers.add_parser("delete-expired-google-access") hmac_create = subparsers.add_parser("hmac-create") hmac_create.add_argument("yaml-input") @@ -459,6 +461,8 @@ def main(): delete_expired_service_accounts(DB) elif args.action == "bucket-access-group-verify": verify_bucket_access_group(DB) + elif args.action == "delete-expired-google-access": + delete_expired_google_access(DB) elif args.action == "sync": sync_users( dbGaP, diff --git a/fence/models.py b/fence/models.py index 6270d7055..09ad33ed6 100644 --- a/fence/models.py +++ b/fence/models.py @@ -461,6 +461,8 @@ class GoogleProxyGroupToGoogleBucketAccessGroup(Base): ), ) + expires = Column(BigInteger) + class UserServiceAccount(Base): __tablename__ = "user_service_account" @@ -887,6 +889,15 @@ def migrate(driver): FOR EACH ROW EXECUTE PROCEDURE process_cert_audit();""" ) + # Google Access expiration + + add_column_if_not_exist( + table_name=GoogleProxyGroupToGoogleBucketAccessGroup.__tablename__, + column=Column("expires", BigInteger()), + driver=driver, + metadata=md, + ) + def add_foreign_key_column_if_not_exist( table_name, diff --git a/fence/resources/google/utils.py b/fence/resources/google/utils.py index c6b7ed455..27dacb479 100644 --- a/fence/resources/google/utils.py +++ b/fence/resources/google/utils.py @@ -515,7 +515,7 @@ def _update_service_account_db_entry( return service_account_db_entry -def get_or_create_proxy_group_id(): +def get_or_create_proxy_group_id(expires=None): """ If no username returned from token or database, create a new proxy group for the give user. Also, add the access privileges. @@ -551,6 +551,7 @@ def get_or_create_proxy_group_id(): project=p.project, access=p.privilege, session=current_session, + expires=expires, ) return proxy_group_id diff --git a/fence/resources/storage/__init__.py b/fence/resources/storage/__init__.py index a9873875c..9f7da292f 100644 --- a/fence/resources/storage/__init__.py +++ b/fence/resources/storage/__init__.py @@ -151,7 +151,14 @@ def create_bucket(self, provider, session, bucketname, project): @check_exist def grant_access( - self, provider, username, project, access, session, google_bulk_mapping=None + self, + provider, + username, + project, + access, + session, + google_bulk_mapping=None, + expires=None, ): """ this should be exposed via admin endpoint @@ -176,6 +183,7 @@ def grant_access( access, session, google_bulk_mapping=google_bulk_mapping, + expires=None, ) @check_exist @@ -369,6 +377,7 @@ def _update_access_to_bucket( access, session, google_bulk_mapping=None, + expires=None, ): # Need different logic for google (since buckets can have multiple # access groups) @@ -412,7 +421,7 @@ def _update_access_to_bucket( ) StorageManager._add_google_db_entry_for_bucket_access( - storage_user, bucket_access_group, session + storage_user, bucket_access_group, session, expires=expires ) else: @@ -489,7 +498,7 @@ def _revoke_access_to_bucket( @staticmethod def _add_google_db_entry_for_bucket_access( - storage_user, bucket_access_group, session + storage_user, bucket_access_group, session, expires=None ): """ Add a db entry specifying that a given user has storage access @@ -507,9 +516,15 @@ def _add_google_db_entry_for_bucket_access( storage_user_access_db_entry = GoogleProxyGroupToGoogleBucketAccessGroup( proxy_group_id=storage_user.google_proxy_group_id, access_group_id=bucket_access_group.id, + expires=expires, ) session.add(storage_user_access_db_entry) session.commit() + # update expiration if doesn't match db + elif expires != storage_user_access_db_entry.expires: + storage_user_access_db_entry.expires = expires + session.add(storage_user_access_db_entry) + session.commit() # FIXME: create a delete() on GoogleProxyGroupToGoogleBucketAccessGroup and use here. # previous attempts to use similar delete() calls on other models resulting in errors diff --git a/fence/scripting/fence_create.py b/fence/scripting/fence_create.py index 9146613ba..ef871061f 100644 --- a/fence/scripting/fence_create.py +++ b/fence/scripting/fence_create.py @@ -674,6 +674,58 @@ def delete_users(DB, usernames): session.commit() +def delete_expired_google_access(DB): + """ + Delete all expired Google data access (e.g. remove proxy groups from Google Bucket + Access Groups if expired). + """ + cirrus_config.update(**config["CIRRUS_CFG"]) + + driver = SQLAlchemyDriver(DB) + with driver.session as session: + current_time = int(time.time()) + + # Get expires field from db, if None, default to NOT expired + records_to_delete = ( + session.query(GoogleProxyGroupToGoogleBucketAccessGroup) + .filter( + (GoogleProxyGroupToGoogleBucketAccessGroup.expires or current_time + 1) + < current_time + ) + .all() + ) + if len(records_to_delete): + with GoogleCloudManager() as manager: + for record in records_to_delete: + try: + with GoogleCloudManager() as manager: + member_email = record.proxy_group.email + access_group_email = record.access_group.email + manager.remove_member_from_group( + member_email, access_group_email + ) + logger.info( + "Removed {} from {}, expired {}. Current time: {} ".format( + member_email, + access_group_email, + record.expires, + current_time, + ) + ) + session.delete(record) + session.commit() + except Exception as e: + logger.error( + "ERROR: Could not remove Google group member {} from access group {}. Detail {}".format( + member_email, access_group_email, e + ) + ) + + logger.info( + f"Removed {len(records_to_delete)} expired Google Access records from db and Google." + ) + + def delete_expired_service_accounts(DB): """ Delete all expired service accounts. diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 8ec52d28b..7cbdce7e0 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -688,7 +688,7 @@ def sync_two_phsids_dict( self.auth_source[user].add(source2) def sync_to_db_and_storage_backend( - self, user_project, user_info, sess, single_visa_sync=False + self, user_project, user_info, sess, single_visa_sync=False, expires=None ): """ sync user access control to database and storage backend @@ -759,6 +759,7 @@ def sync_to_db_and_storage_backend( user_project_lowercase, sess, google_bulk_mapping=google_bulk_mapping, + expires=expires, ) self._grant_from_db( @@ -775,6 +776,7 @@ def sync_to_db_and_storage_backend( user_project_lowercase, sess, google_bulk_mapping=google_bulk_mapping, + expires=expires, ) self._update_from_db(sess, to_update, user_project_lowercase) @@ -989,7 +991,9 @@ def _revoke_from_storage(self, to_delete, sess, google_bulk_mapping=None): google_bulk_mapping=google_bulk_mapping, ) - def _grant_from_storage(self, to_add, user_project, sess, google_bulk_mapping=None): + def _grant_from_storage( + self, to_add, user_project, sess, google_bulk_mapping=None, expires=None + ): """ If a project have storage backend, grant user's access to buckets in the storage backend. @@ -1029,6 +1033,7 @@ def _grant_from_storage(self, to_add, user_project, sess, google_bulk_mapping=No access=access, session=sess, google_bulk_mapping=google_bulk_mapping, + expires=expires, ) def _init_projects(self, user_project, sess): @@ -1589,7 +1594,12 @@ def _update_arborist(self, session, user_yaml): return True def _update_authz_in_arborist( - self, session, user_projects, user_yaml=None, single_user_sync=False + self, + session, + user_projects, + user_yaml=None, + single_user_sync=False, + expires=None, ): """ Assign users policies in arborist from the information in @@ -1660,6 +1670,8 @@ def _update_authz_in_arborist( username = user.username self.arborist_client.create_user_if_not_exist(username) + + # TODO make this smarter - it should do a diff, not revoke all and add self.arborist_client.revoke_all_policies_for_user(username) for project, permissions in user_project_info.items(): @@ -1705,6 +1717,7 @@ def _update_authz_in_arborist( if not single_user_sync: if policy_id not in self._created_policies: try: + # TODO use expires self.arborist_client.update_policy( policy_id, { @@ -2102,7 +2115,7 @@ def sync_visas(self): self._sync_visas(s) # if returns with some failure use telemetry file - def sync_single_user_visas(self, user, sess=None): + def sync_single_user_visas(self, user, sess=None, expires=None): """ Sync a single user's visa during login """ @@ -2169,7 +2182,7 @@ def sync_single_user_visas(self, user, sess=None): if user_projects: self.logger.info("Sync to db and storage backend") self.sync_to_db_and_storage_backend( - user_projects, user_info, sess, single_visa_sync=True + user_projects, user_info, sess, single_visa_sync=True, expires=expires ) else: self.logger.info("No users for syncing") @@ -2178,7 +2191,11 @@ def sync_single_user_visas(self, user, sess=None): if self.arborist_client: self.logger.info("Synchronizing arborist with authorization info...") success = self._update_authz_in_arborist( - sess, user_projects, user_yaml=user_yaml, single_user_sync=True + sess, + user_projects, + user_yaml=user_yaml, + single_user_sync=True, + expires=expires, ) if success: self.logger.info( diff --git a/tests/scripting/test_fence-create.py b/tests/scripting/test_fence-create.py index 78bd4717c..28cc37d6f 100644 --- a/tests/scripting/test_fence-create.py +++ b/tests/scripting/test_fence-create.py @@ -23,7 +23,9 @@ GoogleBucketAccessGroup, CloudProvider, Bucket, + GoogleProxyGroup, ServiceAccountToGoogleBucketAccessGroup, + GoogleProxyGroupToGoogleBucketAccessGroup, GoogleServiceAccountKey, StorageAccess, ) @@ -33,6 +35,7 @@ create_client_action, delete_client_action, delete_expired_service_accounts, + delete_expired_google_access, link_external_bucket, remove_expired_google_service_account_keys, verify_bucket_access_group, @@ -384,9 +387,15 @@ def test_create_refresh_token_with_found_user( assert db_token is not None -def _setup_service_account_to_google_bucket_access_group(db_session): +def _setup_google_access(db_session, access_1_expires=None, access_2_expires=None): """ Setup some testing data. + + Args: + access_1_expires (str, optional): expiration for the Proxy Group -> + Google Bucket Access Group for user 1, defaults to None + access_2_expires (str, optional): expiration for the Proxy Group -> + Google Bucket Access Group for user 2, defaults to None """ cloud_provider = CloudProvider( name="test_provider", @@ -417,22 +426,40 @@ def _setup_service_account_to_google_bucket_access_group(db_session): db_session.add(bucket1) db_session.commit() + gpg1 = GoogleProxyGroup(id=1, email="test1@gmail.com") + gpg2 = GoogleProxyGroup(id=2, email="test2@gmail.com") + db_session.add(gpg1) + db_session.add(gpg2) + db_session.commit() + + gbag1 = GoogleBucketAccessGroup( + bucket_id=bucket1.id, + email="testgroup1@gmail.com", + privileges=["read-storage", "write-storage"], + ) + gbag2 = GoogleBucketAccessGroup( + bucket_id=bucket1.id, + email="testgroup2@gmail.com", + privileges=["read-storage"], + ) + db_session.add(gbag1) + db_session.add(gbag2) + db_session.commit() + db_session.add( - GoogleBucketAccessGroup( - bucket_id=bucket1.id, - email="testgroup1@gmail.com", - privileges=["read-storage", "write-storage"], + GoogleProxyGroupToGoogleBucketAccessGroup( + proxy_group_id=gpg1.id, access_group_id=gbag1.id, expires=access_1_expires ) ) db_session.add( - GoogleBucketAccessGroup( - bucket_id=bucket1.id, - email="testgroup2@gmail.com", - privileges=["read-storage"], + GoogleProxyGroupToGoogleBucketAccessGroup( + proxy_group_id=gpg2.id, access_group_id=gbag2.id, expires=access_2_expires ) ) db_session.commit() + return {"google_proxy_group_ids": {"1": gpg1.id, "2": gpg2.id}} + def test_delete_expired_service_accounts_with_one_fail_first( cloud_manager, app, db_session @@ -449,7 +476,7 @@ def test_delete_expired_service_accounts_with_one_fail_first( HttpError(mock.Mock(status=403), bytes("Permission denied", "utf-8")), {}, ] - _setup_service_account_to_google_bucket_access_group(db_session) + _setup_google_access(db_session) service_accounts = db_session.query(UserServiceAccount).all() google_bucket_access_grps = db_session.query(GoogleBucketAccessGroup).all() @@ -499,7 +526,7 @@ def test_delete_expired_service_accounts_with_one_fail_second( {}, HttpError(mock.Mock(status=403), bytes("Permission denied", "utf-8")), ] - _setup_service_account_to_google_bucket_access_group(db_session) + _setup_google_access(db_session) service_accounts = db_session.query(UserServiceAccount).all() google_bucket_access_grps = db_session.query(GoogleBucketAccessGroup).all() @@ -546,7 +573,7 @@ def test_delete_expired_service_accounts(cloud_manager, app, db_session): cloud_manager.return_value.__enter__.return_value.remove_member_from_group.return_value = ( {} ) - _setup_service_account_to_google_bucket_access_group(db_session) + _setup_google_access(db_session) service_accounts = db_session.query(UserServiceAccount).all() google_bucket_access_grps = db_session.query(GoogleBucketAccessGroup).all() @@ -593,7 +620,7 @@ def test_delete_not_expired_service_account(app, db_session): import fence fence.settings = MagicMock() - _setup_service_account_to_google_bucket_access_group(db_session) + _setup_google_access(db_session) service_account = db_session.query(UserServiceAccount).first() google_bucket_access_grp1 = db_session.query(GoogleBucketAccessGroup).first() @@ -618,6 +645,179 @@ def test_delete_not_expired_service_account(app, db_session): assert len(records) == 1 +def test_delete_not_expired_google_access(app, db_session): + """ + Test the case that there is no expired google access + """ + import fence + + fence.settings = MagicMock() + + current_time = int(time.time()) + # 1 not expired, 2 not expired + access_1_expires = current_time + 3600 + access_2_expires = current_time + 3600 + _setup_google_access( + db_session, access_1_expires=access_1_expires, access_2_expires=access_2_expires + ) + + google_access = db_session.query(GoogleProxyGroupToGoogleBucketAccessGroup).all() + google_proxy_groups = db_session.query(GoogleProxyGroup).all() + google_bucket_access_grps = db_session.query(GoogleBucketAccessGroup).all() + + # check database to make sure all the service accounts exist + pre_deletion_google_access_size = len(google_access) + pre_deletion_google_proxy_groups_size = len(google_proxy_groups) + pre_deletion_google_bucket_access_grps_size = len(google_bucket_access_grps) + + # call function to delete expired service account + delete_expired_google_access(config["DB"]) + + google_access = db_session.query(GoogleProxyGroupToGoogleBucketAccessGroup).all() + google_proxy_groups = db_session.query(GoogleProxyGroup).all() + google_bucket_access_grps = db_session.query(GoogleBucketAccessGroup).all() + + # check database again. Expect nothing is deleted + assert len(google_access) == pre_deletion_google_access_size + assert len(google_proxy_groups) == pre_deletion_google_proxy_groups_size + assert len(google_bucket_access_grps) == pre_deletion_google_bucket_access_grps_size + + +def test_delete_not_specified_expiration_google_access(app, db_session): + """ + Test the case that there is no expiration time specified in the db for google access + In this case, we expect backwards compatible behavior, e.g. they are NOT removed + """ + import fence + + fence.settings = MagicMock() + + current_time = int(time.time()) + access_1_expires = None + access_2_expires = None + _setup_google_access( + db_session, access_1_expires=access_1_expires, access_2_expires=access_2_expires + ) + + google_access = db_session.query(GoogleProxyGroupToGoogleBucketAccessGroup).all() + google_proxy_groups = db_session.query(GoogleProxyGroup).all() + google_bucket_access_grps = db_session.query(GoogleBucketAccessGroup).all() + + # check database to make sure all the service accounts exist + pre_deletion_google_access_size = len(google_access) + pre_deletion_google_proxy_groups_size = len(google_proxy_groups) + pre_deletion_google_bucket_access_grps_size = len(google_bucket_access_grps) + + # call function to delete expired service account + delete_expired_google_access(config["DB"]) + + google_access = db_session.query(GoogleProxyGroupToGoogleBucketAccessGroup).all() + google_proxy_groups = db_session.query(GoogleProxyGroup).all() + google_bucket_access_grps = db_session.query(GoogleBucketAccessGroup).all() + + # check database again. Expect nothing is deleted + assert len(google_access) == pre_deletion_google_access_size + assert len(google_proxy_groups) == pre_deletion_google_proxy_groups_size + assert len(google_bucket_access_grps) == pre_deletion_google_bucket_access_grps_size + + +def test_delete_expired_google_access(cloud_manager, app, db_session): + """ + Test deleting all expired service accounts + """ + import fence + + fence.settings = MagicMock() + cloud_manager.return_value.__enter__.return_value.remove_member_from_group.return_value = ( + {} + ) + + current_time = int(time.time()) + # 1 expired, 2 not expired + access_1_expires = current_time - 3600 + access_2_expires = current_time + 3600 + setup_results = _setup_google_access( + db_session, access_1_expires=access_1_expires, access_2_expires=access_2_expires + ) + + google_access = db_session.query(GoogleProxyGroupToGoogleBucketAccessGroup).all() + google_proxy_groups = db_session.query(GoogleProxyGroup).all() + google_bucket_access_grps = db_session.query(GoogleBucketAccessGroup).all() + + # check database to make sure all the service accounts exist + pre_deletion_google_access_size = len(google_access) + pre_deletion_google_proxy_groups_size = len(google_proxy_groups) + pre_deletion_google_bucket_access_grps_size = len(google_bucket_access_grps) + + # call function to delete expired service account + delete_expired_google_access(config["DB"]) + + google_access = db_session.query(GoogleProxyGroupToGoogleBucketAccessGroup).all() + google_proxy_groups = db_session.query(GoogleProxyGroup).all() + google_bucket_access_grps = db_session.query(GoogleBucketAccessGroup).all() + + # check database again. Expect 1 access is deleted - proxy group and gbag should be intact + assert len(google_access) == pre_deletion_google_access_size - 1 + remaining_ids = [str(gpg_to_gbag.proxy_group_id) for gpg_to_gbag in google_access] + + # b/c expired + assert str(setup_results["google_proxy_group_ids"]["1"]) not in remaining_ids + + # b/c not expired + assert str(setup_results["google_proxy_group_ids"]["2"]) in remaining_ids + + assert len(google_proxy_groups) == pre_deletion_google_proxy_groups_size + assert len(google_bucket_access_grps) == pre_deletion_google_bucket_access_grps_size + + +def test_delete_expired_google_access_with_one_fail_first( + cloud_manager, app, db_session +): + """ + Test the case that there is a failure of removing from google group in GCP. + In this case, we still want the expired record to exist in the db so we can try to + remove it again. + """ + from googleapiclient.errors import HttpError + import fence + + fence.settings = MagicMock() + cirrus.config.update = MagicMock() + cloud_manager.return_value.__enter__.return_value.remove_member_from_group.side_effect = [ + HttpError(mock.Mock(status=403), bytes("Permission denied", "utf-8")), + {}, + ] + + current_time = int(time.time()) + # 1 expired, 2 not expired + access_1_expires = current_time - 3600 + access_2_expires = current_time + 3600 + _setup_google_access( + db_session, access_1_expires=access_1_expires, access_2_expires=access_2_expires + ) + + google_access = db_session.query(GoogleProxyGroupToGoogleBucketAccessGroup).all() + google_proxy_groups = db_session.query(GoogleProxyGroup).all() + google_bucket_access_grps = db_session.query(GoogleBucketAccessGroup).all() + + # check database to make sure all the service accounts exist + pre_deletion_google_access_size = len(google_access) + pre_deletion_google_proxy_groups_size = len(google_proxy_groups) + pre_deletion_google_bucket_access_grps_size = len(google_bucket_access_grps) + + # call function to delete expired service account + delete_expired_google_access(config["DB"]) + + google_access = db_session.query(GoogleProxyGroupToGoogleBucketAccessGroup).all() + google_proxy_groups = db_session.query(GoogleProxyGroup).all() + google_bucket_access_grps = db_session.query(GoogleBucketAccessGroup).all() + + # check database again. Expect nothing is deleted + assert len(google_access) == pre_deletion_google_access_size + assert len(google_proxy_groups) == pre_deletion_google_proxy_groups_size + assert len(google_bucket_access_grps) == pre_deletion_google_bucket_access_grps_size + + def test_verify_bucket_access_group_no_interested_accounts( app, cloud_manager, db_session, setup_test_data ): @@ -1119,7 +1319,7 @@ def test_modify_client_action_modify_allowed_scopes(db_session): client_name = "test123" client = Client( client_id=client_id, - client_secret="secret", + client_secret="secret", # pragma: allowlist secret name=client_name, _allowed_scopes="openid user data", ) @@ -1147,7 +1347,7 @@ def test_modify_client_action_modify_allowed_scopes_append_true(db_session): client_name = "test123" client = Client( client_id=client_id, - client_secret="secret", + client_secret="secret", # pragma: allowlist secret name=client_name, _allowed_scopes="openid user data", ) @@ -1176,7 +1376,7 @@ def test_modify_client_action_modify_append_url(db_session): client_name = "test123" client = Client( client_id=client_id, - client_secret="secret", + client_secret="secret", # pragma: allowlist secret name=client_name, _allowed_scopes="openid user data", redirect_uris="abcd", From 7fe77b36662df8d4946046da11bbfb7bae485404 Mon Sep 17 00:00:00 2001 From: BinamB Date: Thu, 7 Oct 2021 15:43:49 -0500 Subject: [PATCH 028/211] handle caching --- fence/resources/ga4gh/passports.py | 17 ++++++++++++++--- fence/resources/openid/ras_oauth2.py | 6 ++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index f6160b07e..cdd27ce67 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -1,4 +1,5 @@ import base64 +import flask import httpx from authutils.errors import JWTError @@ -32,8 +33,12 @@ def get_gen3_user_ids_from_ga4gh_passports(passports): was_cached = True continue + pkey_cache = flask.current_app.pkey_cache or None + # below function also validates passport (or raises exception) - raw_visas.extend(get_unvalidated_visas_from_valid_passport(passport)) + raw_visas.extend( + get_unvalidated_visas_from_valid_passport(passport, pkey_cache) + ) except Exception as exc: logger.warning(f"invalid passport provided, ignoring. Error: {exc}") continue @@ -93,6 +98,8 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): e ) ) + # ignore malformed/invalid passports + return [] public_key = pkey_cache.get(passport_issuer, {}).get(passport_kid) if not public_key: @@ -107,7 +114,7 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): ) try: logger.info("Trying to Fetch public keys from JWKs url...") - public_key = refresh_cronjob_pkey_cache( + public_key = refresh_pkey_cache( passport_issuer, passport_kid, pkey_cache ) except Exception as e: @@ -133,6 +140,8 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): ) except Exception as e: logger.error("Passport failed validation: {}. Discarding passport.".format(e)) + # ignore malformed/invalid passports + return [] return decoded_passport.get("ga4gh_passport_v1", []) @@ -176,7 +185,7 @@ def put_gen3_user_ids_for_passport_into_cache(passport, user_ids_from_passports) pass -def refresh_cronjob_pkey_cache(issuer, kid, pkey_cache): +def refresh_pkey_cache(issuer, kid, pkey_cache): """ Update app public key cache for a specific Passport Visa issuer @@ -245,4 +254,6 @@ def refresh_cronjob_pkey_cache(issuer, kid, pkey_cache): ) ) + flask.current_app.pkey_cache = pkey_cache + return pkey_cache.get(issuer, {}).get(kid) diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 51e6e3ed2..0c88aafcd 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -11,7 +11,7 @@ from fence.models import GA4GHVisaV1 from fence.resources.ga4gh.passports import ( get_unvalidated_visas_from_valid_passport, - refresh_cronjob_pkey_cache, + refresh_pkey_cache, ) from fence.utils import DEFAULT_BACKOFF_SETTINGS from .idp_oauth2 import Oauth2ClientBase @@ -190,9 +190,7 @@ def update_user_visas(self, user, pkey_cache, db_session=current_session): public_key = pkey_cache.get(visa_issuer, {}).get(visa_kid) if not public_key: try: - public_key = refresh_cronjob_pkey_cache( - visa_issuer, visa_kid, pkey_cache - ) + public_key = refresh_pkey_cache(visa_issuer, visa_kid, pkey_cache) except Exception as e: self.logger.error( "Could not refresh public key cache: {}".format(e) From 4c355d9ffd3a586f730d0ae610018a621a3949d1 Mon Sep 17 00:00:00 2001 From: BinamB Date: Mon, 11 Oct 2021 09:42:17 -0500 Subject: [PATCH 029/211] fix cronjob + extra logs --- fence/job/visa_update_cronjob.py | 4 ++++ fence/resources/ga4gh/passports.py | 8 +------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/fence/job/visa_update_cronjob.py b/fence/job/visa_update_cronjob.py index cff3bc58f..164d5f548 100644 --- a/fence/job/visa_update_cronjob.py +++ b/fence/job/visa_update_cronjob.py @@ -168,6 +168,10 @@ async def updater(self, name, updater_queue, db_session): "User {} doesnt have visa. Skipping . . .".format(user.username) ) + self.logger.info( + "Updater {} updated visa for user {}".format(name, user.username) + ) + updater_queue.task_done() def _pick_client(self, visa): diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index cdd27ce67..1d659c828 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -33,12 +33,8 @@ def get_gen3_user_ids_from_ga4gh_passports(passports): was_cached = True continue - pkey_cache = flask.current_app.pkey_cache or None - # below function also validates passport (or raises exception) - raw_visas.extend( - get_unvalidated_visas_from_valid_passport(passport, pkey_cache) - ) + raw_visas.extend(get_unvalidated_visas_from_valid_passport(passport)) except Exception as exc: logger.warning(f"invalid passport provided, ignoring. Error: {exc}") continue @@ -254,6 +250,4 @@ def refresh_pkey_cache(issuer, kid, pkey_cache): ) ) - flask.current_app.pkey_cache = pkey_cache - return pkey_cache.get(issuer, {}).get(kid) From 7435cefb56c948a952ffa0e7999348bd6a039881 Mon Sep 17 00:00:00 2001 From: BinamB Date: Mon, 11 Oct 2021 10:56:30 -0500 Subject: [PATCH 030/211] remvoe logs --- fence/job/visa_update_cronjob.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/fence/job/visa_update_cronjob.py b/fence/job/visa_update_cronjob.py index 164d5f548..cff3bc58f 100644 --- a/fence/job/visa_update_cronjob.py +++ b/fence/job/visa_update_cronjob.py @@ -168,10 +168,6 @@ async def updater(self, name, updater_queue, db_session): "User {} doesnt have visa. Skipping . . .".format(user.username) ) - self.logger.info( - "Updater {} updated visa for user {}".format(name, user.username) - ) - updater_queue.task_done() def _pick_client(self, visa): From deb00fed994f187049d68e9bca820fcd94675e7f Mon Sep 17 00:00:00 2001 From: Alexander VanTol Date: Tue, 12 Oct 2021 11:50:46 -0500 Subject: [PATCH 031/211] Update README.md From 5fd28aea649686aa40a5f6186b8395d1e0c45312 Mon Sep 17 00:00:00 2001 From: BinamB Date: Tue, 12 Oct 2021 12:07:53 -0500 Subject: [PATCH 032/211] create in context cache --- fence/__init__.py | 3 +++ fence/resources/ga4gh/passports.py | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/fence/__init__.py b/fence/__init__.py index c55a01777..4411d3b48 100755 --- a/fence/__init__.py +++ b/fence/__init__.py @@ -355,6 +355,9 @@ def app_config( _setup_oidc_clients(app) + # initialize public key cache under application context + app.pkey_cache = {} + with app.app_context(): _check_aws_creds_and_region(app) _check_azure_storage(app) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 1d659c828..ed48aff5e 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -83,7 +83,10 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): passport_issuer, passport_kid = None, None if not pkey_cache: - pkey_cache = {} + if flask.has_app_context() and flask.current_app.pkey_cache: + pkey_cache = flask.current_app.pkey_cache + else: + pkey_cache = {} try: passport_issuer = get_iss(passport) @@ -250,4 +253,7 @@ def refresh_pkey_cache(issuer, kid, pkey_cache): ) ) + if flask.has_app_context(): + flask.current_app.pkey_cache = pkey_cache + return pkey_cache.get(issuer, {}).get(kid) From 44c38bbd6e4ab6b92838fb9b8a4aaa03236f48b6 Mon Sep 17 00:00:00 2001 From: BinamB Date: Tue, 12 Oct 2021 12:19:04 -0500 Subject: [PATCH 033/211] add comment --- fence/resources/ga4gh/passports.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index ed48aff5e..d361acece 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -83,6 +83,10 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): passport_issuer, passport_kid = None, None if not pkey_cache: + # This function is shared by both fence-service (running inside of application context) + # and access token polling (running outside of application context) + # Flask doesn't really like running outside of context and setting this cache breaks things + # when running the access token polling. if flask.has_app_context() and flask.current_app.pkey_cache: pkey_cache = flask.current_app.pkey_cache else: @@ -253,6 +257,10 @@ def refresh_pkey_cache(issuer, kid, pkey_cache): ) ) + # This function is shared by both fence-service (running inside of application context) + # and access token polling (running outside of application context) + # Flask doesn't really like running outside of context and setting this cache breaks things + # when running the access token polling. if flask.has_app_context(): flask.current_app.pkey_cache = pkey_cache From 34e9ec64fa8bd14b9b1a69dedd97a2a9e09f472a Mon Sep 17 00:00:00 2001 From: BinamB Date: Tue, 12 Oct 2021 12:36:50 -0500 Subject: [PATCH 034/211] cache for visas --- fence/blueprints/login/ras.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index c6f5e00a2..d22467751 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -63,19 +63,24 @@ def post_login(self, user=None, token_result=None): raise for encoded_visa in encoded_visas: - try: - # Do not move out of loop unless we can assume every visa has same issuer and kid - public_key = get_public_key_for_token( - encoded_visa, attempt_refresh=True - ) - except Exception as e: - # (But don't log the visa contents!) - logger.error( - "Could not get public key to validate visa: {}. Discarding visa.".format( - e + if flask.current_app.pkey_cache: + logger.info("Retrieving public key from flask app ...") + public_key = flask.current_app.pkey_cache + else: + logger.info("Failed to retrieve public key from flask app") + try: + # Do not move out of loop unless we can assume every visa has same issuer and kid + public_key = get_public_key_for_token( + encoded_visa, attempt_refresh=True ) - ) - continue + except Exception as e: + # (But don't log the visa contents!) + logger.error( + "Could not get public key to validate visa: {}. Discarding visa.".format( + e + ) + ) + continue try: # Validate the visa per GA4GH AAI "Embedded access token" format rules. From 7e13de95425984ff6a1923fa5efa9cf4b25ece82 Mon Sep 17 00:00:00 2001 From: BinamB Date: Tue, 12 Oct 2021 13:05:54 -0500 Subject: [PATCH 035/211] remove stuff --- fence/blueprints/login/ras.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index d22467751..c6f5e00a2 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -63,24 +63,19 @@ def post_login(self, user=None, token_result=None): raise for encoded_visa in encoded_visas: - if flask.current_app.pkey_cache: - logger.info("Retrieving public key from flask app ...") - public_key = flask.current_app.pkey_cache - else: - logger.info("Failed to retrieve public key from flask app") - try: - # Do not move out of loop unless we can assume every visa has same issuer and kid - public_key = get_public_key_for_token( - encoded_visa, attempt_refresh=True - ) - except Exception as e: - # (But don't log the visa contents!) - logger.error( - "Could not get public key to validate visa: {}. Discarding visa.".format( - e - ) + try: + # Do not move out of loop unless we can assume every visa has same issuer and kid + public_key = get_public_key_for_token( + encoded_visa, attempt_refresh=True + ) + except Exception as e: + # (But don't log the visa contents!) + logger.error( + "Could not get public key to validate visa: {}. Discarding visa.".format( + e ) - continue + ) + continue try: # Validate the visa per GA4GH AAI "Embedded access token" format rules. From 11d5619c73d75817faeab59560ef3edb99a354c3 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Thu, 14 Oct 2021 12:19:13 -0500 Subject: [PATCH 036/211] fix(google): storage expiration script cleanup, output correct number of removed records --- fence/scripting/fence_create.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/fence/scripting/fence_create.py b/fence/scripting/fence_create.py index ef871061f..ed01ff4a3 100644 --- a/fence/scripting/fence_create.py +++ b/fence/scripting/fence_create.py @@ -694,26 +694,28 @@ def delete_expired_google_access(DB): ) .all() ) - if len(records_to_delete): + num_deleted_records = 0 + if records_to_delete: with GoogleCloudManager() as manager: for record in records_to_delete: try: - with GoogleCloudManager() as manager: - member_email = record.proxy_group.email - access_group_email = record.access_group.email - manager.remove_member_from_group( - member_email, access_group_email - ) - logger.info( - "Removed {} from {}, expired {}. Current time: {} ".format( - member_email, - access_group_email, - record.expires, - current_time, - ) + member_email = record.proxy_group.email + access_group_email = record.access_group.email + manager.remove_member_from_group( + member_email, access_group_email + ) + logger.info( + "Removed {} from {}, expired {}. Current time: {} ".format( + member_email, + access_group_email, + record.expires, + current_time, ) + ) session.delete(record) session.commit() + + num_deleted_records += 1 except Exception as e: logger.error( "ERROR: Could not remove Google group member {} from access group {}. Detail {}".format( @@ -722,7 +724,7 @@ def delete_expired_google_access(DB): ) logger.info( - f"Removed {len(records_to_delete)} expired Google Access records from db and Google." + f"Removed {num_deleted_records} expired Google Access records from db and Google." ) From d163942bf2697b09d842dddcb874239a3c4cf3db Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 15 Oct 2021 09:21:26 -0500 Subject: [PATCH 037/211] use authutils --- fence/__init__.py | 3 - fence/resources/ga4gh/passports.py | 32 +- fence/resources/openid/ras_oauth2.py | 1 + poetry.lock | 627 +++++++++++---------------- pyproject.toml | 3 +- 5 files changed, 270 insertions(+), 396 deletions(-) diff --git a/fence/__init__.py b/fence/__init__.py index 4411d3b48..c55a01777 100755 --- a/fence/__init__.py +++ b/fence/__init__.py @@ -355,9 +355,6 @@ def app_config( _setup_oidc_clients(app) - # initialize public key cache under application context - app.pkey_cache = {} - with app.app_context(): _check_aws_creds_and_region(app) _check_azure_storage(app) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index d361acece..ce7ef14ef 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -82,16 +82,6 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): decoded_passport = {} passport_issuer, passport_kid = None, None - if not pkey_cache: - # This function is shared by both fence-service (running inside of application context) - # and access token polling (running outside of application context) - # Flask doesn't really like running outside of context and setting this cache breaks things - # when running the access token polling. - if flask.has_app_context() and flask.current_app.pkey_cache: - pkey_cache = flask.current_app.pkey_cache - else: - pkey_cache = {} - try: passport_issuer = get_iss(passport) passport_kid = get_kid(passport) @@ -108,22 +98,24 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): if not public_key: try: logger.info("Fetching public key from flask app...") - public_key = get_public_key_for_token(passport, attempt_refresh=True) + public_key = get_public_key_for_token( + passport, attempt_refresh=True, pkey_cache=pkey_cache + ) except Exception as e: logger.info( "Could not fetch public key from flask app to validate passport: {}. Trying to fetch from source.".format( e ) ) - try: - logger.info("Trying to Fetch public keys from JWKs url...") - public_key = refresh_pkey_cache( - passport_issuer, passport_kid, pkey_cache - ) - except Exception as e: - logger.info( - "Could not fetch public key from JWKs key url: {}".format(e) - ) + # try: + # logger.info("Trying to Fetch public keys from JWKs url...") + # public_key = refresh_pkey_cache( + # passport_issuer, passport_kid, pkey_cache + # ) + # except Exception as e: + # logger.info( + # "Could not fetch public key from JWKs key url: {}".format(e) + # ) if not public_key: logger.error( "Could not fetch public key to validate visa: Successfully fetched " diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 0c88aafcd..17fbacef1 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -85,6 +85,7 @@ def get_encoded_visas_v11_userinfo(self, userinfo, pkey_cache=None): Return: list: list of encoded GA4GH visas """ + print("------------------------------") encoded_passport = userinfo.get("passport_jwt_v11") return get_unvalidated_visas_from_valid_passport(encoded_passport, pkey_cache) diff --git a/poetry.lock b/poetry.lock index c01a02ad7..cc2b69c40 100644 --- a/poetry.lock +++ b/poetry.lock @@ -65,36 +65,30 @@ version = "6.0.2" description = "Gen3 auth utility functions" category = "main" optional = false -python-versions = ">=3.6,<4.0" +python-versions = "^3.6" +develop = false [package.dependencies] -authlib = ">=0.11,<1.0" -cached-property = ">=1.4,<2.0" +authlib = "~=0.11" +cached-property = "~=1.4" cdiserrors = "<2.0.0" httpx = ">=0.12.1,<1.0.0" -pyjwt = {version = ">=1.5,<2.0", extras = ["crypto"]} -xmltodict = ">=0.9,<1.0" +pyjwt = {version = "~=1.5", extras = ["crypto"]} +xmltodict = "~=0.9" [package.extras] flask = ["Flask (>=0.10.1)"] fastapi = ["fastapi (>=0.54.1,<0.55.0)"] -[[package]] -name = "aws-xray-sdk" -version = "0.95" -description = "The AWS X-Ray SDK for Python (the SDK) enables Python developers to record and emit information from within their applications to the AWS X-Ray service." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -jsonpickle = "*" -requests = "*" -wrapt = "*" +[package.source] +type = "git" +url = "ssh://git@github.com/uc-cdis/authutils.git" +reference = "master" +resolved_reference = "bb465909bb8b8b86688cc9892436777362df8000" [[package]] name = "azure-core" -version = "1.18.0" +version = "1.19.0" description = "Microsoft Azure Core Library for Python" category = "main" optional = false @@ -106,7 +100,7 @@ six = ">=1.11.0" [[package]] name = "azure-storage-blob" -version = "12.8.1" +version = "12.9.0" description = "Microsoft Azure Blob Storage Client Library for Python" category = "main" optional = false @@ -115,7 +109,7 @@ python-versions = "*" [package.dependencies] azure-core = ">=1.10.0,<2.0.0" cryptography = ">=2.1.4" -msrest = ">=0.6.18" +msrest = ">=0.6.21" [[package]] name = "backoff" @@ -194,7 +188,7 @@ python-versions = ">=3.6" [[package]] name = "cachetools" -version = "4.2.2" +version = "4.2.4" description = "Extensible memoizing collections and decorators" category = "main" optional = false @@ -257,7 +251,7 @@ resolved_reference = "bdfdeb05e45407e839fd954ce6d195d847cd8024" [[package]] name = "certifi" -version = "2021.5.30" +version = "2021.10.8" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -265,7 +259,7 @@ python-versions = "*" [[package]] name = "cffi" -version = "1.14.6" +version = "1.15.0" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -276,7 +270,7 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "2.0.4" +version = "2.0.7" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -375,7 +369,7 @@ test = ["pytest (>=3.6.0,!=3.9.0,!=3.9.1,!=3.9.2)", "pretend", "iso8601", "pytz" [[package]] name = "decorator" -version = "5.0.9" +version = "5.1.0" description = "Decorators for Humans" category = "main" optional = false @@ -396,23 +390,6 @@ idna = ["idna (>=2.1)"] curio = ["curio (>=1.2)", "sniffio (>=1.1)"] trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] -[[package]] -name = "docker" -version = "5.0.2" -description = "A Python library for the Docker Engine API." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pywin32 = {version = "227", markers = "sys_platform == \"win32\""} -requests = ">=2.14.2,<2.18.0 || >2.18.0" -websocket-client = ">=0.32.0" - -[package.extras] -ssh = ["paramiko (>=2.4.2)"] -tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=3.4.7)", "idna (>=2.0.0)"] - [[package]] name = "docopt" version = "0.6.2" @@ -602,7 +579,7 @@ PyYAML = ">=5.1,<6.0" [[package]] name = "google-api-core" -version = "1.31.2" +version = "1.31.3" description = "Google API client core library" category = "main" optional = false @@ -612,7 +589,7 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" google-auth = ">=1.25.0,<2.0dev" googleapis-common-protos = ">=1.6.0,<2.0dev" packaging = ">=14.3" -protobuf = ">=3.12.0" +protobuf = ">=3.12.0,<3.18.0" pytz = "*" requests = ">=2.18.0,<3.0.0dev" six = ">=1.13.0" @@ -672,7 +649,7 @@ six = "*" [[package]] name = "google-cloud-core" -version = "2.0.0" +version = "2.1.0" description = "Google Cloud API client core library" category = "main" optional = false @@ -687,7 +664,7 @@ grpc = ["grpcio (>=1.8.2,<2.0dev)"] [[package]] name = "google-cloud-storage" -version = "1.42.0" +version = "1.42.3" description = "Google Cloud Storage API client library" category = "main" optional = false @@ -698,32 +675,31 @@ google-api-core = {version = ">=1.29.0,<3.0dev", markers = "python_version >= \" google-auth = {version = ">=1.25.0,<3.0dev", markers = "python_version >= \"3.6\""} google-cloud-core = {version = ">=1.6.0,<3.0dev", markers = "python_version >= \"3.6\""} google-resumable-media = {version = ">=1.3.0,<3.0dev", markers = "python_version >= \"3.6\""} +protobuf = {version = "*", markers = "python_version >= \"3.6\""} requests = ">=2.18.0,<3.0.0dev" +six = "*" [[package]] name = "google-crc32c" -version = "1.1.2" +version = "1.3.0" description = "A python wrapper of the C library 'Google CRC32C'" category = "main" optional = false python-versions = ">=3.6" -[package.dependencies] -cffi = ">=1.0.0" - [package.extras] testing = ["pytest"] [[package]] name = "google-resumable-media" -version = "2.0.2" +version = "2.0.3" description = "Utilities for Google Media Downloads and Resumable Uploads" category = "main" optional = false python-versions = ">= 3.6" [package.dependencies] -google-crc32c = ">=1.0,<=1.1.2" +google-crc32c = ">=1.0,<2.0dev" [package.extras] aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)"] @@ -768,18 +744,18 @@ http2 = ["h2 (>=3,<5)"] [[package]] name = "httplib2" -version = "0.19.1" +version = "0.20.1" description = "A comprehensive HTTP client library." category = "main" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pyparsing = ">=2.4.2,<3" [[package]] name = "httpx" -version = "0.19.0" +version = "0.20.0" description = "The next generation HTTP client." category = "main" optional = false @@ -795,6 +771,7 @@ sniffio = "*" [package.extras] brotli = ["brotlicffi", "brotli"] +cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10.0.0,<11.0.0)", "pygments (>=2.0.0,<3.0.0)"] http2 = ["h2 (>=3,<5)"] [[package]] @@ -877,30 +854,6 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "jsondiff" -version = "1.1.1" -description = "Diff JSON and JSON-like structures in Python" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "jsonpickle" -version = "2.0.0" -description = "Python library for serializing any arbitrary object graph into JSON" -category = "dev" -optional = false -python-versions = ">=2.7" - -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["coverage (<5)", "pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black-multipy", "pytest-cov", "ecdsa", "feedparser", "numpy", "pandas", "pymongo", "sklearn", "sqlalchemy", "enum34", "jsonlib"] -"testing.libs" = ["demjson", "simplejson", "ujson", "yajl"] - [[package]] name = "markdown" version = "3.3.4" @@ -917,11 +870,11 @@ testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" -version = "2.0.1" +version = "1.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" [[package]] name = "mock" @@ -941,7 +894,7 @@ test = ["unittest2 (>=1.1.0)"] [[package]] name = "more-itertools" -version = "8.9.0" +version = "8.10.0" description = "More routines for operating on iterables, beyond itertools" category = "dev" optional = false @@ -949,34 +902,41 @@ python-versions = ">=3.5" [[package]] name = "moto" -version = "1.3.7" +version = "1.3.15" description = "A library that allows your python tests to easily mock out the boto library" category = "dev" optional = false python-versions = "*" [package.dependencies] -aws-xray-sdk = ">=0.93,<0.96" boto = ">=2.36.0" -boto3 = ">=1.6.16" -botocore = ">=1.12.13" -cryptography = ">=2.3.0" -docker = ">=2.5.1" -Jinja2 = ">=2.7.3" -jsondiff = "1.1.1" +boto3 = ">=1.9.201" +botocore = ">=1.12.201" +Jinja2 = ">=2.10.1" +MarkupSafe = "<2.0" mock = "*" -pyaml = "*" +more-itertools = "*" python-dateutil = ">=2.1,<3.0.0" -python-jose = "<3.0.0" pytz = "*" requests = ">=2.5" responses = ">=0.9.0" six = ">1.9" werkzeug = "*" xmltodict = "*" +zipp = "*" [package.extras] +acm = ["cryptography (>=2.3.0)"] +all = ["cryptography (>=2.3.0)", "PyYAML (>=5.1)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)", "docker (>=2.5.1)", "jsondiff (>=1.1.2)", "aws-xray-sdk (>=0.93,!=0.96)", "idna (>=2.5,<3)", "cfn-lint (>=0.4.0)", "sshpubkeys (>=3.1.0,<4.0)", "sshpubkeys (>=3.1.0)"] +awslambda = ["docker (>=2.5.1)"] +batch = ["docker (>=2.5.1)"] +cloudformation = ["PyYAML (>=5.1)", "cfn-lint (>=0.4.0)"] +cognitoidp = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)"] +ec2 = ["cryptography (>=2.3.0)", "sshpubkeys (>=3.1.0,<4.0)", "sshpubkeys (>=3.1.0)"] +iam = ["cryptography (>=2.3.0)"] +iotdata = ["jsondiff (>=1.1.2)"] server = ["flask"] +xray = ["aws-xray-sdk (>=0.93,!=0.96)"] [[package]] name = "msrest" @@ -1036,7 +996,7 @@ pyparsing = ">=2.0.2" [[package]] name = "paramiko" -version = "2.7.2" +version = "2.8.0" description = "SSH2 protocol library" category = "main" optional = false @@ -1089,7 +1049,7 @@ twisted = ["twisted"] [[package]] name = "prometheus-flask-exporter" -version = "0.18.2" +version = "0.18.3" description = "Prometheus metrics exporter for Flask" category = "main" optional = false @@ -1097,7 +1057,7 @@ python-versions = "*" [package.dependencies] flask = "*" -prometheus_client = "*" +prometheus-client = "*" [[package]] name = "protobuf" @@ -1126,17 +1086,6 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -[[package]] -name = "pyaml" -version = "21.8.3" -description = "PyYAML-based module to produce pretty and readable YAML-serialized data" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -PyYAML = "*" - [[package]] name = "pyasn1" version = "0.4.8" @@ -1166,7 +1115,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pycryptodome" -version = "3.10.1" +version = "3.11.0" description = "Cryptographic library for Python" category = "main" optional = false @@ -1294,20 +1243,12 @@ pycrypto = ["pycrypto (>=2.6.0,<2.7.0)"] [[package]] name = "pytz" -version = "2021.1" +version = "2021.3" description = "World timezone definitions, modern and historical" category = "main" optional = false python-versions = "*" -[[package]] -name = "pywin32" -version = "227" -description = "Python for Window Extensions" -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "pyyaml" version = "5.4.1" @@ -1351,7 +1292,7 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] [[package]] name = "responses" -version = "0.13.4" +version = "0.14.0" description = "A utility library for mocking out the `requests` Python library." category = "dev" optional = false @@ -1520,18 +1461,6 @@ python-versions = "*" cdislogging = "*" sqlalchemy = ">=1.3.3,<1.4.0" -[[package]] -name = "websocket-client" -version = "1.2.1" -description = "WebSocket client for Python with low level API options" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - [[package]] name = "werkzeug" version = "1.0.1" @@ -1544,14 +1473,6 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] watchdog = ["watchdog"] -[[package]] -name = "wrapt" -version = "1.12.1" -description = "Module for decorators, wrappers and monkey patching." -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "wtforms" version = "2.3.3" @@ -1578,7 +1499,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "zipp" -version = "3.5.0" +version = "3.6.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false @@ -1591,7 +1512,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "d797b83faaedf69db00c086a204145cde81cad1d6365c057c56e4029ec74e7a2" +content-hash = "63afb22aca669a0c0c7ba43e7a419c2eb9dca2fb868802557e87bd9790449918" [metadata.files] addict = [ @@ -1618,21 +1539,14 @@ authlib = [ {file = "Authlib-0.11-py2.py3-none-any.whl", hash = "sha256:3a226f231e962a16dd5f6fcf0c113235805ba206e294717a64fa8e04ae3ad9c4"}, {file = "Authlib-0.11.tar.gz", hash = "sha256:9741db6de2950a0a5cefbdb72ec7ab12f7e9fd530ff47219f1530e79183cbaaf"}, ] -authutils = [ - {file = "authutils-6.0.2-py3-none-any.whl", hash = "sha256:b1ded486669ae181d451052e5c435300b33d3c9cb7edbe75da5ba9530f3e5128"}, - {file = "authutils-6.0.2.tar.gz", hash = "sha256:709e4c1bb95ebe29c7bf3ec1d6b9a0dca030495cdab61afe74064e507cf70ae4"}, -] -aws-xray-sdk = [ - {file = "aws-xray-sdk-0.95.tar.gz", hash = "sha256:9e7ba8dd08fd2939376c21423376206bff01d0deaea7d7721c6b35921fed1943"}, - {file = "aws_xray_sdk-0.95-py2.py3-none-any.whl", hash = "sha256:72791618feb22eaff2e628462b0d58f398ce8c1bacfa989b7679817ab1fad60c"}, -] +authutils = [] azure-core = [ - {file = "azure-core-1.18.0.zip", hash = "sha256:7f17db829c926ab3b922d63b6f0b86ef3c597487fbb264defa8eb4ccb761e8a0"}, - {file = "azure_core-1.18.0-py2.py3-none-any.whl", hash = "sha256:3d7769c031822eab3b3ebd58299999b731b30cedb25d3a6c61e551926749c564"}, + {file = "azure-core-1.19.0.zip", hash = "sha256:18d2a6cd3b7391489f005775fe69e4d0870f9384b755e45185efd45c050e2306"}, + {file = "azure_core-1.19.0-py2.py3-none-any.whl", hash = "sha256:4fbbe8b867ef077df77614b86b7927e4d87aa7a0bd54e771d9ba14f48dae2c4b"}, ] azure-storage-blob = [ - {file = "azure-storage-blob-12.8.1.zip", hash = "sha256:eb37b50ddfb6e558b29f6c8c03b0666514e55d6170bf4624e7261a3af93c6401"}, - {file = "azure_storage_blob-12.8.1-py2.py3-none-any.whl", hash = "sha256:e74c2c49fd04b80225f5b9734f1dbd417d89f280abfedccced3ac21509e1659d"}, + {file = "azure-storage-blob-12.9.0.zip", hash = "sha256:cff66a115c73c90e496c8c8b3026898a3ce64100840276e9245434e28a864225"}, + {file = "azure_storage_blob-12.9.0-py2.py3-none-any.whl", hash = "sha256:859195b4850dcfe77ffafbe53500abb74b001e52e77fe6d9492fa73639a22127"}, ] backoff = [ {file = "backoff-1.11.1-py2.py3-none-any.whl", hash = "sha256:61928f8fa48d52e4faa81875eecf308eccfb1016b018bb6bd21e05b5d90a96c5"}, @@ -1668,8 +1582,8 @@ cachelib = [ {file = "cachelib-0.2.0.tar.gz", hash = "sha256:dcb5fafe6b6b544aaa8d0cacb12d70bbf9bbf72c041f17fcad1618db7bedeada"}, ] cachetools = [ - {file = "cachetools-4.2.2-py3-none-any.whl", hash = "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001"}, - {file = "cachetools-4.2.2.tar.gz", hash = "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"}, + {file = "cachetools-4.2.4-py3-none-any.whl", hash = "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1"}, + {file = "cachetools-4.2.4.tar.gz", hash = "sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693"}, ] cdiserrors = [ {file = "cdiserrors-1.0.0-py3-none-any.whl", hash = "sha256:2e188645832e8c98468267af3e54bc5d3a298078b9869899256251e54dc1599d"}, @@ -1683,59 +1597,64 @@ cdispyutils = [ ] cdisutilstest = [] certifi = [ - {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, - {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, ] cffi = [ - {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"}, - {file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"}, - {file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"}, - {file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"}, - {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"}, - {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"}, - {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"}, - {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"}, - {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"}, - {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"}, - {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"}, - {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"}, - {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"}, - {file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"}, - {file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"}, - {file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"}, - {file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"}, - {file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"}, - {file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"}, - {file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"}, - {file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"}, - {file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"}, - {file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"}, - {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"}, - {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"}, + {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, + {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, + {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, + {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, + {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, + {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, + {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, + {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, + {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, + {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, + {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, + {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, + {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, + {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, + {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, + {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, + {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, - {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, + {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"}, + {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"}, ] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, @@ -1834,17 +1753,13 @@ cryptography = [ {file = "cryptography-2.8.tar.gz", hash = "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651"}, ] decorator = [ - {file = "decorator-5.0.9-py3-none-any.whl", hash = "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323"}, - {file = "decorator-5.0.9.tar.gz", hash = "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"}, + {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"}, + {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"}, ] dnspython = [ {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, ] -docker = [ - {file = "docker-5.0.2-py2.py3-none-any.whl", hash = "sha256:9b17f0723d83c1f3418d2aa17bf90b24dbe97deda06208dd4262fa30a6ee87eb"}, - {file = "docker-5.0.2.tar.gz", hash = "sha256:21ec4998e90dff7a7aaaa098ca8d839c7de412b89e6f6c30908372d58fecf663"}, -] docopt = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] @@ -1897,8 +1812,8 @@ gen3users = [ {file = "gen3users-0.6.0.tar.gz", hash = "sha256:3b9b56798a7d8b34712389dbbab93c00b0f92524f890513f899c31630ea986da"}, ] google-api-core = [ - {file = "google-api-core-1.31.2.tar.gz", hash = "sha256:8500aded318fdb235130bf183c726a05a9cb7c4b09c266bd5119b86cdb8a4d10"}, - {file = "google_api_core-1.31.2-py2.py3-none-any.whl", hash = "sha256:384459a0dc98c1c8cd90b28dc5800b8705e0275a673a7144a513ae80fc77950b"}, + {file = "google-api-core-1.31.3.tar.gz", hash = "sha256:4b7ad965865aef22afa4aded3318b8fa09b20bcc7e8dbb639a3753cf60af08ea"}, + {file = "google_api_core-1.31.3-py2.py3-none-any.whl", hash = "sha256:f52c708ab9fd958862dea9ac94d9db1a065608073fe583c3b9c18537b177f59a"}, ] google-api-python-client = [ {file = "google-api-python-client-1.11.0.tar.gz", hash = "sha256:caf4015800ef1a18d06d117f47f0219c0c0641f21978f6b1bb5ede7912fab97b"}, @@ -1913,47 +1828,61 @@ google-auth-httplib2 = [ {file = "google_auth_httplib2-0.1.0-py2.py3-none-any.whl", hash = "sha256:31e49c36c6b5643b57e82617cb3e021e3e1d2df9da63af67252c02fa9c1f4a10"}, ] google-cloud-core = [ - {file = "google-cloud-core-2.0.0.tar.gz", hash = "sha256:90ee99648ccf9e11a16781a7fc58d13e58f662b439c737d48c24ef18662c2702"}, - {file = "google_cloud_core-2.0.0-py2.py3-none-any.whl", hash = "sha256:31785d1e1d02f90ad3f1b020d4aed63db4865c3394ff7c128a296b6995eef31f"}, + {file = "google-cloud-core-2.1.0.tar.gz", hash = "sha256:35a1f5f02a86e0fa2e28c669f0db4a76d928671a28fbbbb493ab59ba9d1cb9a9"}, + {file = "google_cloud_core-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d5fed11731dae8bc8656a2c9fa8ff17bdfdfd083cba97569324e35b94e7e002"}, ] google-cloud-storage = [ - {file = "google-cloud-storage-1.42.0.tar.gz", hash = "sha256:c1dd3d09198edcf24ec6803dd4545e867d82b998f06a68ead3b6857b1840bdae"}, - {file = "google_cloud_storage-1.42.0-py2.py3-none-any.whl", hash = "sha256:92a9c8b1a6a278c5c12877fe1a966ecd0cae327cf98c6ae50deedf1a32d6cf2b"}, + {file = "google-cloud-storage-1.42.3.tar.gz", hash = "sha256:7754d4dcaa45975514b404ece0da2bb4292acbc67ca559a69e12a19d54fcdb06"}, + {file = "google_cloud_storage-1.42.3-py2.py3-none-any.whl", hash = "sha256:71ee3a0dcf2c139f034a054181cd7658f1ec8f12837d2769c450a8a00fcd4c6d"}, ] google-crc32c = [ - {file = "google-crc32c-1.1.2.tar.gz", hash = "sha256:dff5bd1236737f66950999d25de7a78144548ebac7788d30ada8c1b6ead60b27"}, - {file = "google_crc32c-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8ed8f6dc4f55850cba2eb22b78902ad37f397ee02692d3b8e00842e9af757321"}, - {file = "google_crc32c-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:110157fb19ab5db15603debfaf5fcfbac9627576787d9caf8618ff96821a7a1f"}, - {file = "google_crc32c-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:80abca603187093ea089cd1215c3779040dda55d3cdabc0cd5ea0e10df7bff99"}, - {file = "google_crc32c-1.1.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6789db0b12aab12a0f04de22ed8412dfa5f6abd5a342ea19f15355064e1cc387"}, - {file = "google_crc32c-1.1.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ea170341a4a9078a067b431044cd56c73553425833a7c2bb81734777a230ad4b"}, - {file = "google_crc32c-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:a64e0e8ed6076a8d867fc4622ad821c55eba8dff1b48b18f56b7c2392e22ab9d"}, - {file = "google_crc32c-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9372211acbcc207f63ffaffea1d05f3244a21311e4710721ffff3e8b7a0d24d0"}, - {file = "google_crc32c-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:0ae3cf54e0d4d83c8af1afe96fc0970fbf32f1b29275f3bfd44ce25c4b622a2b"}, - {file = "google_crc32c-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:34a97937f164147aefa53c3277364fd3bfa7fd244cbebbd5a976fa8325fb496b"}, - {file = "google_crc32c-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:91ad96ee2958311d0bb75ffe5c25c87fb521ef547c09e04a8bb6143e75fb1367"}, - {file = "google_crc32c-1.1.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b5ea1055fe470334ced844270e7c808b04fe31e3e6394675daa77f6789ca9eff"}, - {file = "google_crc32c-1.1.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:e6458c41236d37cb982120b070ebcc115687c852bee24cdd18792da2640cf44d"}, - {file = "google_crc32c-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:e5af77656e8d367701f40f80a91c985ca43319f322f0a36ba9f93909d0bc4cb2"}, - {file = "google_crc32c-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:ae7b9e7e2ca1b06c3a68b6ef223947a52c30ffae329b1a2be3402756073f2732"}, - {file = "google_crc32c-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:7c5138ed2e815189ba524756e027ac5833365e86115b1c2e6d9e833974a58d82"}, - {file = "google_crc32c-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6c8a712ffae56c805ca732b735af02860b246bed2c1acb38ea954a8b2dc4581"}, - {file = "google_crc32c-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:49838ede42592154f9fcd21d07c7a43a67b00a36e252f82ae72542fde09dc51f"}, - {file = "google_crc32c-1.1.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:ef2ed6d0ac4de4ac602903e203eccd25ec8e37f1446fe1a3d2953a658035e0a5"}, - {file = "google_crc32c-1.1.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:51f4aa06125bf0641f65fb83268853545dbeb36b98ccfec69ef57dcb6b73b176"}, - {file = "google_crc32c-1.1.2-cp38-cp38-win32.whl", hash = "sha256:1dc6904c0d958f43102c85d70792cca210d3d051ddbeecd0eff10abcd981fdfa"}, - {file = "google_crc32c-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:298a9a922d35b123a73be80233d0f19c6ea01f008743561a8937f9dd83fb586b"}, - {file = "google_crc32c-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ab2b31395fbeeae6d15c98bd7f8b9fb76a18f18f87adc11b1f6dbe8f90d8382f"}, - {file = "google_crc32c-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d4a0d4fb938c2c3c0076445c9bd1215a3bd3df557b88d8b05ec2889ca0c92f8d"}, - {file = "google_crc32c-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:d0630670d27785d7e610e72752dc8087436d00d2c7115e149c0a754babb56d3e"}, - {file = "google_crc32c-1.1.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:364eb36e8d9d34542c17b0c410035b0557edd4300a92ed736b237afaa0fd6dae"}, - {file = "google_crc32c-1.1.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:0dd9b61d0c63043b013349c9ec8a83ec2b05c96410c5bc257da5d0de743fc171"}, - {file = "google_crc32c-1.1.2-cp39-cp39-win32.whl", hash = "sha256:92ed6062792b989e84621e07a5f3d37da9cc3153b77d23a582921f14863af31d"}, - {file = "google_crc32c-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:78cf5b1bd30f3a6033b41aa4ce8c796870bc4645a15d3ef47a4b05d31b0a6dc1"}, + {file = "google-crc32c-1.3.0.tar.gz", hash = "sha256:276de6273eb074a35bc598f8efbc00c7869c5cf2e29c90748fccc8c898c244df"}, + {file = "google_crc32c-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cb6994fff247987c66a8a4e550ef374671c2b82e3c0d2115e689d21e511a652d"}, + {file = "google_crc32c-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9da0a39b53d2fab3e5467329ed50e951eb91386e9d0d5b12daf593973c3b168"}, + {file = "google_crc32c-1.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:eb0b14523758e37802f27b7f8cd973f5f3d33be7613952c0df904b68c4842f0e"}, + {file = "google_crc32c-1.3.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:95c68a4b9b7828ba0428f8f7e3109c5d476ca44996ed9a5f8aac6269296e2d59"}, + {file = "google_crc32c-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c3cf890c3c0ecfe1510a452a165431b5831e24160c5fcf2071f0f85ca5a47cd"}, + {file = "google_crc32c-1.3.0-cp310-cp310-win32.whl", hash = "sha256:3bbce1be3687bbfebe29abdb7631b83e6b25da3f4e1856a1611eb21854b689ea"}, + {file = "google_crc32c-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:c124b8c8779bf2d35d9b721e52d4adb41c9bfbde45e6a3f25f0820caa9aba73f"}, + {file = "google_crc32c-1.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:42ae4781333e331a1743445931b08ebdad73e188fd554259e772556fc4937c48"}, + {file = "google_crc32c-1.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ff71073ebf0e42258a42a0b34f2c09ec384977e7f6808999102eedd5b49920e3"}, + {file = "google_crc32c-1.3.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fe31de3002e7b08eb20823b3735b97c86c5926dd0581c7710a680b418a8709d4"}, + {file = "google_crc32c-1.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7760a88a8d3d705ff562aa93f8445ead54f58fd482e4f9e2bafb7e177375d4"}, + {file = "google_crc32c-1.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a0b9e622c3b2b8d0ce32f77eba617ab0d6768b82836391e4f8f9e2074582bf02"}, + {file = "google_crc32c-1.3.0-cp36-cp36m-win32.whl", hash = "sha256:779cbf1ce375b96111db98fca913c1f5ec11b1d870e529b1dc7354b2681a8c3a"}, + {file = "google_crc32c-1.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:04e7c220798a72fd0f08242bc8d7a05986b2a08a0573396187fd32c1dcdd58b3"}, + {file = "google_crc32c-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e7a539b9be7b9c00f11ef16b55486141bc2cdb0c54762f84e3c6fc091917436d"}, + {file = "google_crc32c-1.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ca60076c388728d3b6ac3846842474f4250c91efbfe5afa872d3ffd69dd4b318"}, + {file = "google_crc32c-1.3.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05340b60bf05b574159e9bd940152a47d38af3fb43803ffe71f11d704b7696a6"}, + {file = "google_crc32c-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:318f73f5484b5671f0c7f5f63741ab020a599504ed81d209b5c7129ee4667407"}, + {file = "google_crc32c-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9f58099ad7affc0754ae42e6d87443299f15d739b0ce03c76f515153a5cda06c"}, + {file = "google_crc32c-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:f52a4ad2568314ee713715b1e2d79ab55fab11e8b304fd1462ff5cccf4264b3e"}, + {file = "google_crc32c-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bab4aebd525218bab4ee615786c4581952eadc16b1ff031813a2fd51f0cc7b08"}, + {file = "google_crc32c-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dda4d8a3bb0b50f540f6ff4b6033f3a74e8bf0bd5320b70fab2c03e512a62812"}, + {file = "google_crc32c-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fec221a051150eeddfdfcff162e6db92c65ecf46cb0f7bb1bf812a1520ec026b"}, + {file = "google_crc32c-1.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:226f2f9b8e128a6ca6a9af9b9e8384f7b53a801907425c9a292553a3a7218ce0"}, + {file = "google_crc32c-1.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a7f9cbea4245ee36190f85fe1814e2d7b1e5f2186381b082f5d59f99b7f11328"}, + {file = "google_crc32c-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a4db36f9721fdf391646685ecffa404eb986cbe007a3289499020daf72e88a2"}, + {file = "google_crc32c-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:12674a4c3b56b706153a358eaa1018c4137a5a04635b92b4652440d3d7386206"}, + {file = "google_crc32c-1.3.0-cp38-cp38-win32.whl", hash = "sha256:650e2917660e696041ab3dcd7abac160b4121cd9a484c08406f24c5964099829"}, + {file = "google_crc32c-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:58be56ae0529c664cc04a9c76e68bb92b091e0194d6e3c50bea7e0f266f73713"}, + {file = "google_crc32c-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:96a8918a78d5d64e07c8ea4ed2bc44354e3f93f46a4866a40e8db934e4c0d74b"}, + {file = "google_crc32c-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:13af315c3a0eec8bb8b8d80b8b128cb3fcd17d7e4edafc39647846345a3f003a"}, + {file = "google_crc32c-1.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6311853aa2bba4064d0c28ca54e7b50c4d48e3de04f6770f6c60ebda1e975267"}, + {file = "google_crc32c-1.3.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ed447680ff21c14aaceb6a9f99a5f639f583ccfe4ce1a5e1d48eb41c3d6b3217"}, + {file = "google_crc32c-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1c1d6236feab51200272d79b3d3e0f12cf2cbb12b208c835b175a21efdb0a73"}, + {file = "google_crc32c-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e0f1ff55dde0ebcfbef027edc21f71c205845585fffe30d4ec4979416613e9b3"}, + {file = "google_crc32c-1.3.0-cp39-cp39-win32.whl", hash = "sha256:fbd60c6aaa07c31d7754edbc2334aef50601b7f1ada67a96eb1eb57c7c72378f"}, + {file = "google_crc32c-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:127f9cc3ac41b6a859bd9dc4321097b1a4f6aa7fdf71b4f9227b9e3ebffb4422"}, + {file = "google_crc32c-1.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fc28e0db232c62ca0c3600884933178f0825c99be4474cdd645e378a10588125"}, + {file = "google_crc32c-1.3.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1926fd8de0acb9d15ee757175ce7242e235482a783cd4ec711cc999fc103c24e"}, + {file = "google_crc32c-1.3.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5da2c81575cc3ccf05d9830f9e8d3c70954819ca9a63828210498c0774fda1a3"}, + {file = "google_crc32c-1.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f712ce54e0d631370e1f4997b3f182f3368179198efc30d477c75d1f44942"}, + {file = "google_crc32c-1.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:7f6fe42536d9dcd3e2ffb9d3053f5d05221ae3bbcefbe472bdf2c71c793e3183"}, ] google-resumable-media = [ - {file = "google-resumable-media-2.0.2.tar.gz", hash = "sha256:36d682161fdcbfa29681212c210fabecbf6849a505a0cbc54b7f70a10a5278a2"}, - {file = "google_resumable_media-2.0.2-py2.py3-none-any.whl", hash = "sha256:91f41314433601f94d485c1f56adfa2a7db538d53c95e0994be68cd1a17314b5"}, + {file = "google-resumable-media-2.0.3.tar.gz", hash = "sha256:b4b4709d04a6a03cbec746c2b5cb18f1f9878bf1ef3cd61908842a3d94c20471"}, + {file = "google_resumable_media-2.0.3-py2.py3-none-any.whl", hash = "sha256:efe23e22bc9838630f0fd185e21de503c731a726e66da90c1572653d8480e8e4"}, ] googleapis-common-protos = [ {file = "googleapis-common-protos-1.53.0.tar.gz", hash = "sha256:a88ee8903aa0a81f6c3cec2d5cf62d3c8aa67c06439b0496b49048fb1854ebf4"}, @@ -1968,12 +1897,12 @@ httpcore = [ {file = "httpcore-0.13.3.tar.gz", hash = "sha256:5d674b57a11275904d4fd0819ca02f960c538e4472533620f322fc7db1ea0edc"}, ] httplib2 = [ - {file = "httplib2-0.19.1-py3-none-any.whl", hash = "sha256:2ad195faf9faf079723f6714926e9a9061f694d07724b846658ce08d40f522b4"}, - {file = "httplib2-0.19.1.tar.gz", hash = "sha256:0b12617eeca7433d4c396a100eaecfa4b08ee99aa881e6df6e257a7aad5d533d"}, + {file = "httplib2-0.20.1-py3-none-any.whl", hash = "sha256:8fa4dbf2fbf839b71f8c7837a831e00fcdc860feca99b8bda58ceae4bc53d185"}, + {file = "httplib2-0.20.1.tar.gz", hash = "sha256:0efbcb8bfbfbc11578130d87d8afcc65c2274c6eb446e59fc674e4d7c972d327"}, ] httpx = [ - {file = "httpx-0.19.0-py3-none-any.whl", hash = "sha256:9bd728a6c5ec0a9e243932a9983d57d3cc4a87bb4f554e1360fce407f78f9435"}, - {file = "httpx-0.19.0.tar.gz", hash = "sha256:92ecd2c00c688b529eda11cedb15161eaf02dee9116712f621c70d9a40b2cdd0"}, + {file = "httpx-0.20.0-py3-none-any.whl", hash = "sha256:33af5aad9bdc82ef1fc89219c1e36f5693bf9cd0ebe330884df563445682c0f8"}, + {file = "httpx-0.20.0.tar.gz", hash = "sha256:09606d630f070d07f9ff28104fbcea429ea0014c1e89ac90b4d8de8286c40e7b"}, ] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, @@ -2028,64 +1957,56 @@ jmespath = [ {file = "jmespath-0.9.2-py2.py3-none-any.whl", hash = "sha256:3f03b90ac8e0f3ba472e8ebff083e460c89501d8d41979771535efe9a343177e"}, {file = "jmespath-0.9.2.tar.gz", hash = "sha256:54c441e2e08b23f12d7fa7d8e6761768c47c969e6aed10eead57505ba760aee9"}, ] -jsondiff = [ - {file = "jsondiff-1.1.1.tar.gz", hash = "sha256:2d0437782de9418efa34e694aa59f43d7adb1899bd9a793f063867ddba8f7893"}, -] -jsonpickle = [ - {file = "jsonpickle-2.0.0-py2.py3-none-any.whl", hash = "sha256:c1010994c1fbda87a48f8a56698605b598cb0fc6bb7e7927559fc1100e69aeac"}, - {file = "jsonpickle-2.0.0.tar.gz", hash = "sha256:0be49cba80ea6f87a168aa8168d717d00c6ca07ba83df3cec32d3b30bfe6fb9a"}, -] markdown = [ {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, ] markupsafe = [ - {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, - {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mock = [ {file = "mock-2.0.0-py2.py3-none-any.whl", hash = "sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1"}, {file = "mock-2.0.0.tar.gz", hash = "sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba"}, ] more-itertools = [ - {file = "more-itertools-8.9.0.tar.gz", hash = "sha256:8c746e0d09871661520da4f1241ba6b908dc903839733c8203b552cffaf173bd"}, - {file = "more_itertools-8.9.0-py3-none-any.whl", hash = "sha256:70401259e46e216056367a0a6034ee3d3f95e0bf59d3aa6a4eb77837171ed996"}, + {file = "more-itertools-8.10.0.tar.gz", hash = "sha256:1debcabeb1df793814859d64a81ad7cb10504c24349368ccf214c664c474f41f"}, + {file = "more_itertools-8.10.0-py3-none-any.whl", hash = "sha256:56ddac45541718ba332db05f464bebfb0768110111affd27f66e0051f276fa43"}, ] moto = [ - {file = "moto-1.3.7-py2.py3-none-any.whl", hash = "sha256:4df37936ff8d6a4b8229aab347a7b412cd2ca4823ff47bd1362ddfbc6c5e4ecf"}, - {file = "moto-1.3.7.tar.gz", hash = "sha256:129de2e04cb250d9f8b2c722ec152ed1b5426ef179b4ebb03e9ec36e6eb3fcc5"}, + {file = "moto-1.3.15-py2.py3-none-any.whl", hash = "sha256:3be7e1f406ef7e9c222dbcbfd8cefa2cb1062200e26deae49b5df446e17be3df"}, + {file = "moto-1.3.15.tar.gz", hash = "sha256:fd98f7b219084ba8aadad263849c4dbe8be73979e035d8dc5c86e11a86f11b7f"}, ] msrest = [ {file = "msrest-0.6.21-py2.py3-none-any.whl", hash = "sha256:c840511c845330e96886011a236440fafc2c9aff7b2df9c0a92041ee2dee3782"}, @@ -2103,8 +2024,8 @@ packaging = [ {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] paramiko = [ - {file = "paramiko-2.7.2-py2.py3-none-any.whl", hash = "sha256:4f3e316fef2ac628b05097a637af35685183111d4bc1b5979bd397c2ab7b5898"}, - {file = "paramiko-2.7.2.tar.gz", hash = "sha256:7f36f4ba2c0d81d219f4595e35f70d56cc94f9ac40a6acdf51d6ca210ce65035"}, + {file = "paramiko-2.8.0-py2.py3-none-any.whl", hash = "sha256:def3ec612399bab4e9f5eb66b0ae5983980db9dd9120d9e9c6ea3ff673865d1c"}, + {file = "paramiko-2.8.0.tar.gz", hash = "sha256:e673b10ee0f1c80d46182d3af7751d033d9b573dd7054d2d0aa46be186c3c1d2"}, ] pbr = [ {file = "pbr-2.0.0-py2.py3-none-any.whl", hash = "sha256:d9b69a26a5cb4e3898eb3c5cea54d2ab3332382167f04e30db5e1f54e1945e45"}, @@ -2119,7 +2040,8 @@ prometheus-client = [ {file = "prometheus_client-0.9.0.tar.gz", hash = "sha256:9da7b32f02439d8c04f7777021c304ed51d9ec180604700c1ba72a4d44dceb03"}, ] prometheus-flask-exporter = [ - {file = "prometheus_flask_exporter-0.18.2.tar.gz", hash = "sha256:fc487e385d95cb5efd045d6a315c4ecf68c42661e7bfde0526af75ed3c4f8c1b"}, + {file = "prometheus_flask_exporter-0.18.3-py3-none-any.whl", hash = "sha256:abc8207c74096444580771fa5899df6dfaa5834229c2a62f12dd31817f5f373a"}, + {file = "prometheus_flask_exporter-0.18.3.tar.gz", hash = "sha256:96013537284154a83d90cedaf0bcd5011c6053d8f1a50e775dfebdb491eba7c7"}, ] protobuf = [ {file = "protobuf-3.17.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ab6bb0e270c6c58e7ff4345b3a803cc59dbee19ddf77a4719c5b635f1d547aa8"}, @@ -2140,13 +2062,9 @@ protobuf = [ {file = "protobuf-3.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ae692bb6d1992afb6b74348e7bb648a75bb0d3565a3f5eea5bec8f62bd06d87"}, {file = "protobuf-3.17.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:99938f2a2d7ca6563c0ade0c5ca8982264c484fdecf418bd68e880a7ab5730b1"}, {file = "protobuf-3.17.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6902a1e4b7a319ec611a7345ff81b6b004b36b0d2196ce7a748b3493da3d226d"}, - {file = "protobuf-3.17.3-cp38-cp38-win32.whl", hash = "sha256:59e5cf6b737c3a376932fbfb869043415f7c16a0cf176ab30a5bbc419cd709c1"}, - {file = "protobuf-3.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ebcb546f10069b56dc2e3da35e003a02076aaa377caf8530fe9789570984a8d2"}, {file = "protobuf-3.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ffbd23640bb7403574f7aff8368e2aeb2ec9a5c6306580be48ac59a6bac8bde"}, {file = "protobuf-3.17.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:26010f693b675ff5a1d0e1bdb17689b8b716a18709113288fead438703d45539"}, {file = "protobuf-3.17.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e76d9686e088fece2450dbc7ee905f9be904e427341d289acbe9ad00b78ebd47"}, - {file = "protobuf-3.17.3-cp39-cp39-win32.whl", hash = "sha256:a38bac25f51c93e4be4092c88b2568b9f407c27217d3dd23c7a57fa522a17554"}, - {file = "protobuf-3.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:85d6303e4adade2827e43c2b54114d9a6ea547b671cb63fafd5011dc47d0e13d"}, {file = "protobuf-3.17.3-py2.py3-none-any.whl", hash = "sha256:2bfb815216a9cd9faec52b16fd2bfa68437a44b67c56bee59bc3926522ecb04e"}, {file = "protobuf-3.17.3.tar.gz", hash = "sha256:72804ea5eaa9c22a090d2803813e280fb273b62d5ae497aaf3553d141c4fdd7b"}, ] @@ -2165,10 +2083,6 @@ py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] -pyaml = [ - {file = "pyaml-21.8.3-py2.py3-none-any.whl", hash = "sha256:aa61d6ebef7cd8ec691620616258d904bfbc152e9cf44557202b8bacc9ce5cce"}, - {file = "pyaml-21.8.3.tar.gz", hash = "sha256:a1636d63c476328a07213d0b7111bb63570f1ab8a3eddf60522630250c23d975"}, -] pyasn1 = [ {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, @@ -2204,36 +2118,36 @@ pycparser = [ {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, ] pycryptodome = [ - {file = "pycryptodome-3.10.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1c5e1ca507de2ad93474be5cfe2bfa76b7cf039a1a32fc196f40935944871a06"}, - {file = "pycryptodome-3.10.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:6260e24d41149268122dd39d4ebd5941e9d107f49463f7e071fd397e29923b0c"}, - {file = "pycryptodome-3.10.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:3f840c49d38986f6e17dbc0673d37947c88bc9d2d9dba1c01b979b36f8447db1"}, - {file = "pycryptodome-3.10.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:2dea65df54349cdfa43d6b2e8edb83f5f8d6861e5cf7b1fbc3e34c5694c85e27"}, - {file = "pycryptodome-3.10.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:e61e363d9a5d7916f3a4ce984a929514c0df3daf3b1b2eb5e6edbb131ee771cf"}, - {file = "pycryptodome-3.10.1-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:2603c98ae04aac675fefcf71a6c87dc4bb74a75e9071ae3923bbc91a59f08d35"}, - {file = "pycryptodome-3.10.1-cp27-cp27m-win32.whl", hash = "sha256:38661348ecb71476037f1e1f553159b80d256c00f6c0b00502acac891f7116d9"}, - {file = "pycryptodome-3.10.1-cp27-cp27m-win_amd64.whl", hash = "sha256:1723ebee5561628ce96748501cdaa7afaa67329d753933296321f0be55358dce"}, - {file = "pycryptodome-3.10.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:77997519d8eb8a4adcd9a47b9cec18f9b323e296986528186c0e9a7a15d6a07e"}, - {file = "pycryptodome-3.10.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:99b2f3fc51d308286071d0953f92055504a6ffe829a832a9fc7a04318a7683dd"}, - {file = "pycryptodome-3.10.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:e0a4d5933a88a2c98bbe19c0c722f5483dc628d7a38338ac2cb64a7dbd34064b"}, - {file = "pycryptodome-3.10.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d3d6958d53ad307df5e8469cc44474a75393a434addf20ecd451f38a72fe29b8"}, - {file = "pycryptodome-3.10.1-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:a8eb8b6ea09ec1c2535bf39914377bc8abcab2c7d30fa9225eb4fe412024e427"}, - {file = "pycryptodome-3.10.1-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:31c1df17b3dc5f39600a4057d7db53ac372f492c955b9b75dd439f5d8b460129"}, - {file = "pycryptodome-3.10.1-cp35-abi3-manylinux1_i686.whl", hash = "sha256:a3105a0eb63eacf98c2ecb0eb4aa03f77f40fbac2bdde22020bb8a536b226bb8"}, - {file = "pycryptodome-3.10.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:a92d5c414e8ee1249e850789052608f582416e82422502dc0ac8c577808a9067"}, - {file = "pycryptodome-3.10.1-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:60386d1d4cfaad299803b45a5bc2089696eaf6cdd56f9fc17479a6f89595cfc8"}, - {file = "pycryptodome-3.10.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:501ab36aae360e31d0ec370cf5ce8ace6cb4112060d099b993bc02b36ac83fb6"}, - {file = "pycryptodome-3.10.1-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:fc7489a50323a0df02378bc2fff86eb69d94cc5639914346c736be981c6a02e7"}, - {file = "pycryptodome-3.10.1-cp35-abi3-win32.whl", hash = "sha256:9b6f711b25e01931f1c61ce0115245a23cdc8b80bf8539ac0363bdcf27d649b6"}, - {file = "pycryptodome-3.10.1-cp35-abi3-win_amd64.whl", hash = "sha256:7fd519b89585abf57bf47d90166903ec7b43af4fe23c92273ea09e6336af5c07"}, - {file = "pycryptodome-3.10.1-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:09c1555a3fa450e7eaca41ea11cd00afe7c91fef52353488e65663777d8524e0"}, - {file = "pycryptodome-3.10.1-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:758949ca62690b1540dfb24ad773c6da9cd0e425189e83e39c038bbd52b8e438"}, - {file = "pycryptodome-3.10.1-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:e3bf558c6aeb49afa9f0c06cee7fb5947ee5a1ff3bd794b653d39926b49077fa"}, - {file = "pycryptodome-3.10.1-pp27-pypy_73-win32.whl", hash = "sha256:f977cdf725b20f6b8229b0c87acb98c7717e742ef9f46b113985303ae12a99da"}, - {file = "pycryptodome-3.10.1-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6d2df5223b12437e644ce0a3be7809471ffa71de44ccd28b02180401982594a6"}, - {file = "pycryptodome-3.10.1-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:98213ac2b18dc1969a47bc65a79a8fca02a414249d0c8635abb081c7f38c91b6"}, - {file = "pycryptodome-3.10.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:12222a5edc9ca4a29de15fbd5339099c4c26c56e13c2ceddf0b920794f26165d"}, - {file = "pycryptodome-3.10.1-pp36-pypy36_pp73-win32.whl", hash = "sha256:6bbf7fee7b7948b29d7e71fcacf48bac0c57fb41332007061a933f2d996f9713"}, - {file = "pycryptodome-3.10.1.tar.gz", hash = "sha256:3e2e3a06580c5f190df843cdb90ea28d61099cf4924334d5297a995de68e4673"}, + {file = "pycryptodome-3.11.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ffd0cac13ff41f2d15ed39dc6ba1d2ad88dd2905d656c33d8235852f5d6151fd"}, + {file = "pycryptodome-3.11.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:ead516e03dfe062aefeafe4a29445a6449b0fc43bc8cb30194b2754917a63798"}, + {file = "pycryptodome-3.11.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4ce6b09547bf2c7cede3a017f79502eaed3e819c13cdb3cb357aea1b004e4cc6"}, + {file = "pycryptodome-3.11.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:014c758af7fa38cab85b357a496b76f4fc9dda1f731eb28358d66fef7ad4a3e1"}, + {file = "pycryptodome-3.11.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a843350d08c3d22f6c09c2f17f020d8dcfa59496165d7425a3fba0045543dda7"}, + {file = "pycryptodome-3.11.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:53989477044be41fa4a63da09d5038c2a34b2f4554cfea2e3933b17186ee9e19"}, + {file = "pycryptodome-3.11.0-cp27-cp27m-win32.whl", hash = "sha256:f9bad2220b80b4ed74f089db012ab5ab5419143a33fad6c8aedcc2a9341eac70"}, + {file = "pycryptodome-3.11.0-cp27-cp27m-win_amd64.whl", hash = "sha256:3c7ed5b07274535979c730daf5817db5e983ea80b04c22579eee8da4ca3ae4f8"}, + {file = "pycryptodome-3.11.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:8f3a60926be78422e662b0d0b18351b426ce27657101c8a50bad80300de6a701"}, + {file = "pycryptodome-3.11.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:fce7e22d96030b35345637c563246c24d4513bd3b413e1c40293114837ab8912"}, + {file = "pycryptodome-3.11.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:bc3c61ff92efdcc14af4a7b81da71d849c9acee51d8fd8ac9841a7620140d6c6"}, + {file = "pycryptodome-3.11.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:b33c9b3d1327d821e28e9cc3a6512c14f8b17570ddb4cfb9a52247ed0fcc5d8b"}, + {file = "pycryptodome-3.11.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:75e78360d1dd6d02eb288fd8275bb4d147d6e3f5337935c096d11dba1fa84748"}, + {file = "pycryptodome-3.11.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:621a90147a5e255fdc2a0fec2d56626b76b5d72ea9e60164c9a5a8976d45b0c9"}, + {file = "pycryptodome-3.11.0-cp35-abi3-manylinux1_i686.whl", hash = "sha256:0ca7a6b4fc1f9fafe990b95c8cda89099797e2cfbf40e55607f2f2f5a3355dcb"}, + {file = "pycryptodome-3.11.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:b59bf823cfafde8ef1105d8984f26d1694dff165adb7198b12e3e068d7999b15"}, + {file = "pycryptodome-3.11.0-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:ce81b9c6aaa0f920e2ab05eb2b9f4ccd102e3016b2f37125593b16a83a4b0cc2"}, + {file = "pycryptodome-3.11.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:ae29fcd56152f417bfba50a36a56a7a5f9fb74ff80bab98704cac704de6568ab"}, + {file = "pycryptodome-3.11.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:ae31cb874f6f0cedbed457c6374e7e54d7ed45c1a4e11a65a9c80968da90a650"}, + {file = "pycryptodome-3.11.0-cp35-abi3-win32.whl", hash = "sha256:6db1f9fa1f52226621905f004278ce7bd90c8f5363ffd5d7ab3755363d98549a"}, + {file = "pycryptodome-3.11.0-cp35-abi3-win_amd64.whl", hash = "sha256:d7e5f6f692421e5219aa3b545eb0cffd832cd589a4b9dcd4a5eb4260e2c0d68a"}, + {file = "pycryptodome-3.11.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:da796e9221dda61a0019d01742337eb8a322de8598b678a4344ca0a436380315"}, + {file = "pycryptodome-3.11.0-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:ed45ef92d21db33685b789de2c015e9d9a18a74760a8df1fc152faee88cdf741"}, + {file = "pycryptodome-3.11.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4169ed515742425ff21e4bd3fabbb6994ffb64434472fb72230019bdfa36b939"}, + {file = "pycryptodome-3.11.0-pp27-pypy_73-win32.whl", hash = "sha256:f19edd42368e9057c39492947bb99570dc927123e210008f2af7cf9b505c6892"}, + {file = "pycryptodome-3.11.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:06162fcfed2f9deee8383fd59eaeabc7b7ffc3af50d3fad4000032deb8f700b0"}, + {file = "pycryptodome-3.11.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:6eda8a3157c91ba60b26a07bedd6c44ab8bda6cd79b6b5ea9744ba62c39b7b1e"}, + {file = "pycryptodome-3.11.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ff701fc283412e651eaab4319b3cd4eaa0827e94569cd37ee9075d5c05fe655"}, + {file = "pycryptodome-3.11.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:2a4bcc8a9977fee0979079cd33a9e9f0d3ddba5660d35ffe874cf84f1dd399d2"}, + {file = "pycryptodome-3.11.0.tar.gz", hash = "sha256:428096bbf7a77e207f418dfd4d7c284df8ade81d2dc80f010e92753a3e406ad0"}, ] pyjwt = [ {file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"}, @@ -2247,8 +2161,6 @@ pynacl = [ {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, - {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, - {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, @@ -2284,22 +2196,8 @@ python-jose = [ {file = "python_jose-2.0.2-py2.py3-none-any.whl", hash = "sha256:3b35cdb0e55a88581ff6d3f12de753aa459e940b50fe7ca5aa25149bc94cb37b"}, ] pytz = [ - {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, - {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, -] -pywin32 = [ - {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"}, - {file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"}, - {file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"}, - {file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"}, - {file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"}, - {file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"}, - {file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"}, - {file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"}, - {file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"}, - {file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"}, - {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"}, - {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"}, + {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, + {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, ] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, @@ -2308,26 +2206,18 @@ pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, @@ -2342,8 +2232,8 @@ requests-oauthlib = [ {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, ] responses = [ - {file = "responses-0.13.4-py2.py3-none-any.whl", hash = "sha256:d8d0f655710c46fd3513b9202a7f0dcedd02ca0f8cf4976f27fa8ab5b81e656d"}, - {file = "responses-0.13.4.tar.gz", hash = "sha256:9476775d856d3c24ae660bbebe29fb6d789d4ad16acd723efbfb6ee20990b899"}, + {file = "responses-0.14.0-py2.py3-none-any.whl", hash = "sha256:57bab4e9d4d65f31ea5caf9de62095032c4d81f591a8fac2f5858f7777b8567b"}, + {file = "responses-0.14.0.tar.gz", hash = "sha256:93f774a762ee0e27c0d9d7e06227aeda9ff9f5f69392f72bb6c6b73f8763563e"}, ] retry = [ {file = "retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606"}, @@ -2422,17 +2312,10 @@ urllib3 = [ userdatamodel = [ {file = "userdatamodel-2.3.3.tar.gz", hash = "sha256:b846b7efd2d002a653474fa3bd7bf2a2c964277ff5f8d9bde8e9d975aca8d130"}, ] -websocket-client = [ - {file = "websocket-client-1.2.1.tar.gz", hash = "sha256:8dfb715d8a992f5712fff8c843adae94e22b22a99b2c5e6b0ec4a1a981cc4e0d"}, - {file = "websocket_client-1.2.1-py2.py3-none-any.whl", hash = "sha256:0133d2f784858e59959ce82ddac316634229da55b498aac311f1620567a710ec"}, -] werkzeug = [ {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, ] -wrapt = [ - {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, -] wtforms = [ {file = "WTForms-2.3.3-py2.py3-none-any.whl", hash = "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c"}, {file = "WTForms-2.3.3.tar.gz", hash = "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"}, @@ -2442,6 +2325,6 @@ xmltodict = [ {file = "xmltodict-0.12.0.tar.gz", hash = "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21"}, ] zipp = [ - {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, - {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, + {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, + {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, ] diff --git a/pyproject.toml b/pyproject.toml index 0008806f1..834e15eb0 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ include = [ [tool.poetry.dependencies] python = "^3.6" authlib = "^0.11" -authutils = "^6.0.2" bcrypt = "^3.1.4" boto3 = "~1.9.91" botocore = "^1.12.253" @@ -50,6 +49,8 @@ werkzeug = "^1.0.0" cachelib = "^0.2.0" azure-storage-blob = "^12.6.0" Flask-WTF = "^0.14.3" +authutils = {git = "ssh://git@github.com/uc-cdis/authutils.git", rev = "master"} + [tool.poetry.dev-dependencies] addict = "^2.2.1" From dba7e15f4b99bf76d166edd4e56db77a9f6bc783 Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 15 Oct 2021 12:55:58 -0500 Subject: [PATCH 038/211] fix poetry sttuff --- fence/resources/openid/ras_oauth2.py | 1 - poetry.lock | 6 +++--- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 17fbacef1..0c88aafcd 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -85,7 +85,6 @@ def get_encoded_visas_v11_userinfo(self, userinfo, pkey_cache=None): Return: list: list of encoded GA4GH visas """ - print("------------------------------") encoded_passport = userinfo.get("passport_jwt_v11") return get_unvalidated_visas_from_valid_passport(encoded_passport, pkey_cache) diff --git a/poetry.lock b/poetry.lock index cc2b69c40..be1adeb5e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -83,8 +83,8 @@ fastapi = ["fastapi (>=0.54.1,<0.55.0)"] [package.source] type = "git" url = "ssh://git@github.com/uc-cdis/authutils.git" -reference = "master" -resolved_reference = "bb465909bb8b8b86688cc9892436777362df8000" +reference = "84ad161" +resolved_reference = "84ad16122504b8961844f3a2c166552180da6767" [[package]] name = "azure-core" @@ -1512,7 +1512,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "63afb22aca669a0c0c7ba43e7a419c2eb9dca2fb868802557e87bd9790449918" +content-hash = "0d358621fded6e7a80fdc2a51206a0d0f847ac7f8affcb5df23a87cc39281905" [metadata.files] addict = [ diff --git a/pyproject.toml b/pyproject.toml index 834e15eb0..bffebb7af 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ include = [ [tool.poetry.dependencies] python = "^3.6" authlib = "^0.11" +authutils = {git = "ssh://git@github.com/uc-cdis/authutils.git", rev = "84ad161"} bcrypt = "^3.1.4" boto3 = "~1.9.91" botocore = "^1.12.253" @@ -49,7 +50,6 @@ werkzeug = "^1.0.0" cachelib = "^0.2.0" azure-storage-blob = "^12.6.0" Flask-WTF = "^0.14.3" -authutils = {git = "ssh://git@github.com/uc-cdis/authutils.git", rev = "master"} [tool.poetry.dev-dependencies] From a456288bfa801d9da2dd5ce9f0d112f21acd6f22 Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 15 Oct 2021 13:49:01 -0500 Subject: [PATCH 039/211] testing with authutils --- authutils-6.0.3.tar.gz | Bin 0 -> 18407 bytes poetry.lock | 25 ++++++++++++------------- pyproject.toml | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) create mode 100644 authutils-6.0.3.tar.gz diff --git a/authutils-6.0.3.tar.gz b/authutils-6.0.3.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..86a93a5ec425fa1a55d0ce7d68b112da449d4a3b GIT binary patch literal 18407 zcmV(}K+wM*iwFn+00002|6z4>XmxaHY;!F(E-)@LE_7jX0PTHiciYIZU_SF#;KFCu zq(i}aSsH7i?8uVr=#EF$N^&N-iY^6`O$s9r-~~X*jF0!XZ$0`6fRto=oJj&?PAmfb zsIIQAuBxu84xR_ke>e$0{7HmEk^Syhc~<#Z^zZ8G#)kPD->eZi|M2+i@T;%?twkS~ZR160{jYDYzWAND+N}Ry{qgR5_NL+BT6k}xfk<-E zT2k`QBFm#R@z#UYuJ@l|G7Gc0x4yc%xv* z;ptCj-rn(nxBu?=;OPA5-SL_C`rWDb{_L>pogSW?zB_ookFUE_>fq??{PgJ6dwfF; ztOeeI7)41`0C0KGlF;~aM820_hjHvpM3{I5jIWH;70`I(!p#Z zlA;V(npFTBq|S`3%Vc|ii<1B=V$(RaB zJXipTaTt4t)N}=iS%L#3=!F*!s1*eTpaIItW>P310gOb>T?0akERDNfmB zdMC0jOkW1Ggg`_I|K0V9)Ek5_=~zy-!k-8OnKucOa4c~0u-|+(xR!u*y}N5c!-v_Y zenV>8#>`!W)C8@xBY=daC%=xS*wQE(0XC;18(`Dz7pwoPL*1o-9!9)sXjTC80(fB# zV45wgrDXBmBm zX^Dl!KHl|z0aB=f9wu{s2h%Z&X?={cbOIj-*I@!ss|5m7k>pr1R0KtDV)=9Ag&w1a zn(Mj)lx>uT3Cm_0;etvDI62HQPy&nrUONM}xfhJ|mbrJ1?eN;2h+!0Z#e8ZH`{y*f zsW62*cu5cwA3{nqOCUR6pje3VNu}TLKsre$|l+{0P!K5Ord9}2&^5*oGW6S11mUD0&mTDZH8 z2G?i!hxgpppKVj5;dDTo&u#zbTyp2Ipw zZU}LGpcbT)B%Fwjn)C=5Q8o$(v`KWWJyIjJ0u*9Lq$4xy`^d`W9$!1-cjFV%2hW6qoq4G*OxqFPEBH&hG0F9N=6Es6rUehOX3if!k!KJX67Je1r))J2dJ zcycLFN5sb~VuhMz+U}O_7Pg>*T$#E@dhZKB<_J)+Sa^BZZtwYeOulT3_j}Ejp^}JU z0cI&M<1WrZKa7cv?lP>Gknk}}B=$XA1vb)!K@{St$jxFQ{O8@JTao5nb|3JcfeAQb z6l3KWL`rDQ+Oo7Ll+W`*Omdrr!=5`6xP=U8kCY{N`f=xDX+_IgHqyHm8*!+>!ZzX! zWZc0lr(K?UoDjd1V&u=n@yymFJ}4x(V^#DBL&>MnV3y8uSiF-kyFu2M8Rx%i(M&`*rr4pP83Z`UaBTFZ|a(R$#$L9q(E-Pglcl9hKhvc^OaF1;8z5K-EBHRMqb&?rT&K6^w zyrU7$zt)9v;8aM{dQysL%pip05I+->lZt!WY*<>h&eA+zA%x*D2I&mtC;kmHA9`_k zm(QXChZ>79ZvucZ1-y|7N?dUngQGox0neqXZQ2=_$(gHRsri_YqzcWjDCSb45}A}T zDp!h9G|U>4Tb`1tcz3`hinFI^CCrt;HiVao_%wE*HPjP_%nCLG?^M{9p@6!cgmc5$ z%3KKeV5DRlhaWAG@@Ps>Y=q8cz~qRIP@;iOlt%~M5bknzFIrPr!vrx_J^`2@jY#$s?WaF zCmU{zD2;j)>hEmS$&iFVeTWjI6V@Jct9xW|nxwH!RNcpf48iTVJrAs&Gf}_->ngd~ zQsGHE0Eo(ivpUut8iMHJ3NyQ7S5kKu+2T;3+|;#%2_n(LEH^neHbKF07?)v?Y@$jdbLgWwfSJ z(gd6dc|ppKJPBZ3)kuMP;Jr^^7tLwX#0ThT5TW`@Em{X8+P*O_OOci_%Q9mvG+h}< zAA2kta#+;s+t%mDsAf{kA}}q2;U-vg9jenuu78{sSV^DW(Eid-St-M1J0|rGw-Ev@ zpTTyJi=p6yAY2C4l*o=)x?lq-U{R|?W6*%y7O+AkfaJ&)EjfCa66Y*fA?z=SPQU-n&!#SpMB>Z}0f8-j7Ge2O#c5d;ges!`Co~I zULTzwAD*%Mz@BX5WbYIv?ETxlQ}5*c>B+mZL*8xqv@ph#1%Q7Fokn!(KqoV-6}sdC zGnb{)EJE>|20Q}l!XiX}hT~fHQnohcImkpfT*a=VoH%=)4kE24GgFu6tYnR~4_d3r zEYse10sK}d#Y%6Zuph;Acz%RCuLt5!QV<;62E2;N*a+~5G6bQC_lz;*Z+jQ|jPByAIG}_z% zZZubCd}fZjxZbPr)VQqZ5;kX=$ISRml*q}n_^NF&XfHhzS5V=w2+B;#tPKQ5cVAi4ksS6RhzB!BYSvUK=0WvB-gNkPcBmFB{@y z8`_v24sRnmBppfj4OjsZ3Cg)aHqR^IW#H`%a8E!iD9(#L@0pF-S`2wa;$>s`j+ei>U0aiy4hRHziAf{{vmE3PmbSEYWx|SG&7^5{- zuzP7gmR2Z|Tb?7^LCKeoT3|f5UZj2%<<4;rC>VcA?@$wBRZ=62@M$${#z!74No<{l zXqj6ci;xjlzD7oB*eJnEl8!k=F&y1kiYzjaHeuA6q7n0FT=2Z!2{$8+o1qwiy23R; zG9K2L&ckd%oJa}R8jEH*&$7&%mPyMo@MO@+QCVdpYPV`P?$4#LVFolu3>chhVgAmd zMoVneurL819v|SoTIU8w-}g>Vpvci*c5n*GS^yk%F5MSw*FF43pxo)RBy_KPV1=%9 zqj0SWN~}&{)nuTC7s{&9HJbb=67ewiV1t0g&P>0Lr<4LH)4#a%jgo?v3Ax+N6)_Q4 zm0F*rNe5p0AWfcXe*tTas>c7L- zNvUT5fKNchIL8AjE-me(ii=ZOrW>F#lvr8qA~AESHe+?P+84%Ug-%EnBso^}0XQ;T zBMQp!$Oh!eJ-cc{domIrYaAS&=<)b(&wyYTPYoUDe%yR!78vw0{=qsTM6ETwlg6d z5EPMO$S+&dIdnmg{P#w%<+ldp?7f0gy+A1!KK#GkwO|vzK#wBJ7#{x*&@#6DVGj{54=+M3@&$?*fe_#?hp57xSWSmPF^^Iz_;_D2g>*yRf!4}FU) zhE?dUy=>sWl3LN~ck&#(%E{FE;BdxCmn) zE?Xnn?S)SO0KQGb7ry)(-j>mQyHY{9$O&Kn{UX>5*8SEb9o*3GO+d-IFH5Wp(p!4F z9()IIs<#RZMlB!^#cTMG%d+bmJm>vc6c1O}ll8JCWpKtNgT^xf0CUMcDt@45K`RpU z32Nf-FAl5O&VIY{Ulae`#D6!>Z$bXcv%z!y?s>0=ffc=8FrEK;@*jNLDy{#mt@X7= z{`(dW`S0in52Sd+j@aHFyg$S@trqO2J7hyERhZ=>Q^ohuW}X!9b38}LMbR6_X+Mm6 zSf(XSdPh{08s+w^O0aR4YQdXh^k-}JdSM**dbIazs6g=Xt?Kym^xfOTGyJ2}5DGQ) z4O&21uT6dL9sG23yz7U5 z9l-1KE{W4{C_lV8JU%?#d%NrRCSuYTSx>%)mZ32E7U~yo5BJUv@g2JEsfOR591sYz z>5xI_{EDo&vHvvoA8r3x-`E7k8*IE-eYvr*(Fg$l_WS>^KMto~w)x+?|F3PYRpkGz zwT-Q2|Nj=x6L00&3I>-($#}<`6{D5!@J-A2{T8K7K@SmW&k2>U$>3Umx<7y|%M%~S zvtYn(bp1EsNkDa5QYIj)iTaTV{a#yLS|m`QX(mp`6i6gXj9~}IS0Fo35!ejFNeWvq zk)|p|Z*ndHZ3fdYgY6y!nGaKpGvz{u_=5!*-J2`v9AS7943G+SJrpOT;B)e)v!jFF z>EWBhzkuwJi9jYE?}OV;`)|WeJ9yUdJFV9KyPr-_TIijf!!Aw&i`?Fa zD3{QACI}J3UVq*jPeT+;T6|1Q*!#<=AQ$6-$l9F6p(CSep6t+Z_AHB829pT@Qg1q= zIHg!j(B^>$=`bSpZIHHHK03uXoJ2K+{s`=onaof~@!HT*Umb9>TW?1SKd<-Rzdi50 zKYa_MX=|5kAE!2#XQ|1c9|cJ~36khK7^k<-WzYF@U?9&2<7lNHCC`0R`Hd*fDDT2q z5f-yNfFl1VFJ{AeFB>5V1q!FIE*ThCL)=K)I!p+94ti{F z?CA;U0kmkO`sk!RdgA#sD1XQE&0Oh$;q$5Gwmd}hQ~{_Yqb-ag{Zvs$#KNSgI5)G%$z zDI!$T7tf@rZP+WdeeKzbk08d56!Y%^h)B2UOWWQuuB(pl~# z&M47=Y!B7MdmfWeNUE@i2 zrnz04o6UcpD92dhi(aPIH*NYH6CxR+=hruJln^(quA+~h$n}b5&RV>*#S?-8-hmcIc}K~(XgD9f6!`pyZj!8l82)Am}&`#mn4Qn-E0GH)p2P6P;x6@cE~=U(o?E1{tTBq+w^qFW zK!RaTh8S%vrW&h|-tE+c)o*SX>WHrt`&c47?6jP|P~ej^VTodDkx-+%l4IJ{s=N0I z$cYqvP}w*Up&wU2>6Hgvb~>$+{Bo-HW693#uQb6V>IeHV#&)-r_+<+%9duqC(DxeV z1cT&nNcsdqnYCpBlW>o6;Q$q{udY@bn&jh!3;1g=wnKKTBWFb=3h3Phmz&7gyOj5A z48s?T+PcF87KK0^gfmRc4179kj(7-ECq(7EzoT=qht9ZJ;6H%r*pxzIL>o}pdC=}b z1xs{tf?Izrm1rAsmE~!5kQKOg7NFbi0$A&p%>^xjOK!{sO4^={8i<4_KAUt1L^-|X z#RL=d^(KSB#(W>*bEZQwsrymp@-Ru$ijQUK7=+axdWLirz0(48CHJ>!dNZ3k@^J!% z>g8>#%^j+8b;7R#e0AlCU>tbo`)~FZ)YdSV+T^l?uei4GUY+jm)yStir19;jDpw^a z9nrMHLR;>31w%%D3d2@I1}3`lam_H&CD>~j8fZCeRO)^~&SFz9AEZ;!?zq&dI;^8s z)c}qxZ?l0UkmIe=5i;j#-bR>q>j5++!{WN_%Q5Bt!nSaMhOCA4 zG_SGVlyxS#U=;40bs~wrx;V~?j3QJ@tuI&}C=!%m%UR0ldTkhNh>>hE)>^}ERhK04 zR95Pgk=L#x`G9QF26sKBoNLYyCd;^+vsvd2$m9ue(5vV`XM$^>05ZO>rMLdiMrn;;vFK15hL zwZB<8qMxR*ZB=BeoV!=4vP(>LSCV;H8b$K7a9SCqvt+oC6)%#%ssclKMLbAIdEzmP z)7j&^IE5WtGB;u`wyIm90(_; zxm-Z!*~}6Z?aH~0*9y~{mSr#5UOkrVX3Z+(&{K`N@_MvZVA=BU8TNy(XggSJH~2!E z!7_V6#a6Jp>ur{81yXvHYhEUJVm%(%6Cv0-_GwkcKYd7868#pbz!mlPVXE&#l-G(R z{B=~+2PvZqW#PuTqVfN3{J;L0{J+*VUv6%01uwTY*H*Wi_`k;gONy#r=m2(~|M%v0 zdHru~Y&Z9ReLMf}sy`I^SRZR?WB=gj%)HSKOWF~;?G{iTODJ{g!vFd6IhWP778d(3 z#as&yEPcXBG`O_tWKrS0*)YP>o`;n_L(A)uqRsyQwf6re{_7vM|2Oep&Hle||K|vr zFBJgp+yA#W%lCh7y=dr=1ys=&H>Ap!F;)1f6jkM8SQWmv0;_x(S;epI*eaic ztMG#kulm$#{dD-#tHaZtf?)5}Tj|$#Mh@KOPN^=Y<4WlSAsJi0;L~NRb@u+%PeXxR(9lY^V`|ZPV~PllcjF%xIQ+r}$$3LXE?dq1->m;;|8Jg0@BbtSmA}`KCp7ziqyIJY-|+wD`G@m=K4<;xZQx%1zqMWB|7+{pP5ke-v;P_mkS9El z2kZQgvI)OKTPz>dH@x9rH}Vdzt<)gsSiBFo+|?PI=($?Q+#tE@V}T*mdd!({$RFUpp=1uo{gj zfE!0acGNS6_WpqG#6Y+-{=S!2@C{|8D#xLR94tvmepRkk*{|c&$n&-Y-6H|bNf&&c z%67@o)=Wd!d-kk~6QGSAC2+F?sLZ{+6;f%_|6llapZ;aV6_?4%a<}|Z%xLK zEJ3!Q?zj;<-bdpSOP@L&yZQ2>x~ChAGZ&p-!=6CSk?6JCO{hDa^LICQaRZRLGlL@jfBGSe7h9 zQXFeJ+2Vp$>-<_^h?j0m-gim&Wl_}Cl_f4*UC~v4VT`BwbH4I#kS3#OJOkOFMKL1) ziQSJr>K z7O^^0n64wQti@#WsvOC{@^Vl|5&|K~YQqRYPx^=>G!1@Ti z^)SF0M5cK6smjTQac0sw&iT6fkcXJ1Vc@RUo@K1o8o(ys2qs|1X6W*&$1V%cPN|Y&EqX4U}yh2*X z&0{ED%U)m_(Opj|o(o|gi-fMoHTrK)4Wr$v)2S>~(a|T3rn$?#wk+MK0@-r15{t;G zPES{Je}?yVgj$Kxng8nO)EWltA^8B~`s!x=K;m=^e=QmXf9+A%pWQ$+|5Wr4f(e0txE{1>B5kPbP)9C+={@>{TjsE`t z{a_%D=l`*`x>45uS64S1{r}sa|14B;-5Xe}FaBl2Cz}1g+5emUzj^-k_J2&q z6(!>@3;_5H|Nr&I|D)mm4gYWWfAjnn`2Q&SP|Py%nHPfId;e!e|65<%Ty6OOw|LO7 zj0aV1Jwp>oXSsTI!CgUF#x?p+%4F%%ct{v zTY+_yy=2q@`Xl9e96*~v@APc%gxmd5%$+vnaWaWG=)=ny5gKu}B3ltI$ zAa;-f()G#VPx7``iI2T(Y$dK=(A$oZ!|QPfo$i!7E+O@XgUNb#!ui^k=+i_s7G((if-fpZkB=eixmb1wr7vulfC7fBJCr<9z-1)xHjo z5AN^D0<=sE^@t4A-+d$n!ze_;moe`;Tr)V7uLGDiM_Q!n( zG<>jwVLVQ=sJNb#8aqx8b)j}gI#UKifiu_k>Aqy)h^pH; zbe#;lX0dk~O8;$BdT;&Tme#eV|K-e^nyR5_x~dpo^CZYiciC|d-D#COK8Ny&TwW1k z7KpCbpLz~&C+uZ;053~P%S7-lyvAVUj-WY%z`!9xBd};d62ju@Q<$qb*-rihq?7!dQc>+7+ z3R)%Q6_*&M1RGO7!sy+W4*jW1&=ROD)$Z}C9VJBNy4JmNMQ_okYU&R25d#sIeEQ|i zX|B_fVMa#;7DXEID4f2Xj4l<(xFrgQs{jUXD-g+d8Wi|_oF>BYn`Xhp=|<)2uC%9f zeU|~#AF6QKjcGvcNdVsL|IPm2-2c_=|I7CO0dSfxxdLSQ{=c@qy;0f!*EX8_KfcK$ zw{E(JRUNRx-XZ;}f>Opd-Z0A-X?4|G`{pkF(!QWezcg2CnacdDBM)|Q6GLwU$~h3_ z`AlTe)bqqU$;3!x-n*lNeQy}$gY;Hpb8naqx?X<bQx@>uxEPLI0;LoDlB0#1L$y^6?8~nN zKdY}+`1MY?j2>6*Z~$T0G~x_n;H}w-hHbKeh#`v_a(~+LCd}qFpkxa5|TI1vE1UEY5JNuVS@_edK9SAyy8^pdIb7>_jMTOLP81+e?3T* zJl&h}l4%d(Fq-6D4}O=-oO~O%&Y{BvrId~>d)F!#1<3YdfWDTpQ7-FUUBT}KCP>*@ z8}saScAUnIY|Ks#nTNUQpisG+dNU=9#Pp?fPkM<_xYrxYyU`^e!<3Q(z=A69;^MG0 zGEB!e@}dID3A{v|ZM05%4qhn7h=W|H?n9e8Wh3`nGk?zAgT4fB646aO9wC_P`Z_x! zm${(kDXk;?abLp@=&Bc*-w5b4qm(NW1=c)#{M7Zv0Kb(Yp>kn5)atq*Ew}`|LZLwB z<<()ix`L)S>~fl&uE#tDs5qsAwS4#ErxG#$S?Q59FWf2-@zs^n2l^7snX+Y3j+Y#_8eK*9yy6{jTR4^(5rr|&fA5{l@ja!x!Tc6PwXmLn zeqjA*PwN zTd%aM;g)SMXin=)394~P7s~k@NMHt2DU+OG>JyY%WGUHrn+Gll3EQ7xqO;ET8qbIX zCP3KB>X@iQxtdEF;x2oJ(TNS0vd(aT9&hc{fzy{q<0Pb`Ad{SH=(XQX`Q-Zo8oBJ) z3^bI&nn&r=cm)xiOs8HngmK}93=4FY&p>jTQ-U=Rt*jPQVEC|WRo4XAx40?hg`;C9 zmm`1vu2$4KTC0sa6i3e{rVW_Dzd;^dqXs;bdIq?DL z7RVvQtaW0jKSQsDz#=AlFmqOGEVy;U5cC(A7F^i?YnIN&*YtZ1O#Eps`Ca{fjm>am z2$vdC`YL@p9no>^cK&DFRCy=I*@ZnyM7PKQVV?A^70*|S`4k(KGj2-*wk|OTlzG}wp}tg#my#088RDcUta|$R>3iL9HSxM^ zQ!65!ML2`THP&c}SBIx}3KWJhBW02%D`GM&=0-}9`xFa7Im-$p$tZ@#ViEr!SwNwH zUX|8SPfD-5QnMhd1HJRR-i4LO9<>L8m%7bZDE;MNTe;WILbN<&nUv~aC3So^*qt8A9 zyf^-H`$hTuZ)>y3|NrgcKf`>U45Cz>^1Ys+wA!VU6HLrd^x`y~%D~Muj>UkD3Mx+^ zrfCqvgJ@9bT;_b9$a&A1dQ|B%F_?iEKks^fIzK--BnelCg(^zRWg%4wi@p+Jh6%Ls z?>##u^*(ftMywE&ZcfAxGa8Gc5CP`0p5=Bcz0K8)-s^X#uZ|854v*C_r%ubBW}^P& zQy)-Tw^p_K1QJLK20je$Bj+2V#-KRy@sqTmSMNT0rsOECzMz(>#8vr2+JJgehLvxn z)t`Q~78%t`3CGC*%y277mB9D-JZ{JG2(O#I14;^#n?gTeCTNmMpWj`l;Uu!oAaTI_ zlEtT8S-b6E!N*kD>ZBC9^3A~HwM~AfQx-5SDMt$Ubw=1{N`N|*5*1;Rb((YlG=`(M z4h+hg!Os}j3UH$D7Z#IHc@h3dwGJznh|Og^!j@xaGTy1^whFX zwCp||v#3__%7#XJa3E6;C}Ta$1&26D-KuPWUXY)h6?BJ2P4kN_m|YcCF0GvS|ABqH z$cI|0p{{*AAWtw|SS%~1MFt1LoGHokGrVNP+kqc>zH^0U&a1$+*eM%-Nvf|{WBi|_ z(2iF~OhkcrmFh>wK9;gnirldgmC#1(RZh?%)S@#X|0$`S7`P`-y8(o@%#Xf|S3)C- z7q^)q*ozDl+c+J-+;Kk@4rD;fVf7YLJIx2z0(5#hT5ET_UC`CJX}@9};tB(ccdHV( z*+_Rvv-`9=hUGlLZ{AHbv~XqGw@p!GQ-X54UU(Nq1vMRHv!oX#J@ElXU%(79FC`}@ z>DJ)ntalV_S`!tMq`R&j)f-f)^d`+dYRf5EdJGjh8)#Lp#q`zC;`f%w1_lbA? zSK-jv5sbRI(8yU&x;rI%-9nnJqEs4umNxh`23;w?7zt*wt)Vh*@v9Q;6XS%Y843nWVM0QQu z!x!=fqYhFj9~17>T4wNOM7=Wf{Ut@)thRqaC2D%BS&9@>R-P-op0hkxwNQ!0l#I_k#QwQNUnc(jFhMe6 zx&-(%&1wVd&eD^DDi7?|@egOY)I-%-VzIO6nW;#fTJ&3GXGExb0aTV z9i=ne#}X}TstqlH#HtDiMBagwc4zrZlYV5y_{nxhtZme#v zt!@RYFSlN7yxeH)zm5Hu?7yFt^zXs;-}SYv?Tt$O$5xa7_nSOu?>G=6%nzZQU^z5m zHbsk5GUn74T9={wThl>A?q8$}3_$%4)dlD++lrB420w9IZA3cG4Z;LHRxsZ$*_p__ z-V5bvIjFyUd#uxYeoeQ-kxx^KN2Jp4mT%W2k4AI}zbZ-OSFNxBVEVH{C>KWMx)_p| z8`_t$xi^6p5MC+xzCV4Ndu{YIz2bHOs1Cr|N?DCCRU`B;m6dUXM+fY5XU5FO*g1^p zHM#hz{2NQ}qNVHk>^t`=rsZAFT&YU8qT;2f=|rrIMY6F1#67s7%Tei4)g9Yd-@b6K zScUdhR@d-y)x|kI+`DH@we`nTyR?LV1+vb}BH@k-5v1ApR{>k~|A{k5OUpK1qPw-J zW1w=Ks`8!_4}BACoQ}stJ6s5LT6`r+m%&;=R@K9jf09g`m9*RShb8hT1x0HcD@xPv zdfMJ5O=0qoL|%qjR;$}c46q6!b^L*OTga>;Uog7qN9i=={Z#8mHaF2`^Vf!b1%U2vW*?3 zotQ4nMuhrfm%Eb0fiUY(Hwo(zbILeG*&mp1F1qsS(R>QTE$`31y3+Not`xfkE%C;S zqZ>XOkViyi#`Q~h8|H&7nil-s@Fg~`?0D|Vy{LM^;93S0tgiW0MbBTjyfJCe!UREo zOWb@#C7;11K&=0YK(ySyKIs_}5jhr{+2K7m6Y}z7m{(`RA}78aLjq_MmC2^=-8W!r zY4cX(=t=)POfp7o{HVHBEQ9xG%#o#yzjQ}?Z3LNac1cu%r|v&io`P5juZG2b#`5ZA zj?@IY{NASQJ6N{ptypPoHyb zc00oxvJYX08x$6nR-gvY-9x7ALLv1MCQn|COz27bD1TWF@t#`ADiMS;@m)UqGBgxC z$c0}|sWgbIGdCHhK;jYLossVFUWx5p^#@cidt+N&R!-czN8wnzew2(--#H~Syxu@) z-th@L+r?gEn%^!6W5BKQStfL-%QcPr>Z-aFv}{NtnN4{KC}%tOB){EqV^4Cd+^)Hs`=VD>GJ4IxU?N3tqlA1@y|=#RQ)~JCMVNET|3R!bKAlL zj^g8|uFKhd+@{S9)^t#*co>iIt&zIntGTOyHzC1t2lSJZ6}u zcgIXSZIA|&j-8^iaz#L=eE6%=R&twGCrgrAaa@>y2PJ|dZA-LlyyW@bl$BS!l3 z@-cZmYq>D1^OBDM^%%{uOkWJ&Ei~ZbdSFD_{0#daM4ZO8t@5MHXfZA|~wM&P;p2 zf|a0Ew$${I8seA|Yg{d|4|QaiIStjor{zv3F@XeDaRa+f4Zs#GT?+8k)!yf>X&cpQ zYjW!Vvh}AOry-t4H!n4UT%CzJwHqA@WDWs?e&ea zGY9=yi&=}42i33xB$?{U4GXI*nO18ZgCAm$P&$xfHc)Z1(h#s*?Z!MDy`!_U_lKvh z{a{hG3!|yqImW)s29pSokrgA@eZ1iVyKCEA%J3wZf_BASS9*iqFLOwr$S#UhuJb7> zil!w?=&7yJaV;&*c*(-*vQ9f^Sp7~!J(ym#T*3dy=zulk~FB{7C`4ovuV9sfZ5(l zo&n|jTBm}xNtxISGZ%Pw8H#L~7iQ#B$vcS|BOZHDALA_S=URG}vb>So>&010IaMvj z{&m3@C4pKXo7wVMjR|teb}vhzflI9znXHELFETIqie`F_vZzlB*;4ktf#~TFiRJs= z>vyO7hwjFQtIuj;$KFtvv{%^>T@wITuFnx!@|d)hOM5fx+KufZ=U+6fyQSaV8p&bT z+BHgiU+x~=&%f=XiT`i>znb{}=6Rg|r*eMzb?*OI+pxar`#-ju{NLZ?dE!myag-E0 zm`EG5AMS#BTu|bj6%M{`Ep!o8ryFG8!BY24#wDv~J}oU~g`0OVTk7jTP+8Az&C`LG~!J z7*ca+CBC|Ht`A`ojIvp))XoxADO^98-Rk=jvD(J|; z>?Ja@8I1>(ZgS6+2^l%9DvwW^Jyje$8y3mIrQp2M*xX%6nWd!*RF}MhZqo>{GX%XO zgy}X%e?hB!?~EbFb{tOn!*B=3XxUT7UC%FH`$7i+T6e453)8mqn#)##Ub$x`VcA3# zi8YPL?-zbgZwoy;eFGlDJIy+`&y;WKexnFaE(%0^J4&=pgj#B+s}h;ZfG6x&Dq~vn zlZUBnewJ)RnN@=&dXehLM9C@8tQDu#3B{kJCdj&p6HqUe6X(t!sjXNITKWcMGTya6 z%7kma=Q~k~ctB32DUN=fO(yvJV)e2v!oDWK8=iPmjkU{aZZI=jE(v_Wj7jIkx`|Wg zMx;kCAf+pH+QG9b!Fte?vz3lBL*Oz$8%BsFvHn!*F;)PzqrI7FYGFi(6&Q2|pNu}(CEe#%=OJ{+NqI_Nv z-iA?(!kE{VOX}6$*2oq@>pUo>Of`1W>~i9l;X>v^PjxW^-x)L2c znRlpLN+ra$7)p6>2a2m~+|HW)9f-# zNcNPMo90R zn>gWb#VY(u7ekzL>K?8FzXhUzcMd6Nf}A$ZuPQaX+9eu!c+O3!qAgyD+PHv8d{ zj*AlfL`u#uCH^E;j=OLk7-^~|1v zl^}C2l~P~67&yzVx}`G|XvR@#((w~qetc7(ZTnt}v}MjPsE! zPOT}m;wzrRJYj*Iwj|9}oqn7%NLMIZp!QspRGv#qV^OMwx^guciVyc3R~qm5X_)1L z__l6b!s=eC^`Ilrx@NSmhi~$s)6PZ1E%43}n}}5kKuJ$kfqhB(dKBEN*-Mz{nD)!! z&940So?{*r5?Qee-A~c?xis<)?U;{PstSk0T&q}?6SFp%AXU*Bvz6kmYsuh=xo@nE zj5C$8L?`%~+bAMNgWeul0+H%KW&g`m^d}#T+xI>3fN+xyd8mAUnlv}q4Vs*n+A0~~ zfpZRByE!lU6uSKkc0EALvuAk8;5aV{Y&MMe{0y}K1{*eZ2XxZ=s9BK&Zl8y%XOCmb z$``{K5t#wF2S@k_zurcKF`sEA3XZ(oqa=^y#aJ3fv^FYV)T=9F*TPeBma}TC675_d zBvaP((90@{MM;|}Bd2Qbsz@Rgj$i9rZLdh@#t2@ymBqPWLwtx&wqzrt3LR8Z^hknn za`b4z{#DQ6n)Bc0{I@y(ZO(t|&wnw-;!86BJS_kF`c`@UZ>=`@-@naMIcBxI$Qx|seW2qYYPL3(#kwx+m053uw#p&p9!U9 zGm&5(IY}SnHK8iKILMRSN7uITNc+2TJbntJ%Q$R?aft~~W$`xojPq-sT(j+uE&EP8 z8i3zjJUBc4Sjo5)Y`*L5^KjogGuo;bFYT&xCheZ z7El4DH0ciZQVQ)XsgoGsiB86pD7TMcFFLIU15C!Bq3!okb57J-cKx($@2OJZ+tJ9f z8B6NyI9URCnsswGVt$uq=FUmWob8wcv{|R6oR>L8#1cpso3;rD?tzvG)tV&{bmbi# zXxbr?vU91R9;}P=_|@w!UMT?sl8K5+7j$PS&J%!$fq;1Y(DmL%d7%y>RK~J&q>8G< zWmy_qVY5-*3x|^^srn_W38v&4RAib(x1i6BMd=Dm7P1Wdklz6DUKwU0(m{=;}a8CCPy zGt+3ttxt~8c1zIdtZ&^-M&I--tFpMDDa4hdWbvC{YFW?jf;#HE;g)#!wmlm_268ew z&Y5@*7cZhD!c#vvjIQS^DAh|tguWsXU0(kFRMvIfR1(;e4U#(qCKI~7!i=Fr9kN*) z!@P+}*8sVn&?|YNG%XgymNM9qR0qko}qfmT|#qHi7?#Xb!=wxw6NNudqh-Q!C(?In6 zMJP~)Ty-|BS;I)%bRAYr7{a#eaUf^xLTw7ziu{{FqM&bw2?gY7)b7xA&rkvjP{~V6 zrXI&>Ka7<~tu0(VN4QpiNP8%!=Ih5Z0eD2mx=7qPM$%k*C~@Ov^E4(-BMH&<(|n61 zqUq|&5^5-l*x5^^^)j;L8ohAPNbDZz=wlWN=P%#cgR#U))U5i2>U4rpZZd_0Cc0Wj z@3Fs4$=$88&ss)tK$Jx^Rm(Mg{|w`GjfPa2&RSzA^!Hn~hyJ@gjs3T=|C;>o>l^DY zH-q&T+b_3Xt~KlI-_iaH%y#$NWfz8|XetpJ$^psarzjG_;CkyW`DzJu}70vWUv1M|hxXHPEyW)ceHxTj?= z1A83x|G2vvtgUtbuokRR^Q<<2YfpxpyQxYku z6baS@68uM4pxHI17Ea*&6cv1&US}fl!z~EAXxN2s#5~mdq2M&o@*BI_p>rnI##>!= z7@4Ypxv_d~HRFu_ls$W}sr~bVGw=OA=riFsed>JfiAfm6SP1d|!)(BX0x_Jaf>3~+ zu`;)P^IkV%eVt{c7Z&lu2kV#nJvSfoa!sH?yDI#YCCYRNrOK~lNtXzqoclucNS6uN z%{~3&?nbcH{lg~F7+y59HI)B3t7~uev;ueI1@_f`!-C*Gmy=& zF87LmZ9KL-h}Ms;vi0a1+mEjC-J@%~Y@M(xeZF#tg657Vm5l0pa_`Y|b`a&ojx^%8 zmA29Op2by)U#1FvxrS7|WF1zSI^{woMGOloRjgXpxF)q(F{>1jL*hPd&B=wAOI08y z6;Hxzl)OqOj>5+fzo>^;tAE_+ONoPK(`ugPX`be3p5|$u=4qbhX`be3p5|$u=4qY> OKmQ*%vh*bYkO2T#D^`~P literal 0 HcmV?d00001 diff --git a/poetry.lock b/poetry.lock index be1adeb5e..5803029a2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -61,30 +61,27 @@ requests = "*" [[package]] name = "authutils" -version = "6.0.2" +version = "6.0.3" description = "Gen3 auth utility functions" category = "main" optional = false -python-versions = "^3.6" -develop = false +python-versions = ">=3.6,<4.0" [package.dependencies] -authlib = "~=0.11" -cached-property = "~=1.4" +authlib = ">=0.11,<1.0" +cached-property = ">=1.4,<2.0" cdiserrors = "<2.0.0" httpx = ">=0.12.1,<1.0.0" -pyjwt = {version = "~=1.5", extras = ["crypto"]} -xmltodict = "~=0.9" +pyjwt = {version = ">=1.5,<2.0", extras = ["crypto"]} +xmltodict = ">=0.9,<1.0" [package.extras] flask = ["Flask (>=0.10.1)"] fastapi = ["fastapi (>=0.54.1,<0.55.0)"] [package.source] -type = "git" -url = "ssh://git@github.com/uc-cdis/authutils.git" -reference = "84ad161" -resolved_reference = "84ad16122504b8961844f3a2c166552180da6767" +type = "file" +url = "authutils-6.0.3.tar.gz" [[package]] name = "azure-core" @@ -1512,7 +1509,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "0d358621fded6e7a80fdc2a51206a0d0f847ac7f8affcb5df23a87cc39281905" +content-hash = "9c90fb78e45d355bb66b3756589360a05f2ad5529c95acaece1b4fd3e96dd369" [metadata.files] addict = [ @@ -1539,7 +1536,9 @@ authlib = [ {file = "Authlib-0.11-py2.py3-none-any.whl", hash = "sha256:3a226f231e962a16dd5f6fcf0c113235805ba206e294717a64fa8e04ae3ad9c4"}, {file = "Authlib-0.11.tar.gz", hash = "sha256:9741db6de2950a0a5cefbdb72ec7ab12f7e9fd530ff47219f1530e79183cbaaf"}, ] -authutils = [] +authutils = [ + {file = "authutils-6.0.3.tar.gz", hash = "sha256:b89c95a97a310f805673df7e5b6794ab7fe643f2b26af397de545db89584e991"}, +] azure-core = [ {file = "azure-core-1.19.0.zip", hash = "sha256:18d2a6cd3b7391489f005775fe69e4d0870f9384b755e45185efd45c050e2306"}, {file = "azure_core-1.19.0-py2.py3-none-any.whl", hash = "sha256:4fbbe8b867ef077df77614b86b7927e4d87aa7a0bd54e771d9ba14f48dae2c4b"}, diff --git a/pyproject.toml b/pyproject.toml index bffebb7af..ecb7b6f50 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ include = [ [tool.poetry.dependencies] python = "^3.6" authlib = "^0.11" -authutils = {git = "ssh://git@github.com/uc-cdis/authutils.git", rev = "84ad161"} +authutils = { path= "./authutils-6.0.3.tar.gz" } bcrypt = "^3.1.4" boto3 = "~1.9.91" botocore = "^1.12.253" From 012f48ef34ec6b76645811b640a3aabcb47bfb3d Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 15 Oct 2021 16:31:46 -0500 Subject: [PATCH 040/211] new gen3authz version --- gen3authz-1.2.0.tar.gz | Bin 0 -> 13006 bytes poetry.lock | 13 ++++++++----- pyproject.toml | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 gen3authz-1.2.0.tar.gz diff --git a/gen3authz-1.2.0.tar.gz b/gen3authz-1.2.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..70fd8197605c8c4e55da226d0db7a022ad52155b GIT binary patch literal 13006 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 Q^U(AE0bh66!vH`508<2wR{#J2 literal 0 HcmV?d00001 diff --git a/poetry.lock b/poetry.lock index 5803029a2..7a7542fe3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -517,7 +517,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "gen3authz" -version = "1.1.0" +version = "1.2.0" description = "Gen3 authz client" category = "main" optional = false @@ -527,7 +527,11 @@ python-versions = ">=3.6,<4.0" backoff = ">=1.6,<2.0" cdiserrors = "<2.0.0" contextvars = {version = ">=2.4,<3.0", markers = "python_version < \"3.7\""} -httpx = ">=0.12.1,<1.0.0" +httpx = ">=0.20.0,<1.0.0" + +[package.source] +type = "file" +url = "gen3authz-1.2.0.tar.gz" [[package]] name = "gen3cirrus" @@ -1509,7 +1513,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "9c90fb78e45d355bb66b3756589360a05f2ad5529c95acaece1b4fd3e96dd369" +content-hash = "00d088769350d770a350dc019c041cf574b749c5e3d2ff56eb67dcaa691c20ef" [metadata.files] addict = [ @@ -1798,8 +1802,7 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] gen3authz = [ - {file = "gen3authz-1.1.0-py3-none-any.whl", hash = "sha256:7b3f7f09455687a6afd0721c76392f696196e9eeb0c2478d1f68a0d3144e6170"}, - {file = "gen3authz-1.1.0.tar.gz", hash = "sha256:58e470e29e8492648dec2f6a4a001fb90c1fae0f65f6e1e18fed827916df9c30"}, + {file = "gen3authz-1.2.0.tar.gz", hash = "sha256:457436e0fdaf8671dfbe72ac0ae1df9b6485d73dd503759e50b22de40b946636"}, ] gen3cirrus = [ {file = "gen3cirrus-2.0.0.tar.gz", hash = "sha256:0bd590c407c42dad5f0b896da0fa30bd01ea6bef5ff7dd11324ec59f14a71793"}, diff --git a/pyproject.toml b/pyproject.toml index ecb7b6f50..43bde9593 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ flask-cors = "^3.0.3" flask-restful = "^0.3.6" flask_sqlalchemy_session = "^1.1" email_validator = "^1.1.1" -gen3authz = "^1.1.0" +gen3authz = { path = "./gen3authz-1.2.0.tar.gz"} gen3cirrus = "^2.0.0" gen3config = "^0.1.7" gen3users = "^0.6.0" From 8114b3d4cfc1cc08473699616c45af22a5f4795f Mon Sep 17 00:00:00 2001 From: John McCann Date: Sun, 17 Oct 2021 12:40:28 -0700 Subject: [PATCH 041/211] feat(visa sync): create policy before granting it --- fence/blueprints/login/ras.py | 8 +- fence/sync/sync_users.py | 114 +++++++++++++++++------------ tests/dbgap_sync/test_user_sync.py | 2 +- 3 files changed, 72 insertions(+), 52 deletions(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index c6f5e00a2..3e4f46e6a 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -14,7 +14,9 @@ from fence.blueprints.login.base import DefaultOAuth2Login, DefaultOAuth2Callback from fence.config import config -from fence.scripting.fence_create import init_syncer + +# TODO comment for this, maybe move to top +import fence.scripting.fence_create from fence.utils import get_valid_expiration logger = get_logger(__name__) @@ -186,12 +188,12 @@ def post_login(self, user=None, token_result=None): if not isinstance(dbGaP, list): dbGaP = [dbGaP] - sync = init_syncer( + sync = fence.scripting.fence_create.init_syncer( dbGaP, None, DB, arborist=arborist, ) - sync.sync_single_user_visas(user, current_session) + sync.sync_single_user_visas(user, user.ga4gh_visas_v1, current_session) super(RASCallback, self).post_login() diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 8ec52d28b..7e8487ef3 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -1589,7 +1589,12 @@ def _update_arborist(self, session, user_yaml): return True def _update_authz_in_arborist( - self, session, user_projects, user_yaml=None, single_user_sync=False + self, + session, + user_projects, + user_yaml=None, + single_user_sync=False, + expiration=None, ): """ Assign users policies in arborist from the information in @@ -1602,6 +1607,7 @@ def _update_authz_in_arborist( Args: user_projects (dict) user_yaml (UserYAML) optional, if there are policies for users in a user.yaml + expiration (datetime.datetime): expiration time Return: bool: success @@ -1702,53 +1708,61 @@ def _update_authz_in_arborist( # so the policy id will be something like 'x.y.z-create' policy_id = _format_policy_id(path, permission) - if not single_user_sync: - if policy_id not in self._created_policies: - try: - self.arborist_client.update_policy( - policy_id, - { - "description": "policy created by fence sync", - "role_ids": [permission], - "resource_paths": [path], - }, - create_if_not_exist=True, - ) - except ArboristError as e: - self.logger.info( - "not creating policy in arborist; {}".format( - str(e) - ) - ) - self._created_policies.add(policy_id) - self.arborist_client.grant_user_policy(username, policy_id) - - if single_user_sync: - policy_id_list.append(policy_id) - policy_json = { - "id": policy_id, - "description": "policy created by fence sync", - "role_ids": [permission], - "resource_paths": [path], - } - policies.append(policy_json) - - if single_user_sync: - try: - self.arborist_client.update_bulk_policy(policies) - self.arborist_client.grant_bulk_user_policy( - username, policy_id_list - ) - except Exception as e: - self.logger.info( - "Couldn't update bulk policy for user {}: {}".format( - username, e - ) - ) + if policy_id not in self._created_policies: + try: + self.arborist_client.update_policy( + policy_id, + { + "description": "policy created by fence sync", + "role_ids": [permission], + "resource_paths": [path], + }, + create_if_not_exist=True, + ) + except ArboristError as e: + self.logger.info( + "not creating policy in arborist; {}".format(str(e)) + ) + self._created_policies.add(policy_id) + self.arborist_client.grant_user_policy(username, policy_id) + # TODO need to add expiration to this function in gen3authz + # self.arborist_client.grant_user_policy(username, policy_id, expiration=expiration) + + # TODO As of 10-11-2021, there's no endpoint yet in Arborist to + # support the creation of policies in bulk. When syncing RAS + # passport authz information at the time of data access, the + # passport policies may need to be created before they can be + # updated. This code has been left commented out for later use + # when bulk Arborist policy creation is suppported. + + # if single_user_sync: + # policy_id_list.append(policy_id) + # policy_json = { + # "id": policy_id, + # "description": "policy created by fence sync", + # "role_ids": [permission], + # "resource_paths": [path], + # } + # policies.append(policy_json) + # if single_user_sync: + # try: + # self.arborist_client.update_bulk_policy(policies) + # self.arborist_client.grant_bulk_user_policy( + # username, policy_id_list + # ) + # except Exception as e: + # self.logger.info( + # "Couldn't update bulk policy for user {}: {}".format( + # username, e + # ) + # ) + # if user_yaml: for policy in user_yaml.policies.get(username, []): self.arborist_client.grant_user_policy(username, policy) + # TODO need to add expiration to this function in gen3authz + # self.arborist_client.grant_user_policy(username, policy, expiration=expiration) if user_yaml: for client_name, client_details in user_yaml.clients.items(): @@ -2102,11 +2116,11 @@ def sync_visas(self): self._sync_visas(s) # if returns with some failure use telemetry file - def sync_single_user_visas(self, user, sess=None): + def sync_single_user_visas(self, user, ga4gh_visas, sess=None, expiration=None): """ + TODO update docstring Sync a single user's visa during login """ - self.ras_sync_client = RASVisa(logger=self.logger) dbgap_config = self.dbGaP[0] enable_common_exchange_area_access = dbgap_config.get( @@ -2130,7 +2144,7 @@ def sync_single_user_visas(self, user, sess=None): projects = {} info = {} - for visa in user.ga4gh_visas_v1: + for visa in ga4gh_visas: project = {} visa_type = self._pick_sync_type(visa) encoded_visa = visa.ga4gh_visa @@ -2178,7 +2192,11 @@ def sync_single_user_visas(self, user, sess=None): if self.arborist_client: self.logger.info("Synchronizing arborist with authorization info...") success = self._update_authz_in_arborist( - sess, user_projects, user_yaml=user_yaml, single_user_sync=True + sess, + user_projects, + user_yaml=user_yaml, + single_user_sync=True, + expiration=expiration, ) if success: self.logger.info( diff --git a/tests/dbgap_sync/test_user_sync.py b/tests/dbgap_sync/test_user_sync.py index 0be912a2c..3bc1d59dd 100644 --- a/tests/dbgap_sync/test_user_sync.py +++ b/tests/dbgap_sync/test_user_sync.py @@ -739,7 +739,7 @@ def test_sync_in_login( user = models.query_for_user( session=db_session, username="TESTUSERB" ) # contains no information - syncer.sync_single_user_visas(user, db_session) + syncer.sync_single_user_visas(user, [], db_session) user = models.query_for_user( session=db_session, username="TESTUSERB" ) # contains only visa information From 82d3f38b3c2878ede8100a3e842cdad9cd9e67ca Mon Sep 17 00:00:00 2001 From: John McCann Date: Sun, 17 Oct 2021 21:28:54 -0700 Subject: [PATCH 042/211] feat(passports.py): sync Authz info to Arborist --- fence/blueprints/data/indexd.py | 6 +- fence/resources/ga4gh/passports.py | 191 +++++++++++++++++++++-------- 2 files changed, 141 insertions(+), 56 deletions(-) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index ebae2f2fa..da1238965 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -43,6 +43,7 @@ get_google_app_creds, give_service_account_billing_access_if_necessary, ) +from fence.resources.ga4gh.passports import get_gen3_users_from_ga4gh_passports from fence.utils import get_valid_expiration_from_request from . import multipart_upload from ...models import AssumeRoleCacheAWS @@ -77,9 +78,8 @@ def get_signed_url_for_file( user_ids_from_passports = None if ga4gh_passports: - user_ids_from_passports = get_gen3_user_ids_from_ga4gh_passports( - ga4gh_passports - ) + # TODO change this to usernames + user_ids_from_passports = get_gen3_users_from_ga4gh_passports(ga4gh_passports) # add the user details to `flask.g.audit_data` first, so they are # included in the audit log if `IndexedFile(file_id)` raises a 404 diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 75b4b691f..a288b35db 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -1,50 +1,106 @@ -def get_gen3_user_ids_from_ga4gh_passports(passports): - user_ids_from_passports = [] +import flask +import collections +import time +import datetime +import gen3authz.client.arborist.client - was_cached = False - raw_visas = [] +# TODO comment regarding circular imports +import fence.scripting.fence_create + +# TODO take this out +import jwt + +from flask_sqlalchemy_session import current_session +from cdislogging import get_logger + +from fence.jwt.validate import validate_jwt +from fence.config import config +from fence.models import query_for_user, GA4GHVisaV1, User +from fence.sync.passport_sync.ras_sync import RASVisa + +logger = get_logger(__name__) + + +def get_gen3_users_from_ga4gh_passports(passports): + logger.info("getting gen3 users from passports") + usernames_from_all_passports = [] for passport in passports: try: # TODO check cache - cached_user_ids = get_gen3_user_ids_for_passport_from_cache(passport) - - if cached_user_ids: + cached_usernames = get_gen3_usernames_for_passport_from_cache(passport) + if cached_usernames: + usernames_from_all_passports.extend(cached_usernames) # existence in the cache means that this passport was validated # previously - user_ids_from_passports.extend(cached_user_ids) - was_cached = True continue # below function also validates passport (or raises exception) - raw_visas.extend(get_unvalidated_visas_from_valid_passport(passport)) + raw_visas = get_unvalidated_visas_from_valid_passport(passport) except Exception as exc: logger.warning(f"invalid passport provided, ignoring. Error: {exc}") continue - for raw_visa in raw_visas: - try: - # below function also validates visa (or raises exception) and - # extracts the subject id - subject_id, issuer = get_sub_iss_from_visa(raw_visa) - - # query idp user table - gen3_user = get_or_create_gen3_user_from_sub_iss(subject_id, issuer) - user_ids_from_passports.append(gen3_user.id) - - except Exception as exc: - logger.warning(f"invalid visa provided, ignoring. Error: {exc}") - continue - - # NOTE: does not validate, assumes validation occurs above. - sync_visa_authorization(raw_visa) - - if not was_cached: - put_gen3_user_ids_for_passport_into_cache(passport, user_ids_from_passports) - - return users_from_passports - + identity_to_visas = collections.defaultdict(list) + min_visa_expiration = int(time.time()) + datetime.timedelta(hours=1).seconds + for raw_visa in raw_visas: + try: + # TODO must be signed with RSA256 + # TODO issuers could be more than just what's below. will need to use config var of some sort + # TODO why is subject_id a big long str? + # TODO conditions field + # issuers = ["https://stsstg.nih.gov"] + # decoded_visa = validate_jwt(raw_visa, attempt_refresh=True, issuers=issuers, options={"verify_aud": False}) + # TODO: ONLY USE THIS FOR DEVELOPMENT + decoded_visa = jwt.decode(raw_visa, verify=False) + identity_to_visas[ + (decoded_visa.get("iss"), decoded_visa.get("sub")) + ].append((raw_visa, decoded_visa)) + min_visa_expiration = min(min_visa_expiration, decoded_visa.get("exp")) + # min_visa_expiration = decoded_visa. + + # below function also validates visa (or raises exception) and + # extracts the subject id + # subject_id, issuer = get_sub_iss_from_visa(raw_visa) + + # query idp user table + # gen3_user = get_or_create_gen3_user_from_sub_iss(decoded_visa.get("sub"), decoded_visa.get("iss")) + # user_ids_from_passports.append(gen3_user.id) + + except Exception as exc: + logger.warning(f"invalid visa provided, ignoring. Error: {exc}") + continue -def get_gen3_user_ids_for_passport_from_cache(passport): + usernames_from_current_passport = [] + for (issuer, subject_id), visas in identity_to_visas.items(): + gen3_user = get_or_create_gen3_user_from_iss_sub(issuer, subject_id) + # NOTE: does not validate, assumes validation occurs above. + # sync_visa_authorization(raw_visa) + + # QUESTION: do all visas in a passport necessarily belong + # to the same Arborist defined user? relevant because you can only + # update policies in Arborist one user at a time + ga4gh_visas = [ + GA4GHVisaV1( + user=gen3_user, + source=decoded_visa["ga4gh_visa_v1"]["source"], + type=decoded_visa["ga4gh_visa_v1"]["type"], + asserted=int(decoded_visa["ga4gh_visa_v1"]["asserted"]), + expires=int(decoded_visa["exp"]), + ga4gh_visa=raw_visa, + ) + for raw_visa, decoded_visa in visas + ] + sync_visa_authorization(gen3_user, ga4gh_visas, min_visa_expiration) + usernames_from_current_passport.append(gen3_user.username) + + put_gen3_usernames_for_passport_into_cache( + passport, usernames_from_current_passport + ) + + return list(set(usernames_from_all_passports)) + + +def get_gen3_usernames_for_passport_from_cache(passport): cached_user_ids = [] # TODO return cached_user_ids @@ -52,7 +108,18 @@ def get_gen3_user_ids_for_passport_from_cache(passport): def get_unvalidated_visas_from_valid_passport(passport): # validate passport, return visas - return [] + # TODO put inside try block (i.e. shouldn't get a 500 for an expired passport) + # TODO if aud is provided, it must contain client id + # TODO dont hardcode issuers. it needed to be hardcoded because I think + # TODO init issuers list within function call + # list of allowed issuers comes from a config variable + + # issuers = ["https://stsstg.nih.gov"] + # decoded_passport = validate_jwt(passport, attempt_refresh=True, issuers=issuers, options={"verify_aud": False}) + # TODO: ONLY USE THIS FOR DEVELOPMENT + decoded_passport = jwt.decode(passport, verify=False) + + return decoded_passport.get("ga4gh_passport_v1", []) def is_raw_visa_valid(raw_visa): @@ -62,7 +129,7 @@ def is_raw_visa_valid(raw_visa): def get_sub_iss_from_visa(raw_visa): - if not is_valid_visa(raw_visa): + if not is_raw_visa_valid(raw_visa): raise Exception() subject_id = None @@ -73,22 +140,40 @@ def get_sub_iss_from_visa(raw_visa): return subject_id, issuer -def sync_valid_visa_authorization(visa): - # DOES NOT VALIDATE VISA - - # syncs authz to backend - - pass - - -def get_or_create_gen3_user_from_sub_iss(subject_id, issuer): - # TODO query idp user table, not there, create user and add row - return None - - -def sync_visa_authorization(raw_visa): - pass - - -def put_gen3_user_ids_for_passport_into_cache(passport, user_ids_from_passports): +def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): + # for idp_name, idp_config in config.get("OPENID_CONNECT", {}).items(): + + # there are issues with syncing when "https://" is part of the username. + # for example, Arborist returns a 301 for a `POST /user/ + # {username}/policy` request, possibly due to the slashes. + # TODO may want to use urllib to get rid of protocol + issuer = issuer.replace("https://", "") + username = issuer + "_" + subject_id + with flask.current_app.db.session as db_session: + user = query_for_user(db_session, username) + if not user: + user = User(username=username) + db_session.add(user) + db_session.commit() + return user + + +def sync_visa_authorization(gen3_user, ga4gh_visas, expiration): + # TODO might need to look in more places for db_url + db_url = config.get("DB") + arborist_client = gen3authz.client.arborist.client.ArboristClient( + arborist_base_url=config.get("ARBORIST"), logger=logger, authz_provider="GA4GH" + ) + # TODO check this + dbgap_config = config.get("dbGaP") + syncer = fence.scripting.fence_create.init_syncer( + dbgap_config, None, db_url, arborist=arborist_client + ) + + syncer.sync_single_user_visas( + gen3_user, ga4gh_visas, current_session, expiration=expiration + ) + + +def put_gen3_usernames_for_passport_into_cache(passport, usernames_from_passports): pass From 46d45bf3ef16c94f2a5688fc470ca6ded26a950a Mon Sep 17 00:00:00 2001 From: BinamB Date: Mon, 18 Oct 2021 09:56:17 -0500 Subject: [PATCH 043/211] update gen3authz --- poetry.lock | 11 ++++------- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7a7542fe3..72791d79e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -517,7 +517,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "gen3authz" -version = "1.2.0" +version = "1.1.1" description = "Gen3 authz client" category = "main" optional = false @@ -529,10 +529,6 @@ cdiserrors = "<2.0.0" contextvars = {version = ">=2.4,<3.0", markers = "python_version < \"3.7\""} httpx = ">=0.20.0,<1.0.0" -[package.source] -type = "file" -url = "gen3authz-1.2.0.tar.gz" - [[package]] name = "gen3cirrus" version = "2.0.0" @@ -1513,7 +1509,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "00d088769350d770a350dc019c041cf574b749c5e3d2ff56eb67dcaa691c20ef" +content-hash = "11feb12da2ded2662dbb9caaddc069a74ff2d6fd7551ba184b7882f69c1722fb" [metadata.files] addict = [ @@ -1802,7 +1798,8 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] gen3authz = [ - {file = "gen3authz-1.2.0.tar.gz", hash = "sha256:457436e0fdaf8671dfbe72ac0ae1df9b6485d73dd503759e50b22de40b946636"}, + {file = "gen3authz-1.1.1-py3-none-any.whl", hash = "sha256:ed34291683d8ebc8015a38f81917933dd64d3bef4c50a5158612e87f65d349a7"}, + {file = "gen3authz-1.1.1.tar.gz", hash = "sha256:0c32a6fd40c94c2f93c987f24e2953e6537e2fe98b029f9c85dde6e39818e014"}, ] gen3cirrus = [ {file = "gen3cirrus-2.0.0.tar.gz", hash = "sha256:0bd590c407c42dad5f0b896da0fa30bd01ea6bef5ff7dd11324ec59f14a71793"}, diff --git a/pyproject.toml b/pyproject.toml index 43bde9593..34fe938c1 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ flask-cors = "^3.0.3" flask-restful = "^0.3.6" flask_sqlalchemy_session = "^1.1" email_validator = "^1.1.1" -gen3authz = { path = "./gen3authz-1.2.0.tar.gz"} +gen3authz = "^1.1.1" gen3cirrus = "^2.0.0" gen3config = "^0.1.7" gen3users = "^0.6.0" From 70e95f24d7acc1313004e729ff4adb73ea57acb5 Mon Sep 17 00:00:00 2001 From: John McCann Date: Mon, 18 Oct 2021 08:02:13 -0700 Subject: [PATCH 044/211] test(sync_single_user_visas): pass user visas arg --- tests/dbgap_sync/test_user_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dbgap_sync/test_user_sync.py b/tests/dbgap_sync/test_user_sync.py index 3bc1d59dd..8ac4180a0 100644 --- a/tests/dbgap_sync/test_user_sync.py +++ b/tests/dbgap_sync/test_user_sync.py @@ -739,7 +739,7 @@ def test_sync_in_login( user = models.query_for_user( session=db_session, username="TESTUSERB" ) # contains no information - syncer.sync_single_user_visas(user, [], db_session) + syncer.sync_single_user_visas(user, user.ga4gh_visas_v1, db_session) user = models.query_for_user( session=db_session, username="TESTUSERB" ) # contains only visa information From fe0c59ec1ba1d14240106a042f1e50be736d6643 Mon Sep 17 00:00:00 2001 From: BinamB Date: Mon, 18 Oct 2021 11:15:35 -0500 Subject: [PATCH 045/211] authutils change --- authutils-6.0.3.tar.gz | Bin 18407 -> 0 bytes authutils-6.0.4.tar.gz | Bin 0 -> 18434 bytes fence/resources/ga4gh/passports.py | 3 + poetry.lock | 286 +++++++++++++++++++++++------ pyproject.toml | 2 +- 5 files changed, 232 insertions(+), 59 deletions(-) delete mode 100644 authutils-6.0.3.tar.gz create mode 100644 authutils-6.0.4.tar.gz diff --git a/authutils-6.0.3.tar.gz b/authutils-6.0.3.tar.gz deleted file mode 100644 index 86a93a5ec425fa1a55d0ce7d68b112da449d4a3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18407 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 OKmQ*%vh*bYkO2T#D^`~P diff --git a/authutils-6.0.4.tar.gz b/authutils-6.0.4.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..39df8938611681eb8a7b050aa61b7be8e3b6a588 GIT binary patch literal 18434 zcmV)4K+3-#iwFn+00002|6z4>XmxaHY;!F(E-)@ME_7jX0PTJId)vmbXn*EkfdhZ{ znsg~xZzW3A8(ESaoj9_tBq!}rbtsTrQiwr-2LL6r{(S%TJCA(=ASKz3o3=pn)grKu z+1c57?d)vuJb3>7Y54vJ5e`N6hu`E`c$@$S2U;o>in+w+e`u5t!R%!jO@2qbB!CP(C|8M?y_dWa5aBw5M*U>;Exo9mZ z`LoFKC{4WeV72T0XPC^wZ0@bEu5K=@bW;@5-RIBm?(Tw+x(U*3{5Uw8Kr)O^t-yY!WE|oewKEF6Se(@IHPy=g$ zcPK_t5)}Yk9<(Ghz8sP7{1N9{@gnk18!su+RxJ2_{RIzOGoe{f^z9#HW5it zhAYh~fDO{=Jd4IRg_qtXBJ%(ws2CM<4;OKoMSrLMWIJ^=ikq{Ld9fEg(My< z0K_muA0TT4W3kTGSf&$P0Wo0ud6p#Q$BIm9Fp+%O)T`$ap`VkY*T^tR* zoh6`#z_-aHO=L^5pm!G)H{1?)7{GuX zwQXbOE<$R8R@xCjLerDqL{n^O6pa9zQ;`j@>Gsy@|LRb8DWHcDuNs;az`Ounm;;!m zOysIXXtFO7K*At`sdQSm;51YI*L3E4ZK#WXWq!w=b@&f)avKe2*jDD*^dTD%?*ZB< zM?e7XNtEYA@I+_42xwX=q;?Ly4PXhv@|u(>ZJLRZ$TFx-Uq^)Tci6=w9Y!#;kXDtN z;3yfyGeQL{LN7@RFODV=_73xyr=#Ky$t$N0VN!;GeYN(eA=wNU=&JQUipH~yzQeS{ z!eSq9`o97xR6!4uIlqJHn8ma{Mp-(6kAs^q0jSji0jfxHEEy_-qBpVpIr2h}(L>F3 z-2uurO2dR@GmUUTr39QD<`^ge#sIIK0o&XQMtaNKJI8i-?M}on3cX@JwTJynn!T$q zg*$jj5ECClN;69!O4Km)0$|k1VNJm9@WR_Lio<@a*1yG9E80Plx6PF_Mwafp2&^#k=Vufncqx^OMr-9&>M ziy6RV6(9p(WoF_wqNzkG2Gq!P;t9ZTnyFvVgq&u3HOaYnGH$3Ad|w27*IE<>a{Uy#juqR^XMNxul6fe-gQ$xjC-CG_ zppJ--SJVnM%OviWN()<2L9R^QBfa+pAaew$SggD}OxkVEWj)U zX57VD=!Y@U(Ori15>h^9iNwB#tH4IOFo;4t6}ed~g#Wy|R1|66W%mL98JK`0Mln{7 zL8XM&ERm&Ep?sbfVv^e|9AxfHpa>a|jFcsK`cd++wxV?{8|ht(jW|?bVH@!VI__YW zla!|(C&VwM8u<%xJR_RKdxZpdtco6CDETxR%+gs7i+2)c?~wInMmtg}m&l`WLcAYH z5$A{yTT6+^to#$ecj(z`HSnuTxTIg}VXGB*ugYU15c%Vz)Tws^!1M*s6R3=WI5_}q z_hS}eF8*f*^b=#ZgB0-1;u;lRYxyxFS`WNes8(UO2YUEQv+~YoEL8Wr z&cnEvuZ%JQzamiVZqorW@`^N<_8)ertSzVkYix>iT;X(volj?ds0&yGLY>AT5W0Q? zFte!5>5Wt@Y`e`?^EEFfWx3L0jW{RX!V|P%P0(qGO!r@4cG^%)Obc9KU~&{nMFIeG zHi$ajl*ek#9;oZk*iCpVNXt_IksY0mMyTh35D+o2U;cmKC}~#k#ArSs6=kUv5}Q#Y zLlp2Fsb0hBG)8kXO<>Xq%gDPVXoEP60N-5J8XRDe8nLmf`DOyEH_yW?qSZOdfcq$m zMnp>X*$cm&cfbrz6Coucus_g9v{p~m$`#ar*us_47QkmE&jn6)afef(WL@ALk8u9A zEtCVNLYmf-Qbc10AsmPJnV6h3+}lQAY27+Y^L&L6hQk=7Gt{5>H_UwK#o=8(iwYcS zEXFJZ0AmVxqZ5?4;xYzDGJyflrKxS&8JNkLt6{16n2@Fl&9ExwQle6slrbt-iZV3J z8j~VVX;my8aEapVDOw40rLYa*r6N9!U1$yM#38eS&A>Yowr41yt|#H#aJDiR0zMci z-NxZZOSC+i5>y+Zvl%csq9fF3;NMhBWY=tF5x#(vb&V-R2r;BJ5rSuVl*V8av3ys& zYqzECst%8I29!2Nco96dH(**L7$Pz)TlLk}AwHFXh9n=#mc(}?Mpd7Et50^^7*!hW zD74?%sgofIf%y<6NGEJP=2rK};xtKPn`pX^2^oUhaeE$EJ!hhT1=dx1vt`1QbpQ~R z2WNGxJ2V8*#T90xV^>ml7un)apx)HAlnEly!YnsAHg-YOz*A1rNNg6>LP8}L8hhWUmAWm)&9wwi{pbM9~R&Hg7A&YPWFK+x7Clm zx-1q@vr;OUrTMa&Ql@7Ja~KZE#xz7HYSD^JDMUZ7)r@3n#2I)XG=%O$@M<+vkKtOp z60t!Wu?TZCr))31tYTIxC`Qoft^zYuFdAGN3^`PlFNNVdo0&UAX0L140p&#_!)H)r zj*SGYwx4F*Dwac~>sekdX&cmFX;fM%q(FcL$dd&PX2TU6SN`#DU6JtnfJ(7iDniexyOD7Dq07{yrF{!`Og=4pgr<-`Cq)Gj9SR)NjLdO$WS z(;p*d(Y{43ii}l3YAZSGSpg_HzO1RhM&vkbkkKUt z+urBLm}XMVA}}q2;U-vh9je_&u78piSV`~RkbLQpQvdF}HF|b%XFf_m#@FhujO;23DvP zkQ~{frAH4_;+zGm+}4f|B3^E&vHGI6F~SM@&9p{?UN4c|qEBD`1ehcZb74ia3gWKu zI!QA%6aO=d~{MLJC>2>x_}+tUaQU>Wp|0Sr)1g*H0SQ(N)Rs@0_CO-h3r z5x|(*3B1E7C({WxZAacOpnn2h=6VrmQ2TQ>V9D}E6U(pxnnf~Rj2EnHCQPpV+<@9f zSkY-vHc{=;=)QC&w}XByVEO+3xp#c-doT9SkIxlye>uMR;mz9%@0b0vv;C8cXV23?q|Ibz>T;izys`F1Yt@)#+WRJe-wLHz z>2(zLqnI|&k5Te^p#CHU!NG07tC*af03%r1ma9eWWB~ycw#y(9<2VBCYalvZz1!V& zT)Eowb1%KOSy4crejN2lp(EhN=#S9*7OKkvBM@@h9j{*>%sn0Pfle7UWpP9uOOF6e zZ8!^*5p9x= zWcUWG0Eq+@+#s9h74U7~?GI2UAQlwo#h&+#ptcspFE^;VyUVq_S-5n&O=+Qnn>1xF zD0w=a&35v+g5EX~#7}_L5rknf5Il$}yFn%Qn-krMNrIsz#v#UNjTP)(+K;6diu9J} z$aYZk<*gPN53Uz!Uq!jI-2(>359u9RLTpNEgb_ZihRygWq9uu~-4LyF%WV;I;>y>^ zNDUh$cuCVSyC{aE8&8o%2GS>tHd8cW{)`Kr*E`{6q;WG8BQRIE2582^8rON4O^6dI z5#Ryi80>_qKW{l@*dR5r|j=7<4g6 zl&f`NaP)ou^c0F5|791akh}%JQRgyz!4BQSZv@Jn-b=#px(8P1$}kGoo1oO{6jn_J zW_Y2z8eL<_k0KEda}NXrEOuu4ecYuKK$-sKm2Zp`^h`);H&?_&Tvb|qmL(l{?ZY&A zs^bN$HL4o_kB&#yI+>|Cs24y9pyF#dq;+74ZEJ59*GfJIe*Ru>gpyIu005tWigAt` zR9ss6Nfj5TvP?HXWvH>T*+pvRR0(6Xwb~aZWQBG}6(l)U^Z__>Tq6(mQC7G+pECFb z!34sK^nSd=oZ5HQ?iu6c2(!Tr?pQN1n9an?IXqY1CBY4FmUdaMsJJAOp=C3<t@30AFk02vp@g?2oN{x>aX@(MO)D)#v9LwrY`{XSP9haRVOI^cGkby z*$6h)zg^$hY7PMXJD&fU&Osf*ec+%-C-K)f|FgQbu~k0*x4FHx+MNIS8qZ~srg1P$ zMUl;~TBPyqdA_C!zoo)d;Vn^!-?Ft>c>78u8x+{-VGXoIl|KM#%V=0Acj+Ho9G-h` z4{ktt8mHc$2B7nN4|Wj!48(BuXaB0DoYv4TYm=yn`K^`>W-t^OmJN!0B=W1)bPin* zB>%k;Z2PSN1$(ccRxeP?g%AI4Zw&#*9z~WhJpP}cWo-NXBrein1okYITn)a3{t)gs z>Z@04Ykmtg$M^W+&-h|JSnK|2ja!(`f4#%nA1qv9ms>t=`W9Kp;~!-6X_5L@A6u=L zU>CeA)u3kKKd6*Wr7gJU;WpUE)+hujokpb^|GgS)ZPr(C5yn7VwMMerOP>G$e4Bs3qY;EYQKjb{P?=8}Ch{J_kDRwU>Xw8Y_G99ARFe!u!( zbN;(I|J^*l2mLS42G8}o=e-^ttmyTE>HN3T|KQtpY5i|+udg@y-`993en(GuAk`xd z#P;^#{Sm%twLqHgk`Jv^VU~+b6+b|qc~ZR1aUUTUMQiF~Q&FiCc{G-$mDmC;CT0mW|O?~el{&ak@ z=ZC{dl=!@TaBz0Ce{r-2%9;oZAzvOIy*|R1LlFz}^35+Nuixw+!t3-diPLZ>KfF3R zIXc^az32BPV$v5`Priqip)kf4>KCt%_Ro*-9ft0yhTonZ5(u;DkU{ADhOD^p|1|y| z?f+Td*xX)U3pTb^zunl_XcT~dd-*@?kHhKb3IBWL|Ju%4MgQMk+t_L3|JQh)cq`9V z@Nj9AjCZ|RF%R(51FGAS zIsti2)Q?Q)_uA{yB8387GjTemgG92#7$i8p0^Na%fG`XvDTrVqO;w8Cb2P-nVH&@d+#^X&eKq}PrP@Ry9&*=~6$A`VMqgO|N0o@@JflRW{ z_S@5&{BOfQ2DhE|--exb@T}u^TCIaOKb@ks&^x~XDNYKD6z@aSOK3k6l!#%kKktpF zAu1*<-X@|$0twXYXoZ4LO zr6z-Z6eRH^NTQoyoZdc{J?GDXfjl3Kqm_P?JoioISE4wlybI?=Sj_SOiu|9vm<{K> zXt?(QFdGd&{_ji_n0{gpCYGyq7!^Y?@|0=P&P6=x0`j^ZQ_CJ|j1&$1J`(xfhmW0I z)2;6xCm3~GL=;&jH|UY1F~IB=D4dRU$%An<#ErDAj|oA~L67~7Jv{+EfEJxpADpyD zPduLn>9QA6oYDBcgX*mwLN4DCYNGUStf&t}|U1l-;PbL{!X~ zTvPzqSRRHmu>9qx*G@wQwxF(S7?IoYGv&&C=AF}CJV)j*vZ1tTBnu-vM=~A`!f7N8 zx4_|JZp*(!$R;w5&ZEkBdPe#3Z-vVPDHbn(w#ins62Yp`phqyHSt4seJxe)&GeI~{jmO=3iy`1=ze} z8DIUPE;NmS9I#se9t_NV&$FaYK=0b6wmVKW7z~{t!jij{v}-)+&NR1cbF=wx6BQUs ze9_C)`X-^zXF?=H^!)lJjuYah)m8N2BZXek%vp<Ve2m^O~+qxNg3wfqgJ z3ZjY^MOoH-)pt&K4aQkIo3__F-XC!Zx1*$J+aK0D9dE^>&-Uw$`(RtN7)roZD?OTm z|6|WHI>~PBNFPipUx-{d@f;psEbdZI=c1}v;<-gC%NnDz`__v0A4o9F$q=Kh#Z+Sz z($Y>%SiNw=qmKAWv5zIX!*0vz3l%;|6P7Bb76~=FCpo5Ftt!1wK~JR92Nl7I2>r17 zQLjAcveRjm^p`Uwk0m>|ztRMgsvjK0c(%K()Gu3U>0t2UfWFr-Cm1AuL((S@%B(F5 zn1p+j3kRrpeRZ|k&?Fx(T)9R({YRX|G@Ty7%c=u(#1cnn`GYU>UYSQG+n z5Y8|$Gw|uGIpQHuof4I^e8=Eq4})>Dz<&VKu_=Yrh&G_G^C0O#1xs{tilV=kO0*BT z%JQ^1$Oc?H3ovYV0j%}Q=8_h{6*uMrC4JAv3`9Z{pG-OgqMY9HVuA_!a+5(|W4;gZ zDbpdD)cq)Pd6=YW#mBOA49aQ`BSX51-f024n)~ZCeK(sr`f&n<>h*1_&0VT;eaf!_ ze0A-KU>tZC2e0-Q)YdSV+7z;cuei4GUYs55*XXCaWby5)D%T|_9o4kLLR+r5f*~V6 zg<-290~1~Oux1$P6701M4YV9KDs8_YXSJ!957Mb!9oA8+Y5+%;x7k4w$njR` z2$}OVZzD{5^#GcZVR6&;<(P7RVOu!bJ$vXiCG$ZThgsYGQsO02o0U*4TCUc@9AV}^ zJr|^BqSpx1D%w-BWQ<>o7s|yad#hbxJ*^s>wI&@t7CD}AL)OB2n%8)5$~Kc+Fba3h zI*~+QT^#2{MkiECtuI&}s1lT6%UR3mdTkhNh$q?PS!)fuRb7(EQ(37~Mqay)*=g?U+$B}O5- zABV5-KoFfluwldlVR6hyUI>3unxZ0F7QMD_j+Jf8mZ~l~1%*w~r!(1Eop;G5WaDYl z1!ZRoyT5u?`DHm2VaJxwJilH47!@yl;YhGD2yi^@x6&#hOsQe~I8>aHd8uq=w?Y2maoN@vM%AuC>_e^nKR@``wnlJdl37N@hvd2tF7TrxLe zFSe;$p#p94-pggHyrC?Y??O6{R8@TZmxv-JDVF{fAhjA$rPJ18#au3+i)?18igxAP z#%qP?P0PBMY_A?mcC%&`^3hX`y7GFoR$$rl@Cp8dFX%g1>^JyKpTRPJLB&_Fx94q^ zeFai`lxtolcw##q*b^bx+V*Kx)jz#SSW^8KnZOnE_hF{*LyXsoCj4bg)CU=(3w7Zp zxT1;wZsNcGnc~0JHox86+z!6o-dtPV`KDRG|Mu};QdRxT0I>Vwzc+Wv>wkM=r@8;@ z>&1Up_ehm5&s5>3(y1yRk5%D&>tK~HPge13`)rlZhpX^|K3?^) z)%xk^rx!>^TrereBO!~kQ?j#kMaq> zL0>Fy)i=E1U*&1!e2W9c(d>Wk4Q@&RRl*8$?)p}rhu~M-jeTTQfaH+gVGtpD0Vdzt< z)uPB`BFo+|?Pfzos-os;^4 zUis=QCkM zY3~o{P7H)gaDfRONH%As?2cB)=+GtL)baYUFuag6@%k=41#yPi4ClXltgS z>pgo`#RY6ll^HCvha>e_E_+nY|3`upY^%ustn?L6({|0F?ipDe04O(<&1R$|H^didW zg;!vyY(v-AQlo|Drs3cn-eVNz^4c=YuMq^8@=mPu<8<(Dch}=P1X^_oZXJ(UohwY& zkyqAYvUydGbYOWosG|vil4P}El%S{lc_kV?HMgBhJvSEv&{p=I20>tbgx-31z!_Ag zc<-so$%g05u)81;#dEu~r3RI1sL}-Hb1Gq1R2t&aTDnvf`geDzzU{eL(Vo)b_NTkv zQ}`zVofD61W45R4Y1b`iYlc`}0ickMFx2(_v8xitwNKt$9PvR*>@?>aUV%Ksin=p- z7<4$mh2|3mibfG1rI8Z~?|ua`ZY|0qxPN;5cq2&>%K|T~9u}BAngx5xB6y+Rw0)}P zcfB4R$c)6UXaA=I844u(Gh!VOzsI z)D)>B-%eG{0L&_Va~e1;J>grV@%~ZFM2JOhLx7%H#T_i*>8w!K_5s(h=?WHlq&P+J zWh}ra-Yc2i%^e8lK@91_KLL7rGQ^dSHWQdmc&|1Q7}-!NQKoE-4p?>N71H|LJRZes z*$Ye~y6Y*!b0O?wk-`FP2S9TR17=DRBd{HBBnB{B6tAnHJ9a+IDY;PFCJagYbU^>@ z`O#SqSdri9dcIQPxHDz8OH17~u+d)S7T8X%G4UU;j<6ePd=nY7`RiD}l1rAXUU`bq zxFur7XP539<{K6j%CJqx$QHo_AOJ~UvF)MRE*}4^BYo|BOrVt|ImB((GMeZPi(!gG{l3 zDjfh6dUuXDMHax?UG}lJ&Yx@REGs+f$I+lvRKLTL|KL1%jr?!qeX*ja7l|JQgpjwPE<3rqq%y_p9r0E2!wcn9KN%TAfkzbD_f7FGmF zIEnB=IbAguSro=HfZ!n&gD{^@FlB8tP;G$VRBTagja@8Y0_tln=p|IWR}xqfHExO_dfh`#RU!4PAXu7Z+D;z6cP_0c8~(v z_36=1^0rrrkNs?HC9Yr4+pf~X>v0I3?v^{_l1K09W&!JRvt>b;>iL_G|8rig;Pc|> z)$s{+bb5CDGhVd&y*U`!0{XJQL zmT93Lk%9WVkECE2g-G}^<~@gN21oLB0Mllo8+rapMI~vz`aqU-hN z-t~3G!SkCe88eu6lnmk-&bAjviSYWF2;b$M-)<$HzJ2lf_@JJ&e>y&Xas2E4$paSA zi-Ylx2mAl`sD2SG>tylzY2Sz6z#=FU=BVD57T2Q}*y2S-?aMWG&esOX_%YL%Q3kqum)e4P!7Zo*ESy+!>RMbvoVWDTVSz5oJrG-N3pMG)u)B1m# z`#=6+{@-uv{J&eP+uQ4n1Gw@3;xUWQP5=3T{NJVgFKatHn;VV)_iH>)KtisdS5jVa ziN};cF!dumz1z}9f9evn1S(6lyS-|k5~6Zl>t4B{x9C$fb%*(g2N9Qi`t{CfuG5mo zjE)H`I%&kCaQb#Kx>O+JmgqQK1u%G9fk?j7puq2wG!ahRG%F@fH!5Fur9GYNy9}8A zP=(8GOapRH0`NxuH}b!^|ErPz%jEw6IL+r=0kT~FudVNFROJ8KMsxqiS9zr9rh8b` z1}n%88CMmQGPbe6EMKJ6Rd4N^yYx%@f-e2iT&-m)^RJHF*hL|R(FT-rAj_b!(y3;1qaJdRxgr4#&`qlcQ0YO5O9mtQ&jtiD>u zuXoF3^tft=0|>*W5odS?-kP0g*d`x{7_zD%=f@e327|8Y?adOHDwqhUazh$NsRbSq zwCp#C=|%NygIB< zSFjX^T~4#p^_Zss6{obZmhXM|SR&@1l^sd*!mSb!Utc?YU@Y<7O*FXSK;j%&X+FXL z51u}m*5~?~{?N2dNoC+a%D54Fc^r*zig@nD>7B?{VsR^CFHI1{!+ zhKQsulN2MFDO(ohc*#-J=qj@374MMS!m-SVD2#jlJMVOk?fKh4l>d3zO&p zkBo?EeZyKPBa>7%i@CCf7{{HBsW@IV z50l5MNF~!!1#n4Oc|l<{d+DfU7FARTpv@?;v8RBMfHUWkM6s*#o#eTMm}b^)z0$6R zTeiWVIju7#sKzB-sNi!Tff-DtOmc>)Pf%x(rDWr69=IeVY=4G{&N|;|JR=gA06~`3 zF;RzdHJ3ERU5*T+9UCrX?co4D-rB1Jr!SAjNl05kCOOy8YrmQD&i5rWa@DaJXegC6 zkJ6{{3L-k0PQ7Rd<3fQ93v`yxKy#W?f;CXBtQOS4@L|`gt_iSjaZ}6-N83)wonYQ_ zML$k!z;Oak0sI}~(%>rhhF%MSMNIZ!=B(COP;|o(^cR>GT-g9?md?gE^m`6W{An)vUA?@SO#zV%#u37b;- zN=Q*uEIUUPvx_X0GZAX-EdS0E=tU`RkWmP*aAkd>s~b=iU`~PMso?1+NJ{drk}=98 zqf|Gh22ASeSY0=uKx39l%>!kgb~LE3l;Nec#Bzo>sS2x}K79O6H(X7;F5A?GNM{kw zpmB{gTH@8=X-R>~FlMAo(qu(Urp4T7DN;_c5|p#9K$?tVXsj0T57Gq`3g}g79rdL4 zx+g6Q@;cBvzw2FEiR{sOFwDfR%d*6^sO#F6KZAT4M|OVwDlcyPog;5z)|g`zjBnRk_fhiT`Q*|IPki^L*a^-zbDNGL1fY3-I3apF3OS z{XZ<;?EihO^PgcpPX*l9oeC;XAf{;$!^3D$ z=v?N!pU8R7nR-;|b1|5K8b9xPKU`d#9+8HtkA*5q%Vi-|35&iEVTK8`@$Y>*CG`Px zj!vu)jBZZE4>KBzq7VV*vYzF3E4|Itjo!;QXD^Nq506gNHm6R@o@Szc=TmP`TDMlU z`UDba3kE(s-bcYV#*9IA;=@PjL9gC@^i1hd+I&GPRjI4;hx7sUqz)_JO0PftYAtfA zmlBSX1DN4fnks?s@qXN{6%k%HeTS43BsYb=$4twHZT*?@(}^fb62=y!LVji!CFxDCqTfO5rxWE$FFb?`YY5I&M*I z;*}kZ_TWIL9#F=5mB3@pF)cDU6lPCJ?w{c$Bi=6j$n)K6baP$@uE$RK_)A)S#T(=QB$al&LSiBc z#H%zvI`+1dWm4phkEnz;TCZ|~7NHh{3HeV+^~AtEx!VmOv}JzuWxNs^QM|a#48dMx zVA#g#0OpSSsc;|zS`MqXkk)BFxDjB})7DzMxj(<4Ir5>uz5|5ol&rC(?MY)8xXON5}1{{6SlKF>nn0NA$)loV_IhJU> z>_I$6#;MpFvPMH&EDojqvs6Y^#y~==@|Kpa9THS6QnSZnO;q)RQoO5Qq?J^y;fjof z2$OTKbjo`MQxk!Qzbi6Y8-u?iAzcn=KL9Z{zA;a_A648Z&k)dlD++l!H520w9IZA1pn4Z;K?RxsZ$`I#uZ-V5b!IhenE zd#uxYenYpzQA|^cTck4XmT%Xjh(-(vzb^t`=uH`+?T&YU8qT;2f=|rrIMY6F1#65UNm!s09s=KzczJ2Llu?p?2tghkZ zs*7`axOdN*YU_`wc4aC53S^y`MZz5wB1p6GZvwXJ|08FRmY!|AM0a~rpMlDCs>*wI zJoHVlaXKCo?QkKqY4MdPT?T6%vZ`*D{F7wjtfbwp-zQQmvbl+No4+*dBUi`78rj~@aB1ccg z=V6jDYU4-MtzsE0qcKO8cK*^G?X@S!bhArNC3x!oV-+cgmGEj<>}M>mUgndUK$qXy zlzj)w7NZp_t&N;ztnZDN8(Y>61M&$AxYzWe4n8jG^bXBG#e*`GX;yOhdrrHZ;|L(82=LBGcX+SF_MZ9!Dj3<=R+p6%_wLbgtUW(UMyc=Yk{MoaAT;lIhn@Xm zFEPz;7nCvJR{1Ow`l!ndjr;n#x)ii-NF$j|bxEV8@cwe+FGEo6}?ry*9 z=HboQ96M=OW|LtnIVaYD1v@-1L(0R(~JF z>ttG}>#F%$Z(HfWQGEE=btQt2;@#YQO&gqw_wg7%dp0S^wE|^3eCYrD$*2G z?MM_OGL3GEN&Gw;4R*HHzIl?fiL$aCYy@jA`{_-?A)dFO6o!CDHgp15D0w&%z5e!Q z-6;)u9OKeY0AYXh{5{`$=CwCLXvHFFt1lhze|ekB#w0$ocd@W{@qpej&;WF%09TPr z;pn0R*l#}?`(2MUD0#Nu9YM7X@z^$=c#3*>!-kK*SR}Nv!sj#?7tgJL9~tYDBVD=M z3r>nu01{KvV`h4Kcfz#O1}!q_*r_cmR||B?o5nibCAVpHvLqQMpOSo79?WXlM`xhyuDt}S}x7%yyC4xJw~(L6PNdl?%@_c;^AF9Vbunf(@{#(Q<$Hp z-=V@jn;;*tQ_h~hi;VloMUP=H%lBY$Agd2Q^sodStAq;tYfXk28D%RV*Pv*B&{Djn zm5-eVdaxeaT1BoM;`#9G!NRRhw3!4U)&e1a1k_WJmn_bEvZuX^Z1y1S9Gt(ZhV5Ml4Jv@D+hB&5#9M_BdN*#GTorY@Q(|V_q&_DvKxPe`#24D-8 zE*%Wk)!yc=OB~H{YjW!VvhA*2ry-t4H=i|vT%CzJa~%T_WbOfjeiPy6t^lvy2R)=@ zD&O4*c_13qZCmp#Plz=3EkYVqPKVCG&Z0A4dexAlPNP@6$Md|J9I#yqVf98BJK*xS zA{X(95o`7iuCGZ*lQEpIUBL&sfj8Vn19X@cvbfV4RYeY9z;1Pe0`{jAlj#Yu;1*a4DEb>)VIRhC?`wE@KsabzeP$Z;j8^S06{u-xp% z{2jgH^YgbyXO=%0O! z$XLoQs#I>!DjJHWB}?h4tM4?dP}Uf@V{>ATZJC{ z5?3o@CP@h^Bu#T>W4!gwe9VdY#2CtYg_+`Tt@BK!v|x|dpUWqiq(cow0PRD~ruE?j zM!c6I3d(u8&IE}`nGg&!9(Z>ds%)7TW)zFblEjP=w@avxaTfM-tvyR!-stW1>MW(I zsutt8yx_BvM=j9JY<;Z81-V4r%TnmzQY%I#xuLv`%nQDtd!J8d)TfhdDSzKU^z_k+ zWvkGZfG^*i9UN6f;6v9U5u(G~Z8&yqsSH3pSgN{y1g>14pt2OMXqgh-`yJZW6u&jCE+1OQ1{cX8fl*2;nT$bHu1kr{)Z<1cUk$MB>!2eCv!BS!b~GMzo=|wUOvuS;b$NW!eXa5!u!EQaoC?k>j?LYL zlv!Mcz;!7q>^6-MJ43J^Lzr%}g&VZW_YN6iY{%iGKMZ$qjFw+)-1YqOwLfH%XdAq8 zFHqagYcE>~dgY#(gk=*|B-S({zhC-2Ef#uq`VI`IaGG^)A1dF}{z?&^Toj1-c9dw_ z7QGFfu1aJt54>T=(x}!JyxbJ$Z3M|ilzTi_V&AHc%qd9)nziDzMW^_awCLHgbP_5^ z)6%)~Ng9k+gOH(t}pz3xq4NXVWB3$A4V=vjkT+4 zZa6brE(v_bjLG2Dx`|W!f8-BYKuXtY*NSIVg7u&&XKyQK2Ek>1&UG_h=78@n?qpjl2PJ+i_rWm!0NWF#vj_B5O3E$`t;na5Fdfl1^_oJ$Nb2jg>3M zY#;HpoX977w;WV*>Fn=ln=3FYKzI--tR(O0%XQP+SGAq8|e%cdO*hwU5uIlvT>?ONO*#fiYvSjjHSr&^j zE!35}$xyt%XWQAFa+ro$E{JdI#wD!orD_k_%C2ih$L#nfA3jMg8g7Ai&e%k(Qb0<2 zsw(VDt}j5vy_&s-iH>Q%tlsR&fA868Q%7Peexds*`o7Rqme7v-h_$M4ILx()Wd);a zmkCl8#R^y{4!f2Po|yaQ+Q>LlsY`T%FDXV5F&gyt$Wn+@2P*qxrcVEIfP;PC7Y_)9 zY{*09`}3r`!EVsxJk?&w00Red=-SPB%FzVvXCUm?%(z!8&S8io- zE)a+hiLIAxWK^MpN{S{)FiwspP1wKbK3uc^+wA`~`@hZpZ~gu+o|XBW%uf&5|6Sc# z-!8BJ?bRm#|JQjc+pKo}o0dsjJM4if4I;|q;26`i4GcMaD-yQIRYLzK!{+|!!x-s8^0KVe6NK6Zk-R6i|l zw1qig$kr2kz7kUxddWqkaJ zafu0VW$`w}{_|^}LhS92E&EPG8i3zjJUIItxzcedX~3R$z{7p(%xJ4#y|k+aa$no= zHrNG#Y@RIG_NV%-C-D6Zxo0Qoa2D%~OcXnz$d!?}@iIV$xd`YKuVSvf` zGqmGAYW9hG%dQ`n?LAc{d^;LhK4VFp9VgoZPqS|BotWRHnYnY+a%Vg40PWUksUUPt z5wZl*#jb6_fxDq)j@-?X2)go)4>j!&N!ht{&<@tcdHm{i7q1k70m(#Fr3>1#6z2&* z!~>nU{m}JZM|q((B2>n@bfk)^#C2I3TgRWHycZ59QBsXBR})OhRj9}`i*CW58;jBv zsK{4)y(o!_UayUZ^3>Ji(oJFJ%63(hW^vE{L3uQsoDY04o;n?3in@lkc`t8@qZoz{ zJ}2Pvt^F;4QmuV-dh#D8^2wN*&z_k^J8pdnaJO56PG^1VZaVs=XIYiS1x-hl`Lr{B z^Ghx3*1?PDEAE2Q{fVtn@WVC zeAj1elc$B%7TqJGj1?R4p->Hro*h5RL(mq{>RTGA=?Rp%@8M{h$tX7&UqW+AUZ29`c$-qVTSd-VPH{k#MKo2c(1W z{cmi-KaKzQtDOH?dA8zlfZVP(D@H5d;2U|7DiQ-}3ziFYHf^=0;ox037QJDF-lM;@ zK0L+z>v=YK`cZ$(>C5F)O<|aU-TVqt{BynbWx)4?mA)0A^8l3fZ-Y_v9xpO0_riB@ zURfZ6m11DNeERI^6~IhNVI23g?qy()qyC@wR)e**?w{6zRcf9s#1&;B{&{aL*zEpk zU47L)aC{|SQkGF_XFcGSiR=Va0E`e;5krd|8+)Smj{k zJ*_k*`8U?u+QjbQ-z}`$^`4SSNu@}!CXnDi!UD~%akX#)@269)LX)j?<^k z=bo5^QH+HU|KHCBTqqF3nJNecI2bE)+c)oZBi7ehR(fF(KYXx$x!-g1F)!By8nmmz zPg$Z&hfu2gN|tnq0Lr;9RF8C-gx%cJKksb>+uc8H5{==t#5C)mzuU9l9Pn@S|F5Y3 zZ&daFwMPH{D*FFUt^U8Uxzlife@FfQ^v72#$0sk}d`10#ZF{@C{?~VQwi^BaYdk-R zB1HXTDS_il_jfeZ7RoSDryX(C~8~s+| ppb=Wl(>%@7Jk8TQ&C@*1(>%@7Jk8TQ&GV?|{{#QMLX`lJ0RY>>O_Bfr literal 0 HcmV?d00001 diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index ce7ef14ef..990a16b5c 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -82,6 +82,9 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): decoded_passport = {} passport_issuer, passport_kid = None, None + if not pkey_cache: + pkey_cache = {} + try: passport_issuer = get_iss(passport) passport_kid = get_kid(passport) diff --git a/poetry.lock b/poetry.lock index 72791d79e..d37cb5489 100644 --- a/poetry.lock +++ b/poetry.lock @@ -61,7 +61,7 @@ requests = "*" [[package]] name = "authutils" -version = "6.0.3" +version = "6.0.4" description = "Gen3 auth utility functions" category = "main" optional = false @@ -81,7 +81,20 @@ fastapi = ["fastapi (>=0.54.1,<0.55.0)"] [package.source] type = "file" -url = "authutils-6.0.3.tar.gz" +url = "authutils-6.0.4.tar.gz" + +[[package]] +name = "aws-xray-sdk" +version = "0.95" +description = "The AWS X-Ray SDK for Python (the SDK) enables Python developers to record and emit information from within their applications to the AWS X-Ray service." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +jsonpickle = "*" +requests = "*" +wrapt = "*" [[package]] name = "azure-core" @@ -387,6 +400,23 @@ idna = ["idna (>=2.1)"] curio = ["curio (>=1.2)", "sniffio (>=1.1)"] trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] +[[package]] +name = "docker" +version = "5.0.3" +description = "A Python library for the Docker Engine API." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pywin32 = {version = "227", markers = "sys_platform == \"win32\""} +requests = ">=2.14.2,<2.18.0 || >2.18.0" +websocket-client = ">=0.32.0" + +[package.extras] +ssh = ["paramiko (>=2.4.2)"] +tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=3.4.7)", "idna (>=2.0.0)"] + [[package]] name = "docopt" version = "0.6.2" @@ -851,6 +881,30 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "jsondiff" +version = "1.1.1" +description = "Diff JSON and JSON-like structures in Python" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "jsonpickle" +version = "2.0.0" +description = "Python library for serializing any arbitrary object graph into JSON" +category = "dev" +optional = false +python-versions = ">=2.7" + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["coverage (<5)", "pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black-multipy", "pytest-cov", "ecdsa", "feedparser", "numpy", "pandas", "pymongo", "sklearn", "sqlalchemy", "enum34", "jsonlib"] +"testing.libs" = ["demjson", "simplejson", "ujson", "yajl"] + [[package]] name = "markdown" version = "3.3.4" @@ -867,11 +921,11 @@ testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" -version = "1.1.1" +version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +python-versions = ">=3.6" [[package]] name = "mock" @@ -899,41 +953,34 @@ python-versions = ">=3.5" [[package]] name = "moto" -version = "1.3.15" +version = "1.3.7" description = "A library that allows your python tests to easily mock out the boto library" category = "dev" optional = false python-versions = "*" [package.dependencies] +aws-xray-sdk = ">=0.93,<0.96" boto = ">=2.36.0" -boto3 = ">=1.9.201" -botocore = ">=1.12.201" -Jinja2 = ">=2.10.1" -MarkupSafe = "<2.0" +boto3 = ">=1.6.16" +botocore = ">=1.12.13" +cryptography = ">=2.3.0" +docker = ">=2.5.1" +Jinja2 = ">=2.7.3" +jsondiff = "1.1.1" mock = "*" -more-itertools = "*" +pyaml = "*" python-dateutil = ">=2.1,<3.0.0" +python-jose = "<3.0.0" pytz = "*" requests = ">=2.5" responses = ">=0.9.0" six = ">1.9" werkzeug = "*" xmltodict = "*" -zipp = "*" [package.extras] -acm = ["cryptography (>=2.3.0)"] -all = ["cryptography (>=2.3.0)", "PyYAML (>=5.1)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)", "docker (>=2.5.1)", "jsondiff (>=1.1.2)", "aws-xray-sdk (>=0.93,!=0.96)", "idna (>=2.5,<3)", "cfn-lint (>=0.4.0)", "sshpubkeys (>=3.1.0,<4.0)", "sshpubkeys (>=3.1.0)"] -awslambda = ["docker (>=2.5.1)"] -batch = ["docker (>=2.5.1)"] -cloudformation = ["PyYAML (>=5.1)", "cfn-lint (>=0.4.0)"] -cognitoidp = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)"] -ec2 = ["cryptography (>=2.3.0)", "sshpubkeys (>=3.1.0,<4.0)", "sshpubkeys (>=3.1.0)"] -iam = ["cryptography (>=2.3.0)"] -iotdata = ["jsondiff (>=1.1.2)"] server = ["flask"] -xray = ["aws-xray-sdk (>=0.93,!=0.96)"] [[package]] name = "msrest" @@ -1083,6 +1130,17 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pyaml" +version = "21.10.1" +description = "PyYAML-based module to produce pretty and readable YAML-serialized data" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +PyYAML = "*" + [[package]] name = "pyasn1" version = "0.4.8" @@ -1246,6 +1304,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "pywin32" +version = "227" +description = "Python for Window Extensions" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "pyyaml" version = "5.4.1" @@ -1458,6 +1524,18 @@ python-versions = "*" cdislogging = "*" sqlalchemy = ">=1.3.3,<1.4.0" +[[package]] +name = "websocket-client" +version = "1.2.1" +description = "WebSocket client for Python with low level API options" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + [[package]] name = "werkzeug" version = "1.0.1" @@ -1470,6 +1548,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] watchdog = ["watchdog"] +[[package]] +name = "wrapt" +version = "1.13.2" +description = "Module for decorators, wrappers and monkey patching." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + [[package]] name = "wtforms" version = "2.3.3" @@ -1509,7 +1595,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "11feb12da2ded2662dbb9caaddc069a74ff2d6fd7551ba184b7882f69c1722fb" +content-hash = "4e8521a47aa2a7cafac36952fa4416c4a01ae007f371f47aab661a8086dc4bae" [metadata.files] addict = [ @@ -1537,7 +1623,11 @@ authlib = [ {file = "Authlib-0.11.tar.gz", hash = "sha256:9741db6de2950a0a5cefbdb72ec7ab12f7e9fd530ff47219f1530e79183cbaaf"}, ] authutils = [ - {file = "authutils-6.0.3.tar.gz", hash = "sha256:b89c95a97a310f805673df7e5b6794ab7fe643f2b26af397de545db89584e991"}, + {file = "authutils-6.0.4.tar.gz", hash = "sha256:eab0c53e3a2b482e427ec30c8325d44ba9b9171ced92c001ac211df3f113b613"}, +] +aws-xray-sdk = [ + {file = "aws-xray-sdk-0.95.tar.gz", hash = "sha256:9e7ba8dd08fd2939376c21423376206bff01d0deaea7d7721c6b35921fed1943"}, + {file = "aws_xray_sdk-0.95-py2.py3-none-any.whl", hash = "sha256:72791618feb22eaff2e628462b0d58f398ce8c1bacfa989b7679817ab1fad60c"}, ] azure-core = [ {file = "azure-core-1.19.0.zip", hash = "sha256:18d2a6cd3b7391489f005775fe69e4d0870f9384b755e45185efd45c050e2306"}, @@ -1759,6 +1849,10 @@ dnspython = [ {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, ] +docker = [ + {file = "docker-5.0.3-py2.py3-none-any.whl", hash = "sha256:7a79bb439e3df59d0a72621775d600bc8bc8b422d285824cb37103eab91d1ce0"}, + {file = "docker-5.0.3.tar.gz", hash = "sha256:d916a26b62970e7c2f554110ed6af04c7ccff8e9f81ad17d0d40c75637e227fb"}, +] docopt = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] @@ -1956,44 +2050,52 @@ jmespath = [ {file = "jmespath-0.9.2-py2.py3-none-any.whl", hash = "sha256:3f03b90ac8e0f3ba472e8ebff083e460c89501d8d41979771535efe9a343177e"}, {file = "jmespath-0.9.2.tar.gz", hash = "sha256:54c441e2e08b23f12d7fa7d8e6761768c47c969e6aed10eead57505ba760aee9"}, ] +jsondiff = [ + {file = "jsondiff-1.1.1.tar.gz", hash = "sha256:2d0437782de9418efa34e694aa59f43d7adb1899bd9a793f063867ddba8f7893"}, +] +jsonpickle = [ + {file = "jsonpickle-2.0.0-py2.py3-none-any.whl", hash = "sha256:c1010994c1fbda87a48f8a56698605b598cb0fc6bb7e7927559fc1100e69aeac"}, + {file = "jsonpickle-2.0.0.tar.gz", hash = "sha256:0be49cba80ea6f87a168aa8168d717d00c6ca07ba83df3cec32d3b30bfe6fb9a"}, +] markdown = [ {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, ] markupsafe = [ - {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, - {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, ] mock = [ {file = "mock-2.0.0-py2.py3-none-any.whl", hash = "sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1"}, @@ -2004,8 +2106,8 @@ more-itertools = [ {file = "more_itertools-8.10.0-py3-none-any.whl", hash = "sha256:56ddac45541718ba332db05f464bebfb0768110111affd27f66e0051f276fa43"}, ] moto = [ - {file = "moto-1.3.15-py2.py3-none-any.whl", hash = "sha256:3be7e1f406ef7e9c222dbcbfd8cefa2cb1062200e26deae49b5df446e17be3df"}, - {file = "moto-1.3.15.tar.gz", hash = "sha256:fd98f7b219084ba8aadad263849c4dbe8be73979e035d8dc5c86e11a86f11b7f"}, + {file = "moto-1.3.7-py2.py3-none-any.whl", hash = "sha256:4df37936ff8d6a4b8229aab347a7b412cd2ca4823ff47bd1362ddfbc6c5e4ecf"}, + {file = "moto-1.3.7.tar.gz", hash = "sha256:129de2e04cb250d9f8b2c722ec152ed1b5426ef179b4ebb03e9ec36e6eb3fcc5"}, ] msrest = [ {file = "msrest-0.6.21-py2.py3-none-any.whl", hash = "sha256:c840511c845330e96886011a236440fafc2c9aff7b2df9c0a92041ee2dee3782"}, @@ -2082,6 +2184,10 @@ py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] +pyaml = [ + {file = "pyaml-21.10.1-py2.py3-none-any.whl", hash = "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0"}, + {file = "pyaml-21.10.1.tar.gz", hash = "sha256:c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383"}, +] pyasn1 = [ {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, @@ -2198,6 +2304,20 @@ pytz = [ {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, ] +pywin32 = [ + {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"}, + {file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"}, + {file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"}, + {file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"}, + {file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"}, + {file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"}, + {file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"}, + {file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"}, + {file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"}, + {file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"}, + {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"}, + {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"}, +] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, @@ -2311,10 +2431,60 @@ urllib3 = [ userdatamodel = [ {file = "userdatamodel-2.3.3.tar.gz", hash = "sha256:b846b7efd2d002a653474fa3bd7bf2a2c964277ff5f8d9bde8e9d975aca8d130"}, ] +websocket-client = [ + {file = "websocket-client-1.2.1.tar.gz", hash = "sha256:8dfb715d8a992f5712fff8c843adae94e22b22a99b2c5e6b0ec4a1a981cc4e0d"}, + {file = "websocket_client-1.2.1-py2.py3-none-any.whl", hash = "sha256:0133d2f784858e59959ce82ddac316634229da55b498aac311f1620567a710ec"}, +] werkzeug = [ {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, ] +wrapt = [ + {file = "wrapt-1.13.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3de7b4d3066cc610054e7aa2c005645e308df2f92be730aae3a47d42e910566a"}, + {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:8164069f775c698d15582bf6320a4f308c50d048c1c10cf7d7a341feaccf5df7"}, + {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9adee1891253670575028279de8365c3a02d3489a74a66d774c321472939a0b1"}, + {file = "wrapt-1.13.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a70d876c9aba12d3bd7f8f1b05b419322c6789beb717044eea2c8690d35cb91b"}, + {file = "wrapt-1.13.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3f87042623530bcffea038f824b63084180513c21e2e977291a9a7e65a66f13b"}, + {file = "wrapt-1.13.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e634136f700a21e1fcead0c137f433dde928979538c14907640607d43537d468"}, + {file = "wrapt-1.13.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3e33c138d1e3620b1e0cc6fd21e46c266393ed5dae0d595b7ed5a6b73ed57aa0"}, + {file = "wrapt-1.13.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:283e402e5357e104ac1e3fba5791220648e9af6fb14ad7d9cc059091af2b31d2"}, + {file = "wrapt-1.13.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:ccb34ce599cab7f36a4c90318697ead18312c67a9a76327b3f4f902af8f68ea1"}, + {file = "wrapt-1.13.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:fbad5ba74c46517e6488149514b2e2348d40df88cd6b52a83855b7a8bf04723f"}, + {file = "wrapt-1.13.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:724ed2bc9c91a2b9026e5adce310fa60c6e7c8760b03391445730b9789b9d108"}, + {file = "wrapt-1.13.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:83f2793ec6f3ef513ad8d5b9586f5ee6081cad132e6eae2ecb7eac1cc3decae0"}, + {file = "wrapt-1.13.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:0473d1558b93e314e84313cc611f6c86be779369f9d3734302bf185a4d2625b1"}, + {file = "wrapt-1.13.2-cp35-cp35m-win32.whl", hash = "sha256:15eee0e6fd07f48af2f66d0e6f2ff1916ffe9732d464d5e2390695296872cad9"}, + {file = "wrapt-1.13.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bc85d17d90201afd88e3d25421da805e4e135012b5d1f149e4de2981394b2a52"}, + {file = "wrapt-1.13.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6ee5f8734820c21b9b8bf705e99faba87f21566d20626568eeb0d62cbeaf23c"}, + {file = "wrapt-1.13.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:53c6706a1bcfb6436f1625511b95b812798a6d2ccc51359cd791e33722b5ea32"}, + {file = "wrapt-1.13.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fbe6aebc9559fed7ea27de51c2bf5c25ba2a4156cf0017556f72883f2496ee9a"}, + {file = "wrapt-1.13.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:0582180566e7a13030f896c2f1ac6a56134ab5f3c3f4c5538086f758b1caf3f2"}, + {file = "wrapt-1.13.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:bff0a59387a0a2951cb869251257b6553663329a1b5525b5226cab8c88dcbe7e"}, + {file = "wrapt-1.13.2-cp36-cp36m-win32.whl", hash = "sha256:df3eae297a5f1594d1feb790338120f717dac1fa7d6feed7b411f87e0f2401c7"}, + {file = "wrapt-1.13.2-cp36-cp36m-win_amd64.whl", hash = "sha256:1eb657ed84f4d3e6ad648483c8a80a0cf0a78922ef94caa87d327e2e1ad49b48"}, + {file = "wrapt-1.13.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0cdedf681db878416c05e1831ec69691b0e6577ac7dca9d4f815632e3549580"}, + {file = "wrapt-1.13.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:87ee3c73bdfb4367b26c57259995935501829f00c7b3eed373e2ad19ec21e4e4"}, + {file = "wrapt-1.13.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3e0d16eedc242d01a6f8cf0623e9cdc3b869329da3f97a15961d8864111d8cf0"}, + {file = "wrapt-1.13.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:8318088860968c07e741537030b1abdd8908ee2c71fbe4facdaade624a09e006"}, + {file = "wrapt-1.13.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d90520616fce71c05dedeac3a0fe9991605f0acacd276e5f821842e454485a70"}, + {file = "wrapt-1.13.2-cp37-cp37m-win32.whl", hash = "sha256:22142afab65daffc95863d78effcbd31c19a8003eca73de59f321ee77f73cadb"}, + {file = "wrapt-1.13.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d0d717e10f952df7ea41200c507cc7e24458f4c45b56c36ad418d2e79dacd1d4"}, + {file = "wrapt-1.13.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:593cb049ce1c391e0288523b30426c4430b26e74c7e6f6e2844bd99ac7ecc831"}, + {file = "wrapt-1.13.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8860c8011a6961a651b1b9f46fdbc589ab63b0a50d645f7d92659618a3655867"}, + {file = "wrapt-1.13.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ada5e29e59e2feb710589ca1c79fd989b1dd94d27079dc1d199ec954a6ecc724"}, + {file = "wrapt-1.13.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:fdede980273aeca591ad354608778365a3a310e0ecdd7a3587b38bc5be9b1808"}, + {file = "wrapt-1.13.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:af9480de8e63c5f959a092047aaf3d7077422ded84695b3398f5d49254af3e90"}, + {file = "wrapt-1.13.2-cp38-cp38-win32.whl", hash = "sha256:c65e623ea7556e39c4f0818200a046cbba7575a6b570ff36122c276fdd30ab0a"}, + {file = "wrapt-1.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:b20703356cae1799080d0ad15085dc3213c1ac3f45e95afb9f12769b98231528"}, + {file = "wrapt-1.13.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c5c4cf188b5643a97e87e2110bbd4f5bc491d54a5b90633837b34d5df6a03fe"}, + {file = "wrapt-1.13.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:82223f72eba6f63eafca87a0f614495ae5aa0126fe54947e2b8c023969e9f2d7"}, + {file = "wrapt-1.13.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:81a4cf257263b299263472d669692785f9c647e7dca01c18286b8f116dbf6b38"}, + {file = "wrapt-1.13.2-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:728e2d9b7a99dd955d3426f237b940fc74017c4a39b125fec913f575619ddfe9"}, + {file = "wrapt-1.13.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7574de567dcd4858a2ffdf403088d6df8738b0e1eabea220553abf7c9048f59e"}, + {file = "wrapt-1.13.2-cp39-cp39-win32.whl", hash = "sha256:c7ac2c7a8e34bd06710605b21dd1f3576764443d68e069d2afba9b116014d072"}, + {file = "wrapt-1.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e6d1a8eeef415d7fb29fe017de0e48f45e45efd2d1bfda28fc50b7b330859ef"}, + {file = "wrapt-1.13.2.tar.gz", hash = "sha256:dca56cc5963a5fd7c2aa8607017753f534ee514e09103a6c55d2db70b50e7447"}, +] wtforms = [ {file = "WTForms-2.3.3-py2.py3-none-any.whl", hash = "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c"}, {file = "WTForms-2.3.3.tar.gz", hash = "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"}, diff --git a/pyproject.toml b/pyproject.toml index 34fe938c1..7b95ef913 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ include = [ [tool.poetry.dependencies] python = "^3.6" authlib = "^0.11" -authutils = { path= "./authutils-6.0.3.tar.gz" } +authutils = { path= "./authutils-6.0.4.tar.gz" } bcrypt = "^3.1.4" boto3 = "~1.9.91" botocore = "^1.12.253" From 5a2eb6513594beb229957d54a203d51f42c23aa7 Mon Sep 17 00:00:00 2001 From: John McCann Date: Mon, 18 Oct 2021 09:50:18 -0700 Subject: [PATCH 046/211] feat(visa sync): don't revoke policies --- fence/sync/sync_users.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 7e8487ef3..bed876a29 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -1666,7 +1666,8 @@ def _update_authz_in_arborist( username = user.username self.arborist_client.create_user_if_not_exist(username) - self.arborist_client.revoke_all_policies_for_user(username) + if not single_user_sync: + self.arborist_client.revoke_all_policies_for_user(username) for project, permissions in user_project_info.items(): # check if this is a dbgap project, if it is, we need to get the right From 7f51cbbcd4ddaeba22faabc1ccc165d026c9571f Mon Sep 17 00:00:00 2001 From: BinamB Date: Tue, 19 Oct 2021 11:00:38 -0500 Subject: [PATCH 047/211] new authutils test --- authutils-6.0.3.tar.gz | Bin 0 -> 18441 bytes authutils-6.0.4.tar.gz | Bin 18434 -> 0 bytes gen3authz-1.2.0.tar.gz | Bin 13006 -> 0 bytes poetry.lock | 8 ++++---- pyproject.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 authutils-6.0.3.tar.gz delete mode 100644 authutils-6.0.4.tar.gz delete mode 100644 gen3authz-1.2.0.tar.gz diff --git a/authutils-6.0.3.tar.gz b/authutils-6.0.3.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..c447314cd7a8cff37da289cbd9689e866d6b0c45 GIT binary patch literal 18441 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 diff --git a/gen3authz-1.2.0.tar.gz b/gen3authz-1.2.0.tar.gz deleted file mode 100644 index 70fd8197605c8c4e55da226d0db7a022ad52155b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13006 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 Q^U(AE0bh66!vH`508<2wR{#J2 diff --git a/poetry.lock b/poetry.lock index d37cb5489..e9a6a5298 100644 --- a/poetry.lock +++ b/poetry.lock @@ -61,7 +61,7 @@ requests = "*" [[package]] name = "authutils" -version = "6.0.4" +version = "6.0.3" description = "Gen3 auth utility functions" category = "main" optional = false @@ -81,7 +81,7 @@ fastapi = ["fastapi (>=0.54.1,<0.55.0)"] [package.source] type = "file" -url = "authutils-6.0.4.tar.gz" +url = "authutils-6.0.3.tar.gz" [[package]] name = "aws-xray-sdk" @@ -1595,7 +1595,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "4e8521a47aa2a7cafac36952fa4416c4a01ae007f371f47aab661a8086dc4bae" +content-hash = "11feb12da2ded2662dbb9caaddc069a74ff2d6fd7551ba184b7882f69c1722fb" [metadata.files] addict = [ @@ -1623,7 +1623,7 @@ authlib = [ {file = "Authlib-0.11.tar.gz", hash = "sha256:9741db6de2950a0a5cefbdb72ec7ab12f7e9fd530ff47219f1530e79183cbaaf"}, ] authutils = [ - {file = "authutils-6.0.4.tar.gz", hash = "sha256:eab0c53e3a2b482e427ec30c8325d44ba9b9171ced92c001ac211df3f113b613"}, + {file = "authutils-6.0.3.tar.gz", hash = "sha256:2716eadd6eb038e4f4ae1efa5c37df31f76726c5057ab49db4da81acc29b4b5e"}, ] aws-xray-sdk = [ {file = "aws-xray-sdk-0.95.tar.gz", hash = "sha256:9e7ba8dd08fd2939376c21423376206bff01d0deaea7d7721c6b35921fed1943"}, diff --git a/pyproject.toml b/pyproject.toml index 7b95ef913..34fe938c1 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ include = [ [tool.poetry.dependencies] python = "^3.6" authlib = "^0.11" -authutils = { path= "./authutils-6.0.4.tar.gz" } +authutils = { path= "./authutils-6.0.3.tar.gz" } bcrypt = "^3.1.4" boto3 = "~1.9.91" botocore = "^1.12.253" From ab5ad61be02feecf888142de12c95ef5a27b4d44 Mon Sep 17 00:00:00 2001 From: John McCann Date: Wed, 20 Oct 2021 07:19:41 -0700 Subject: [PATCH 048/211] feat(passports.py): add validate_visa function --- fence/jwt/validate.py | 19 ++-- fence/resources/ga4gh/passports.py | 141 +++++++++++++++-------------- 2 files changed, 84 insertions(+), 76 deletions(-) diff --git a/fence/jwt/validate.py b/fence/jwt/validate.py index 84c8eea4f..128d318c8 100644 --- a/fence/jwt/validate.py +++ b/fence/jwt/validate.py @@ -41,9 +41,11 @@ def validate_jwt( encoded_token=None, aud=None, scope={"openid"}, + require_purpose=True, purpose=None, public_key=None, attempt_refresh=False, + issuers=None, **kwargs ): """ @@ -94,12 +96,13 @@ def validate_jwt( aud = config["BASE_URL"] iss = config["BASE_URL"] - issuers = [iss] - oidc_iss = ( - config.get("OPENID_CONNECT", {}).get("fence", {}).get("api_base_url", None) - ) - if oidc_iss: - issuers.append(oidc_iss) + if issuers is None: + issuers = [iss] + oidc_iss = ( + config.get("OPENID_CONNECT", {}).get("fence", {}).get("api_base_url", None) + ) + if oidc_iss: + issuers.append(oidc_iss) try: token_iss = jwt.decode(encoded_token, verify=False).get("iss") except jwt.InvalidTokenError as e: @@ -173,12 +176,12 @@ def validate_jwt( raise JWTError(msg) if purpose: validate_purpose(claims, purpose) - if "pur" not in claims: + if require_purpose and "pur" not in claims: raise JWTError("token {} missing purpose (`pur`) claim".format(claims["jti"])) # For refresh tokens and API keys specifically, check that they are not # blacklisted. - if claims["pur"] == "refresh" or claims["pur"] == "api_key": + if require_purpose and (claims["pur"] == "refresh" or claims["pur"] == "api_key"): if is_blacklisted(claims["jti"]): raise JWTError("token is blacklisted") diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index a288b35db..3dab3cc1d 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -1,4 +1,5 @@ import flask +import os import collections import time import datetime @@ -7,9 +8,6 @@ # TODO comment regarding circular imports import fence.scripting.fence_create -# TODO take this out -import jwt - from flask_sqlalchemy_session import current_session from cdislogging import get_logger @@ -44,28 +42,16 @@ def get_gen3_users_from_ga4gh_passports(passports): min_visa_expiration = int(time.time()) + datetime.timedelta(hours=1).seconds for raw_visa in raw_visas: try: - # TODO must be signed with RSA256 - # TODO issuers could be more than just what's below. will need to use config var of some sort - # TODO why is subject_id a big long str? - # TODO conditions field - # issuers = ["https://stsstg.nih.gov"] - # decoded_visa = validate_jwt(raw_visa, attempt_refresh=True, issuers=issuers, options={"verify_aud": False}) - # TODO: ONLY USE THIS FOR DEVELOPMENT - decoded_visa = jwt.decode(raw_visa, verify=False) + validated_decoded_visa = validate_visa(raw_visa) identity_to_visas[ - (decoded_visa.get("iss"), decoded_visa.get("sub")) - ].append((raw_visa, decoded_visa)) - min_visa_expiration = min(min_visa_expiration, decoded_visa.get("exp")) - # min_visa_expiration = decoded_visa. - - # below function also validates visa (or raises exception) and - # extracts the subject id - # subject_id, issuer = get_sub_iss_from_visa(raw_visa) - - # query idp user table - # gen3_user = get_or_create_gen3_user_from_sub_iss(decoded_visa.get("sub"), decoded_visa.get("iss")) - # user_ids_from_passports.append(gen3_user.id) - + ( + validated_decoded_visa.get("iss"), + validated_decoded_visa.get("sub"), + ) + ].append((raw_visa, validated_decoded_visa)) + min_visa_expiration = min( + min_visa_expiration, validated_decoded_visa.get("exp") + ) except Exception as exc: logger.warning(f"invalid visa provided, ignoring. Error: {exc}") continue @@ -73,23 +59,19 @@ def get_gen3_users_from_ga4gh_passports(passports): usernames_from_current_passport = [] for (issuer, subject_id), visas in identity_to_visas.items(): gen3_user = get_or_create_gen3_user_from_iss_sub(issuer, subject_id) - # NOTE: does not validate, assumes validation occurs above. - # sync_visa_authorization(raw_visa) - # QUESTION: do all visas in a passport necessarily belong - # to the same Arborist defined user? relevant because you can only - # update policies in Arborist one user at a time ga4gh_visas = [ GA4GHVisaV1( user=gen3_user, - source=decoded_visa["ga4gh_visa_v1"]["source"], - type=decoded_visa["ga4gh_visa_v1"]["type"], - asserted=int(decoded_visa["ga4gh_visa_v1"]["asserted"]), - expires=int(decoded_visa["exp"]), + source=validated_decoded_visa["ga4gh_visa_v1"]["source"], + type=validated_decoded_visa["ga4gh_visa_v1"]["type"], + asserted=int(validated_decoded_visa["ga4gh_visa_v1"]["asserted"]), + expires=int(validated_decoded_visa["exp"]), ga4gh_visa=raw_visa, ) - for raw_visa, decoded_visa in visas + for raw_visa, validated_decoded_visa in visas ] + # NOTE: does not validate, assumes validation occurs above. sync_visa_authorization(gen3_user, ga4gh_visas, min_visa_expiration) usernames_from_current_passport.append(gen3_user.username) @@ -107,40 +89,56 @@ def get_gen3_usernames_for_passport_from_cache(passport): def get_unvalidated_visas_from_valid_passport(passport): - # validate passport, return visas - # TODO put inside try block (i.e. shouldn't get a 500 for an expired passport) - # TODO if aud is provided, it must contain client id - # TODO dont hardcode issuers. it needed to be hardcoded because I think - # TODO init issuers list within function call - # list of allowed issuers comes from a config variable - - # issuers = ["https://stsstg.nih.gov"] - # decoded_passport = validate_jwt(passport, attempt_refresh=True, issuers=issuers, options={"verify_aud": False}) - # TODO: ONLY USE THIS FOR DEVELOPMENT - decoded_passport = jwt.decode(passport, verify=False) - - return decoded_passport.get("ga4gh_passport_v1", []) - - -def is_raw_visa_valid(raw_visa): - # check signature - # is a type we recognize? - return False - - -def get_sub_iss_from_visa(raw_visa): - if not is_raw_visa_valid(raw_visa): - raise Exception() - - subject_id = None - issuer = None + return [] + + +def validate_visa(raw_visa): + # TODO check that there is no JKU field in header? + decoded_visa = validate_jwt( + raw_visa, + attempt_refresh=True, + scope={"openid"}, + require_purpose=False, + issuers=config.get("GA4GH_VISA_ISSUER_ALLOWLIST", []), + options={"require_iat": True, "require_exp": True, "verify_aud": False}, + ) + for claim in ["sub", "ga4gh_visa_v1"]: + if claim not in decoded_visa: + raise Exception(f'Visa does not contain REQUIRED "{claim}" claim') + + if "aud" in decoded_visa: + raise Exception('Visa MUST NOT contain "aud" claim') + + # TODO may want to set these fields and values in config-default.yaml + field_to_expected_value = { + "type": "https://ras.nih.gov/visas/v1.1", + "asserted": None, + "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", + "source": "https://ncbi.nlm.nih.gov/gap", + } + for field, expected_value in field_to_expected_value.items(): + if field not in decoded_visa["ga4gh_visa_v1"]: + raise Exception( + f'"ga4gh_visa_v1" claim does not contain REQUIRED "{field}" field' + ) + if expected_value: + if decoded_visa["ga4gh_visa_v1"][field] != expected_value: + raise Exception( + f'"{field}" field in "ga4gh_visa_v1" does not equal expected value "{expected_value}"' + ) - # TODO + if "conditions" in decoded_visa["ga4gh_visa_v1"]: + logger.warning( + 'Condition checking is not yet supported, but a visa was received that contained the "conditions" field' + ) + if decoded_visa["ga4gh_visa_v1"]["conditions"]: + raise Exception('"conditions" field in "ga4gh_visa_v1" is not empty') - return subject_id, issuer + return decoded_visa def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): + # TODO update mapping table # for idp_name, idp_config in config.get("OPENID_CONNECT", {}).items(): # there are issues with syncing when "https://" is part of the username. @@ -159,15 +157,22 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): def sync_visa_authorization(gen3_user, ga4gh_visas, expiration): - # TODO might need to look in more places for db_url - db_url = config.get("DB") + # TODO set expiration for Google Access arborist_client = gen3authz.client.arborist.client.ArboristClient( arborist_base_url=config.get("ARBORIST"), logger=logger, authz_provider="GA4GH" ) - # TODO check this - dbgap_config = config.get("dbGaP") + + dbgap_config = os.environ.get("dbGaP") or config.get("dbGaP") + if not isinstance(dbgap_config, list): + dbgap_config = [dbgap_config] + DB = os.environ.get("FENCE_DB") or config.get("DB") + if DB is None: + try: + from fence.settings import DB + except ImportError: + pass syncer = fence.scripting.fence_create.init_syncer( - dbgap_config, None, db_url, arborist=arborist_client + dbgap_config, None, DB, arborist=arborist_client ) syncer.sync_single_user_visas( From 6c44c01e4a1e819d820e135c062c698bc785f491 Mon Sep 17 00:00:00 2001 From: BinamB Date: Wed, 20 Oct 2021 14:37:27 -0500 Subject: [PATCH 049/211] test test --- authutils-6.0.3.tar.gz | Bin 18441 -> 0 bytes authutils-6.0.4.tar.gz | Bin 0 -> 18432 bytes poetry.lock | 8 ++++---- pyproject.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 authutils-6.0.3.tar.gz create mode 100644 authutils-6.0.4.tar.gz diff --git a/authutils-6.0.3.tar.gz b/authutils-6.0.3.tar.gz deleted file mode 100644 index c447314cd7a8cff37da289cbd9689e866d6b0c45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18441 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 literal 0 HcmV?d00001 diff --git a/poetry.lock b/poetry.lock index e9a6a5298..65c5fa4fe 100644 --- a/poetry.lock +++ b/poetry.lock @@ -61,7 +61,7 @@ requests = "*" [[package]] name = "authutils" -version = "6.0.3" +version = "6.0.4" description = "Gen3 auth utility functions" category = "main" optional = false @@ -81,7 +81,7 @@ fastapi = ["fastapi (>=0.54.1,<0.55.0)"] [package.source] type = "file" -url = "authutils-6.0.3.tar.gz" +url = "authutils-6.0.4.tar.gz" [[package]] name = "aws-xray-sdk" @@ -1595,7 +1595,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "11feb12da2ded2662dbb9caaddc069a74ff2d6fd7551ba184b7882f69c1722fb" +content-hash = "4e8521a47aa2a7cafac36952fa4416c4a01ae007f371f47aab661a8086dc4bae" [metadata.files] addict = [ @@ -1623,7 +1623,7 @@ authlib = [ {file = "Authlib-0.11.tar.gz", hash = "sha256:9741db6de2950a0a5cefbdb72ec7ab12f7e9fd530ff47219f1530e79183cbaaf"}, ] authutils = [ - {file = "authutils-6.0.3.tar.gz", hash = "sha256:2716eadd6eb038e4f4ae1efa5c37df31f76726c5057ab49db4da81acc29b4b5e"}, + {file = "authutils-6.0.4.tar.gz", hash = "sha256:2bfed40ea514d61f98d71c2fe73681ee2674d7bb7bad8395613a26f2b98e6165"}, ] aws-xray-sdk = [ {file = "aws-xray-sdk-0.95.tar.gz", hash = "sha256:9e7ba8dd08fd2939376c21423376206bff01d0deaea7d7721c6b35921fed1943"}, diff --git a/pyproject.toml b/pyproject.toml index 34fe938c1..7b95ef913 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ include = [ [tool.poetry.dependencies] python = "^3.6" authlib = "^0.11" -authutils = { path= "./authutils-6.0.3.tar.gz" } +authutils = { path= "./authutils-6.0.4.tar.gz" } bcrypt = "^3.1.4" boto3 = "~1.9.91" botocore = "^1.12.253" From a5de7655b37f194dce8a76b0cf108e448279a615 Mon Sep 17 00:00:00 2001 From: BinamB Date: Thu, 21 Oct 2021 16:22:30 -0500 Subject: [PATCH 050/211] use fence validate jwt --- authutils-6.0.4.tar.gz | Bin 18432 -> 0 bytes authutils-6.1.0.tar.gz | Bin 0 -> 18527 bytes fence/jwt/validate.py | 19 +++++---- fence/resources/ga4gh/passports.py | 65 ++++++++++++++++------------- poetry.lock | 8 ++-- pyproject.toml | 2 +- 6 files changed, 51 insertions(+), 43 deletions(-) delete mode 100644 authutils-6.0.4.tar.gz create mode 100644 authutils-6.1.0.tar.gz diff --git a/authutils-6.0.4.tar.gz b/authutils-6.0.4.tar.gz deleted file mode 100644 index 91c0276a71da7c0ec29513b56513bc8f4b09374e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18432 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 diff --git a/authutils-6.1.0.tar.gz b/authutils-6.1.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..3732c99e1d33edc4714006daae61af7947c8eddf GIT binary patch literal 18527 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 |h*TMr!pKuZl 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 eET84Ge3sAhSw72W`7EDDJ^vpUDR=z=0.54.1,<0.55.0)"] [package.source] type = "file" -url = "authutils-6.0.4.tar.gz" +url = "authutils-6.1.0.tar.gz" [[package]] name = "aws-xray-sdk" @@ -1595,7 +1595,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "4e8521a47aa2a7cafac36952fa4416c4a01ae007f371f47aab661a8086dc4bae" +content-hash = "79f17286d35ee683ccde196f96d10b7fa081dcb061631d9090912a70e2dd150e" [metadata.files] addict = [ @@ -1623,7 +1623,7 @@ authlib = [ {file = "Authlib-0.11.tar.gz", hash = "sha256:9741db6de2950a0a5cefbdb72ec7ab12f7e9fd530ff47219f1530e79183cbaaf"}, ] authutils = [ - {file = "authutils-6.0.4.tar.gz", hash = "sha256:2bfed40ea514d61f98d71c2fe73681ee2674d7bb7bad8395613a26f2b98e6165"}, + {file = "authutils-6.1.0.tar.gz", hash = "sha256:d1a86853d679a5f0d9f4a2c88d25427887158926cb0c5cac87f15b0552fdef30"}, ] aws-xray-sdk = [ {file = "aws-xray-sdk-0.95.tar.gz", hash = "sha256:9e7ba8dd08fd2939376c21423376206bff01d0deaea7d7721c6b35921fed1943"}, diff --git a/pyproject.toml b/pyproject.toml index 7b95ef913..d23158f40 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ include = [ [tool.poetry.dependencies] python = "^3.6" authlib = "^0.11" -authutils = { path= "./authutils-6.0.4.tar.gz" } +authutils = { path= "./authutils-6.1.0.tar.gz" } bcrypt = "^3.1.4" boto3 = "~1.9.91" botocore = "^1.12.253" From a2cf3d3944da403849fbc0c31529ca35e96e7f5a Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 22 Oct 2021 12:54:30 -0500 Subject: [PATCH 051/211] use fence jwt --- fence/blueprints/login/ras.py | 4 +- fence/jwt/validate.py | 4 +- fence/resources/ga4gh/passports.py | 114 ++------------------------- fence/resources/openid/ras_oauth2.py | 39 ++++----- 4 files changed, 28 insertions(+), 133 deletions(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index c6f5e00a2..b30d56b50 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -3,7 +3,7 @@ import os from distutils.util import strtobool from authutils.errors import JWTError -from authutils.token.core import validate_jwt +from fence.jwt.validate import validate_jwt from authutils.token.keys import get_public_key_for_token from cdislogging import get_logger from flask_sqlalchemy_session import current_session @@ -88,6 +88,7 @@ def post_login(self, user=None, token_result=None): # Embedded token must contain scope claim, which must include openid scope={"openid"}, issuers=config.get("GA4GH_VISA_ISSUER_ALLOWLIST", []), + require_purpose=False, # Embedded token must contain iss, sub, iat, exp claims # options={"require": ["iss", "sub", "iat", "exp"]}, # ^ FIXME 2021-05-13: Above needs pyjwt>=v2.0.0, which requires cryptography>=3. @@ -98,6 +99,7 @@ def post_login(self, user=None, token_result=None): options={ "require_iat": True, "require_exp": True, + "verify_aud": False, }, ) diff --git a/fence/jwt/validate.py b/fence/jwt/validate.py index 128d318c8..71375aede 100644 --- a/fence/jwt/validate.py +++ b/fence/jwt/validate.py @@ -46,6 +46,7 @@ def validate_jwt( public_key=None, attempt_refresh=False, issuers=None, + pkey_cache=None, **kwargs ): """ @@ -109,8 +110,9 @@ def validate_jwt( raise JWTError(e) attempt_refresh = attempt_refresh and (token_iss != iss) public_key = public_key or authutils.token.keys.get_public_key_for_token( - encoded_token, attempt_refresh=attempt_refresh + encoded_token, attempt_refresh=attempt_refresh, pkey_cache=pkey_cache ) + try: claims = authutils.token.validate.validate_jwt( encoded_token=encoded_token, diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index cef451f65..be0a921d1 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -3,6 +3,7 @@ import httpx from authutils.errors import JWTError + # from authutils.token.core import get_iss, get_keys_url, get_kid, validate_jwt from authutils.token.core import get_iss, get_keys_url, get_kid from fence.jwt.validate import validate_jwt @@ -100,32 +101,7 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): return [] public_key = pkey_cache.get(passport_issuer, {}).get(passport_kid) - # if not public_key: - # try: - # logger.info("Fetching public key from flask app...") - # public_key = get_public_key_for_token( - # passport, attempt_refresh=True, pkey_cache=pkey_cache - # ) - # except Exception as e: - # logger.info( - # "Could not fetch public key from flask app to validate passport: {}. Trying to fetch from source.".format( - # e - # ) - # ) - # # try: - # # logger.info("Trying to Fetch public keys from JWKs url...") - # # public_key = refresh_pkey_cache( - # # passport_issuer, passport_kid, pkey_cache - # # ) - # # except Exception as e: - # # logger.info( - # # "Could not fetch public key from JWKs key url: {}".format(e) - # # ) - # if not public_key: - # logger.error( - # "Could not fetch public key to validate visa: Successfully fetched " - # "issuer's keys but did not find the visa's key id among them. Discarding visa." - # ) + try: decoded_passport = validate_jwt( encoded_token=passport, @@ -141,11 +117,16 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): "verify_aud": False, }, ) + + if "sub" not in decoded_passport: + raise JWTError("Visa is missing the 'sub' claim.") + if "aud" in decoded_passport: + raise JWTError("Visa contains 'aud' calim") except Exception as e: logger.error("Passport failed validation: {}. Discarding passport.".format(e)) # ignore malformed/invalid passports return [] - # TODO: need to check that aud is not in passport + return decoded_passport.get("ga4gh_passport_v1", []) @@ -186,82 +167,3 @@ def sync_visa_authorization(raw_visa): def put_gen3_user_ids_for_passport_into_cache(passport, user_ids_from_passports): pass - - -def refresh_pkey_cache(issuer, kid, pkey_cache): - """ - Update app public key cache for a specific Passport Visa issuer - - Args: - issuer(str): Passport Visa issuer. Can be found under `issuer` in a Passport or a Visa - kid(str): Passsport Visa kid. Can be found in the header of an encoded Passport or encoded Visa - pkey_cache (dict): app cache of public keys_dir - - Return: - dict: public key for given issuer - """ - jwks_url = get_keys_url(issuer) - try: - jwt_public_keys = httpx.get(jwks_url).json()["keys"] - except Exception as e: - raise JWTError( - "Could not get public key to validate Passport/Visa: Could not fetch keys from JWKs url: {}".format( - e - ) - ) - - issuer_public_keys = {} - try: - for key in jwt_public_keys: - if "kty" in key and key["kty"] == "RSA": - logger.debug( - "Serializing RSA public key (kid: {}) to PEM format.".format( - key["kid"] - ) - ) - # Decode public numbers https://tools.ietf.org/html/rfc7518#section-6.3.1 - n_padded_bytes = base64.urlsafe_b64decode( - key["n"] + "=" * (4 - len(key["n"]) % 4) - ) - e_padded_bytes = base64.urlsafe_b64decode( - key["e"] + "=" * (4 - len(key["e"]) % 4) - ) - n = int.from_bytes(n_padded_bytes, "big", signed=False) - e = int.from_bytes(e_padded_bytes, "big", signed=False) - # Serialize and encode public key--PyJWT decode/validation requires PEM - rsa_public_key = rsa.RSAPublicNumbers(e, n).public_key( - default_backend() - ) - public_bytes = rsa_public_key.public_bytes( - serialization.Encoding.PEM, - serialization.PublicFormat.SubjectPublicKeyInfo, - ) - # Cache the encoded key by issuer - issuer_public_keys[key["kid"]] = public_bytes - else: - logger.debug( - "Key type (kty) is not 'RSA'; assuming PEM format. " - "Skipping key serialization. (kid: {})".format(key[0]) - ) - issuer_public_keys[key[0]] = key[1] - - pkey_cache.update({issuer: issuer_public_keys}) - logger.info( - "Refreshed cronjob pkey cache for Passport/Visa issuer {}".format(issuer) - ) - except Exception as e: - logger.error( - "Could not refresh cronjob pkey cache for issuer {}: " - "Something went wrong during serialization: {}. Discarding Passport/Visa.".format( - issuer, e - ) - ) - - # This function is shared by both fence-service (running inside of application context) - # and access token polling (running outside of application context) - # Flask doesn't really like running outside of context and setting this cache breaks things - # when running the access token polling. - if flask.has_app_context(): - flask.current_app.pkey_cache = pkey_cache - - return pkey_cache.get(issuer, {}).get(kid) diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 0c88aafcd..e57d9d317 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -5,14 +5,13 @@ from jose import jwt as jose_jwt from authutils.errors import JWTError -from authutils.token.core import get_iss, get_keys_url, get_kid, validate_jwt +from authutils.token.core import get_iss, get_kid + from fence.config import config +from fence.jwt.validate import validate_jwt from fence.models import GA4GHVisaV1 -from fence.resources.ga4gh.passports import ( - get_unvalidated_visas_from_valid_passport, - refresh_pkey_cache, -) +from fence.resources.ga4gh.passports import get_unvalidated_visas_from_valid_passport from fence.utils import DEFAULT_BACKOFF_SETTINGS from .idp_oauth2 import Oauth2ClientBase @@ -168,7 +167,6 @@ def update_user_visas(self, user, pkey_cache, db_session=current_session): token = self.get_access_token(user, token_endpoint, db_session) userinfo = self.get_userinfo(token) encoded_visas = self.get_encoded_visas_v11_userinfo(userinfo, pkey_cache) - except Exception as e: err_msg = "Could not retrieve visas" self.logger.exception("{}: {}".format(err_msg, e)) @@ -188,31 +186,17 @@ def update_user_visas(self, user, pkey_cache, db_session=current_session): # See if pkey is in cronjob cache; if not, update cache. public_key = pkey_cache.get(visa_issuer, {}).get(visa_kid) - if not public_key: - try: - public_key = refresh_pkey_cache(visa_issuer, visa_kid, pkey_cache) - except Exception as e: - self.logger.error( - "Could not refresh public key cache: {}".format(e) - ) - continue - if not public_key: - self.logger.error( - "Could not get public key to validate visa: Successfully fetched " - "issuer's keys but did not find the visa's key id among them. Discarding visa." - ) - continue # Not raise: If issuer not publishing pkey, does not make sense to retry - + i = 1 try: # Validate the visa per GA4GH AAI "Embedded access token" format rules. # pyjwt also validates signature and expiration. decoded_visa = validate_jwt( - encoded_visa, - public_key, - # Embedded token must not contain aud claim - aud=None, + encoded_token=encoded_visa, + public_key=public_key, + attempt_refresh=True, # Embedded token must contain scope claim, which must include openid scope={"openid"}, + require_purpose=False, issuers=config.get("GA4GH_VISA_ISSUER_ALLOWLIST", []), # Embedded token must contain iss, sub, iat, exp claims # options={"require": ["iss", "sub", "iat", "exp"]}, @@ -221,15 +205,20 @@ def update_user_visas(self, user, pkey_cache, db_session=current_session): # For now, pyjwt 1.7.1 is able to require iat and exp; # authutils' validate_jwt (i.e. the function being called) checks issuers already (see above); # and we will check separately for sub below. + pkey_cache=pkey_cache, options={ "require_iat": True, "require_exp": True, + "verify_aud": False, }, ) # Also require 'sub' claim (see note above about pyjwt and the options arg). if "sub" not in decoded_visa: raise JWTError("Visa is missing the 'sub' claim.") + # Embedded token must not contain aud claim + if "aud" in decoded_visa: + raise JWTError("Visa contains 'aud' calim") except Exception as e: self.logger.error( "Visa failed validation: {}. Discarding visa.".format(e) From efc4ce605b129b659456a1ddd5de21b45c1fe745 Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 22 Oct 2021 13:11:00 -0500 Subject: [PATCH 052/211] fix aud stuff --- fence/blueprints/login/ras.py | 5 +++-- fence/resources/ga4gh/passports.py | 1 - fence/resources/openid/ras_oauth2.py | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index b30d56b50..ecb561256 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -83,8 +83,6 @@ def post_login(self, user=None, token_result=None): decoded_visa = validate_jwt( encoded_visa, public_key, - # Embedded token must not contain aud claim - aud=None, # Embedded token must contain scope claim, which must include openid scope={"openid"}, issuers=config.get("GA4GH_VISA_ISSUER_ALLOWLIST", []), @@ -106,6 +104,9 @@ def post_login(self, user=None, token_result=None): # Also require 'sub' claim (see note above about pyjwt and the options arg). if "sub" not in decoded_visa: raise JWTError("Visa is missing the 'sub' claim.") + # Embedded token must not contain aud claim + if "aud" not in decoded_visa: + raise JWTError("Visa is contains the 'aud' claim.") if not user.idp_to_users: # map user to idp self.map_user_idp_info( diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index be0a921d1..bdfb40bd5 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -108,7 +108,6 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): public_key=public_key, attempt_refresh=True, require_purpose=False, - aud=None, scope={"openid"}, issuers=config.get("GA4GH_VISA_ISSUER_ALLOWLIST", []), options={ diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index e57d9d317..0965a6298 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -186,7 +186,6 @@ def update_user_visas(self, user, pkey_cache, db_session=current_session): # See if pkey is in cronjob cache; if not, update cache. public_key = pkey_cache.get(visa_issuer, {}).get(visa_kid) - i = 1 try: # Validate the visa per GA4GH AAI "Embedded access token" format rules. # pyjwt also validates signature and expiration. From e40f763461f9225f758112b9aec26d663bfada6d Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 22 Oct 2021 13:38:48 -0500 Subject: [PATCH 053/211] fix jwt --- fence/blueprints/login/ras.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index ecb561256..2d2379f5e 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -3,7 +3,6 @@ import os from distutils.util import strtobool from authutils.errors import JWTError -from fence.jwt.validate import validate_jwt from authutils.token.keys import get_public_key_for_token from cdislogging import get_logger from flask_sqlalchemy_session import current_session @@ -14,6 +13,7 @@ from fence.blueprints.login.base import DefaultOAuth2Login, DefaultOAuth2Callback from fence.config import config +from fence.jwt.validate import validate_jwt from fence.scripting.fence_create import init_syncer from fence.utils import get_valid_expiration @@ -81,8 +81,8 @@ def post_login(self, user=None, token_result=None): # Validate the visa per GA4GH AAI "Embedded access token" format rules. # pyjwt also validates signature and expiration. decoded_visa = validate_jwt( - encoded_visa, - public_key, + encoded_token=encoded_visa, + public_key=public_key, # Embedded token must contain scope claim, which must include openid scope={"openid"}, issuers=config.get("GA4GH_VISA_ISSUER_ALLOWLIST", []), From cc9ce7995ee0709db82aac70f3d7315aef0eb999 Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 22 Oct 2021 13:49:58 -0500 Subject: [PATCH 054/211] fix aud check --- fence/blueprints/login/ras.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index 2d2379f5e..21816946b 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -105,8 +105,8 @@ def post_login(self, user=None, token_result=None): if "sub" not in decoded_visa: raise JWTError("Visa is missing the 'sub' claim.") # Embedded token must not contain aud claim - if "aud" not in decoded_visa: - raise JWTError("Visa is contains the 'aud' claim.") + if "aud" in decoded_visa: + raise JWTError("Visa contains the 'aud' claim.") if not user.idp_to_users: # map user to idp self.map_user_idp_info( From 546cf2b7688d3d9380861086d391ce350e4aa839 Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 22 Oct 2021 14:08:07 -0500 Subject: [PATCH 055/211] remove aud check for passports --- fence/resources/ga4gh/passports.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index bdfb40bd5..2b027c4d6 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -119,8 +119,6 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): if "sub" not in decoded_passport: raise JWTError("Visa is missing the 'sub' claim.") - if "aud" in decoded_passport: - raise JWTError("Visa contains 'aud' calim") except Exception as e: logger.error("Passport failed validation: {}. Discarding passport.".format(e)) # ignore malformed/invalid passports From 1b044348be2f93f1236b26e1153e8301a0119d32 Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 22 Oct 2021 14:15:42 -0500 Subject: [PATCH 056/211] Remove unused imports --- fence/job/visa_update_cronjob.py | 8 +------- fence/jwt/validate.py | 1 - fence/resources/ga4gh/passports.py | 15 +++------------ 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/fence/job/visa_update_cronjob.py b/fence/job/visa_update_cronjob.py index cff3bc58f..969f89f5f 100644 --- a/fence/job/visa_update_cronjob.py +++ b/fence/job/visa_update_cronjob.py @@ -3,15 +3,9 @@ import time from cdislogging import get_logger -from userdatamodel.driver import SQLAlchemyDriver from fence.config import config -from fence.models import ( - GA4GHVisaV1, - User, - UpstreamRefreshToken, - query_for_user, -) +from fence.models import User from fence.resources.openid.ras_oauth2 import RASOauth2Client as RASClient diff --git a/fence/jwt/validate.py b/fence/jwt/validate.py index 71375aede..5c6f336c3 100644 --- a/fence/jwt/validate.py +++ b/fence/jwt/validate.py @@ -1,7 +1,6 @@ import authutils.errors import authutils.token.keys import authutils.token.validate -import flask import jwt from fence.config import config diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 2b027c4d6..2f1f9ad9e 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -1,19 +1,10 @@ -import base64 -import flask -import httpx - from authutils.errors import JWTError - -# from authutils.token.core import get_iss, get_keys_url, get_kid, validate_jwt -from authutils.token.core import get_iss, get_keys_url, get_kid -from fence.jwt.validate import validate_jwt -from authutils.token.keys import get_public_key_for_token +from authutils.token.core import get_iss, get_kid from cdislogging import get_logger -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization from fence.config import config +from fence.jwt.validate import validate_jwt + logger = get_logger(__name__) From 52fd15a0ba13f9283d8cdd743801be1628fff61a Mon Sep 17 00:00:00 2001 From: BinamB Date: Tue, 26 Oct 2021 09:30:27 -0500 Subject: [PATCH 057/211] remove get public token --- fence/blueprints/login/ras.py | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index 21816946b..c35e489e9 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -2,18 +2,17 @@ import jwt import os from distutils.util import strtobool +from urllib.parse import urlparse, parse_qs + from authutils.errors import JWTError -from authutils.token.keys import get_public_key_for_token from cdislogging import get_logger from flask_sqlalchemy_session import current_session -from urllib.parse import urlparse, parse_qs - -from fence.models import GA4GHVisaV1, IdentityProvider from gen3authz.client.arborist.client import ArboristClient from fence.blueprints.login.base import DefaultOAuth2Login, DefaultOAuth2Callback from fence.config import config from fence.jwt.validate import validate_jwt +from fence.models import GA4GHVisaV1, IdentityProvider from fence.scripting.fence_create import init_syncer from fence.utils import get_valid_expiration @@ -63,26 +62,11 @@ def post_login(self, user=None, token_result=None): raise for encoded_visa in encoded_visas: - try: - # Do not move out of loop unless we can assume every visa has same issuer and kid - public_key = get_public_key_for_token( - encoded_visa, attempt_refresh=True - ) - except Exception as e: - # (But don't log the visa contents!) - logger.error( - "Could not get public key to validate visa: {}. Discarding visa.".format( - e - ) - ) - continue - try: # Validate the visa per GA4GH AAI "Embedded access token" format rules. # pyjwt also validates signature and expiration. decoded_visa = validate_jwt( encoded_token=encoded_visa, - public_key=public_key, # Embedded token must contain scope claim, which must include openid scope={"openid"}, issuers=config.get("GA4GH_VISA_ISSUER_ALLOWLIST", []), From 157ae81f0232e776005d84b580ffff8c9a792a9a Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 26 Oct 2021 09:47:25 -0500 Subject: [PATCH 058/211] feat(expiration): default db entries to NOT expired if no expiration provided --- fence/scripting/fence_create.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fence/scripting/fence_create.py b/fence/scripting/fence_create.py index ed01ff4a3..76829678f 100644 --- a/fence/scripting/fence_create.py +++ b/fence/scripting/fence_create.py @@ -5,7 +5,6 @@ import json import pprint import asyncio - from cirrus import GoogleCloudManager from cirrus.google_cloud.errors import GoogleAuthError from cirrus.config import config as cirrus_config @@ -24,6 +23,7 @@ User, ProjectToBucket, ) +from sqlalchemy import and_ from fence.blueprints.link import ( force_update_user_google_account_expiration, @@ -685,12 +685,14 @@ def delete_expired_google_access(DB): with driver.session as session: current_time = int(time.time()) - # Get expires field from db, if None, default to NOT expired + # Get expires field from db, if None default to NOT expired records_to_delete = ( session.query(GoogleProxyGroupToGoogleBucketAccessGroup) .filter( - (GoogleProxyGroupToGoogleBucketAccessGroup.expires or current_time + 1) - < current_time + and_( + GoogleProxyGroupToGoogleBucketAccessGroup.expires.isnot(None), + GoogleProxyGroupToGoogleBucketAccessGroup.expires < current_time, + ) ) .all() ) From 07c2ffc2bc86440ac17c6ca2b07e3f982bbe3e9e Mon Sep 17 00:00:00 2001 From: BinamB Date: Wed, 27 Oct 2021 16:09:55 -0500 Subject: [PATCH 059/211] test authutils --- authutils-6.1.0.tar.gz | Bin 18527 -> 0 bytes authutils-6.1.2.tar.gz | Bin 0 -> 18561 bytes poetry.lock | 26 +++++++++++++------------- pyproject.toml | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) delete mode 100644 authutils-6.1.0.tar.gz create mode 100644 authutils-6.1.2.tar.gz diff --git a/authutils-6.1.0.tar.gz b/authutils-6.1.0.tar.gz deleted file mode 100644 index 3732c99e1d33edc4714006daae61af7947c8eddf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18527 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 |h*TMr!pKuZl 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 M14>U#@Bokj0HK1O^8f$< literal 0 HcmV?d00001 diff --git a/poetry.lock b/poetry.lock index bfbc30903..8c7321f55 100644 --- a/poetry.lock +++ b/poetry.lock @@ -61,7 +61,7 @@ requests = "*" [[package]] name = "authutils" -version = "6.1.0" +version = "6.1.2" description = "Gen3 auth utility functions" category = "main" optional = false @@ -81,7 +81,7 @@ fastapi = ["fastapi (>=0.54.1,<0.55.0)"] [package.source] type = "file" -url = "authutils-6.1.0.tar.gz" +url = "authutils-6.1.2.tar.gz" [[package]] name = "aws-xray-sdk" @@ -719,7 +719,7 @@ testing = ["pytest"] [[package]] name = "google-resumable-media" -version = "2.0.3" +version = "2.1.0" description = "Utilities for Google Media Downloads and Resumable Uploads" category = "main" optional = false @@ -1093,7 +1093,7 @@ twisted = ["twisted"] [[package]] name = "prometheus-flask-exporter" -version = "0.18.3" +version = "0.18.4" description = "Prometheus metrics exporter for Flask" category = "main" optional = false @@ -1355,7 +1355,7 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] [[package]] name = "responses" -version = "0.14.0" +version = "0.15.0" description = "A utility library for mocking out the `requests` Python library." category = "dev" optional = false @@ -1595,7 +1595,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "79f17286d35ee683ccde196f96d10b7fa081dcb061631d9090912a70e2dd150e" +content-hash = "63953e7bdfc0749415d59f4eddbd8501ab793ac967bb8b83854addd6f5af5ece" [metadata.files] addict = [ @@ -1623,7 +1623,7 @@ authlib = [ {file = "Authlib-0.11.tar.gz", hash = "sha256:9741db6de2950a0a5cefbdb72ec7ab12f7e9fd530ff47219f1530e79183cbaaf"}, ] authutils = [ - {file = "authutils-6.1.0.tar.gz", hash = "sha256:d1a86853d679a5f0d9f4a2c88d25427887158926cb0c5cac87f15b0552fdef30"}, + {file = "authutils-6.1.2.tar.gz", hash = "sha256:17e69064586e793958c4aecc5999136f45bee3587b279f5527d0b5dd3f1da0af"}, ] aws-xray-sdk = [ {file = "aws-xray-sdk-0.95.tar.gz", hash = "sha256:9e7ba8dd08fd2939376c21423376206bff01d0deaea7d7721c6b35921fed1943"}, @@ -1974,8 +1974,8 @@ google-crc32c = [ {file = "google_crc32c-1.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:7f6fe42536d9dcd3e2ffb9d3053f5d05221ae3bbcefbe472bdf2c71c793e3183"}, ] google-resumable-media = [ - {file = "google-resumable-media-2.0.3.tar.gz", hash = "sha256:b4b4709d04a6a03cbec746c2b5cb18f1f9878bf1ef3cd61908842a3d94c20471"}, - {file = "google_resumable_media-2.0.3-py2.py3-none-any.whl", hash = "sha256:efe23e22bc9838630f0fd185e21de503c731a726e66da90c1572653d8480e8e4"}, + {file = "google-resumable-media-2.1.0.tar.gz", hash = "sha256:725b989e0dd387ef2703d1cc8e86217474217f4549593c477fd94f4024a0f911"}, + {file = "google_resumable_media-2.1.0-py2.py3-none-any.whl", hash = "sha256:cdc75ea0361e39704dc7df7da59fbd419e73c8bc92eac94d8a020d36baa9944b"}, ] googleapis-common-protos = [ {file = "googleapis-common-protos-1.53.0.tar.gz", hash = "sha256:a88ee8903aa0a81f6c3cec2d5cf62d3c8aa67c06439b0496b49048fb1854ebf4"}, @@ -2141,8 +2141,8 @@ prometheus-client = [ {file = "prometheus_client-0.9.0.tar.gz", hash = "sha256:9da7b32f02439d8c04f7777021c304ed51d9ec180604700c1ba72a4d44dceb03"}, ] prometheus-flask-exporter = [ - {file = "prometheus_flask_exporter-0.18.3-py3-none-any.whl", hash = "sha256:abc8207c74096444580771fa5899df6dfaa5834229c2a62f12dd31817f5f373a"}, - {file = "prometheus_flask_exporter-0.18.3.tar.gz", hash = "sha256:96013537284154a83d90cedaf0bcd5011c6053d8f1a50e775dfebdb491eba7c7"}, + {file = "prometheus_flask_exporter-0.18.4-py3-none-any.whl", hash = "sha256:dc3587a9890c86b9a9f0a489fa7db5c782f9e7f4616a2dec25d80350131d8c05"}, + {file = "prometheus_flask_exporter-0.18.4.tar.gz", hash = "sha256:66d6eb2124f2d97bc983ae7573322dce9104291a3052b436e6a7fb43a25abdbd"}, ] protobuf = [ {file = "protobuf-3.17.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ab6bb0e270c6c58e7ff4345b3a803cc59dbee19ddf77a4719c5b635f1d547aa8"}, @@ -2351,8 +2351,8 @@ requests-oauthlib = [ {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, ] responses = [ - {file = "responses-0.14.0-py2.py3-none-any.whl", hash = "sha256:57bab4e9d4d65f31ea5caf9de62095032c4d81f591a8fac2f5858f7777b8567b"}, - {file = "responses-0.14.0.tar.gz", hash = "sha256:93f774a762ee0e27c0d9d7e06227aeda9ff9f5f69392f72bb6c6b73f8763563e"}, + {file = "responses-0.15.0-py2.py3-none-any.whl", hash = "sha256:5955ad3468fe8eb5fb736cdab4943457b7768f8670fa3624b4e26ff52dfe20c0"}, + {file = "responses-0.15.0.tar.gz", hash = "sha256:866757987d1962aa908d9c8b3185739faefd72a359e95459de0c2e4e5369c9b2"}, ] retry = [ {file = "retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606"}, diff --git a/pyproject.toml b/pyproject.toml index d23158f40..9b98c3652 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ include = [ [tool.poetry.dependencies] python = "^3.6" authlib = "^0.11" -authutils = { path= "./authutils-6.1.0.tar.gz" } +authutils = { path= "./authutils-6.1.2.tar.gz" } bcrypt = "^3.1.4" boto3 = "~1.9.91" botocore = "^1.12.253" From 3a4c36e38d89b2da8d7a1ca331e312ded36aa51a Mon Sep 17 00:00:00 2001 From: BinamB Date: Thu, 28 Oct 2021 10:05:27 -0500 Subject: [PATCH 060/211] resolve --- fence/blueprints/login/ras.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index c35e489e9..61de6ebd4 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -68,8 +68,8 @@ def post_login(self, user=None, token_result=None): decoded_visa = validate_jwt( encoded_token=encoded_visa, # Embedded token must contain scope claim, which must include openid - scope={"openid"}, - issuers=config.get("GA4GH_VISA_ISSUER_ALLOWLIST", []), + scope={"openid", "ga4gh_passport_v1"}, + issuers=config["GA4GH_VISA_ISSUER_ALLOWLIST"], require_purpose=False, # Embedded token must contain iss, sub, iat, exp claims # options={"require": ["iss", "sub", "iat", "exp"]}, From c1bb992b176e4382addb6386f98b1f8ab77c42b9 Mon Sep 17 00:00:00 2001 From: BinamB Date: Thu, 28 Oct 2021 14:55:59 -0500 Subject: [PATCH 061/211] update authutils --- authutils-6.1.2.tar.gz | Bin 18561 -> 0 bytes poetry.lock | 11 ++++------- pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) delete mode 100644 authutils-6.1.2.tar.gz diff --git a/authutils-6.1.2.tar.gz b/authutils-6.1.2.tar.gz deleted file mode 100644 index d3779dafb4f4276e925a5aed81938ca7ee87eedf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18561 zcmV)2K+L}%iwFn+00002|6z4>XmxaHY;!F(E-@}LE_7jX0PTJId)vmbXn*EkfdhZ{ znsg~xFH4D1^+uLtM<Q=mlPrp-~m9%tUuqs{mx^b07yx;8 zLHA#JkpCpl`nESIqKVjD+ge*&-Cp0=+78xN*H*W-T7PJsfBh$%71y&Oiu0ANU@cgG z{^n@^@c8WTi?9E!&CLbte|>9h<3(xxuWzru_=C6FtpDHq@$P%}rs3dPcyFSCNOI9y zQu1e!%nT*`_C|$h1uL&UtQf?Sn0YbraRA{-`(8>A$1d^+4ySu z^rtg#@A$yme|vmzbbj>q_{@9t_SAcKcG&e!4^K|t9=zMf*Ig=gaCCNldi3%gzM%%z z0`EYKq9iH+xIAb{XnZ*$-^;JVIQAwYOuPa{T8M0tdtoy425BRR9~L(|Hz+uM01|OGM@YNKi2<<{mEMG>iUD{mFLfY82OD;X$Y4EQE^5m;RU$)}YP;##wb+~GJ zC$cU~Uk0;;Ktu`u-Svvp8-y_FSWdRWp9lk)Hwlw)EO7F$-+VT>mVkA=yK6zihuNoo zLu%W`%w2@k1g*3qfP|(ezmBHZ(kL1MHm4#RVAJgvtN*J*-KBsYM!afhRsiz?cwr7; znlh2A7NN<$NB{|g2&U3$-Gb9h`CrqS@3o;W{+0P1d)DDU#K~p8GVOo ziG{^J-u8b5QmBF+CUbrV(=m%_eT=en0v`w0VFFOA1p-u&$(G!ZIp%y%Vrwkf=UTEIm|Io0*nD(I|H`47mW0lxp$83@Ya!YaGI%K(1e_3do{_%P|ArMD21>eLT71Aivp@e z<0ye%s&iiD$BIvloaNv3%IK5WLu!_DN6kyAk%>td>Gdh5VMdgKc%flTL?+@ntb^o+ z5Z4E4K{`poiRh?FkAM+nqi{e%qHD=WjnoQIh#irR%&hMtE0;39cE-zVKrdOVKaD`S z;FK)YaA0H3oDdBUr6!_UN*Om)3%)M`zH2Rt0=a$)UB`-T=d(WW4#_-}-a*twkP~=v zDNskm$17@unq?AqOQnUas32FS?vdX60+2ZZR4i6r9wzNQUysR`ZLz%9Y#Azv7#3ib z0yFO7EcC;e=;$uPdI>2XvqWOw!&P7-T^K|mo{HQo7Q%ntT`Gz+@3Q-V{|rpP5u+F@ z$DmR|YnI5;s!%@93o*%U77j9ZCQyV7NJh#MJpCy7SXoDlB^ zQp7nT#MV+GGAsWW@Ev;gS`GZ_5-#c2de~|O-mCK12t@ukDRt^y12BC7^aLuSAWjZI z+x?hDn2Z0J0sX|-?H~nwv$#fu*IIteh}Hw|HL6wE?Y+n7J@uvApkRJaikZ3atNaY4R0z6z>Fe4V14oME<#-+Ns|?t`5Z=!KUYBO$;N0n zrSmW@<}0I2z^@1tyW4btjJzVvrTvE;Dr*aBz#5w(9alJ=Vdv9XAL;@Yfl#M$2!yWR z0L(0Eb9y5c3)^n9)qKs1Nm;J+SR>Afx9|jQSQB&-BGdgBn4LCM6Vn107?>P|Qjq|_ zoDHImH|4Qfvj^%rG(PAOu7V?3e!^I7*roJTaONNJUv{g~Vpm z$PfiQN2=FwI*rlXOcR)N!ZPwM3EChIBfvM8wFU=Rq(*G)YQCAk>do^oi)eL@GT=VS zq7jjjefGj{=N&ME(?m##2<#6u60Ox!wQ>bDAhvL&v<2{4$#a2|UEJYRC|MVHMCS{Dum7)v{ zv&N*zQ(6^E2V9~!dx}=VTq$frc&Ug_V;5ROJ8{UYU^DPeh3y#%sOw2MH=M1^g@6x6 zO1E+N(Go3>rUcbS=xhc|j_3$A8u&NW64^DIS%fd(WL;wl5kd@UO@!cC9;GqZL@eJG z@7if;yQ;$@odKnd5ncq3?G2dL2!@DE%T|50b%;-8pdrbJvL*2yiBZ*O-|CYcH%664 zI|}W0cIsqELSQ~b3DOB$kGa)7vN%oB*e06pV?u`DcHEu^R?nFzV1ad&-fWrhWE}uR z<-u7U>kbV;ba90l>DZOj-9@%I6sR|KEoFj8v@pv}j*VRqHSm;Ks;jv%_f%~VaKn@o zT97h0OhhNIxU4XOOKI@sfx1W=c93c5`Im+tPPKph_WWr7(1*qMz94+#vXgzF%5C*y zuP%!P)U1>WW@)~xrj+R!!W@P}vM~+OiCVNGQwq_~Yc(U88gT|52o0h85WHH=)ML07 zuS9InMl8Y{%_-YUFRPf<3W^bQx}(4h6^sVg215>2**w~FOZ>3WuzOWFoCSQ?d93Mmj^0rF%)gV}He2RYYslc0YN3}#d;LSUE8O4YiAjyj5T`u^y1k z%Jj#GS+s9aiy~uHklISldR72RjxTE}un{>98)S6JMX0qpT5gR7Z(MsdTSnZBwO5pm zYM|+wWiujUalXJ5vAuJ8DN%dawdKt-091CX94V}|ZuBLSDvfmHxMj4ZQnCb`33)-r zj@$`gTh(ZRdEmWEK#JxxY2rO}G>Fjrr53FX67AoZm$gXCnPs`L7P_vCrjI?A9XYJ( z^=oX2oWzg)L4B{+Zf>l{bpLDL9ds{ZqcVNe*#RBhPkjJS_N^} zc%7t~nu-6JMRNBD1$qv1f+C%!6a;@d!R={;2Cxiz#{dQ>r$QSY=&7yvXVq#_^CqQ1 zjR;^&Z3o^#l#}U%o3y9FUAYjH4`S+er`Z* zBdq8&D4VEuX>?yYliNYR7O;GO@60FM6_`O)E- z_x9A@mVf)o+dKZN_v6v=0jN6>?*P9??`UpD5)oe+T5eLag2+!ADn2y_BbQJ`22oaJ zv4G6;qw_b1UBLSB%F*$w)1%|phd&)2pLe~V4o~-g04Vof9=$m_|0|KutE2Pd!!r&a z*pqFX?481dy?e8F>YcnhJ$ZX}$kLW~3uD|_0QjfSX+*mYv@^q2p-V0>b6GmgB2>?5 zz$2h8EJE~WIIiU{Wp87igHD9QRqQ&-iL>YFAktI5?cd=y6UEMsXD#-mwclp7L)HIX2=3xLFHxgAu8x)N1SX!JJZAAZA6=- zBN@H{D?lPa1vkj%c?EnMczXks35W&7d9mj`BdD!K@yj);?(T9eZx$}yZc|$5;5tp& z3re0&XS1DruAsM#1o0DKbp&CU3Vv=BJiE)TAT4M#fm-b`ng(AJ> zIkFwpe0i${#)Ioc+E-ETZ1;eH@k4rtmJpkg8exP_t6?)fifBn zO1aipG|PFGWoEZbdX9l7gH?{kDmziTRljk6E|m>4pgCf|;8ZK~cNR5TYNLjQ3Hb2% z0Oe|37#w}yJ2`@Ed`0r}vUDyzYS&x-yKy^(H8_I)zn} zff-&XuSVBc@}o$^!`uS_0gIiPejj%!1yH7cap@Z)1w9i|+RYU)5m%K~pJho0Ui%J4rv`&V%yrA#kG>pfuFzE8=+*>GXTIRpkkck z1{IfvBE26kF{k!jwR^@mIl^pkjXTy%3}!R&Vh+!xcR_FioTXjXODZmjWN6t;F8Orr zfKacE1$4}1=R0gd*(1mZSbRzMxl-e!M4F+*8Z|{}700sr(>{62LdRvN2stbix!)IZ zD#M>w0A#B25Dk5i?jd8&TDF(c!IuILLl~?Ai!|`hRKJztO=vq4!T~`MIjJ>3GcdGd zJOhFQl>o#-QjYAC-V&p)=hblq%?X0v{}VLlf12|@eEw(S#m3e*o8PPkn`0QJigXfxjq^XNYe0CV^}o5by4{@r`5Mnfk*0AlO+}H-FI%MX?RvhZ z3csboRN*aAh~KicSa|zdBpVdi>0u4DM3p}PYRhO?D0kuSpC6og@Aj`jc^aqQp9Y}w zd=GXI{S3r#_GkaHrJUB#E^CvhiTSOT4Q4PD7?ur+d?fPA)^rYC5G4P-5p4Ob0R?-n zpjIzX%Y_gBZ+9)&gfGye$TEh<{}Z%~ZNHzyMLLYYo~4qj!MD&K!W~C_^=fU+Z=vS+ z9$)+!U#tgf-9N2y3)A_pcUb#_g)8jxg^!!QMHce-2ibgDr2gf{R_hhm1vjM{)GYi5 zmC~uS1$RB%2K(3=g+Qg#s8r*>SA!Ru^%Y!%F%Xxnk?i)uCjbE7rr`@;{ta)-=)PU4 zpj_mHum64#Y$8oe(!mY=-UO7a`?AE!Aibrx>%lhwr+TZvVAKKvQN4x_xh%WB!E@f9 zMe%TjBUvw7QU_;TGH5F&Aqu~c;7PKNkpP(fU|KhM3dDiHE4gYV>e>cxE{V&f3 z&-J_Ky&fK{==Fl>{I}Ep;M-Pd{cmlpuQmGL*LWy?M^AVl)gunX_V(cYA--v~K$`B5 z53N*TmWxak-$$Q$QoPG?A0Zb-ZycxnFz#WQmUQVIQBi7?+p{Wx;4amIH^&&y*6Q`b zIPUdG_G_p>@$s$d`1ADbo5M5wqtp;8HS`TyKwYm*eeWIobacGyhr>yf_`H3ve|org zez*(Dng|OaUmhI3ImDMk5exJ3?Jvh~-tHa1>+~*(({Lz1ygocWJl%V<>-Q#N(id4z zzK52fFvb?@7jF*t&JOV%hVH3`-<=!~2(#&sLFoL3toZjK|2M1hf4!0aUq$|ZWBfmx z>l>TEc!P}>tKV*HY%~hMzrFk)_Q&D$^MwDs@_%i6t)l;Lt!->I^8af*PrQ|9D|omx zO2#|htQf6)gKt{C@3$yz3Py-Xe@>`kO$OHj%>4m~EKj_r$btcf(e+=4Cjr%MNu7Ya zChA8f^n2}fX^}#Ku9-L;(?KFxVhj=-UxDsGML-yalN3ZSk)|p|Z*ndHZ3fdY191EY|czku$Li9jYEAA{RY`)|WeJ9yUdJFV9K+n-KQTj-sggA^x)MT++!>Ls+F2};DU*Pr*s(-0Mt z7H<<1_Wp7zD8zUmvNmUN=*UwwPj+ZKdzQtlgUJK{sW+X`Ii*-k(C2|0=`bRWZIHfP z-a5r|IEiWs;}JL}Gnt{1;!Ud)E`UNqeO z0GN%2AOCkI3QRw-3lqy#JB*5<7_#-5&l9zcsust-=uqbHtE zgYtJg-^`UB7(O3cZp%Y-KMwouOF1m~BL8wfGYa6V`~zsftfRJ%W0+bb<`dF-vA7lS zu8;8*ey6U%06(?;eN>7$=I@U2;q4>dJgY@(ilkf5LJiZVoHD|PJ2^jvH*^B%PV`l6 zoSYpDX`bv{0qtMut?Dwx5bEnfi0-(8b;)H{7kuWpLu7r7tfJ7jBF@v8p*;4&ykFWgK!#2!!2<5 znA`F%5weMlqw}aTo}N*@{9EDjK#IjnVNBO1%txN1#OUt3EKSDNh+w78vLxT}_Q<^Q zhz$?T=@3MVJ<;X|ECI6nfYy*w;b)r(!xVW!(IHdJYm&}#CvirJ2COOo|A1{nJQ0O4 z78qutSg~mw%0ZtO{sArx|MJ2=LoS4We4mLihGkHELoX*j-X5?}N( zwZ2K{^O+FI5Iw)XiKB$LX>}ES_(-8wG;`MCB@s^u3RniU7p9FP`l$WdX)S*Ps)DHE zMNyVDU-g|6UW0L#&Zh0Pj`v4g!tE$2+V+R_PRCpE=(GKL<389HErt?s)k=@1;Q!e5 zj83vsJJJV}$`>LRPCSRl7mK^p)48Z>mUwQF%Cg4j?7p?){Ra{Zb27weYcbVWg|xI& z6IL(W@TeocQtV@i?y%Ex`a*?I(uAdosYODK?n;hnSF1|z6VMas^g%^%B0@i`e$*=u zy6kjXCH>`8$z#dR?XNV!r0NIzF`n&iEA`7(S~?iKIH2z}%n1g`-;nePgfeT(0w&=e z<-!3fUSD0UHZ;k{3m5R0U~GpRSVzH%N)^!31(%!1IJ%T&HXg$li`u%w1Qvxr8-z1V z%nW=wYmRsbR3}8`EZ;FW*~4JmEbt$|bZkl?HKGkD>^w+%P{9(NoS^8hr4sE!uChFB z4zdB)&H@bET>xwSvbmr|aLJ9iKuO=TF$0kh#V3;vfhec9yqI8uzT9LG*qHA_e9Cl4 zCUrl`TplKATJfbsXM?X%WP`$owwYfu8u1@$>fUmAR z5sU-xeE;>{g4!AeQ=3AT@D3>JRSn?C@-{n20y*9)9U*g` z=52&&w;n)KGAyp!z8q8TFKi1(yK4`@j|)yWN)=Atfy6Dv(}`;$0Em5Zpd0#PxBh@P1$CW3r6A2StpX{ ztBd2T$moPhsr3cR166`DY&mN=U9Sy;4e=zKJZr6Cx2j7Lc`7S)%E)Wik$gZtX@k3- zGR`$;2o!~0tX?iCU0!_Unc-Uf`4mt5411&-ESsc#a)?GFJ_m;CKuGEPP=QsgNDZTW zuG5_MFhl&zX{_)#1B^Nfxs%c9rz&9Sm=*;3U-r=YMY`eY_MtMe||gls%bx}fZA zq1UVG)?if$k2=w~Wggn0<CIz_8xgc^LurIE|<1vux6p;ocB$Tk4A4IES=iltsc=&)7bVZvRBTP zRjTX~SKXCl9+pLsJT077M(HdWE@Z`v^slPIP+k!aQc|9H%;I$RI4@2?f=lK`?8P>9 zD^#FO-g~)hl{b{-@?A*hk*bQ1{|ZsWB*oId0;E<0s&v{~teDFMbe_#DRne}T+jy-o zy=htZlI_)F$!^xHLOyz`QCD7%)(R|p9zMZ;@CAJbi~RLn_I!RTbpaE+sy&MCjLvRs-GDEc3=GW=5~4gZ*6Qh_kVr8`0r{w z6vkK|Yi(oy;ONY}(E&?35WDRbP!UV0b?d_a`O7(%)wLEDdq2fo3lA)P!bvo^wCZG0 z;l0@~!rh*Sl|Dny>yo04{Qpw&zd8T)50n4R`L9O)FO>g$g64A-fcxbC_GbD1&#f2D z{hwdw@qKgHKakViYVjZ5ZJ1GpM42cJ%5iMBK1#(gu0EZk!pK(pB$dxcsqnRPn9AqV zRQTRGPvxrvRrJL@Qsv7tRrslNs>;V>RruaISmn!;Rs7mMTjlfND*T|2SAA@?emeZ= z<>6^hL9qAojg0F%qX2Gmr&Jfyaiz3_kc_Qg@aeMEI(zr>r=#=p#Vs)mvndWuXlP~F zF}3KtF+~KQcjF%xIQ+r}$$3LXE?bTKZ`OY!|C{I0@;}8t*FX0d_nf=eb!GF1NZX(t?d&3Ut8a9&i{Tr|F5wCdBOv^vCjV}pYU7s#qw5t z!yEopo<{yR^1qS)jr@Oz{MWa)KT-j>FaPV-i>P73R+YhnvUqYn4NmAO->U=4;dI(+JutmksaTQT;B7EmDlgJZ^wen>`qXN*C^DJI zvNue7nljZn8N9&-FY4*dGE8o2!Ne*~Pii=_*}yJO4NRh(PVZu%G+j#Pq`sh6zB>at_6ALh{jld$5sJCO{hDa^LICQaRZRLGlL@jfBGSe86PQXOkK+2Vp$>-<{a z5ii}Cyzi3k%c4_PSC+bPbwyYGg)#2t&-u!~L7I%B@eFi>7M&RZNbC;1h%$QN6<8|U z(AAaHXrZ}jIJm)kjKW-ATZZ{Hf&f$AiIskw4sLdKJibGqRhQt_@rc#A!gL*ZWi2L~ zSLH|tmY0J%nh+>SRvShMddi+qu+pb1?vIW%p?i1lC9Bt%nDkL1l_}pQ@Z} zc+L#F3ldR0w@X`UP^pG0O<+E!5_UzUAug?@OI4x2xkL4B*UgIdln%E)-SM8nKMClZ zcw8H^J!Mb3Zb4f!#PSLNg>;0WuJ?~!l{l__{Pz5i4_acUIp6RKea~ zPZ^#IVIPZxuE;g^Z%+-Q-Kw*xEK||3Cyk}K%e}TN+o%HBageR&brRG%2J9jE0OR`VX8l0obPRtj8U=stQP<^Byh+D&O|QJ8HNl@iCDDYIQ#>aKx}_A0l)c5;o0|A2Lb-ALn`$e7Jv$NH6AvSjtjQ;fzf5i>r! zbl))Fu&7XmZ8}D_2qpjlNcxIx56yP*_-7pfH2Xh|{omOCjs4%){|~VLC8vLM5a50B zA8V@{W&3}1b+fVmzyAKuLL=9`fyMgbUp9QAk^hbSZ{&aT{OieoOvV)@rMPe!~Yxp-|+wD`91LeQS`o;W#SVr1iknE&x-xGzP7pA@c*yzpko;~#yA7!jhK6C zu06i<+e;Jg>Z*4g{(TY_y(|xV(=58hO$R7^b%pyGt%cuv>sMD@FDE~KF1!gkz58l2 zO$^chdne-9Yl~a7$Lnd*uK2 z_S%cG{};%xk^f)g;W(CTJ}oc_^z?cjumBAD;ot_uzm}acpMOujZ!N3{l5i5?g>t%T zF0v?$WdOlLDh6RbpJ2+`XrS5v!Kvb8rgJ1qc?O`Dh`R+cySzKkw-s1h*-J(pU_4Tu z#{sk%^iI$APPpA4#oTFAZYN_Y9gJavVDfLX&=>EgX(Ezhck9KAjqNTfT)jN{yVzY{ z-TbEO$?kVc$Zo3`|5odJ9=+0U+7!#Q>&xADpJNpM@d#NuEuT4z-gTU>{UR3NjQE(4Td8Nc`8mn$x4uy#@b8+^M1U7(P70I`D<(5_Dof0DPo zN_^~PV=Hm}g5GwN9$t?_=ya#t8J9eKPd5u#mzymM!c@=SeEgsDY6YK{hp&&0siTwA zqo47j-5(GCN?)9^KllH#{Wdx|3xdFTU-SFFet3WM<9z*()xHjo5AN^D0<=sE^@t4A z-+d$n!ze_;moe`;Tr)V7uLGDi6Wz%3S1Kw=^A*qSc@_>Mj>mlqG<>6j zVLVQ=sJNb#8aqx8b)j}g22%z@fiu_k>Aqy)h^pH;be#;lX0dk~O81xx6C8ED&9!*Dmegli3Oqip3TUuO?USNwC8MQCh*co3N#P2};IZ8%px8^KnW!*eV zgDv~X9xo`Y6;vxU?p;*WTxDTBeo;|7m4$_#(PnA=ewG#rsek&#^-t^nZSMd0hxvcM zsq_E7Sl!xMZydml{}+#0d~W*B2ju@Q<$qb*-rihq{J&r0c>)r01-+8;ic37E1cIp_ z;pyF$KKfIape0aQs@?5X`;-us>st596}?5Ds;N87M?8qQJZ5x6V9`k< z9);7llhLID8Mj2o;VOW^+X_VTodyMdAE$|M;-*zV1!nmot*&}&-`u5N+81=`m*#3MQ<;Bt-A@#$KZ|OxO0cGYp)Q@m3%4&(I^^t&tY&B#X#B)7P)lN*Qx@>uxOg191WG6PHAfFMAJtYhurI%I_*s3mj$iMT%jj{{ z4hIm1O(V|m47@cv(XdTE5HVy`L(Y#g9t{Rv)7zURFjX)SQ01C5j#3LeBxu=p4vb^< zqlEO$b1b(wXPUldPS~KrxE=+nbgwv7x*mZ(-+dLvxsZ@T!(R?krBC;!yky#gIE*HF z*Mr|BHz(i5t#jybL8+x<%iguhMFG0K7+|cWY?RA-S6A?RfeTW$*2X-0ogJrfqZ@Nj zL*`*_Iw(}`rru4-DlvU2-IHEo6z=uL@@{kq$S|ek0I;A6ytp_ljSSQAjl8IUasn?= zXCJN8o`V<4G2$Q>s{7EUPT9!)*36%C_n2NDD4OuTUtEd3kkMp{`&l4!fLY zr|U6K0V+;uV=dqP@UcY9KPx+u=7n1&BEGtE`oLJ?yX$Ch&4I)@u+n^l0UkVkGOf?m z75$-Uo07`Ff0S_}^zt|wUl;M*i_<%it;FJ1#Kxarq#mz*f`u(PjBqAwi3|}*UnVI= zGE=rJ%JGt;sL@qq&nw;mw}oSw5m6ZT{CD2T9N$yA8_aJ(R151F=oco@10ER zg^0O|Y}u*|>?qYOaK4%$SI1#bGm&$aEc%AEP(~)HY!-864Ka>88&h$-XdWhySCLAl zr3&DZvhsq$YWC7m%Pgv>5I~zzVq;GMBLQd5C5d83GhUx=_LAKms$EN}1#gQ=g#DB1_4}+dObdNZ9@i6PS$OTwCd zocY|YVGIx|&a|+sf>k!10Am8AWD+K;T3(2$RhLev$lPWyQW6zbmB+Bn z(i2z#VVmgsjMv+OqUb|tfP3I+6>RAiNHI8u0Z{k^MW$~g^||+kV`cNEP(KJ z7Agw$Lz21x60{yv^)nK+`Ob6l*kN-mJ#aw_NiC=aqvtvg9zPX-EC#e1v3XcuPzw+yN3zPZ*LolhY0cFRYmv=mcOTbPjT^lbgbmVa3) zA-TXVE+FTNQa$Cr+lGn>^LHpKo@g;0Z=_?^cQL*eC zRm?83P|ifCwX^&?PoNj2xJE`Hz`~XFiLS0eRe(7KmZyTJpCBp8ze>g^lZ;Z`m>Mvt zt7COtg943NDm4$3dD_vSzEp;n(h|!V;-o69diwD3JKbb4D#MtOGD(vaF_{)~qoqhW#Y#}lx&mo3ilMPu#6L(EP$-~RrFGPk+Uu^gEXeCX z@BFTJVI{Ih>%lM+yDrNT*P^a#TmB64X&l-4^{c$hZN0>=YA{=2EqVrdq7Ww*K6acL zwS*V;w3uJ%aa}Ni@{&30axV9mG};YDfQ?E%4^{>Ku*dA+#;v*l4@TXnL59|BlG>G9rG$?c~bKXzn zyyr|ks`Qx{%s`EwcfB9Z&rc3X!_~(^6{Y2}kg9}5Uy3lp1lsubo}H3>5_^eAnO1Xwpib-(9ESB(nA(alrhN)u&xqyX|1X$5h#B zrxd#K&A{ZfO@60SRxm9sM=JPrPS|HkfjX5M6=9NXnsfj(hNHJO49b?|j_agUIk~%@ zWm9SIqSV@qA;fnmxK2QJQF2~;xr4=)5n2@V`a7j?o8A`m)UtQ9>^>d0s5bG+jz)WM zpi>VhV?E3TA90YjRoMZ(AU`=9=njpV<`;V~yXsiE^m5|=2lnqwhLL04DIYEn1i@}8ar=)sf;GW#=1`ygZKl(CW35_UT+-8PgFETJ}<8%OX z$Nf||kO3`+)mupGG#^|Gu<2=Qt=;iQj^`m{TSVd9G@q5{oGrpL>Y?Q;EJz{QF^oWW;m{@M)UW4%VHeI|Wr9 z*sbFq&T^@TszmE3Zvs3A`t90wRod0;yod5hP5BfU}#0c|4s32HA8Zn!q$0-?eY71@4(EY9H zAfoUuvIPcU{)g%U^p@?#$S{MSxUDuK1Lp=|f)Oj2@0a{c6khLza)u z+uP8F$OKYf?lbhJ;_0H1exfSO75nSs_#iqY7ONDasA~OWE9;Knn=3RD9o^ zzRA5dMw(u6y8u)NU~Q#rMwqG*MwrUVIKra?4!SdA=56d8#`KCpd{zF9rFYTN^?df7 zdllF6u4k@PrCU+)Qq*)JR>mUPSOMZ5+|cEybgAl&?W}KKxL2$~dn>DJc)9B0oF4Ao zv!>emW2#+R%D)0xXJ(ObM}-K|Z2X&mt@{7S8Kk9W8!yq_+SF&Da-FL3o*fT;6KtH0 z$3#0^2yI$?B}$jUT8FHvnr*);3m@rQh|mzfHQrSxH_zs>%#@Y z8IpG`;wdIAR|g7+%S`#anz|Mghak5Ftg~Xz>kf+PYjgQz8wW}|XSy&O5!#Pk?n)8| z!mN+FNm!4VQ^q0c{=j^5(Un(^=2IYUd4KlRm9BSnrPwW4i8o#x-SFOk+#)J7u3y93 zFdt;mwBYZCFR^oF$8%TiMKuxz*D|1Bbcq?-BWPBbb8KX9S zRNX3;!7>_iWNGIw-O*lqf=oBN;T!hQ)ry^6F(isR?xXolV(yuxv3} zvC`VeS;qR_c)77Rg~=OvEIGX>H>*!hjdtA{6iGB6%oIETMDX(YiCdj{6YA>7cNtWCdz4p*(~>F4PQO z0;R}%m}xH3J}P@w^u4FphRQnOP+6BZ+6=)44>~WP=|_6s)drtD6hXQeOft`~!|3JI z?y5haf)T`Rbu&7NiXNT)+V!Jkl$sNI)(lqmd8}`=rO%GISD0703)&yB$9$FveYWPB z#(s5GU3*#?rP0i$fqenGmK{jLVT>0a_fuG_JZMD*hBWYw4yKlx&$8hnn@dTS%@X?( zXIM*u15y{a;eouELYA^JKkY1@GY_`t1%6+!X^_MAOb)4WP5FEEvSe_H(bK? zlkskLj!0`)D3?zt+x30dA(ry)(!C_#s@>+QfzMsY4^Yu5`om$1_6W6BLt8_Y)=C$8 zJHRc#u5nVvxzRXUmD368%d*{VP&a@@xATr!gJc~_UhPY6Bu~gY`#7rk+Eeaj6q&>B z_6c{jEmMDbeMyk9zXwXrWLl_8!})S`TlK?HeE8UP<+6`TmbrhNwwaX#;xT^s80dWv zHNY-?svL@7o)aK@Y5S)Pf_nzkX8mfX1=NU22XfYBgG14ujm>bOX?eKA8s5&|5^KW- zkr@nUlWC484BI8_ce=n@a2I}ev!l~=nys8FEz4YnDsN_~xgDEA{hMO$V<9YyoC^NE zpqG~v9dLTK=ez5vevo>wXHp$OM3(bif+d0bR2`+L$Yh=x4cvD$^e-3fYcsQyr*>u2 z$!yX`j|Ng`4wh<>reN?yq8O2ZdRD1^O84k^%xy__9$-gBOal~b8c;5 zIUS`20vkCGMJMr(?O=O&SE_w`uS-umF16h6e(UT?USS3{8Uu&1G2dh-~ zY9s;qc18PxRsd=M_^2ei)_EUQ%@KYFc$7bTumr4=e}mHr zn?24;^qL&cAq%!4XztJf+Azu*Er;~DCTCy7X))OZ56Vi(^8kjMJiCrMxvd_TCq(*XEJ7Mp4)V@`{m}6|y=us(s?jUn@xf>Bfj8Vn1N3bdvbfV4RYeY9rJk_8 zGcvgmB~PKPB**gCju^^`3hOavgSV9a9sZ`>t9$Hk^}NHfDis-}+EABU{v73@a#HVS zY4U5@_u6|D{O-}2U+*NPrO{cE|J|Lx<<6j2?6g_1%&x9>fGXHsWAbEIDSqIvy=u5LLvn^NQ_z zOt;cIIy-xJcxrhsmQ=eiE&)50+^2iL9;{s?0yJ*LF?JvCn?ab`l>lur4P`hSEWZ0y z7#R)QMRlxvn%~h9OYqioW@$K1tJ+m!w?O`Om&@82tx9-BM6l3O}80!DQyEVIL5`U~$aL!~hD!i)lQSyq`b;@%ncG0wt%uJvhqVjlxuS!dipHSvXySj@KjIdIbRWV7!WKowu_Q&5&i9!es^mq z%@&0vVJ1al_tWpTX`UwkM-%_q0|cO-D#EYautrID$!$>kyIc}@r zU64*7t`jCpJ>rh*L*i_y$eqY;{w`+YK%QUcQg86bNtV8!FUk2qww={%on5m$JxHtd zWXGd!v}@17feJo{)^j^<50q)Fi4Hm1Xzbw{lqvGj^_i!Dy}5Bb%D`0gWO0C%fMJ9M zIV;JXOwHjI!qt^?eGvQS)P{yi?Jhx;cF~g^tlU0M3!TiuxMVZ7m%y-+u}N3<6-C@2 z0vu7oVeu4@OOR-ikon8enX23x6onezxhQ%T<iJdW5~Zu4 zBL}mW$jpW`9`vnHc)m=?%4wB(e9|Yb@?fynnPScg&MS}2-G!7{UWPz+DJt+bjSxFS zFd0LbZnJqDw95BR8Deb5;iNwdcW{iB4{hA_{PML=WFcs?yK*m7+rHO8wi5KpJu?Z* zCaOrRX+(a%@OxS;^z8I47|7u?>)bxoF7L1@!jp>v5#NpyZO)>Dq0?0t8OQ^V*s-*u zwb3rO<9TmFvJqu<220FM)sZoHno&f09N#8+uMc4Qc5)cYaCx&1%rHz$laX zuKiIaT=YHHYQzu=DouGb*4f*Izb{rV>oRQABzVM_397MnS=65+SQrC*p%AImba9J;Foo|*$#VTs9YUnP`E2Txgm0jww zZY-?Bu}JzlT_$C`MlmX5URy4ymwRW2J&KjjJmul(xFDRTUValzOPSBRD(^e`=P>AiG$*;^ER(~B z*|>ZjUK4=NZd0J`p}*tNR_w=5$)Kfp(|W8G3JG4@W;l^1rR2$hZB85PhhCleo8F&y#=3nwp|QS3*OwKww>KR*`}#8cSErpI_uJRHOu#z3l8)TFg|5Xzt`^X__$#Z%k~Sup zZkJ&~vZuWKG|!3w&>I!5qX|`B;eZM^bAvDGgLc!ym(m^G4IhH+FQ59~cpsL_-YZB~@3B@u#__M?JfWc#2OU;rB$l*-hlbc3AZ z(L|=n;?$ZlE56|N))Q9PNhE2m>h$AmAG=D~0<-6$Wb#~E7K<`1)RoK0P`tlqr`epZ zn1)#{h;Qr0C9LkHdJo#Yu4_gI0{P}X1d?1d+yd_$vWZxw0G0GqRoItYUxA8yHG2^g z9n*eUz1fxj-m?Kt2(kC^rH({b{6hCr^nLNJETJ9u5o=Z9aF}Zo%L;?nE)%3G3JAj zByc;FK|OnX%(Hwkq*0NH@augb*z@?tyjb7Ksz+{)ryAP^rC)i2q|s6q#o z6kU>FoE%-6uz%BixMu&i+5c_!f1CZ^`u$%#fb%(-zaFyxySlx;RbKyFtIhonU+1Z8 zv)cKOS|)Mvum`F%h$wR>L+Q2IpLKUe(RBDP-WzrHcOSD?yC~0)xr(7wjI|=IH|gw0 zB^{z$(HUxX*KiHAODdc?zNwtWJ+1jZ!r)itTC!ozvY;+S$sSZWQl*u9!uB041YyTk zML!Wr%V#3NJaUr0$ZLYt^#Y(zavxpW$0PmkD!%+Fj4tCdRg6nafGmr*>6`$+_9^<_ z{@Akbw4?#}-Nl2mPp~T;m(msNdiy-wcg~Eq>eWlTY9RNu9dCnO0LbRaf^C1Q-+BVy z-;#TFk`8CF&d5cv6N+p}o0ha{w(YS26)%jt=3QvY%3#NP{5>?AFdue_aCy7mj+AD2 z>@x0#bh!mIK%o*niK*Up`xc$)e-b8t!HX>BU z!gQpHs>Fp^8e0diqr4XmCs9%jDOVFrxo)V)G>dM*o*RqO6}ZS(d%Y-$ie9gcC;imb z^U_UW=E`uy5&re|4|#RX00pZN$ie)CH$>)BmUM}0Tka#x>g z;@JQ)kdx7I_QZR*co8KL?)u4LbUj}|sa_f)^c9Kd^78k`vaRbTBf*|*klZ0Knb7Su zW(+0jkj>f{W+5g+1Ef5mSMsJI_1kS&g7JM>u~D$85tZ>}yDE;7A&NWUED&21)Gidu ze^cHxqob^_=FF0#RwCI*Ef7zX_u-s22vHRw<&Y19^1<9p z4M$#vR6%=PbL!$Bn~3WYyBo!p>B?(k>KT3Txwc1rf=Vtad%U!r!z;EI4RcBR$cK0R z-b7#`k)FQ@1S2G*mgY*j?<2dbyu~MnEhpXo(*9s8n59QRH{dgt-j~G}Ni95$hn#%|!Zrlh@tuqw*`#t+ZoAW=7|M%b3 z|4a8je6tyRySll)wb|VN@Nen=1!g<_e3$QI{J-m~8|C{SHa51m8~^XuIsdcrY{laM zxgBp-j8?wEH}WD~BnHwJEEno*+GZt) z2a24$;4J)?*wPEr()&qVr07+`ir;eoFci@FvM^V$%E8EcdTC7ZZ>;rV6T5?dUtry? z_moshDn*Jlfdu~%7HD>jtA!J|KScu{r`MTC{BR4(E;@GM8!->{ekeE%y!^(Yb{L$A zweePGeT+=iz{F`ix0(q?f69?P*wp^{!I}4NAMBZMoIZ6v_rxTOVl0IC|9&>$LV+00 zR6!`f!C0BwzIm@3vA)i-(hH0D;e++d{hphTdATOgpj{Py$`WNdgi_^KvZPA{P|kg! zdZf!l?B<^Sd3Ph&>i%hyXbi8VrdbF5-JbpCynUnpH~Rm-um1mHV`Hsh0sn^j|H+T9 zSB{Qfz5R;%|Jv5pMrr-8Z*Oll`v2E>eiB89`p3%8tjz6rpr5vmLFL;q8`CZQk%**q zHk(YsY`)_yQSDl9qJchcOGwJnY#-|-whPQG0s-D}6IsBbNG^mFBew3!{x zs4%U4+?vM2ebB1x(c9Va-ku#nzZm^3hWMLd@Xpdvafi3s$f8d5lMJuHnovsUH(@fK zfo_I%xmWya%@7Jk8TQ&C@*1^XTXQ M14>U#@Bokj0HK1O^8f$< diff --git a/poetry.lock b/poetry.lock index 8c7321f55..1f8676514 100644 --- a/poetry.lock +++ b/poetry.lock @@ -61,7 +61,7 @@ requests = "*" [[package]] name = "authutils" -version = "6.1.2" +version = "6.1.0" description = "Gen3 auth utility functions" category = "main" optional = false @@ -79,10 +79,6 @@ xmltodict = ">=0.9,<1.0" flask = ["Flask (>=0.10.1)"] fastapi = ["fastapi (>=0.54.1,<0.55.0)"] -[package.source] -type = "file" -url = "authutils-6.1.2.tar.gz" - [[package]] name = "aws-xray-sdk" version = "0.95" @@ -1595,7 +1591,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "63953e7bdfc0749415d59f4eddbd8501ab793ac967bb8b83854addd6f5af5ece" +content-hash = "05fc2802e427c72c626874251df018ea3ceaccdc0297382ae97bbf274b664bb6" [metadata.files] addict = [ @@ -1623,7 +1619,8 @@ authlib = [ {file = "Authlib-0.11.tar.gz", hash = "sha256:9741db6de2950a0a5cefbdb72ec7ab12f7e9fd530ff47219f1530e79183cbaaf"}, ] authutils = [ - {file = "authutils-6.1.2.tar.gz", hash = "sha256:17e69064586e793958c4aecc5999136f45bee3587b279f5527d0b5dd3f1da0af"}, + {file = "authutils-6.1.0-py3-none-any.whl", hash = "sha256:682dba636694c36fb35af1d9ff576bb8436337c3899f0ef434cda5918d661db9"}, + {file = "authutils-6.1.0.tar.gz", hash = "sha256:7263af0b2ce3a0db19236fd123b34f795d07e07111b7bd18a51808568ddfdc2e"}, ] aws-xray-sdk = [ {file = "aws-xray-sdk-0.95.tar.gz", hash = "sha256:9e7ba8dd08fd2939376c21423376206bff01d0deaea7d7721c6b35921fed1943"}, diff --git a/pyproject.toml b/pyproject.toml index 9b98c3652..335f8988f 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ include = [ [tool.poetry.dependencies] python = "^3.6" authlib = "^0.11" -authutils = { path= "./authutils-6.1.2.tar.gz" } +authutils = "^6.1.0" bcrypt = "^3.1.4" boto3 = "~1.9.91" botocore = "^1.12.253" From 4b31f99a80a3c5899e503fbef549177c8ed53b7a Mon Sep 17 00:00:00 2001 From: John McCann Date: Fri, 29 Oct 2021 11:45:26 -0700 Subject: [PATCH 062/211] feat(passports.py): read user from mapping table --- fence/__init__.py | 7 +++- fence/blueprints/login/base.py | 40 --------------------- fence/blueprints/login/ras.py | 5 --- fence/models.py | 23 +++--------- fence/resources/ga4gh/passports.py | 54 +++++++++++++++++++--------- fence/resources/openid/ras_oauth2.py | 37 +++++++++++++++++-- 6 files changed, 84 insertions(+), 82 deletions(-) diff --git a/fence/__init__.py b/fence/__init__.py index c55a01777..0ef4e1088 100755 --- a/fence/__init__.py +++ b/fence/__init__.py @@ -14,13 +14,14 @@ from werkzeug.middleware.dispatcher import DispatcherMiddleware from azure.storage.blob import BlobServiceClient from azure.core.exceptions import ResourceNotFoundError +from urllib.parse import urlparse from fence.auth import logout, build_redirect_url from fence.blueprints.data.indexd import S3IndexedFileLocation from fence.blueprints.login.utils import allowed_login_redirects, domain from fence.errors import UserError from fence.jwt import keys -from fence.models import migrate +from fence.models import migrate, IdentityProvider from fence.oidc.client import query_client from fence.oidc.server import server from fence.resources.audit.client import AuditServiceClient @@ -424,6 +425,10 @@ def _setup_oidc_clients(app): HTTP_PROXY=config.get("HTTP_PROXY"), logger=logger, ) + # TODO maybe get this from discovery doc or from the config file + split_url = urlparse(app.ras_client.discovery_url) + issuer = f"{split_url.scheme}://{split_url.netloc}" + app.issuer_to_idp = {issuer: IdentityProvider.ras} # Add OIDC client for Synapse if configured. if "synapse" in oidc: diff --git a/fence/blueprints/login/base.py b/fence/blueprints/login/base.py index a65abf3e2..61ce1ca44 100644 --- a/fence/blueprints/login/base.py +++ b/fence/blueprints/login/base.py @@ -6,7 +6,6 @@ from fence.blueprints.login.redirect import validate_redirect from fence.config import config from fence.errors import UserError -from fence.models import IdentityProvider, IdPToUser class DefaultOAuth2Login(Resource): @@ -111,45 +110,6 @@ def get(self): def post_login(self, user=None, token_result=None): prepare_login_log(self.idp_name) - def map_user_idp_info( - self, user, idp_sub, provider, current_session, extra_info=None - ): - """ - Map user to idp. - NOTE: Only do this if and only if the passport has been validated. - Args: - user (User): User object - idp_sub (str): sub provided by the IdP - provider (str): name of the Identity Provider as seen on db - extra_info (dict): any info sent by the IdP that could be useful - """ - idp = ( - current_session.query(IdentityProvider) - .filter(IdentityProvider.name == provider) - .first() - ) - if not idp: - # this is for cases where we might receive a passport that we haven't seen before. - # add to db if we haven't seen this before. - idp = IdentityProvider( - name=provider, description="IdP from foreign Passport" - ) - current_session.add(idp) - current_session.commit() - - idp_id = idp.id - user_id = user.id - - user_to_idp = IdPToUser( - sub=idp_sub, - fk_to_idp=idp_id, - fk_to_User=user_id, - extra_info=extra_info, - ) - - current_session.add(user_to_idp) - current_session.commit() - def prepare_login_log(idp_name): flask.g.audit_data = { diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index 3e4f46e6a..49cabcdae 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -106,11 +106,6 @@ def post_login(self, user=None, token_result=None): # Also require 'sub' claim (see note above about pyjwt and the options arg). if "sub" not in decoded_visa: raise JWTError("Visa is missing the 'sub' claim.") - if not user.idp_to_users: - # map user to idp - self.map_user_idp_info( - user, userinfo.get("sub"), IdentityProvider.ras, current_session - ) except Exception as e: logger.error("Visa failed validation: {}. Discarding visa.".format(e)) continue diff --git a/fence/models.py b/fence/models.py index 6270d7055..378a97abf 100644 --- a/fence/models.py +++ b/fence/models.py @@ -592,34 +592,21 @@ class UpstreamRefreshToken(Base): expires = Column(BigInteger, nullable=False) -class IdPToUser(Base): - # IdP & IdP sub mapping to Gen3 User sub +class IssSubPairToUser(Base): + # issuer & sub pair mapping to Gen3 User sub - __tablename__ = "idp_to_user" + __tablename__ = "iss_sub_pair_to_user" + iss = Column(String(), primary_key=True) sub = Column(String(), primary_key=True) - fk_to_idp = Column( - Integer, - ForeignKey(IdentityProvider.id, ondelete="CASCADE"), - nullable=False, - primary_key=True, - ) # foreign key for identity_provider table - idp = relationship( - "IdentityProvider", - backref=backref( - "idp_to_users", - cascade="all, delete-orphan", - passive_deletes=True, - ), - ) fk_to_User = Column( Integer, ForeignKey(User.id, ondelete="CASCADE"), nullable=False ) # foreign key for User table user = relationship( "User", backref=backref( - "idp_to_users", + "iss_sub_pairs", cascade="all, delete-orphan", passive_deletes=True, ), diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 3dab3cc1d..7ccb36b94 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -5,6 +5,8 @@ import datetime import gen3authz.client.arborist.client +from urllib.parse import urlparse, quote_plus + # TODO comment regarding circular imports import fence.scripting.fence_create @@ -13,7 +15,13 @@ from fence.jwt.validate import validate_jwt from fence.config import config -from fence.models import query_for_user, GA4GHVisaV1, User +from fence.models import ( + query_for_user, + GA4GHVisaV1, + User, + IdentityProvider, + IssSubPairToUser, +) from fence.sync.passport_sync.ras_sync import RASVisa logger = get_logger(__name__) @@ -109,7 +117,8 @@ def validate_visa(raw_visa): if "aud" in decoded_visa: raise Exception('Visa MUST NOT contain "aud" claim') - # TODO may want to set these fields and values in config-default.yaml + # TODO may want to set these fields and values in config-default.yaml or + # bring to top of file field_to_expected_value = { "type": "https://ras.nih.gov/visas/v1.1", "asserted": None, @@ -138,22 +147,35 @@ def validate_visa(raw_visa): def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): - # TODO update mapping table - # for idp_name, idp_config in config.get("OPENID_CONNECT", {}).items(): - - # there are issues with syncing when "https://" is part of the username. - # for example, Arborist returns a 301 for a `POST /user/ - # {username}/policy` request, possibly due to the slashes. - # TODO may want to use urllib to get rid of protocol - issuer = issuer.replace("https://", "") - username = issuer + "_" + subject_id with flask.current_app.db.session as db_session: - user = query_for_user(db_session, username) - if not user: - user = User(username=username) - db_session.add(user) + iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get( + (issuer, subject_id) + ) + if not (iss_sub_pair_to_user and iss_sub_pair_to_user.user): + # There are issues with syncing when the unencoded scheme + # (i.e. "https://") is part of the username. For example, + # Arborist returns a 301 for a `POST /user/ + # {username}/policy` request, possibly due to the double + # slashes. + username = quote_plus(issuer) + subject_id + gen3_user = User(username=username) + idp_name = flask.current_app.issuer_to_idp.get(issuer) + if idp_name: + idp = ( + db_session.query(IdentityProvider) + .filter(IdentityProvider.name == idp_name) + .first() + ) + gen3_user.identity_provider = idp + + iss_sub_pair_to_user = IssSubPairToUser(iss=issuer, sub=subject_id) + iss_sub_pair_to_user.user = gen3_user + + db_session.add(gen3_user) + db_session.add(iss_sub_pair_to_user) db_session.commit() - return user + + return iss_sub_pair_to_user.user def sync_visa_authorization(gen3_user, ga4gh_visas, expiration): diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 58120ad83..9b4096708 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -14,7 +14,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa from fence.config import config -from fence.models import GA4GHVisaV1 +from fence.models import GA4GHVisaV1, IdentityProvider, User, IssSubPairToUser from fence.utils import DEFAULT_BACKOFF_SETTINGS from .idp_oauth2 import Oauth2ClientBase @@ -198,6 +198,12 @@ def get_user_id(self, code): self.logger.info("Using {} field as username.".format(field_name)) + email = userinfo.get("email") + issuer = self.get_value_from_discovery_doc("issuer") + subject_id = userinfo.get("sub") + # TODO log error message if issuer, subject_id not available + self.map_iss_sub_pair_to_user(issuer, subject_id, username, email) + # Save userinfo and token in flask.g for later use in post_login flask.g.userinfo = userinfo flask.g.tokens = token @@ -207,7 +213,34 @@ def get_user_id(self, code): self.logger.exception("{}: {}".format(err_msg, e)) return {"error": err_msg} - return {"username": username, "email": userinfo.get("email")} + return {"username": username, "email": email} + + @staticmethod + def map_iss_sub_pair_to_user(issuer, subject_id, username, email): + user = query_for_user(username) + if not user: + user = User(username=username, email=email) + idp = ( + current_session.query(IdentityProvider) + .filter(IdentityProvider.name == IdentityProvider.ras) + .first() + ) + if not idp: + idp = IdentityProvider(name=IdentityProvider.ras) + user.identity_provider = idp + current_session.add(user) + + iss_sub_pair_to_user = current_session.query(IssSubPairToUser).get( + (issuer, subject_id) + ) + if not iss_sub_pair_to_user: + iss_sub_pair_to_user = IssSubPairToUser(iss=issuer, sub=subject_id) + iss_sub_pair_to_user.user = user + current_session.add(iss_sub_pair_to_user) + elif iss_sub_pair_to_user.user.username != user.username: + iss_sub_pair_to_user.user.username = user.username + # TODO change username in Arborist as well + current_session.commit() def refresh_cronjob_pkey_cache(self, issuer, kid, pkey_cache): """ From afb6fc3a519d53822654d5b85c7573a72dd1ecaa Mon Sep 17 00:00:00 2001 From: John McCann Date: Mon, 1 Nov 2021 09:42:01 -0700 Subject: [PATCH 063/211] fix(map_iss_sub_pair_to_user): fix duplicate user --- .secrets.baseline | 4 +- fence/resources/openid/ras_oauth2.py | 53 ++++++++++-------- tests/ras/test_ras.py | 81 +++++++++++++++------------- 3 files changed, 79 insertions(+), 59 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index c7a49e349..97a11be1c 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -250,7 +250,7 @@ "filename": "tests/ras/test_ras.py", "hashed_secret": "d9db6fe5c14dc55edd34115cdf3958845ac30882", "is_verified": false, - "line_number": 327 + "line_number": 103 } ], "tests/test-fence-config.yaml": [ @@ -263,5 +263,5 @@ } ] }, - "generated_at": "2021-08-18T02:36:18Z" + "generated_at": "2021-11-01T16:38:19Z" } diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 9b4096708..3cfff5477 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -14,7 +14,13 @@ from cryptography.hazmat.primitives.asymmetric import rsa from fence.config import config -from fence.models import GA4GHVisaV1, IdentityProvider, User, IssSubPairToUser +from fence.models import ( + GA4GHVisaV1, + IdentityProvider, + User, + IssSubPairToUser, + query_for_user, +) from fence.utils import DEFAULT_BACKOFF_SETTINGS from .idp_oauth2 import Oauth2ClientBase @@ -217,30 +223,35 @@ def get_user_id(self, code): @staticmethod def map_iss_sub_pair_to_user(issuer, subject_id, username, email): - user = query_for_user(username) - if not user: - user = User(username=username, email=email) - idp = ( - current_session.query(IdentityProvider) - .filter(IdentityProvider.name == IdentityProvider.ras) - .first() + with flask.current_app.db.session as db_session: + iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get( + (issuer, subject_id) ) - if not idp: - idp = IdentityProvider(name=IdentityProvider.ras) - user.identity_provider = idp - current_session.add(user) + if iss_sub_pair_to_user: + if iss_sub_pair_to_user.user.username != username: + # TODO change username in Arborist + iss_sub_pair_to_user.user.username = username + iss_sub_pair_to_user.user.email = email + db_session.commit() + return + + user = query_for_user(db_session, username) + if not user: + user = User(username=username, email=email) + idp = ( + db_session.query(IdentityProvider) + .filter(IdentityProvider.name == IdentityProvider.ras) + .first() + ) + if not idp: + idp = IdentityProvider(name=IdentityProvider.ras) + user.identity_provider = idp + db_session.add(user) - iss_sub_pair_to_user = current_session.query(IssSubPairToUser).get( - (issuer, subject_id) - ) - if not iss_sub_pair_to_user: iss_sub_pair_to_user = IssSubPairToUser(iss=issuer, sub=subject_id) iss_sub_pair_to_user.user = user - current_session.add(iss_sub_pair_to_user) - elif iss_sub_pair_to_user.user.username != user.username: - iss_sub_pair_to_user.user.username = user.username - # TODO change username in Arborist as well - current_session.commit() + db_session.add(iss_sub_pair_to_user) + db_session.commit() def refresh_cronjob_pkey_cache(self, issuer, kid, pkey_cache): """ diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index 2f964b25a..3a9e063db 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -10,13 +10,15 @@ from fence.blueprints.login.ras import RASCallback from fence.config import config from fence.models import ( + query_for_user, User, UpstreamRefreshToken, GA4GHVisaV1, IdentityProvider, - IdPToUser, + IssSubPairToUser, ) from fence.resources.openid.ras_oauth2 import RASOauth2Client as RASClient +from fence.resources.ga4gh.passports import get_or_create_gen3_user_from_iss_sub from tests.dbgap_sync.conftest import add_visa_manually from fence.job.visa_update_cronjob import Visa_Token_Update @@ -643,46 +645,53 @@ def test_visa_update_cronjob( assert visa.ga4gh_visa == encoded_visa -def test_map_user_idp_info_for_ras(db_session): - """ - test regular flow where user and idp already exists in database and map it to the idp_to_user table - """ - - ras_callback = RASCallback() - test_user = add_test_user(db_session) - user_sub = "sub12345" - provider = IdentityProvider.ras - - query_idp_to_user = db_session.query(IdPToUser).all() - assert len(query_idp_to_user) == 0 - - ras_callback.map_user_idp_info(test_user, user_sub, provider, db_session) +def test_map_iss_sub_pair_to_user_when_iss_and_sub_are_not_mapped(db_session): + iss = "http://domain.tld" + sub = "123" + username = "johnsmith" + email = "johnsmith@domain.tld" + oidc = config.get("OPENID_CONNECT", {}) + ras_client = RASClient( + oidc["ras"], + HTTP_PROXY=config.get("HTTP_PROXY"), + logger=logger, + ) - query_idp_to_user = db_session.query(IdPToUser).first() - assert query_idp_to_user.sub == user_sub - assert str(query_idp_to_user.fk_to_User) == str(test_user.id) + assert not query_for_user(db_session, username) + iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() + assert len(iss_sub_pair_to_user_records) == 0 + ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) -def test_map_idp_info_for_unknown_idp(db_session): - """ - Test flow where user exists in database but idp does not - """ - ras_callback = RASCallback() - test_user = add_test_user(db_session) - user_sub = "sub12345" - provider = "new_idp" + iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((iss, sub)) + assert iss_sub_pair_to_user.user.username == username + assert iss_sub_pair_to_user.user.email == email + iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() + assert len(iss_sub_pair_to_user_records) == 1 - query_idp_to_user = db_session.query(IdPToUser).all() - assert len(query_idp_to_user) == 0 - query_idp = db_session.query(IdentityProvider).all() - n_idp = len(query_idp) +def test_map_iss_sub_pair_to_user_when_iss_and_sub_are_already_mapped(db_session): + iss = "http://domain.tld" + sub = "123" + username = "johnsmith" + email = "johnsmith@domain.tld" + oidc = config.get("OPENID_CONNECT", {}) + ras_client = RASClient( + oidc["ras"], + HTTP_PROXY=config.get("HTTP_PROXY"), + logger=logger, + ) - ras_callback.map_user_idp_info(test_user, user_sub, provider, db_session) + get_or_create_gen3_user_from_iss_sub(iss, sub) + iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() + assert len(iss_sub_pair_to_user_records) == 1 + iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((iss, sub)) + assert iss_sub_pair_to_user.user.username == "http%3A%2F%2Fdomain.tld123" - query_idp_to_user = db_session.query(IdPToUser).first() - assert query_idp_to_user.sub == user_sub - assert str(query_idp_to_user.fk_to_User) == str(test_user.id) + ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) - query_idp = db_session.query(IdentityProvider).all() - assert len(query_idp) == n_idp + 1 + iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() + assert len(iss_sub_pair_to_user_records) == 1 + iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((iss, sub)) + assert iss_sub_pair_to_user.user.username == username + assert iss_sub_pair_to_user.user.email == email From 7143998e95e9f28b1e7b4e8734228db211e46a3d Mon Sep 17 00:00:00 2001 From: John McCann Date: Fri, 5 Nov 2021 10:10:05 -0700 Subject: [PATCH 064/211] fix(map_iss_sub_pair_to_user): return username --- fence/blueprints/login/ras.py | 5 ++-- fence/resources/ga4gh/passports.py | 28 ++++++++----------- fence/resources/openid/ras_oauth2.py | 19 ++++++++++--- tests/ras/test_ras.py | 40 +++++++++++++++++++++++----- 4 files changed, 62 insertions(+), 30 deletions(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index 49cabcdae..5734e1abc 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -1,6 +1,9 @@ import flask import jwt import os + +# the whole fence_create module is imported to avoid issue with circular imports +import fence.scripting.fence_create from distutils.util import strtobool from authutils.errors import JWTError from authutils.token.core import validate_jwt @@ -15,8 +18,6 @@ from fence.blueprints.login.base import DefaultOAuth2Login, DefaultOAuth2Callback from fence.config import config -# TODO comment for this, maybe move to top -import fence.scripting.fence_create from fence.utils import get_valid_expiration logger = get_logger(__name__) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 7ccb36b94..ab1160656 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -3,14 +3,11 @@ import collections import time import datetime -import gen3authz.client.arborist.client -from urllib.parse import urlparse, quote_plus - -# TODO comment regarding circular imports +# the whole fence_create module is imported to avoid issue with circular imports import fence.scripting.fence_create -from flask_sqlalchemy_session import current_session +from gen3authz.client.arborist.client import ArboristClient from cdislogging import get_logger from fence.jwt.validate import validate_jwt @@ -47,6 +44,7 @@ def get_gen3_users_from_ga4gh_passports(passports): continue identity_to_visas = collections.defaultdict(list) + # TODO need to subtract 5 minutes min_visa_expiration = int(time.time()) + datetime.timedelta(hours=1).seconds for raw_visa in raw_visas: try: @@ -105,7 +103,7 @@ def validate_visa(raw_visa): decoded_visa = validate_jwt( raw_visa, attempt_refresh=True, - scope={"openid"}, + scope={"openid", "ga4gh_passport_v1"}, require_purpose=False, issuers=config.get("GA4GH_VISA_ISSUER_ALLOWLIST", []), options={"require_iat": True, "require_exp": True, "verify_aud": False}, @@ -152,12 +150,7 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): (issuer, subject_id) ) if not (iss_sub_pair_to_user and iss_sub_pair_to_user.user): - # There are issues with syncing when the unencoded scheme - # (i.e. "https://") is part of the username. For example, - # Arborist returns a 301 for a `POST /user/ - # {username}/policy` request, possibly due to the double - # slashes. - username = quote_plus(issuer) + subject_id + username = subject_id + issuer[len("https://") :] gen3_user = User(username=username) idp_name = flask.current_app.issuer_to_idp.get(issuer) if idp_name: @@ -179,8 +172,7 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): def sync_visa_authorization(gen3_user, ga4gh_visas, expiration): - # TODO set expiration for Google Access - arborist_client = gen3authz.client.arborist.client.ArboristClient( + arborist_client = ArboristClient( arborist_base_url=config.get("ARBORIST"), logger=logger, authz_provider="GA4GH" ) @@ -197,9 +189,11 @@ def sync_visa_authorization(gen3_user, ga4gh_visas, expiration): dbgap_config, None, DB, arborist=arborist_client ) - syncer.sync_single_user_visas( - gen3_user, ga4gh_visas, current_session, expiration=expiration - ) + with flask.current_app.db.session as db_session: + # TODO set expiration for Google Access + syncer.sync_single_user_visas( + gen3_user, ga4gh_visas, db_session, expiration=expiration + ) def put_gen3_usernames_for_passport_into_cache(passport, usernames_from_passports): diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 3cfff5477..1ecc76bdb 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -208,7 +208,9 @@ def get_user_id(self, code): issuer = self.get_value_from_discovery_doc("issuer") subject_id = userinfo.get("sub") # TODO log error message if issuer, subject_id not available - self.map_iss_sub_pair_to_user(issuer, subject_id, username, email) + username = self.map_iss_sub_pair_to_user( + issuer, subject_id, username, email + ) # Save userinfo and token in flask.g for later use in post_login flask.g.userinfo = userinfo @@ -227,15 +229,23 @@ def map_iss_sub_pair_to_user(issuer, subject_id, username, email): iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get( (issuer, subject_id) ) + user = query_for_user(db_session, username) if iss_sub_pair_to_user: - if iss_sub_pair_to_user.user.username != username: + # only change the username if there exists one user created + # from the DRS endpoint. two users means that a user logged + # in through RAS, didn't get sub mapped, accessed the DRS + # endpoint, and is now logging in again, in which case we + # render the user created from the first login stale, and + # choose to proceed with the user created from the DRS + # endpoint + # TODO improve comment + if not user and iss_sub_pair_to_user.user.username != username: # TODO change username in Arborist iss_sub_pair_to_user.user.username = username iss_sub_pair_to_user.user.email = email db_session.commit() - return + return iss_sub_pair_to_user.user.username - user = query_for_user(db_session, username) if not user: user = User(username=username, email=email) idp = ( @@ -252,6 +262,7 @@ def map_iss_sub_pair_to_user(issuer, subject_id, username, email): iss_sub_pair_to_user.user = user db_session.add(iss_sub_pair_to_user) db_session.commit() + return iss_sub_pair_to_user.user.username def refresh_cronjob_pkey_cache(self, issuer, kid, pkey_cache): """ diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index 3a9e063db..69aeb1a0d 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -646,8 +646,8 @@ def test_visa_update_cronjob( def test_map_iss_sub_pair_to_user_when_iss_and_sub_are_not_mapped(db_session): - iss = "http://domain.tld" - sub = "123" + iss = "https://domain.tld" + sub = "123_abc" username = "johnsmith" email = "johnsmith@domain.tld" oidc = config.get("OPENID_CONNECT", {}) @@ -661,8 +661,9 @@ def test_map_iss_sub_pair_to_user_when_iss_and_sub_are_not_mapped(db_session): iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() assert len(iss_sub_pair_to_user_records) == 0 - ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) + username_to_login = ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) + assert username_to_login == username iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((iss, sub)) assert iss_sub_pair_to_user.user.username == username assert iss_sub_pair_to_user.user.email == email @@ -671,8 +672,8 @@ def test_map_iss_sub_pair_to_user_when_iss_and_sub_are_not_mapped(db_session): def test_map_iss_sub_pair_to_user_when_iss_and_sub_are_already_mapped(db_session): - iss = "http://domain.tld" - sub = "123" + iss = "https://domain.tld" + sub = "123_abc" username = "johnsmith" email = "johnsmith@domain.tld" oidc = config.get("OPENID_CONNECT", {}) @@ -686,12 +687,37 @@ def test_map_iss_sub_pair_to_user_when_iss_and_sub_are_already_mapped(db_session iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() assert len(iss_sub_pair_to_user_records) == 1 iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((iss, sub)) - assert iss_sub_pair_to_user.user.username == "http%3A%2F%2Fdomain.tld123" + assert iss_sub_pair_to_user.user.username == "123_abcdomain.tld" - ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) + username_to_login = ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) + assert username_to_login == username iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() assert len(iss_sub_pair_to_user_records) == 1 iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((iss, sub)) assert iss_sub_pair_to_user.user.username == username assert iss_sub_pair_to_user.user.email == email + + +def test_map_iss_sub_pair_to_user_when_iss_and_sub_are_already_mapped_and_user_exists( + db_session, +): + iss = "https://domain.tld" + sub = "123_abc" + username = "johnsmith" + email = "johnsmith@domain.tld" + oidc = config.get("OPENID_CONNECT", {}) + ras_client = RASClient( + oidc["ras"], + HTTP_PROXY=config.get("HTTP_PROXY"), + logger=logger, + ) + user = User(username=username, email=email) + db_session.add(user) + db_session.commit() + + get_or_create_gen3_user_from_iss_sub(iss, sub) + username_to_login = ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) + assert username_to_login == "123_abcdomain.tld" + iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((iss, sub)) + assert iss_sub_pair_to_user.user.username == "123_abcdomain.tld" From 1354966d7c8c4eece22bbe0aec06ef12a0cadac6 Mon Sep 17 00:00:00 2001 From: John McCann Date: Fri, 5 Nov 2021 14:47:00 -0700 Subject: [PATCH 065/211] fix(ras_sync.py): use dbgap permission expiration --- fence/sync/passport_sync/ras_sync.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fence/sync/passport_sync/ras_sync.py b/fence/sync/passport_sync/ras_sync.py index 9ab5fd5fe..b429cce35 100644 --- a/fence/sync/passport_sync/ras_sync.py +++ b/fence/sync/passport_sync/ras_sync.py @@ -34,15 +34,15 @@ def _parse_single_visa( if time.time() < expires: for permission in ras_dbgap_permissions: phsid = permission.get("phs_id", "") - version = permission.get("version", "") - participant_set = permission.get("participant_set", "") consent_group = permission.get("consent_group", "") full_phsid = phsid if parse_consent_code and consent_group: full_phsid += "." + consent_group privileges = {"read-storage", "read"} - project[full_phsid] = privileges - info["tags"] = {"dbgap_role": permission.get("role", "")} + permission_expiration = permission.get("expiration") + if permission_expiration and expires <= permission_expiration: + project[full_phsid] = privileges + info["tags"] = {"dbgap_role": permission.get("role", "")} else: # Remove visas if its invalid or expired user.ga4gh_visas_v1 = [] From 295834ee13d911a2ebd0d209e3ac137672263481 Mon Sep 17 00:00:00 2001 From: John McCann Date: Sun, 7 Nov 2021 01:22:12 -0700 Subject: [PATCH 066/211] test(ga4gh): get_or_create_gen3_user_from_iss_sub --- fence/resources/ga4gh/passports.py | 2 ++ tests/ga4gh/test_ga4gh.py | 49 ++++++++++++++++++++++++++++++ tests/ras/test_ras.py | 6 ++-- 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 tests/ga4gh/test_ga4gh.py diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index ab1160656..dc5b98f84 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -159,6 +159,8 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): .filter(IdentityProvider.name == idp_name) .first() ) + if not idp: + idp = IdentityProvider(name=idp_name) gen3_user.identity_provider = idp iss_sub_pair_to_user = IssSubPairToUser(iss=issuer, sub=subject_id) diff --git a/tests/ga4gh/test_ga4gh.py b/tests/ga4gh/test_ga4gh.py new file mode 100644 index 000000000..f18481a65 --- /dev/null +++ b/tests/ga4gh/test_ga4gh.py @@ -0,0 +1,49 @@ +import time +import jwt + +from cdislogging import get_logger + +from fence.config import config +from fence.models import IdentityProvider, IssSubPairToUser +from fence.resources.openid.ras_oauth2 import RASOauth2Client +from fence.resources.ga4gh.passports import get_or_create_gen3_user_from_iss_sub + +logger = get_logger(__name__, log_level="debug") + + +def test_get_or_create_gen3_user_from_iss_sub_without_prior_login(db_session): + iss = "https://sts.nih.gov" + sub = "123_abc" + + user = get_or_create_gen3_user_from_iss_sub(iss, sub) + + assert user.username == "123_abcsts.nih.gov" + assert user.identity_provider.name == IdentityProvider.ras + iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() + assert len(iss_sub_pair_to_user_records) == 1 + assert iss_sub_pair_to_user_records[0].user.username == "123_abcsts.nih.gov" + + +def test_get_or_create_gen3_user_from_iss_sub_after_prior_login(db_session): + iss = "https://sts.nih.gov" + sub = "123_abc" + username = "johnsmith" + email = "johnsmith@domain.tld" + oidc = config.get("OPENID_CONNECT", {}) + ras_client = RASOauth2Client( + oidc["ras"], + HTTP_PROXY=config.get("HTTP_PROXY"), + logger=logger, + ) + ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) + iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() + assert len(iss_sub_pair_to_user_records) == 1 + assert iss_sub_pair_to_user_records[0].user.username == username + + user = get_or_create_gen3_user_from_iss_sub(iss, sub) + + iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() + assert len(iss_sub_pair_to_user_records) == 1 + assert iss_sub_pair_to_user_records[0].user.username == username + assert user.username == username + assert user.email == email diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index 69aeb1a0d..7f5cc81f4 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -645,7 +645,7 @@ def test_visa_update_cronjob( assert visa.ga4gh_visa == encoded_visa -def test_map_iss_sub_pair_to_user_when_iss_and_sub_are_not_mapped(db_session): +def test_map_iss_sub_pair_to_user_with_no_prior_DRS_access(db_session): iss = "https://domain.tld" sub = "123_abc" username = "johnsmith" @@ -671,7 +671,7 @@ def test_map_iss_sub_pair_to_user_when_iss_and_sub_are_not_mapped(db_session): assert len(iss_sub_pair_to_user_records) == 1 -def test_map_iss_sub_pair_to_user_when_iss_and_sub_are_already_mapped(db_session): +def test_map_iss_sub_pair_to_user_with_prior_DRS_access(db_session): iss = "https://domain.tld" sub = "123_abc" username = "johnsmith" @@ -699,7 +699,7 @@ def test_map_iss_sub_pair_to_user_when_iss_and_sub_are_already_mapped(db_session assert iss_sub_pair_to_user.user.email == email -def test_map_iss_sub_pair_to_user_when_iss_and_sub_are_already_mapped_and_user_exists( +def test_map_iss_sub_pair_to_user_with_prior_login_and_prior_DRS_access( db_session, ): iss = "https://domain.tld" From ef8a22b648ed82ea269689787ebfef32c2cda7a6 Mon Sep 17 00:00:00 2001 From: John McCann Date: Sun, 7 Nov 2021 01:58:37 -0700 Subject: [PATCH 067/211] chore(expected ga4gh_visa_v1 fields): use config --- fence/config-default.yaml | 8 +++++++- fence/resources/ga4gh/passports.py | 9 +-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/fence/config-default.yaml b/fence/config-default.yaml index 117c40abe..e3fcf8b30 100755 --- a/fence/config-default.yaml +++ b/fence/config-default.yaml @@ -871,7 +871,13 @@ GA4GH_VISA_ISSUER_ALLOWLIST: - '{{BASE_URL}}' - 'https://sts.nih.gov' - 'https://stsstg.nih.gov' -# Number of projects that can be registered to a Google Service Accont +GA4GH_VISA_V1_CLAIM_REQUIRED_FIELDS: + type: "https://ras.nih.gov/visas/v1.1" + asserted: 0 + value: "https://stsstg.nih.gov/passport/dbgap/v1.1" + source: "https://ncbi.nlm.nih.gov/gap" + +# Number of projects that can be registered to a Google Service Account SERVICE_ACCOUNT_LIMIT: 6 # Global sync visas during login diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index dc5b98f84..38e79e4d9 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -115,14 +115,7 @@ def validate_visa(raw_visa): if "aud" in decoded_visa: raise Exception('Visa MUST NOT contain "aud" claim') - # TODO may want to set these fields and values in config-default.yaml or - # bring to top of file - field_to_expected_value = { - "type": "https://ras.nih.gov/visas/v1.1", - "asserted": None, - "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", - "source": "https://ncbi.nlm.nih.gov/gap", - } + field_to_expected_value = config.get("GA4GH_VISA_V1_CLAIM_REQUIRED_FIELDS") for field, expected_value in field_to_expected_value.items(): if field not in decoded_visa["ga4gh_visa_v1"]: raise Exception( From 30228241544505ebaf9876fd76f4a6992758a5d9 Mon Sep 17 00:00:00 2001 From: John McCann Date: Sun, 7 Nov 2021 15:50:44 -0800 Subject: [PATCH 068/211] chore(ga4gh): add logging statements --- fence/config-default.yaml | 1 + fence/resources/ga4gh/passports.py | 25 ++++++++++++++++-- fence/resources/openid/ras_oauth2.py | 39 ++++++++++++++++++---------- 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/fence/config-default.yaml b/fence/config-default.yaml index e3fcf8b30..6e668326d 100755 --- a/fence/config-default.yaml +++ b/fence/config-default.yaml @@ -876,6 +876,7 @@ GA4GH_VISA_V1_CLAIM_REQUIRED_FIELDS: asserted: 0 value: "https://stsstg.nih.gov/passport/dbgap/v1.1" source: "https://ncbi.nlm.nih.gov/gap" +DELETE_EXPIRED_GOOGLE_ACCESS_JOB_FREQUENCY_IN_SECONDS: 300 # Number of projects that can be registered to a Google Service Account SERVICE_ACCOUNT_LIMIT: 6 diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 38e79e4d9..2b5233286 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -62,6 +62,19 @@ def get_gen3_users_from_ga4gh_passports(passports): logger.warning(f"invalid visa provided, ignoring. Error: {exc}") continue + delete_expired_google_access_job_frequency = config.get( + "DELETE_EXPIRED_GOOGLE_ACCESS_JOB_FREQUENCY_IN_SECONDS", 300 + ) + min_visa_expiration -= delete_expired_google_access_job_frequency + if min_visa_expiration <= int(time.now()): + logger.warning( + "the passport's minimum visa expiration time fell within " + f"{delete_expired_google_access_job_frequency} seconds of now, " + "which is the frequency of the delete_expired_google_access job. " + "for this reason, the passport will be ignored" + ) + continue + usernames_from_current_passport = [] for (issuer, subject_id), visas in identity_to_visas.items(): gen3_user = get_or_create_gen3_user_from_iss_sub(issuer, subject_id) @@ -108,6 +121,8 @@ def validate_visa(raw_visa): issuers=config.get("GA4GH_VISA_ISSUER_ALLOWLIST", []), options={"require_iat": True, "require_exp": True, "verify_aud": False}, ) + # TODO log jti? + # TODO log txn? for claim in ["sub", "ga4gh_visa_v1"]: if claim not in decoded_visa: raise Exception(f'Visa does not contain REQUIRED "{claim}" claim') @@ -129,11 +144,12 @@ def validate_visa(raw_visa): if "conditions" in decoded_visa["ga4gh_visa_v1"]: logger.warning( - 'Condition checking is not yet supported, but a visa was received that contained the "conditions" field' + 'condition checking is not yet supported, but a visa was received that contained the "conditions" field' ) if decoded_visa["ga4gh_visa_v1"]["conditions"]: raise Exception('"conditions" field in "ga4gh_visa_v1" is not empty') + logger.info("visa was successfully validated") return decoded_visa @@ -142,7 +158,12 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get( (issuer, subject_id) ) - if not (iss_sub_pair_to_user and iss_sub_pair_to_user.user): + if not iss_sub_pair_to_user: + logger.info( + "creating a new Fence user with a username formed from subject " + "id and issuer. mapping subject id and issuer combination to " + "said user" + ) username = subject_id + issuer[len("https://") :] gen3_user = User(username=username) idp_name = flask.current_app.issuer_to_idp.get(issuer) diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 1ecc76bdb..71b231830 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -207,7 +207,10 @@ def get_user_id(self, code): email = userinfo.get("email") issuer = self.get_value_from_discovery_doc("issuer") subject_id = userinfo.get("sub") - # TODO log error message if issuer, subject_id not available + if not issuer or not subject_id: + err_msg = "Could not determine both issuer and subject id" + self.logger.error(err_msg) + return {"error": err_msg} username = self.map_iss_sub_pair_to_user( issuer, subject_id, username, email ) @@ -223,30 +226,39 @@ def get_user_id(self, code): return {"username": username, "email": email} - @staticmethod - def map_iss_sub_pair_to_user(issuer, subject_id, username, email): + def map_iss_sub_pair_to_user(self, issuer, subject_id, username, email): with flask.current_app.db.session as db_session: iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get( (issuer, subject_id) ) user = query_for_user(db_session, username) if iss_sub_pair_to_user: - # only change the username if there exists one user created - # from the DRS endpoint. two users means that a user logged - # in through RAS, didn't get sub mapped, accessed the DRS - # endpoint, and is now logging in again, in which case we - # render the user created from the first login stale, and - # choose to proceed with the user created from the DRS - # endpoint - # TODO improve comment - if not user and iss_sub_pair_to_user.user.username != username: - # TODO change username in Arborist + if not user: + self.logger.info( + "Issuer and subject id have already been mapped to a " + "Fence user created from the DRS/data endpoints. " + "Changing said user's username to the username " + "returned from the RAS userinfo endpoint." + ) + # TODO also change username in Arborist iss_sub_pair_to_user.user.username = username iss_sub_pair_to_user.user.email = email db_session.commit() + elif iss_sub_pair_to_user.user.username != username: + self.logger.warning( + "Two users exist in the Fence database corresponding " + "to the RAS user who is currently trying to log in: one " + "created from an earlier login and one created from " + "the DRS/data endpoints. The one created from the " + "DRS/data endpoints will be logged in, rendering the " + "other one inaccessible." + ) return iss_sub_pair_to_user.user.username if not user: + self.logger.info( + "Creating a user in the Fence database before mapping issuer and subject id" + ) user = User(username=username, email=email) idp = ( db_session.query(IdentityProvider) @@ -258,6 +270,7 @@ def map_iss_sub_pair_to_user(issuer, subject_id, username, email): user.identity_provider = idp db_session.add(user) + self.logger.info("Mapping issuer and subject id to Fence user") iss_sub_pair_to_user = IssSubPairToUser(iss=issuer, sub=subject_id) iss_sub_pair_to_user.user = user db_session.add(iss_sub_pair_to_user) From 3943334679f23a5bbe55061104a86a254059809b Mon Sep 17 00:00:00 2001 From: John McCann Date: Sun, 7 Nov 2021 17:02:57 -0800 Subject: [PATCH 069/211] chore(RAS client init): get iss from discovery doc --- fence/__init__.py | 12 +++++++++--- fence/resources/ga4gh/passports.py | 1 - 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/fence/__init__.py b/fence/__init__.py index 0ef4e1088..5840b0e1c 100755 --- a/fence/__init__.py +++ b/fence/__init__.py @@ -425,9 +425,15 @@ def _setup_oidc_clients(app): HTTP_PROXY=config.get("HTTP_PROXY"), logger=logger, ) - # TODO maybe get this from discovery doc or from the config file - split_url = urlparse(app.ras_client.discovery_url) - issuer = f"{split_url.scheme}://{split_url.netloc}" + issuer = app.ras_client.get_value_from_discovery_doc("issuer", "") + if not issuer: + logger.warn( + "Unable to determine RAS issuer from discovery doc. Instead, " + "RAS issuer will be set to the base url of the RAS " + "client's discovery url." + ) + split_url = urlparse(app.ras_client.discovery_url) + issuer = f"{split_url.scheme}://{split_url.netloc}" app.issuer_to_idp = {issuer: IdentityProvider.ras} # Add OIDC client for Synapse if configured. diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 2b5233286..408e7a3ab 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -44,7 +44,6 @@ def get_gen3_users_from_ga4gh_passports(passports): continue identity_to_visas = collections.defaultdict(list) - # TODO need to subtract 5 minutes min_visa_expiration = int(time.time()) + datetime.timedelta(hours=1).seconds for raw_visa in raw_visas: try: From 06729f5eb2483f6ed8d6ef7b985d85a4d1c209a5 Mon Sep 17 00:00:00 2001 From: John McCann Date: Sun, 7 Nov 2021 23:37:19 -0800 Subject: [PATCH 070/211] fix(ga4gh): make miscellaneous small changes --- fence/resources/ga4gh/passports.py | 8 ++++++-- fence/resources/openid/ras_oauth2.py | 11 +++++++++-- fence/sync/sync_users.py | 2 +- tests/dbgap_sync/conftest.py | 12 ++++++------ tests/ras/test_ras.py | 12 ++++++------ 5 files changed, 28 insertions(+), 17 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 7ae6a8e4e..a86bf5b17 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -44,6 +44,9 @@ def get_gen3_users_from_ga4gh_passports(passports): logger.warning(f"invalid passport provided, ignoring. Error: {exc}") continue + if not raw_visas: + continue + identity_to_visas = collections.defaultdict(list) min_visa_expiration = int(time.time()) + datetime.timedelta(hours=1).seconds for raw_visa in raw_visas: @@ -66,7 +69,7 @@ def get_gen3_users_from_ga4gh_passports(passports): "DELETE_EXPIRED_GOOGLE_ACCESS_JOB_FREQUENCY_IN_SECONDS", 300 ) min_visa_expiration -= delete_expired_google_access_job_frequency - if min_visa_expiration <= int(time.now()): + if min_visa_expiration <= int(time.time()): logger.warning( "the passport's minimum visa expiration time fell within " f"{delete_expired_google_access_job_frequency} seconds of now, " @@ -97,6 +100,7 @@ def get_gen3_users_from_ga4gh_passports(passports): put_gen3_usernames_for_passport_into_cache( passport, usernames_from_current_passport ) + usernames_from_all_passports.extend(usernames_from_current_passport) return list(set(usernames_from_all_passports)) @@ -236,7 +240,7 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): db_session.add(iss_sub_pair_to_user) db_session.commit() - return iss_sub_pair_to_user.user + return iss_sub_pair_to_user.user def sync_visa_authorization(gen3_user, ga4gh_visas, expiration): diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index e30b460e4..04e2802b4 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -1,6 +1,10 @@ import backoff import flask import requests + +# the whole passports module is imported to avoid issue with circular imports +import fence.resources.ga4gh.passports + from flask_sqlalchemy_session import current_session from jose import jwt as jose_jwt @@ -17,7 +21,6 @@ query_for_user, ) from fence.jwt.validate import validate_jwt -from fence.resources.ga4gh.passports import get_unvalidated_visas_from_valid_passport from fence.utils import DEFAULT_BACKOFF_SETTINGS from .idp_oauth2 import Oauth2ClientBase @@ -91,7 +94,11 @@ def get_encoded_visas_v11_userinfo(self, userinfo, pkey_cache=None): list: list of encoded GA4GH visas """ encoded_passport = userinfo.get("passport_jwt_v11") - return get_unvalidated_visas_from_valid_passport(encoded_passport, pkey_cache) + return ( + fence.resources.ga4gh.passports.get_unvalidated_visas_from_valid_passport( + encoded_passport, pkey_cache + ) + ) def get_user_id(self, code): diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index a58ddb13f..73b458233 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -2122,7 +2122,7 @@ def sync_visas(self): self._sync_visas(s) # if returns with some failure use telemetry file - def sync_single_user_visas(self, user, sess=None, expires=None): + def sync_single_user_visas(self, user, ga4gh_visas, sess=None, expires=None): """ TODO update docstring Sync a single user's visa during login diff --git a/tests/dbgap_sync/conftest.py b/tests/dbgap_sync/conftest.py index 3a3e254da..c9243910b 100644 --- a/tests/dbgap_sync/conftest.py +++ b/tests/dbgap_sync/conftest.py @@ -232,7 +232,7 @@ def add_visa_manually(db_session, user, rsa_private_key, kid): "participant_set": "p1", "consent_group": "c1", "role": "designated user", - "expiration": "2020-11-14 00:00:00", + "expiration": int(time.time()) + 1001, }, { "consent_name": "General Research Use (IRB, PUB)", @@ -241,7 +241,7 @@ def add_visa_manually(db_session, user, rsa_private_key, kid): "participant_set": "p1", "consent_group": "c1", "role": "designated user", - "expiration": "2020-11-14 00:00:00", + "expiration": int(time.time()) + 1001, }, { "consent_name": "Disease-Specific (Cardiovascular Disease)", @@ -250,7 +250,7 @@ def add_visa_manually(db_session, user, rsa_private_key, kid): "participant_set": "p1", "consent_group": "c1", "role": "designated user", - "expiration": "2020-11-14 00:00:00", + "expiration": int(time.time()) + 1001, }, { "consent_name": "Health/Medical/Biomedical (IRB)", @@ -259,7 +259,7 @@ def add_visa_manually(db_session, user, rsa_private_key, kid): "participant_set": "p2", "consent_group": "c3", "role": "designated user", - "expiration": "2020-11-14 00:00:00", + "expiration": int(time.time()) + 1001, }, { "consent_name": "Disease-Specific (Focused Disease Only, IRB, NPU)", @@ -268,7 +268,7 @@ def add_visa_manually(db_session, user, rsa_private_key, kid): "participant_set": "p2", "consent_group": "c2", "role": "designated user", - "expiration": "2020-11-14 00:00:00", + "expiration": int(time.time()) + 1001, }, { "consent_name": "Disease-Specific (Autism Spectrum Disorder)", @@ -277,7 +277,7 @@ def add_visa_manually(db_session, user, rsa_private_key, kid): "participant_set": "p3", "consent_group": "c1", "role": "designated user", - "expiration": "2020-11-14 00:00:00", + "expiration": int(time.time()) + 1001, }, ], } diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index 7f5cc81f4..471d8aaae 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -661,9 +661,9 @@ def test_map_iss_sub_pair_to_user_with_no_prior_DRS_access(db_session): iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() assert len(iss_sub_pair_to_user_records) == 0 - username_to_login = ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) + username_to_log_in = ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) - assert username_to_login == username + assert username_to_log_in == username iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((iss, sub)) assert iss_sub_pair_to_user.user.username == username assert iss_sub_pair_to_user.user.email == email @@ -689,9 +689,9 @@ def test_map_iss_sub_pair_to_user_with_prior_DRS_access(db_session): iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((iss, sub)) assert iss_sub_pair_to_user.user.username == "123_abcdomain.tld" - username_to_login = ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) + username_to_log_in = ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) - assert username_to_login == username + assert username_to_log_in == username iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() assert len(iss_sub_pair_to_user_records) == 1 iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((iss, sub)) @@ -717,7 +717,7 @@ def test_map_iss_sub_pair_to_user_with_prior_login_and_prior_DRS_access( db_session.commit() get_or_create_gen3_user_from_iss_sub(iss, sub) - username_to_login = ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) - assert username_to_login == "123_abcdomain.tld" + username_to_log_in = ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) + assert username_to_log_in == "123_abcdomain.tld" iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((iss, sub)) assert iss_sub_pair_to_user.user.username == "123_abcdomain.tld" From 39b886001ffc59ea620d394494c65c53ea24561f Mon Sep 17 00:00:00 2001 From: John McCann Date: Mon, 8 Nov 2021 06:32:35 -0800 Subject: [PATCH 071/211] fix(RAS client): provide default when getting iss --- fence/resources/openid/ras_oauth2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 04e2802b4..4d441a7ba 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -150,7 +150,7 @@ def get_user_id(self, code): self.logger.info("Using {} field as username.".format(field_name)) email = userinfo.get("email") - issuer = self.get_value_from_discovery_doc("issuer") + issuer = self.get_value_from_discovery_doc("issuer", "") subject_id = userinfo.get("sub") if not issuer or not subject_id: err_msg = "Could not determine both issuer and subject id" From d38c08ecd2bc7abef8835715c4a784cbea46b081 Mon Sep 17 00:00:00 2001 From: John McCann Date: Tue, 9 Nov 2021 12:11:19 -0800 Subject: [PATCH 072/211] docs(ga4gh): add docstrings --- fence/resources/ga4gh/passports.py | 59 +++++++++++++++++++++++++++- fence/resources/openid/ras_oauth2.py | 21 ++++++++++ fence/sync/sync_users.py | 17 ++++++-- tests/ga4gh/test_ga4gh.py | 10 +++++ tests/ras/test_ras.py | 22 +++++++++++ 5 files changed, 124 insertions(+), 5 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index a86bf5b17..6430900d6 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -26,6 +26,23 @@ def get_gen3_users_from_ga4gh_passports(passports): + """ + Validate passports and embedded visas, using each valid visa's identity + established by combination to possibly create and definitely + determine a Fence user whose username is added to the list returned by + this function. In the process of determining Fence users from visas, visa + authorization information is also persisted in Fence and synced to + Arborist. + + Args: + passports (list): a list of raw encoded passport strings, each + including header, payload, and signature + + Return: + list: a list of strings, each being the username of a Fence user who + corresponds to a valid visa identity embedded within the passports + passed in. + """ logger.info("getting gen3 users from passports") usernames_from_all_passports = [] for passport in passports: @@ -168,6 +185,18 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): def validate_visa(raw_visa): + """ + Validate a raw visa in accordance with: + - GA4GH AAI spec (https://github.com/ga4gh/data-security/blob/master/AAI/AAIConnectProfile.md) + - GA4GH DURI spec (https://github.com/ga4gh-duri/ga4gh-duri.github.io/blob/master/researcher_ids/ga4gh_passport_v1.md) + + Args: + raw_visa (str): a raw, encoded visa including header, payload, and signature + + Return: + dict: the decoded payload if validation was successful. an exception + is raised if validation was unsuccessful + """ # TODO check that there is no JKU field in header? decoded_visa = validate_jwt( raw_visa, @@ -210,6 +239,19 @@ def validate_visa(raw_visa): def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): + """ + Get a user from the Fence database corresponding to the visa identity + indicated by the combination. If a Fence user has + not yet been created for the given combination, + create and return such a user. + + Args: + issuer (str): the issuer of a given visa + subject_id (str): the subject of a given visa + + Return: + userdatamodel.user.User: the Fence user corresponding to issuer and subject_id + """ with flask.current_app.db.session as db_session: iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get( (issuer, subject_id) @@ -244,6 +286,22 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): def sync_visa_authorization(gen3_user, ga4gh_visas, expiration): + """ + Wrapper around UserSyncer.sync_single_user_visas method, which parses + authorization information from the provided visas, persists it in Fence, + and syncs it to Arborist. + + Args: + gen3_user (userdatamodel.user.User): the Fence user whose visas' + authz info is being synced + ga4gh_visas (list): a list of fence.models.GA4GHVisaV1 objects + that are parsed and synced + expiration (int): time at which synced Arborist policies and + inclusion in any GBAG are set to expire + + Return: + None + """ arborist_client = ArboristClient( arborist_base_url=config.get("ARBORIST"), logger=logger, authz_provider="GA4GH" ) @@ -262,7 +320,6 @@ def sync_visa_authorization(gen3_user, ga4gh_visas, expiration): ) with flask.current_app.db.session as db_session: - # TODO set expiration for Google Access syncer.sync_single_user_visas( gen3_user, ga4gh_visas, db_session, expires=expiration ) diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 4d441a7ba..fe4077e42 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -172,6 +172,27 @@ def get_user_id(self, code): return {"username": username, "email": email} def map_iss_sub_pair_to_user(self, issuer, subject_id, username, email): + """ + Map combination to a Fence user whose username + equals the username argument passed into this function. + + One exception to this is when two Fence users exist who both + correspond to the user who is trying to log in. Please see logged + warning for more details. + + Args: + issuer (str): RAS issuer + subject_id (str): RAS subject + username (str): username of the Fence user who is being mapped to + email (str): email to populate the mapped Fence user with in cases + when this function creates the mapped user or changes + its username + + Return: + str: username that should be logged in. this will be equal to + username that was passed in in all cases except for the + exception noted above + """ with flask.current_app.db.session as db_session: iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get( (issuer, subject_id) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 73b458233..90bb62d20 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -1767,8 +1767,6 @@ def _update_authz_in_arborist( if user_yaml: for policy in user_yaml.policies.get(username, []): self.arborist_client.grant_user_policy(username, policy) - # TODO need to add expiration to this function in gen3authz - # self.arborist_client.grant_user_policy(username, policy, expiration=expiration) if user_yaml: for client_name, client_details in user_yaml.clients.items(): @@ -2124,8 +2122,19 @@ def sync_visas(self): def sync_single_user_visas(self, user, ga4gh_visas, sess=None, expires=None): """ - TODO update docstring - Sync a single user's visa during login + Sync a single user's visas during login or DRS/data access + + Args: + user (userdatamodel.user.User): Fence user whose visas' + authz info is being synced + ga4gh_visas (list): a list of fence.models.GA4GHVisaV1 objects + that are parsed and synced + sess (sqlalchemy.orm.session.Session): database session + expires (int): time at which synced Arborist policies and + inclusion in any GBAG are set to expire + + Return: + None """ self.ras_sync_client = RASVisa(logger=self.logger) dbgap_config = self.dbGaP[0] diff --git a/tests/ga4gh/test_ga4gh.py b/tests/ga4gh/test_ga4gh.py index f18481a65..587da5bde 100644 --- a/tests/ga4gh/test_ga4gh.py +++ b/tests/ga4gh/test_ga4gh.py @@ -12,6 +12,11 @@ def test_get_or_create_gen3_user_from_iss_sub_without_prior_login(db_session): + """ + Test get_or_create_gen3_user_from_iss_sub when the visa's + combination are not present in the mapping table beforehand (i.e. the user + has not previously logged in) + """ iss = "https://sts.nih.gov" sub = "123_abc" @@ -25,6 +30,11 @@ def test_get_or_create_gen3_user_from_iss_sub_without_prior_login(db_session): def test_get_or_create_gen3_user_from_iss_sub_after_prior_login(db_session): + """ + Test get_or_create_gen3_user_from_iss_sub when the visa's + combination are present in the mapping table beforehand (i.e. the user + has previously logged in) + """ iss = "https://sts.nih.gov" sub = "123_abc" username = "johnsmith" diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index 471d8aaae..6f3faee62 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -646,6 +646,12 @@ def test_visa_update_cronjob( def test_map_iss_sub_pair_to_user_with_no_prior_DRS_access(db_session): + """ + Test RASOauth2Client.map_iss_sub_pair_to_user when the username passed in + (e.g. eRA username) does not already exist in the Fence database and that + user's combination has not already been mapped through a prior + DRS/data access request. + """ iss = "https://domain.tld" sub = "123_abc" username = "johnsmith" @@ -672,6 +678,14 @@ def test_map_iss_sub_pair_to_user_with_no_prior_DRS_access(db_session): def test_map_iss_sub_pair_to_user_with_prior_DRS_access(db_session): + """ + Test RASOauth2Client.map_iss_sub_pair_to_user when the username passed in + (e.g. eRA username) does not already exist in the Fence database but that + user's combination has already been mapped to an existing user + created during a prior DRS/data access request. In this case, that + existing user's username is changed from sub+iss to the username passed + in. + """ iss = "https://domain.tld" sub = "123_abc" username = "johnsmith" @@ -702,6 +716,14 @@ def test_map_iss_sub_pair_to_user_with_prior_DRS_access(db_session): def test_map_iss_sub_pair_to_user_with_prior_login_and_prior_DRS_access( db_session, ): + """ + Test RASOauth2Client.map_iss_sub_pair_to_user when the username passed in + (e.g. eRA username) already exists in the Fence database and that + user's combination has already been mapped to a separate user + created during a prior DRS/data access request. In this case, + map_iss_sub_pair_to_user returns the user created from prior DRS/data + access, rendering the other user (e.g. the eRA one) inaccessible. + """ iss = "https://domain.tld" sub = "123_abc" username = "johnsmith" From 254ccc6ccf55cbfea1927cd846ccfafd0ac8c219 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Thu, 11 Nov 2021 11:12:20 -0600 Subject: [PATCH 073/211] chore(tests): generalize assertion logic to look for only expected fields --- tests/login/test_microsoft_login.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/login/test_microsoft_login.py b/tests/login/test_microsoft_login.py index 43aaf2c40..23343a7d2 100755 --- a/tests/login/test_microsoft_login.py +++ b/tests/login/test_microsoft_login.py @@ -24,7 +24,8 @@ def test_get_user_id(microsoft_oauth2_client): return_value=return_value, ): user_id = microsoft_oauth2_client.get_user_id(code="123") - assert user_id == expected_value # nosec + for key, value in expected_value.items(): + assert return_value[key] == value def test_get_user_id_missing_claim(microsoft_oauth2_client): From 1de81cefb2738d911bf7ed66f29fa6a2e306d01e Mon Sep 17 00:00:00 2001 From: John McCann Date: Thu, 11 Nov 2021 21:50:21 -0800 Subject: [PATCH 074/211] style(ga4gh): use brackets to access config object --- fence/resources/ga4gh/passports.py | 10 +++++----- tests/ga4gh/test_ga4gh.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 6430900d6..69dc3f78a 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -203,7 +203,7 @@ def validate_visa(raw_visa): attempt_refresh=True, scope={"openid", "ga4gh_passport_v1"}, require_purpose=False, - issuers=config.get("GA4GH_VISA_ISSUER_ALLOWLIST", []), + issuers=config["GA4GH_VISA_ISSUER_ALLOWLIST"], options={"require_iat": True, "require_exp": True, "verify_aud": False}, ) # TODO log jti? @@ -215,7 +215,7 @@ def validate_visa(raw_visa): if "aud" in decoded_visa: raise Exception('Visa MUST NOT contain "aud" claim') - field_to_expected_value = config.get("GA4GH_VISA_V1_CLAIM_REQUIRED_FIELDS") + field_to_expected_value = config["GA4GH_VISA_V1_CLAIM_REQUIRED_FIELDS"] for field, expected_value in field_to_expected_value.items(): if field not in decoded_visa["ga4gh_visa_v1"]: raise Exception( @@ -303,13 +303,13 @@ def sync_visa_authorization(gen3_user, ga4gh_visas, expiration): None """ arborist_client = ArboristClient( - arborist_base_url=config.get("ARBORIST"), logger=logger, authz_provider="GA4GH" + arborist_base_url=config["ARBORIST"], logger=logger, authz_provider="GA4GH" ) - dbgap_config = os.environ.get("dbGaP") or config.get("dbGaP") + dbgap_config = os.environ.get("dbGaP") or config["dbGaP"] if not isinstance(dbgap_config, list): dbgap_config = [dbgap_config] - DB = os.environ.get("FENCE_DB") or config.get("DB") + DB = os.environ.get("FENCE_DB") or config["DB"] if DB is None: try: from fence.settings import DB diff --git a/tests/ga4gh/test_ga4gh.py b/tests/ga4gh/test_ga4gh.py index 587da5bde..b84a31950 100644 --- a/tests/ga4gh/test_ga4gh.py +++ b/tests/ga4gh/test_ga4gh.py @@ -39,10 +39,10 @@ def test_get_or_create_gen3_user_from_iss_sub_after_prior_login(db_session): sub = "123_abc" username = "johnsmith" email = "johnsmith@domain.tld" - oidc = config.get("OPENID_CONNECT", {}) + oidc = config["OPENID_CONNECT"] ras_client = RASOauth2Client( oidc["ras"], - HTTP_PROXY=config.get("HTTP_PROXY"), + HTTP_PROXY=config["HTTP_PROXY"], logger=logger, ) ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) From 1c7c225614f69f9fbd7ae01ae836ce5e12365921 Mon Sep 17 00:00:00 2001 From: John McCann Date: Thu, 11 Nov 2021 22:46:12 -0800 Subject: [PATCH 075/211] chore(ga4gh): rename var for authz removal job frq --- fence/config-default.yaml | 14 +++++++++----- fence/resources/ga4gh/passports.py | 18 ++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/fence/config-default.yaml b/fence/config-default.yaml index 6e668326d..5d285a91e 100755 --- a/fence/config-default.yaml +++ b/fence/config-default.yaml @@ -864,6 +864,14 @@ ASSUME_ROLE_CACHE_SECONDS: 1800 # will have access to download data. REGISTER_USERS_ON: false REGISTERED_USERS_GROUP: '' + +# Number of projects that can be registered to a Google Service Account +SERVICE_ACCOUNT_LIMIT: 6 + +# ////////////////////////////////////////////////////////////////////////////////////// +# GA4GH SUPPORT: DATA ACCESS AND AUTHORIZATION SYNCING +# ////////////////////////////////////////////////////////////////////////////////////// + # RAS refresh_tokens expire in 15 days RAS_REFRESH_EXPIRATION: 1296000 # List of JWT issuers from which Fence will accept GA4GH visas @@ -876,11 +884,7 @@ GA4GH_VISA_V1_CLAIM_REQUIRED_FIELDS: asserted: 0 value: "https://stsstg.nih.gov/passport/dbgap/v1.1" source: "https://ncbi.nlm.nih.gov/gap" -DELETE_EXPIRED_GOOGLE_ACCESS_JOB_FREQUENCY_IN_SECONDS: 300 - -# Number of projects that can be registered to a Google Service Account -SERVICE_ACCOUNT_LIMIT: 6 - +EXPIRED_AUTHZ_REMOVAL_JOB_FREQ_IN_SECONDS: 300 # Global sync visas during login # None(Default): Allow per client i.e. a fence client can pick whether or not to sync their visas during login with parse_visas param in /authorization endpoint # True: Parse for all clients i.e. a fence client will always sync their visas during login diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 69dc3f78a..8c0d2df16 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -61,6 +61,9 @@ def get_gen3_users_from_ga4gh_passports(passports): logger.warning(f"invalid passport provided, ignoring. Error: {exc}") continue + # an empty raw_visas list means that either the current passport is + # invalid or that it has no visas. in both cases, the current passport + # is ignored and we move on to the next passport if not raw_visas: continue @@ -82,16 +85,15 @@ def get_gen3_users_from_ga4gh_passports(passports): logger.warning(f"invalid visa provided, ignoring. Error: {exc}") continue - delete_expired_google_access_job_frequency = config.get( - "DELETE_EXPIRED_GOOGLE_ACCESS_JOB_FREQUENCY_IN_SECONDS", 300 - ) - min_visa_expiration -= delete_expired_google_access_job_frequency + expired_authz_removal_job_freq_in_seconds = config[ + "EXPIRED_AUTHZ_REMOVAL_JOB_FREQ_IN_SECONDS" + ] + min_visa_expiration -= expired_authz_removal_job_freq_in_seconds if min_visa_expiration <= int(time.time()): logger.warning( - "the passport's minimum visa expiration time fell within " - f"{delete_expired_google_access_job_frequency} seconds of now, " - "which is the frequency of the delete_expired_google_access job. " - "for this reason, the passport will be ignored" + "the passport's earliest valid visa expiration time is set to " + f"occur within {expired_authz_removal_job_freq_in_seconds} " + "seconds from now, which is too soon an expiration to handle." ) continue From 77befca2be3f382b05c785afedd17bc6acdda745 Mon Sep 17 00:00:00 2001 From: John McCann Date: Fri, 12 Nov 2021 08:26:15 -0800 Subject: [PATCH 076/211] chore(ga4gh): use list for allowed values config --- fence/config-default.yaml | 11 +++++++---- fence/resources/ga4gh/passports.py | 14 +++++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/fence/config-default.yaml b/fence/config-default.yaml index 5d285a91e..41dcdde4b 100755 --- a/fence/config-default.yaml +++ b/fence/config-default.yaml @@ -880,10 +880,13 @@ GA4GH_VISA_ISSUER_ALLOWLIST: - 'https://sts.nih.gov' - 'https://stsstg.nih.gov' GA4GH_VISA_V1_CLAIM_REQUIRED_FIELDS: - type: "https://ras.nih.gov/visas/v1.1" - asserted: 0 - value: "https://stsstg.nih.gov/passport/dbgap/v1.1" - source: "https://ncbi.nlm.nih.gov/gap" + type: + - "https://ras.nih.gov/visas/v1" + - "https://ras.nih.gov/visas/v1.1" + value: + - "https://stsstg.nih.gov/passport/dbgap/v1.1" + source: + - "https://ncbi.nlm.nih.gov/gap" EXPIRED_AUTHZ_REMOVAL_JOB_FREQ_IN_SECONDS: 300 # Global sync visas during login # None(Default): Allow per client i.e. a fence client can pick whether or not to sync their visas during login with parse_visas param in /authorization endpoint diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 8c0d2df16..f96c9d862 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -217,17 +217,17 @@ def validate_visa(raw_visa): if "aud" in decoded_visa: raise Exception('Visa MUST NOT contain "aud" claim') - field_to_expected_value = config["GA4GH_VISA_V1_CLAIM_REQUIRED_FIELDS"] - for field, expected_value in field_to_expected_value.items(): + field_to_allowed_values = config["GA4GH_VISA_V1_CLAIM_REQUIRED_FIELDS"] + for field, allowed_values in field_to_allowed_values.items(): if field not in decoded_visa["ga4gh_visa_v1"]: raise Exception( f'"ga4gh_visa_v1" claim does not contain REQUIRED "{field}" field' ) - if expected_value: - if decoded_visa["ga4gh_visa_v1"][field] != expected_value: - raise Exception( - f'"{field}" field in "ga4gh_visa_v1" does not equal expected value "{expected_value}"' - ) + if decoded_visa["ga4gh_visa_v1"][field] not in allowed_values: + raise Exception( + f'"{field}" field in "ga4gh_visa_v1" is not equal to one of the allowed_values: {allowed_values}' + ) + # TODO special processing for asserted field if "conditions" in decoded_visa["ga4gh_visa_v1"]: logger.warning( From 60a7ceaec3b5d0a000747253eb35277092d1d5e7 Mon Sep 17 00:00:00 2001 From: John McCann Date: Fri, 12 Nov 2021 10:25:25 -0800 Subject: [PATCH 077/211] feat(ga4gh DRS endpoint): accept POSTing passports --- fence/blueprints/data/blueprint.py | 12 ++---------- fence/blueprints/ga4gh.py | 7 +++++++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/fence/blueprints/data/blueprint.py b/fence/blueprints/data/blueprint.py index 2f9004089..d19460421 100755 --- a/fence/blueprints/data/blueprint.py +++ b/fence/blueprints/data/blueprint.py @@ -297,21 +297,13 @@ def upload_file(file_id): return flask.jsonify(result) -@blueprint.route("/download/", methods=["GET", "POST"]) +@blueprint.route("/download/", methods=["GET"]) @enable_audit_logging def download_file(file_id): """ Get a presigned url to download a file given by file_id. """ - ga4gh_passports = None - if flask.request.method == "POST": - ga4gh_passports = flask.request.get_json(force=True, silent=True).get( - config["GA4GH_DRS_POSTED_PASSPORT_FIELD"] - ) - - result = get_signed_url_for_file( - "download", file_id, ga4gh_passports=ga4gh_passports - ) + result = get_signed_url_for_file("download", file_id) if not "redirect" in flask.request.args or not "url" in result: return flask.jsonify(result) return flask.redirect(result["url"]) diff --git a/fence/blueprints/ga4gh.py b/fence/blueprints/ga4gh.py index 7e1f86569..8a8e65e3a 100644 --- a/fence/blueprints/ga4gh.py +++ b/fence/blueprints/ga4gh.py @@ -23,9 +23,16 @@ def get_ga4gh_signed_url(object_id, access_id): if not access_id: raise UserError("Access ID/Protocol is required.") + ga4gh_passports = None + if flask.request.method == "POST": + ga4gh_passports = flask.request.get_json(force=True, silent=True).get( + config["GA4GH_DRS_POSTED_PASSPORT_FIELD"] + ) + result = get_signed_url_for_file( "download", object_id, requested_protocol=access_id, + ga4gh_passports=ga4gh_passports, ) return flask.jsonify(result) From 2c26fced87d0a00f80fd98e77ed0309431a5f5b8 Mon Sep 17 00:00:00 2001 From: John McCann Date: Fri, 12 Nov 2021 12:30:56 -0800 Subject: [PATCH 078/211] chore(GA4GH visa validation): check asserted field --- fence/resources/ga4gh/passports.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index f96c9d862..799b4587a 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -227,7 +227,23 @@ def validate_visa(raw_visa): raise Exception( f'"{field}" field in "ga4gh_visa_v1" is not equal to one of the allowed_values: {allowed_values}' ) - # TODO special processing for asserted field + + if "asserted" not in decoded_visa["ga4gh_visa_v1"]: + raise Exception( + '"ga4gh_visa_v1" claim does not contain REQUIRED "asserted" field' + ) + asserted = decoded_visa["ga4gh_visa_v1"]["asserted"] + if type(asserted) not in (int, float): + raise Exception( + '"ga4gh_visa_v1" claim object\'s "asserted" field\'s type is not ' + "JSON numeric" + ) + if decoded_visa["iat"] < asserted: + raise Exception( + "the Passport Visa Assertion Source made the claim after the visa " + 'was minted (i.e. "ga4gh_visa_v1" claim object\'s "asserted" ' + 'field is greater than the visa\'s "iat" claim)' + ) if "conditions" in decoded_visa["ga4gh_visa_v1"]: logger.warning( From 4d06991a8541b013bc8ce797475d3edbf0f8b026 Mon Sep 17 00:00:00 2001 From: John McCann Date: Fri, 12 Nov 2021 14:24:18 -0800 Subject: [PATCH 079/211] refactor(ga4gh): return [users], not [usernames] --- fence/blueprints/ga4gh.py | 1 + fence/resources/ga4gh/passports.py | 28 ++++++++++++++++++---------- fence/resources/openid/ras_oauth2.py | 1 + 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/fence/blueprints/ga4gh.py b/fence/blueprints/ga4gh.py index 8a8e65e3a..f4e9b6f45 100644 --- a/fence/blueprints/ga4gh.py +++ b/fence/blueprints/ga4gh.py @@ -1,6 +1,7 @@ import flask from flask import request from fence.errors import UserError +from fence.config import config from fence.blueprints.data.indexd import ( get_signed_url_for_file, diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 799b4587a..696e21bd9 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -44,13 +44,14 @@ def get_gen3_users_from_ga4gh_passports(passports): passed in. """ logger.info("getting gen3 users from passports") - usernames_from_all_passports = [] + users_from_all_passports = [] + user_ids_from_all_passports = [] for passport in passports: try: # TODO check cache - cached_usernames = get_gen3_usernames_for_passport_from_cache(passport) - if cached_usernames: - usernames_from_all_passports.extend(cached_usernames) + cached_user_ids = get_gen3_user_ids_for_passport_from_cache(passport) + if cached_user_ids: + user_ids_from_all_passports.extend(cached_user_ids) # existence in the cache means that this passport was validated # previously continue @@ -97,7 +98,7 @@ def get_gen3_users_from_ga4gh_passports(passports): ) continue - usernames_from_current_passport = [] + users_from_current_passport = [] for (issuer, subject_id), visas in identity_to_visas.items(): gen3_user = get_or_create_gen3_user_from_iss_sub(issuer, subject_id) @@ -114,17 +115,24 @@ def get_gen3_users_from_ga4gh_passports(passports): ] # NOTE: does not validate, assumes validation occurs above. sync_visa_authorization(gen3_user, ga4gh_visas, min_visa_expiration) - usernames_from_current_passport.append(gen3_user.username) + users_from_current_passport.append(gen3_user) put_gen3_usernames_for_passport_into_cache( - passport, usernames_from_current_passport + passport, users_from_current_passport ) - usernames_from_all_passports.extend(usernames_from_current_passport) + users_from_all_passports.extend(users_from_current_passport) - return list(set(usernames_from_all_passports)) + # TODO use user_ids_from_all_passports that were returned from cache to + # query db for users and add those queried users to + # users_from_all_passports + # the same user could have been added to users_from_all_passports more + # than one time, making the dictionary comprehension below necessary to + # return a list of unique users + return list({u.username: u for u in users_from_all_passports}.values()) -def get_gen3_usernames_for_passport_from_cache(passport): + +def get_gen3_user_ids_for_passport_from_cache(passport): cached_user_ids = [] # TODO return cached_user_ids diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index fe4077e42..564c51d8a 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -200,6 +200,7 @@ def map_iss_sub_pair_to_user(self, issuer, subject_id, username, email): user = query_for_user(db_session, username) if iss_sub_pair_to_user: if not user: + # TODO just say DRS, not DRS/data self.logger.info( "Issuer and subject id have already been mapped to a " "Fence user created from the DRS/data endpoints. " From 24f79cd38e636a2ab02c431be13659500484d654 Mon Sep 17 00:00:00 2001 From: John McCann Date: Fri, 12 Nov 2021 18:55:56 -0800 Subject: [PATCH 080/211] chore(GA4GH visa validation): check "JKU" absent --- fence/resources/ga4gh/passports.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 696e21bd9..6e8f6492e 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -3,6 +3,7 @@ import collections import time import datetime +import jwt # the whole fence_create module is imported to avoid issue with circular imports import fence.scripting.fence_create @@ -207,7 +208,12 @@ def validate_visa(raw_visa): dict: the decoded payload if validation was successful. an exception is raised if validation was unsuccessful """ - # TODO check that there is no JKU field in header? + if jwt.get_unverified_header(raw_visa).get("jku"): + raise Exception( + "Visa Document Tokens are not currently supported by passing " + '"jku" in the header. Only Visa Access Tokens are supported.' + ) + decoded_visa = validate_jwt( raw_visa, attempt_refresh=True, @@ -216,8 +222,6 @@ def validate_visa(raw_visa): issuers=config["GA4GH_VISA_ISSUER_ALLOWLIST"], options={"require_iat": True, "require_exp": True, "verify_aud": False}, ) - # TODO log jti? - # TODO log txn? for claim in ["sub", "ga4gh_visa_v1"]: if claim not in decoded_visa: raise Exception(f'Visa does not contain REQUIRED "{claim}" claim') From 44108f3c23593a08ef7d8aa79651dc6d6c1b386d Mon Sep 17 00:00:00 2001 From: John McCann Date: Fri, 12 Nov 2021 19:02:09 -0800 Subject: [PATCH 081/211] style(GA4GH): capitalize every logs' first word --- fence/resources/ga4gh/passports.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 6e8f6492e..01df218ab 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -44,7 +44,7 @@ def get_gen3_users_from_ga4gh_passports(passports): corresponds to a valid visa identity embedded within the passports passed in. """ - logger.info("getting gen3 users from passports") + logger.info("Getting gen3 users from passports") users_from_all_passports = [] user_ids_from_all_passports = [] for passport in passports: @@ -60,7 +60,7 @@ def get_gen3_users_from_ga4gh_passports(passports): # below function also validates passport (or raises exception) raw_visas = get_unvalidated_visas_from_valid_passport(passport) except Exception as exc: - logger.warning(f"invalid passport provided, ignoring. Error: {exc}") + logger.warning(f"Invalid passport provided, ignoring. Error: {exc}") continue # an empty raw_visas list means that either the current passport is @@ -84,7 +84,7 @@ def get_gen3_users_from_ga4gh_passports(passports): min_visa_expiration, validated_decoded_visa.get("exp") ) except Exception as exc: - logger.warning(f"invalid visa provided, ignoring. Error: {exc}") + logger.warning(f"Invalid visa provided, ignoring. Error: {exc}") continue expired_authz_removal_job_freq_in_seconds = config[ @@ -93,7 +93,7 @@ def get_gen3_users_from_ga4gh_passports(passports): min_visa_expiration -= expired_authz_removal_job_freq_in_seconds if min_visa_expiration <= int(time.time()): logger.warning( - "the passport's earliest valid visa expiration time is set to " + "The passport's earliest valid visa expiration time is set to " f"occur within {expired_authz_removal_job_freq_in_seconds} " "seconds from now, which is too soon an expiration to handle." ) @@ -252,19 +252,19 @@ def validate_visa(raw_visa): ) if decoded_visa["iat"] < asserted: raise Exception( - "the Passport Visa Assertion Source made the claim after the visa " + "The Passport Visa Assertion Source made the claim after the visa " 'was minted (i.e. "ga4gh_visa_v1" claim object\'s "asserted" ' 'field is greater than the visa\'s "iat" claim)' ) if "conditions" in decoded_visa["ga4gh_visa_v1"]: logger.warning( - 'condition checking is not yet supported, but a visa was received that contained the "conditions" field' + 'Condition checking is not yet supported, but a visa was received that contained the "conditions" field' ) if decoded_visa["ga4gh_visa_v1"]["conditions"]: raise Exception('"conditions" field in "ga4gh_visa_v1" is not empty') - logger.info("visa was successfully validated") + logger.info("Visa was successfully validated") return decoded_visa @@ -288,8 +288,8 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): ) if not iss_sub_pair_to_user: logger.info( - "creating a new Fence user with a username formed from subject " - "id and issuer. mapping subject id and issuer combination to " + "Creating a new Fence user with a username formed from subject " + "id and issuer. Mapping subject id and issuer combination to " "said user" ) username = subject_id + issuer[len("https://") :] From d96babfd8aafc0fb289b5e5417414d8e6f7d8d47 Mon Sep 17 00:00:00 2001 From: John McCann Date: Sat, 13 Nov 2021 18:16:12 -0800 Subject: [PATCH 082/211] chore(GA4GH): init issuer_to_idp w/o network --- fence/__init__.py | 17 +++++++---------- fence/resources/ga4gh/passports.py | 6 ++++++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/fence/__init__.py b/fence/__init__.py index 5840b0e1c..40bccf40c 100755 --- a/fence/__init__.py +++ b/fence/__init__.py @@ -401,6 +401,7 @@ def _set_authlib_cfgs(app): def _setup_oidc_clients(app): oidc = config.get("OPENID_CONNECT", {}) + app.issuer_to_idp = {} # Add OIDC client for Google if configured. if "google" in oidc: @@ -425,16 +426,12 @@ def _setup_oidc_clients(app): HTTP_PROXY=config.get("HTTP_PROXY"), logger=logger, ) - issuer = app.ras_client.get_value_from_discovery_doc("issuer", "") - if not issuer: - logger.warn( - "Unable to determine RAS issuer from discovery doc. Instead, " - "RAS issuer will be set to the base url of the RAS " - "client's discovery url." - ) - split_url = urlparse(app.ras_client.discovery_url) - issuer = f"{split_url.scheme}://{split_url.netloc}" - app.issuer_to_idp = {issuer: IdentityProvider.ras} + for allowed_issuer in config["GA4GH_VISA_ISSUER_ALLOWLIST"]: + if app.ras_client.discovery_url.startswith(allowed_issuer): + app.issuer_to_idp[allowed_issuer] = IdentityProvider.ras + break + else: + logger.warn("Could not determine issuer for the RAS OIDC client") # Add OIDC client for Synapse if configured. if "synapse" in oidc: diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 01df218ab..dc74074f7 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -304,6 +304,12 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): if not idp: idp = IdentityProvider(name=idp_name) gen3_user.identity_provider = idp + else: + logger.info( + "The user will be created without a linked identity " + "provider since it could not be determined based on " + "the issuer" + ) iss_sub_pair_to_user = IssSubPairToUser(iss=issuer, sub=subject_id) iss_sub_pair_to_user.user = gen3_user From 6360c7662b9893024657fb90af79afbaa9a240bd Mon Sep 17 00:00:00 2001 From: John McCann Date: Sun, 14 Nov 2021 15:43:52 -0800 Subject: [PATCH 083/211] test(ga4gh): add https://sts.nih.gov to allowlist --- fence/resources/ga4gh/passports.py | 9 ++++----- tests/test-fence-config.yaml | 5 +++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index dc74074f7..fb0a2d1b7 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -30,8 +30,8 @@ def get_gen3_users_from_ga4gh_passports(passports): """ Validate passports and embedded visas, using each valid visa's identity established by combination to possibly create and definitely - determine a Fence user whose username is added to the list returned by - this function. In the process of determining Fence users from visas, visa + determine a Fence user who is added to the list returned by this + function. In the process of determining Fence users from visas, visa authorization information is also persisted in Fence and synced to Arborist. @@ -40,9 +40,8 @@ def get_gen3_users_from_ga4gh_passports(passports): including header, payload, and signature Return: - list: a list of strings, each being the username of a Fence user who - corresponds to a valid visa identity embedded within the passports - passed in. + list: a list of users, each corresponding to a valid visa identity + embedded within the passports passed in """ logger.info("Getting gen3 users from passports") users_from_all_passports = [] diff --git a/tests/test-fence-config.yaml b/tests/test-fence-config.yaml index c41ff1484..f09f98824 100755 --- a/tests/test-fence-config.yaml +++ b/tests/test-fence-config.yaml @@ -469,7 +469,7 @@ INDEXD: null # this is the username which fence uses to make authenticated requests to indexd INDEXD_USERNAME: 'gdcapi' # this is the password which fence uses to make authenticated requests to indexd -INDEXD_PASSWORD: 'fake_password' +INDEXD_PASSWORD: 'fake_password' # pragma: allowlist secret # url where authz microservice is running ARBORIST: '/arborist' @@ -483,7 +483,7 @@ ARBORIST: '/arborist' AZ_BLOB_CREDENTIALS: 'fake connection string' # AZ_BLOB_CONTAINER_URL: 'https://storageaccount.blob.core.windows.net/container/' -# this is the container used for uploading, and should match the storage account +# this is the container used for uploading, and should match the storage account # used in the connection string for AZ_BLOB_CREDENTIALS AZ_BLOB_CONTAINER_URL: 'https://myfakeblob.blob.core.windows.net/my-fake-container/' @@ -615,4 +615,5 @@ ASSUME_ROLE_CACHE_SECONDS: 1800 # List of JWT issuers from which Fence will accept GA4GH visas GA4GH_VISA_ISSUER_ALLOWLIST: + - 'https://sts.nih.gov' - 'https://stsstg.nih.gov' From d0c197dd36bb1f5644e2e16d3c234cf94592c73c Mon Sep 17 00:00:00 2001 From: John McCann Date: Mon, 15 Nov 2021 05:17:29 -0800 Subject: [PATCH 084/211] chore(GA4GH DRS access): only support v1.1 --- fence/config-default.yaml | 1 - fence/resources/ga4gh/passports.py | 5 +++++ fence/resources/openid/ras_oauth2.py | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/fence/config-default.yaml b/fence/config-default.yaml index 41dcdde4b..00c954d25 100755 --- a/fence/config-default.yaml +++ b/fence/config-default.yaml @@ -881,7 +881,6 @@ GA4GH_VISA_ISSUER_ALLOWLIST: - 'https://stsstg.nih.gov' GA4GH_VISA_V1_CLAIM_REQUIRED_FIELDS: type: - - "https://ras.nih.gov/visas/v1" - "https://ras.nih.gov/visas/v1.1" value: - "https://stsstg.nih.gov/passport/dbgap/v1.1" diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index fb0a2d1b7..cecfb1b0b 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -362,3 +362,8 @@ def sync_visa_authorization(gen3_user, ga4gh_visas, expiration): def put_gen3_usernames_for_passport_into_cache(passport, usernames_from_passports): pass + + +# TODO to be called after login +def map_gen3_iss_sub_pair_to_user(gen3_issuer, gen3_subject_id, gen3_user): + pass diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 564c51d8a..fa44e1bfd 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -203,7 +203,7 @@ def map_iss_sub_pair_to_user(self, issuer, subject_id, username, email): # TODO just say DRS, not DRS/data self.logger.info( "Issuer and subject id have already been mapped to a " - "Fence user created from the DRS/data endpoints. " + "Fence user created from the DRS endpoint. " "Changing said user's username to the username " "returned from the RAS userinfo endpoint." ) @@ -216,8 +216,8 @@ def map_iss_sub_pair_to_user(self, issuer, subject_id, username, email): "Two users exist in the Fence database corresponding " "to the RAS user who is currently trying to log in: one " "created from an earlier login and one created from " - "the DRS/data endpoints. The one created from the " - "DRS/data endpoints will be logged in, rendering the " + "the DRS endpoint. The one created from the " + "DRS endpoint will be logged in, rendering the " "other one inaccessible." ) return iss_sub_pair_to_user.user.username From 1364aca496a69843e0c2c9a794659fa96b044d32 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Mon, 15 Nov 2021 09:35:06 -0600 Subject: [PATCH 085/211] feat(passport): refactoring and removal of usersync passport sync --- bin/fence_create.py | 6 +- fence/blueprints/data/indexd.py | 6 +- fence/blueprints/login/ras.py | 130 +++++---------- fence/job/visa_update_cronjob.py | 12 +- fence/resources/ga4gh/passports.py | 38 ++--- fence/resources/openid/ras_oauth2.py | 103 ++++-------- fence/scripting/fence_create.py | 37 +++-- fence/sync/passport_sync/ras_sync.py | 53 ++++--- fence/sync/sync_users.py | 213 +++---------------------- tests/dbgap_sync/test_user_sync.py | 227 ++++++++++++++------------- tests/ras/test_ras.py | 14 +- 11 files changed, 297 insertions(+), 542 deletions(-) diff --git a/bin/fence_create.py b/bin/fence_create.py index da4a5126a..9ed8e50e5 100755 --- a/bin/fence_create.py +++ b/bin/fence_create.py @@ -34,7 +34,7 @@ force_update_google_link, migrate_database, google_list_authz_groups, - update_user_visas, + access_token_polling_job, ) from fence.settings import CONFIG_SEARCH_FOLDERS @@ -408,7 +408,6 @@ def main(): "STORAGE_CREDENTIALS" ) usersync = config.get("USERSYNC", {}) - sync_from_visas = usersync.get("sync_from_visas", False) fallback_to_dbgap_sftp = usersync.get("fallback_to_dbgap_sftp", False) arborist = None @@ -474,7 +473,6 @@ def main(): sync_from_local_yaml_file=args.yaml, folder=args.folder, arborist=arborist, - sync_from_visas=sync_from_visas, fallback_to_dbgap_sftp=fallback_to_dbgap_sftp, ) elif args.action == "dbgap-download-access-files": @@ -576,7 +574,7 @@ def main(): elif args.action == "migrate": migrate_database(DB) elif args.action == "update-visas": - update_user_visas( + access_token_polling_job( DB, chunk_size=args.chunk_size, concurrency=args.concurrency, diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index da1238965..20530fb11 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -43,7 +43,7 @@ get_google_app_creds, give_service_account_billing_access_if_necessary, ) -from fence.resources.ga4gh.passports import get_gen3_users_from_ga4gh_passports +from fence.resources.ga4gh.passports import sync_gen3_users_authz_from_ga4gh_passports from fence.utils import get_valid_expiration_from_request from . import multipart_upload from ...models import AssumeRoleCacheAWS @@ -79,7 +79,9 @@ def get_signed_url_for_file( user_ids_from_passports = None if ga4gh_passports: # TODO change this to usernames - user_ids_from_passports = get_gen3_users_from_ga4gh_passports(ga4gh_passports) + user_ids_from_passports = sync_gen3_users_authz_from_ga4gh_passports( + ga4gh_passports + ) # add the user details to `flask.g.audit_data` first, so they are # included in the audit log if `IndexedFile(file_id)` raises a 404 diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index 77389083c..1a1da2b9e 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -17,6 +17,8 @@ from fence.jwt.validate import validate_jwt from fence.models import GA4GHVisaV1, IdentityProvider from fence.utils import get_valid_expiration +import fence.resources.ga4gh.passports + logger = get_logger(__name__) @@ -46,67 +48,48 @@ def post_login(self, user=None, token_result=None): # where only iss/exp/jti differ # TODO: This is not IdP-specific and will need a rethink when # we have multiple IdPs - user.ga4gh_visas_v1 = [] - current_session.commit() userinfo = flask.g.userinfo - encoded_visas = [] - - try: - encoded_visas = flask.current_app.ras_client.get_encoded_visas_v11_userinfo( - userinfo + global_parse_visas_on_login = config["GLOBAL_PARSE_VISAS_ON_LOGIN"] + usersync = config.get("USERSYNC", {}) + parse_visas = global_parse_visas_on_login or ( + global_parse_visas_on_login == None + and ( + strtobool(query_params.get("parse_visas")[0]) + if query_params.get("parse_visas") + else False ) - except Exception as e: - err_msg = "Could not retrieve visas" - logger.error("{}: {}".format(e, err_msg)) - raise + ) + # do an on-the-fly usersync for this user to give them instant access after logging in through RAS + # if GLOBAL_PARSE_VISAS_ON_LOGIN is true then we want to run it regardless of whether or not the client sent parse_visas on request + if parse_visas: + # Close previous db sessions. Leaving it open causes a race condition where we're viewing user.project_access while trying to update it in usersync + # not closing leads to partially updated records + current_session.close() - for encoded_visa in encoded_visas: + # get passport then call sync on it try: - # Validate the visa per GA4GH AAI "Embedded access token" format rules. - # pyjwt also validates signature and expiration. - decoded_visa = validate_jwt( - encoded_token=encoded_visa, - # Embedded token must contain scope claim, which must include openid - scope={"openid", "ga4gh_passport_v1"}, - issuers=config["GA4GH_VISA_ISSUER_ALLOWLIST"], - require_purpose=False, - # Embedded token must contain iss, sub, iat, exp claims - # options={"require": ["iss", "sub", "iat", "exp"]}, - # ^ FIXME 2021-05-13: Above needs pyjwt>=v2.0.0, which requires cryptography>=3. - # Once we can unpin and upgrade cryptography and pyjwt, switch to above "options" arg. - # For now, pyjwt 1.7.1 is able to require iat and exp; - # authutils' validate_jwt (i.e. the function being called) checks issuers already (see above); - # and we will check separately for sub below. - options={ - "require_iat": True, - "require_exp": True, - "verify_aud": False, - }, + passport = ( + flask.current_app.ras_client.get_encoded_passport_v11_userinfo( + userinfo + ) ) - - # Also require 'sub' claim (see note above about pyjwt and the options arg). - if "sub" not in decoded_visa: - raise JWTError("Visa is missing the 'sub' claim.") - # Embedded token must not contain aud claim - if "aud" in decoded_visa: - raise JWTError("Visa contains the 'aud' claim.") except Exception as e: - logger.error("Visa failed validation: {}. Discarding visa.".format(e)) - continue - - visa = GA4GHVisaV1( - user=user, - source=decoded_visa["ga4gh_visa_v1"]["source"], - type=decoded_visa["ga4gh_visa_v1"]["type"], - asserted=int(decoded_visa["ga4gh_visa_v1"]["asserted"]), - expires=int(decoded_visa["exp"]), - ga4gh_visa=encoded_visa, + err_msg = "Could not retrieve passport or visas" + logger.error("{}: {}".format(e, err_msg)) + raise + + # now sync authz updates + user_ids_from_passports = fence.resources.ga4gh.passports.sync_gen3_users_authz_from_ga4gh_passports( + [passport], pkey_cache=pkey_cache ) - current_session.add(visa) - current_session.commit() + + # TODO? + # put_gen3_usernames_for_passport_into_cache( + # passport, usernames_from_current_passport + # ) # Store refresh token in db assert "refresh_token" in flask.g.tokens, "No refresh_token in user tokens" @@ -116,6 +99,7 @@ def post_login(self, user=None, token_result=None): decoded_id = jwt.decode(id_token, verify=False) # Add 15 days to iat to calculate refresh token expiration time + # TODO do they really not provide exp? issued_time = int(decoded_id.get("iat")) expires = config["RAS_REFRESH_EXPIRATION"] @@ -134,48 +118,4 @@ def post_login(self, user=None, token_result=None): user=user, refresh_token=refresh_token, expires=expires + issued_time ) - global_parse_visas_on_login = config["GLOBAL_PARSE_VISAS_ON_LOGIN"] - usersync = config.get("USERSYNC", {}) - sync_from_visas = usersync.get("sync_from_visas", False) - parse_visas = global_parse_visas_on_login or ( - global_parse_visas_on_login == None - and ( - strtobool(query_params.get("parse_visas")[0]) - if query_params.get("parse_visas") - else False - ) - ) - # if sync_from_visas and (global_parse_visas_on_login or global_parse_visas_on_login == None): - # Check if user has any project_access from a previous session or from usersync AND if fence is configured to use visas as authZ source - # if not do an on-the-fly usersync for this user to give them instant access after logging in through RAS - # If GLOBAL_PARSE_VISAS_ON_LOGIN is true then we want to run it regardless of whether or not the client sent parse_visas on request - if sync_from_visas and parse_visas and not user.project_access: - # Close previous db sessions. Leaving it open causes a race condition where we're viewing user.project_access while trying to update it in usersync - # not closing leads to partially updated records - current_session.close() - - DB = os.environ.get("FENCE_DB") or config.get("DB") - if DB is None: - try: - from fence.settings import DB - except ImportError: - pass - - arborist = ArboristClient( - arborist_base_url=config["ARBORIST"], - logger=get_logger("user_syncer.arborist_client"), - authz_provider="user-sync", - ) - dbGaP = os.environ.get("dbGaP") or config.get("dbGaP") - if not isinstance(dbGaP, list): - dbGaP = [dbGaP] - - sync = fence.scripting.fence_create.init_syncer( - dbGaP, - None, - DB, - arborist=arborist, - ) - sync.sync_single_user_visas(user, user.ga4gh_visas_v1, current_session) - super(RASCallback, self).post_login() diff --git a/fence/job/visa_update_cronjob.py b/fence/job/visa_update_cronjob.py index 969f89f5f..78b1e4e49 100644 --- a/fence/job/visa_update_cronjob.py +++ b/fence/job/visa_update_cronjob.py @@ -61,8 +61,10 @@ async def update_tokens(self, db_session): Initialize a producer-consumer workflow. Producer: Collects users from db and feeds it to the workers - Worker: Takes in the users from the Producer and passes it to the Updater to update the tokens and passes those updated tokens for JWT validation - Updater: Updates refresh_tokens and visas by calling the update_user_visas from the correct client + Worker: Takes in the users from the Producer and passes it to the Updater to + update the tokens and passes those updated tokens for JWT validation + Updater: Updates refresh_tokens and visas by calling the update_user_authorization from + the correct client """ start_time = time.time() @@ -143,6 +145,8 @@ async def updater(self, name, updater_queue, db_session): """ while True: user = await updater_queue.get() + # IF WE NEED TO UPDATE THEIR VISAS, DETERMINE WHICH CLIENT TO USE + # if idp is RAS then update visas? use that info to determine client? if user.ga4gh_visas_v1: for visa in user.ga4gh_visas_v1: client = self._pick_client(visa) @@ -151,7 +155,7 @@ async def updater(self, name, updater_queue, db_session): name, user.username ) ) - client.update_user_visas(user, self.pkey_cache, db_session) + client.update_user_authorization(user, self.pkey_cache, db_session) else: # clear expired refresh tokens if user.upstream_refresh_tokens: @@ -159,7 +163,7 @@ async def updater(self, name, updater_queue, db_session): db_session.commit() self.logger.info( - "User {} doesnt have visa. Skipping . . .".format(user.username) + "User {} doesn't have visa. Skipping . . .".format(user.username) ) updater_queue.task_done() diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 6430900d6..350ad60e3 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -25,7 +25,7 @@ logger = get_logger(__name__) -def get_gen3_users_from_ga4gh_passports(passports): +def sync_gen3_users_authz_from_ga4gh_passports(passports, pkey_cache=None): """ Validate passports and embedded visas, using each valid visa's identity established by combination to possibly create and definitely @@ -56,7 +56,9 @@ def get_gen3_users_from_ga4gh_passports(passports): continue # below function also validates passport (or raises exception) - raw_visas = get_unvalidated_visas_from_valid_passport(passport) + raw_visas = get_unvalidated_visas_from_valid_passport( + passport, pkey_cache=pkey_cache + ) except Exception as exc: logger.warning(f"invalid passport provided, ignoring. Error: {exc}") continue @@ -111,7 +113,9 @@ def get_gen3_users_from_ga4gh_passports(passports): for raw_visa, validated_decoded_visa in visas ] # NOTE: does not validate, assumes validation occurs above. - sync_visa_authorization(gen3_user, ga4gh_visas, min_visa_expiration) + sync_validated_visa_authorization( + gen3_user, ga4gh_visas, min_visa_expiration + ) usernames_from_current_passport.append(gen3_user.username) put_gen3_usernames_for_passport_into_cache( @@ -285,12 +289,15 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): return iss_sub_pair_to_user.user -def sync_visa_authorization(gen3_user, ga4gh_visas, expiration): +def sync_validated_visa_authorization(gen3_user, ga4gh_visas, expiration): """ Wrapper around UserSyncer.sync_single_user_visas method, which parses authorization information from the provided visas, persists it in Fence, and syncs it to Arborist. + IMPORTANT NOTE: THIS DOES NOT VALIDATE THE VISAS. ENSURE THIS IS DONE + BEFORE THIS. + Args: gen3_user (userdatamodel.user.User): the Fence user whose visas' authz info is being synced @@ -302,28 +309,21 @@ def sync_visa_authorization(gen3_user, ga4gh_visas, expiration): Return: None """ - arborist_client = ArboristClient( - arborist_base_url=config.get("ARBORIST"), logger=logger, authz_provider="GA4GH" - ) - - dbgap_config = os.environ.get("dbGaP") or config.get("dbGaP") - if not isinstance(dbgap_config, list): - dbgap_config = [dbgap_config] - DB = os.environ.get("FENCE_DB") or config.get("DB") - if DB is None: - try: - from fence.settings import DB - except ImportError: - pass + default_args = fence.scripting.fence_create.get_default_init_syncer_inputs() syncer = fence.scripting.fence_create.init_syncer( - dbgap_config, None, DB, arborist=arborist_client + STORAGE_CREDENTIALS=None, **default_args ) with flask.current_app.db.session as db_session: - syncer.sync_single_user_visas( + synced_visas = syncer.sync_single_user_visas( gen3_user, ga4gh_visas, db_session, expires=expiration ) + # after syncing authorization, perist the visas that were parsed successfully + for visa in synced_visas: + db_session.add(visa) + db_session.commit() + def put_gen3_usernames_for_passport_into_cache(passport, usernames_from_passports): pass diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index fe4077e42..7cde9a16d 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -1,9 +1,12 @@ import backoff import flask +import copy import requests # the whole passports module is imported to avoid issue with circular imports import fence.resources.ga4gh.passports +import fence.scripting.fence_create +import fence.resources.ga4gh.passports from flask_sqlalchemy_session import current_session from jose import jwt as jose_jwt @@ -82,6 +85,18 @@ def get_userinfo(self, token): return {} return res.json() + def get_encoded_passport_v11_userinfo(self, userinfo): + """ + Return encoded passport after extracting from userinfo response + + Args: + userinfo (dict): userinfo response + + Return: + str: encoded ga4gh passport + """ + return userinfo.get("passport_jwt_v11") + def get_encoded_visas_v11_userinfo(self, userinfo, pkey_cache=None): """ Return encoded visas after extracting and validating passport from userinfo response @@ -93,7 +108,7 @@ def get_encoded_visas_v11_userinfo(self, userinfo, pkey_cache=None): Return: list: list of encoded GA4GH visas """ - encoded_passport = userinfo.get("passport_jwt_v11") + encoded_passport = self.get_encoded_passport_v11_userinfo(userinfo) return ( fence.resources.ga4gh.passports.get_unvalidated_visas_from_valid_passport( encoded_passport, pkey_cache @@ -244,91 +259,29 @@ def map_iss_sub_pair_to_user(self, issuer, subject_id, username, email): return iss_sub_pair_to_user.user.username @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) - def update_user_visas(self, user, pkey_cache, db_session=current_session): + def update_user_authorization(self, user, pkey_cache, db_session=current_session): """ Updates user's RAS refresh token and uses the new access token to retrieve new visas from - RAS's /userinfo endpoint and update the db with the new visa. - - delete user's visas from db if we're not able to get a new access_token - - delete user's visas from db if we're not able to get new visas - - only visas which pass validation are added to the database + RAS's /userinfo endpoint and update access """ - # Note: in the cronjob this is called per-user per-visa. - # So it should be noted that when there are more clients than just RAS, - # this code as it stands can remove visas that the user has from other clients. - user.ga4gh_visas_v1 = [] - db_session.commit() - try: token_endpoint = self.get_value_from_discovery_doc("token_endpoint", "") token = self.get_access_token(user, token_endpoint, db_session) userinfo = self.get_userinfo(token) - encoded_visas = self.get_encoded_visas_v11_userinfo(userinfo, pkey_cache) + passport = self.get_encoded_passport_v11_userinfo(userinfo) except Exception as e: err_msg = "Could not retrieve visas" self.logger.exception("{}: {}".format(err_msg, e)) raise - for encoded_visa in encoded_visas: - try: - visa_issuer = get_iss(encoded_visa) - visa_kid = get_kid(encoded_visa) - except Exception as e: - self.logger.error( - "Could not get issuer or kid from visa: {}. Discarding visa.".format( - e - ) - ) - continue # Not raise: If visa malformed, does not make sense to retry - - # See if pkey is in cronjob cache; if not, update cache. - public_key = pkey_cache.get(visa_issuer, {}).get(visa_kid) - try: - # Validate the visa per GA4GH AAI "Embedded access token" format rules. - # pyjwt also validates signature and expiration. - decoded_visa = validate_jwt( - encoded_token=encoded_visa, - public_key=public_key, - attempt_refresh=True, - # Embedded token must contain scope claim, which must include openid - scope={"openid"}, - require_purpose=False, - issuers=config.get("GA4GH_VISA_ISSUER_ALLOWLIST", []), - # Embedded token must contain iss, sub, iat, exp claims - # options={"require": ["iss", "sub", "iat", "exp"]}, - # ^ FIXME 2021-05-13: Above needs pyjwt>=v2.0.0, which requires cryptography>=3. - # Once we can unpin and upgrade cryptography and pyjwt, switch to above "options" arg. - # For now, pyjwt 1.7.1 is able to require iat and exp; - # authutils' validate_jwt (i.e. the function being called) checks issuers already (see above); - # and we will check separately for sub below. - pkey_cache=pkey_cache, - options={ - "require_iat": True, - "require_exp": True, - "verify_aud": False, - }, - ) - - # Also require 'sub' claim (see note above about pyjwt and the options arg). - if "sub" not in decoded_visa: - raise JWTError("Visa is missing the 'sub' claim.") - # Embedded token must not contain aud claim - if "aud" in decoded_visa: - raise JWTError("Visa contains 'aud' calim") - except Exception as e: - self.logger.error( - "Visa failed validation: {}. Discarding visa.".format(e) - ) - continue - - visa = GA4GHVisaV1( - user=user, - source=decoded_visa["ga4gh_visa_v1"]["source"], - type=decoded_visa["ga4gh_visa_v1"]["type"], - asserted=int(decoded_visa["ga4gh_visa_v1"]["asserted"]), - expires=int(decoded_visa["exp"]), - ga4gh_visa=encoded_visa, + # now sync authz updates + user_ids_from_passports = ( + fence.resources.ga4gh.passports.sync_gen3_users_authz_from_ga4gh_passports( + [passport], pkey_cache=pkey_cache ) + ) - current_db_session = db_session.object_session(visa) - current_db_session.add(visa) - db_session.commit() + # TODO? + # put_gen3_usernames_for_passport_into_cache( + # passport, usernames_from_current_passport + # ) diff --git a/fence/scripting/fence_create.py b/fence/scripting/fence_create.py index 76829678f..0febc9bae 100644 --- a/fence/scripting/fence_create.py +++ b/fence/scripting/fence_create.py @@ -54,6 +54,8 @@ from fence.sync.sync_users import UserSyncer from fence.utils import create_client, get_valid_expiration +from gen3authz.client.arborist.client import ArboristClient + logger = get_logger(__name__) @@ -200,6 +202,30 @@ def _remove_client_service_accounts(db_session, client): ) +def get_default_init_syncer_inputs(): + DB = os.environ.get("FENCE_DB") or config.get("DB") + if DB is None: + try: + from fence.settings import DB + except ImportError: + pass + + arborist = ArboristClient( + arborist_base_url=config["ARBORIST"], + logger=get_logger("user_syncer.arborist_client"), + authz_provider="ras", + ) + dbGaP = os.environ.get("dbGaP") or config.get("dbGaP") + if not isinstance(dbGaP, list): + dbGaP = [dbGaP] + + return { + "DB": DB, + "arborist": arborist, + "dbGaP": dbGaP, + } + + def init_syncer( dbGaP, STORAGE_CREDENTIALS, @@ -210,7 +236,6 @@ def init_syncer( sync_from_local_yaml_file=None, arborist=None, folder=None, - sync_from_visas=False, fallback_to_dbgap_sftp=False, ): """ @@ -270,7 +295,6 @@ def init_syncer( sync_from_local_yaml_file=sync_from_local_yaml_file, arborist=arborist, folder=folder, - sync_from_visas=sync_from_visas, fallback_to_dbgap_sftp=fallback_to_dbgap_sftp, ) @@ -313,7 +337,6 @@ def sync_users( sync_from_local_yaml_file=None, arborist=None, folder=None, - sync_from_visas=False, fallback_to_dbgap_sftp=False, ): syncer = init_syncer( @@ -326,15 +349,11 @@ def sync_users( sync_from_local_yaml_file, arborist, folder, - sync_from_visas, fallback_to_dbgap_sftp, ) if not syncer: exit(1) - if sync_from_visas: - syncer.sync_visas() - else: - syncer.sync() + syncer.sync() def create_sample_data(DB, yaml_file_path): @@ -1573,7 +1592,7 @@ def google_list_authz_groups(db): return google_authz -def update_user_visas( +def access_token_polling_job( db, chunk_size=None, concurrency=None, thread_pool_size=None, buffer_size=None ): """ diff --git a/fence/sync/passport_sync/ras_sync.py b/fence/sync/passport_sync/ras_sync.py index b429cce35..f734b6fd3 100644 --- a/fence/sync/passport_sync/ras_sync.py +++ b/fence/sync/passport_sync/ras_sync.py @@ -9,7 +9,7 @@ class RASVisa(DefaultVisa): Class representing RAS visas """ - def _init__(self, logger): + def __init__(self, logger): super(RASVisa, self).__init__( logger=logger, ) @@ -17,36 +17,37 @@ def _init__(self, logger): def _parse_single_visa( self, user, encoded_visa, expires, parse_consent_code, db_session ): + """ + Return user information from the visa. + + IMPORTANT NOTE: THIS DOES NOT VALIDATE THE ENCODED VISA. ENSURE THIS IS DONE + BEFORE THIS. + """ decoded_visa = {} - try: - decoded_visa = jwt.decode(encoded_visa, verify=False) - except Exception as e: - self.logger.warning("Couldn't decode visa {}".format(e)) - # Remove visas if its invalid or expired - user.ga4gh_visas_v1 = [] - db_session.commit() - finally: - ras_dbgap_permissions = decoded_visa.get("ras_dbgap_permissions", []) + + # do not verify again, assume this happens upstream + # note that this can fail, upstream should handle the case that parsing fails + decoded_visa = jwt.decode(encoded_visa, verify=False) + + ras_dbgap_permissions = decoded_visa.get("ras_dbgap_permissions", []) project = {} info = {} info["tags"] = {} - if time.time() < expires: - for permission in ras_dbgap_permissions: - phsid = permission.get("phs_id", "") - consent_group = permission.get("consent_group", "") - full_phsid = phsid - if parse_consent_code and consent_group: - full_phsid += "." + consent_group - privileges = {"read-storage", "read"} - permission_expiration = permission.get("expiration") - if permission_expiration and expires <= permission_expiration: - project[full_phsid] = privileges - info["tags"] = {"dbgap_role": permission.get("role", "")} - else: - # Remove visas if its invalid or expired - user.ga4gh_visas_v1 = [] - db_session.commit() + if time.time() >= expires: + raise Exception("visa is expired") + + for permission in ras_dbgap_permissions: + phsid = permission.get("phs_id", "") + consent_group = permission.get("consent_group", "") + full_phsid = phsid + if parse_consent_code and consent_group: + full_phsid += "." + consent_group + privileges = {"read-storage", "read"} + permission_expiration = permission.get("expiration") + if permission_expiration and expires <= permission_expiration: + project[full_phsid] = privileges + info["tags"] = {"dbgap_role": permission.get("role", "")} info["email"] = user.email or "" info["display_name"] = user.display_name or "" diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 90bb62d20..df2e96ae1 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -291,7 +291,6 @@ def __init__( sync_from_local_yaml_file=None, arborist=None, folder=None, - sync_from_visas=False, fallback_to_dbgap_sftp=False, ): """ @@ -307,7 +306,6 @@ def __init__( ArboristClient instance if the syncer should also create resources in arborist folder: a local folder where dbgap telemetry files will sync to - sync_from_visas: use visa for sync instead of dbgap fallback_to_dbgap_sftp: fallback to telemetry files when visa sync fails """ self.sync_from_local_csv_dir = sync_from_local_csv_dir @@ -327,7 +325,6 @@ def __init__( ) self.arborist_client = arborist self.folder = folder - self.sync_from_visas = sync_from_visas self.fallback_to_dbgap_sftp = fallback_to_dbgap_sftp self.auth_source = defaultdict(set) @@ -1945,196 +1942,24 @@ def parse_user_visas(self, db_session): return (user_projects, user_info) - def _sync_visas(self, sess): - - self.logger.info("Running usersync with Visas") - self.logger.info( - "Fallback to telemetry files: {}".format(self.fallback_to_dbgap_sftp) - ) - - self.ras_sync_client = RASVisa(logger=self.logger) - - dbgap_config = self.dbGaP[0] - user_projects, user_info = self.parse_user_visas(sess) - enable_common_exchange_area_access = dbgap_config.get( - "enable_common_exchange_area_access", False - ) - study_common_exchange_areas = dbgap_config.get( - "study_common_exchange_areas", {} - ) - - try: - user_yaml = UserYAML.from_file( - self.sync_from_local_yaml_file, encrypted=False, logger=self.logger - ) - except (EnvironmentError, AssertionError) as e: - self.logger.error(str(e)) - self.logger.error("aborting early") - return - - # parse projects - user_projects = self.parse_projects(user_projects) - user_yaml.projects = self.parse_projects(user_yaml.projects) - - if self.fallback_to_dbgap_sftp: - # Collect user_info and user_projects from telemetry - user_projects_telemetry = {} - user_info_telemetry = {} - if self.is_sync_from_dbgap_server: - self.logger.debug( - "Pulling telemetry files from {} dbgap sftp servers".format( - len(self.dbGaP) - ) - ) - ( - user_projects_telemetry, - user_info_telemetry, - ) = self._merge_multiple_dbgap_sftp(self.dbGaP, sess) - local_csv_file_list = [] - if self.sync_from_local_csv_dir: - local_csv_file_list = glob.glob( - os.path.join(self.sync_from_local_csv_dir, "*") - ) - - # if syncing from local csv dir dbgap configurations - # come from the first dbgap instance in the fence config file - user_projects_csv, user_info_csv = self._get_user_permissions_from_csv_list( - local_csv_file_list, - encrypted=False, - session=sess, - dbgap_config=self.dbGaP[0], - ) - user_projects_csv = self.parse_projects(user_projects_csv) - user_projects_telemetry = self.parse_projects(user_projects_telemetry) - - # merge all user info dicts into "user_info". - # the user info (such as email) in the user.yaml files - # overrides the user info from the CSV files. - self.sync_two_user_info_dict(user_info_csv, user_info_telemetry) - - # merge all access info dicts into "user_projects". - # the access info is combined - if the user.yaml access is - # ["read"] and the CSV file access is ["read-storage"], the - # resulting access is ["read", "read-storage"]. - self.sync_two_phsids_dict( - user_projects_csv, - user_projects_telemetry, - source1="local_csv", - source2="dbgap", - ) - - # sync phsids so that this adds projects if visas were invalid or adds users that dont have visas. - # `phsids2_overrides_phsids1=True` because We want visa to be the source of truth when its available and not merge any telemetry file info into this. - # We only want visa to be used when visa is not valid or available - self.sync_two_phsids_dict( - user_projects_telemetry, - user_projects, - source1="dbgap", - source2="visa", - phsids2_overrides_phsids1=False, - ) - self.sync_two_user_info_dict(user_info_telemetry, user_info) - - if self.parse_consent_code and enable_common_exchange_area_access: - self.logger.info( - f"using study to common exchange area mapping: {study_common_exchange_areas}" - ) - - # merge all user info dicts into "user_info". - # the user info (such as email) in the user.yaml files - # overrides the user info from the CSV files. - self.sync_two_user_info_dict(user_yaml.user_info, user_info) - - # merge all access info dicts into "user_projects". - # the access info is combined - if the user.yaml access is - # ["read"] and the CSV file access is ["read-storage"], the - # resulting access is ["read", "read-storage"]. - self.sync_two_phsids_dict( - user_yaml.projects, user_projects, source1="user_yaml", source2="visa" - ) - - self._process_user_projects( - user_projects, - enable_common_exchange_area_access, - study_common_exchange_areas, - dbgap_config, - sess, - ) - - # Note: if there are multiple dbgap sftp servers configured - # this parameter is always from the config for the first dbgap sftp server - # not any additional ones - if self.parse_consent_code: - self._grant_all_consents_to_c999_users( - user_projects, user_yaml.project_to_resource - ) - # update fence db - if user_projects: - self.logger.info("Sync to db and storage backend") - self.sync_to_db_and_storage_backend(user_projects, user_info, sess) - else: - self.logger.info("No users for syncing") - - # update the Arborist DB (resources, roles, policies, groups) - if user_yaml.authz: - if not self.arborist_client: - raise EnvironmentError( - "yaml file contains authz section but sync is not configured with" - " arborist client--did you run sync with --arborist arg?" - ) - self.logger.info("Synchronizing arborist...") - success = self._update_arborist(sess, user_yaml) - if success: - self.logger.info("Finished synchronizing arborist") - else: - self.logger.error("Could not synchronize successfully") - exit(1) - else: - self.logger.info("No `authz` section; skipping arborist sync") - - # update arborist db (user access) - if self.arborist_client: - self.logger.info("Synchronizing arborist with authorization info...") - success = self._update_authz_in_arborist(sess, user_projects, user_yaml) - if success: - self.logger.info( - "Finished synchronizing authorization info to arborist" - ) - else: - self.logger.error( - "Could not synchronize authorization info successfully to arborist" - ) - exit(1) - else: - self.logger.error("No arborist client set; skipping arborist sync") - - # Logging authz source - for u, s in self.auth_source.items(): - self.logger.info("Access for user {} from {}".format(u, s)) - - def sync_visas(self): - if self.session: - self._sync_visas(self.session) - else: - with self.driver.session as s: - self._sync_visas(s) - # if returns with some failure use telemetry file - def sync_single_user_visas(self, user, ga4gh_visas, sess=None, expires=None): """ Sync a single user's visas during login or DRS/data access + IMPORTANT NOTE: THIS DOES NOT VALIDATE THE VISA. ENSURE THIS IS DONE + BEFORE THIS. + Args: user (userdatamodel.user.User): Fence user whose visas' authz info is being synced ga4gh_visas (list): a list of fence.models.GA4GHVisaV1 objects - that are parsed and synced + that are ALREADY VALIDATED sess (sqlalchemy.orm.session.Session): database session expires (int): time at which synced Arborist policies and inclusion in any GBAG are set to expire Return: - None + list of successfully parsed visas """ self.ras_sync_client = RASVisa(logger=self.logger) dbgap_config = self.dbGaP[0] @@ -2158,19 +1983,30 @@ def sync_single_user_visas(self, user, ga4gh_visas, sess=None, expires=None): user_info = dict() projects = {} info = {} + parsed_visas = [] for visa in ga4gh_visas: project = {} visa_type = self._pick_sync_type(visa) encoded_visa = visa.ga4gh_visa - project, info = visa_type._parse_single_visa( - user, - encoded_visa, - visa.expires, - self.parse_consent_code, - sess, - ) + + try: + project, info = visa_type._parse_single_visa( + user, + encoded_visa, + visa.expires, + self.parse_consent_code, + sess, + ) + except Exception: + logging.warning( + f"ignoring unsuccessfully parsed or expired visa: {encoded_visa}" + ) + continue + projects = {**projects, **project} + parsed_visas.append(visa) + user_projects[user.username] = projects user_info[user.username] = info @@ -2194,7 +2030,6 @@ def sync_single_user_visas(self, user, ga4gh_visas, sess=None, expires=None): user_projects, user_yaml.project_to_resource ) - # update fence db if user_projects: self.logger.info("Sync to db and storage backend") self.sync_to_db_and_storage_backend( @@ -2223,3 +2058,5 @@ def sync_single_user_visas(self, user, ga4gh_visas, sess=None, expires=None): ) else: self.logger.error("No arborist client set; skipping arborist sync") + + return parsed_visas diff --git a/tests/dbgap_sync/test_user_sync.py b/tests/dbgap_sync/test_user_sync.py index 8ac4180a0..b4e07aebb 100644 --- a/tests/dbgap_sync/test_user_sync.py +++ b/tests/dbgap_sync/test_user_sync.py @@ -613,118 +613,118 @@ def mock_merge(dbgap_servers, sess): assert syncer._process_dbgap_files.call_count == 2 -@pytest.mark.parametrize("syncer", ["cleversafe", "google"], indirect=True) -@pytest.mark.parametrize("parse_consent_code_config", [False, True]) -@pytest.mark.parametrize("fallback_to_dbgap_sftp", [False, True]) -def test_user_sync_with_visas( - syncer, - db_session, - storage_client, - parse_consent_code_config, - fallback_to_dbgap_sftp, - monkeypatch, -): - # patch the sync to use the parameterized config value - monkeypatch.setitem( - syncer.dbGaP[0], "parse_consent_code", parse_consent_code_config - ) - monkeypatch.setattr(syncer, "parse_consent_code", parse_consent_code_config) - monkeypatch.setattr(syncer, "fallback_to_dbgap_sftp", fallback_to_dbgap_sftp) - monkeypatch.setattr(syncer, "sync_from_visas", True) - - syncer.sync_visas() - - users = db_session.query(models.User).all() - - user = models.query_for_user( - session=db_session, username="TESTUSERB" - ) # contains only visa information - - backup_user = models.query_for_user( - session=db_session, username="TESTUSERD" - ) # Contains invalid visa and also in telemetry file - - expired_user = models.query_for_user( - session=db_session, - username="expired_visa_user", - ) - invalid_user = models.query_for_user( - session=db_session, username="invalid_visa_user" - ) - - assert len(invalid_user.project_access) == 0 - assert len(expired_user.project_access) == 0 - - assert len(invalid_user.ga4gh_visas_v1) == 0 - assert len(expired_user.ga4gh_visas_v1) == 0 - - if fallback_to_dbgap_sftp: - assert len(users) == 14 - - if parse_consent_code_config: - assert equal_project_access( - user.project_access, - { - "phs000991.c1": ["read", "read-storage"], - "phs000961.c1": ["read", "read-storage"], - "phs000279.c1": ["read", "read-storage"], - "phs000286.c3": ["read", "read-storage"], - "phs000289.c2": ["read", "read-storage"], - "phs000298.c1": ["read", "read-storage"], - }, - ) - assert equal_project_access( - backup_user.project_access, - { - "phs000179.c1": ["read", "read-storage"], - }, - ) - else: - assert equal_project_access( - user.project_access, - { - "phs000991": ["read", "read-storage"], - "phs000961": ["read", "read-storage"], - "phs000279": ["read", "read-storage"], - "phs000286": ["read", "read-storage"], - "phs000289": ["read", "read-storage"], - "phs000298": ["read", "read-storage"], - }, - ) - assert equal_project_access( - backup_user.project_access, - { - "phs000179": ["read", "read-storage"], - }, - ) - - else: - assert len(users) == 12 - assert len(backup_user.project_access) == 0 - if parse_consent_code_config: - assert equal_project_access( - user.project_access, - { - "phs000991.c1": ["read", "read-storage"], - "phs000961.c1": ["read", "read-storage"], - "phs000279.c1": ["read", "read-storage"], - "phs000286.c3": ["read", "read-storage"], - "phs000289.c2": ["read", "read-storage"], - "phs000298.c1": ["read", "read-storage"], - }, - ) - else: - assert equal_project_access( - user.project_access, - { - "phs000991": ["read", "read-storage"], - "phs000961": ["read", "read-storage"], - "phs000279": ["read", "read-storage"], - "phs000286": ["read", "read-storage"], - "phs000289": ["read", "read-storage"], - "phs000298": ["read", "read-storage"], - }, - ) +# @pytest.mark.parametrize("syncer", ["cleversafe", "google"], indirect=True) +# @pytest.mark.parametrize("parse_consent_code_config", [False, True]) +# @pytest.mark.parametrize("fallback_to_dbgap_sftp", [False, True]) +# def test_user_sync_with_visas( +# syncer, +# db_session, +# storage_client, +# parse_consent_code_config, +# fallback_to_dbgap_sftp, +# monkeypatch, +# ): +# # patch the sync to use the parameterized config value +# monkeypatch.setitem( +# syncer.dbGaP[0], "parse_consent_code", parse_consent_code_config +# ) +# monkeypatch.setattr(syncer, "parse_consent_code", parse_consent_code_config) +# monkeypatch.setattr(syncer, "fallback_to_dbgap_sftp", fallback_to_dbgap_sftp) +# monkeypatch.setattr(syncer, "sync_from_visas", True) + +# syncer.sync_visas() + +# users = db_session.query(models.User).all() + +# user = models.query_for_user( +# session=db_session, username="TESTUSERB" +# ) # contains only visa information + +# backup_user = models.query_for_user( +# session=db_session, username="TESTUSERD" +# ) # Contains invalid visa and also in telemetry file + +# expired_user = models.query_for_user( +# session=db_session, +# username="expired_visa_user", +# ) +# invalid_user = models.query_for_user( +# session=db_session, username="invalid_visa_user" +# ) + +# assert len(invalid_user.project_access) == 0 +# assert len(expired_user.project_access) == 0 + +# assert len(invalid_user.ga4gh_visas_v1) == 0 +# assert len(expired_user.ga4gh_visas_v1) == 0 + +# if fallback_to_dbgap_sftp: +# assert len(users) == 14 + +# if parse_consent_code_config: +# assert equal_project_access( +# user.project_access, +# { +# "phs000991.c1": ["read", "read-storage"], +# "phs000961.c1": ["read", "read-storage"], +# "phs000279.c1": ["read", "read-storage"], +# "phs000286.c3": ["read", "read-storage"], +# "phs000289.c2": ["read", "read-storage"], +# "phs000298.c1": ["read", "read-storage"], +# }, +# ) +# assert equal_project_access( +# backup_user.project_access, +# { +# "phs000179.c1": ["read", "read-storage"], +# }, +# ) +# else: +# assert equal_project_access( +# user.project_access, +# { +# "phs000991": ["read", "read-storage"], +# "phs000961": ["read", "read-storage"], +# "phs000279": ["read", "read-storage"], +# "phs000286": ["read", "read-storage"], +# "phs000289": ["read", "read-storage"], +# "phs000298": ["read", "read-storage"], +# }, +# ) +# assert equal_project_access( +# backup_user.project_access, +# { +# "phs000179": ["read", "read-storage"], +# }, +# ) + +# else: +# assert len(users) == 12 +# assert len(backup_user.project_access) == 0 +# if parse_consent_code_config: +# assert equal_project_access( +# user.project_access, +# { +# "phs000991.c1": ["read", "read-storage"], +# "phs000961.c1": ["read", "read-storage"], +# "phs000279.c1": ["read", "read-storage"], +# "phs000286.c3": ["read", "read-storage"], +# "phs000289.c2": ["read", "read-storage"], +# "phs000298.c1": ["read", "read-storage"], +# }, +# ) +# else: +# assert equal_project_access( +# user.project_access, +# { +# "phs000991": ["read", "read-storage"], +# "phs000961": ["read", "read-storage"], +# "phs000279": ["read", "read-storage"], +# "phs000286": ["read", "read-storage"], +# "phs000289": ["read", "read-storage"], +# "phs000298": ["read", "read-storage"], +# }, +# ) @pytest.mark.parametrize("syncer", ["google"], indirect=True) @@ -739,10 +739,11 @@ def test_sync_in_login( user = models.query_for_user( session=db_session, username="TESTUSERB" ) # contains no information - syncer.sync_single_user_visas(user, user.ga4gh_visas_v1, db_session) + synced_visas = syncer.sync_single_user_visas(user, user.ga4gh_visas_v1, db_session) user = models.query_for_user( session=db_session, username="TESTUSERB" ) # contains only visa information user1 = models.query_for_user(session=db_session, username="USER_1") assert len(user1.project_access) == 0 # other users are not affected assert len(user.project_access) == 6 + assert len(synced_visas) diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index 6f3faee62..fe34bccaf 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -179,7 +179,7 @@ def test_update_visa_token( kid: rsa_public_key, } } - ras_client.update_user_visas(test_user, pkey_cache=pkey_cache) + ras_client.update_user_authorization(test_user, pkey_cache=pkey_cache) query_visa = db_session.query(GA4GHVisaV1).first() assert query_visa.ga4gh_visa @@ -244,7 +244,7 @@ def test_update_visa_empty_passport_returned( kid: rsa_public_key, } } - ras_client.update_user_visas(test_user, pkey_cache=pkey_cache) + ras_client.update_user_authorization(test_user, pkey_cache=pkey_cache) query_visa = db_session.query(GA4GHVisaV1).first() assert query_visa == None @@ -321,7 +321,7 @@ def test_update_visa_empty_visa_returned( logger=logger, ) - ras_client.update_user_visas(test_user, pkey_cache={}) + ras_client.update_user_authorization(test_user, pkey_cache={}) query_visa = db_session.query(GA4GHVisaV1).first() assert query_visa == None @@ -430,7 +430,7 @@ def test_update_visa_token_with_invalid_visa( kid: rsa_public_key, } } - ras_client.update_user_visas(test_user, pkey_cache=pkey_cache) + ras_client.update_user_authorization(test_user, pkey_cache=pkey_cache) query_visas = db_session.query(GA4GHVisaV1).filter_by(user=test_user).all() assert len(query_visas) == 2 @@ -456,7 +456,7 @@ def test_update_visa_fetch_pkey( ): """ Test that when the RAS client's pkey cache is empty, the client's - update_user_visas can fetch and serialize the visa issuer's public keys and + update_user_authorization can fetch and serialize the visa issuer's public keys and validate a visa using the correct key. """ mock_discovery.return_value = "https://ras/token_endpoint" @@ -524,7 +524,7 @@ def test_update_visa_fetch_pkey( test_user = add_test_user(db_session) # Pass in an empty pkey cache so that the client will have to hit the jwks endpoint. - ras_client.update_user_visas(test_user, pkey_cache={}) + ras_client.update_user_authorization(test_user, pkey_cache={}) # Check that the new visa passed validation, indicating a successful pkey fetch query_visa = db_session.query(GA4GHVisaV1).first() @@ -536,7 +536,7 @@ def test_update_visa_fetch_pkey( @mock.patch( "fence.resources.openid.ras_oauth2.RASOauth2Client.get_value_from_discovery_doc" ) -def test_visa_update_cronjob( +def dont_test_visa_update_cronjob( mock_discovery, mock_get_token, mock_userinfo, From 7ed6e6edf441cd113527d8575f67c60e34f735e1 Mon Sep 17 00:00:00 2001 From: John McCann Date: Mon, 15 Nov 2021 12:06:40 -0800 Subject: [PATCH 086/211] docs(GA4GH): log more detail (jti, username, etc) --- fence/resources/ga4gh/passports.py | 51 ++++++++++++++++------------ fence/resources/openid/ras_oauth2.py | 25 +++++++------- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index cecfb1b0b..379186f0b 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -213,6 +213,7 @@ def validate_visa(raw_visa): '"jku" in the header. Only Visa Access Tokens are supported.' ) + logger.info("Attempting to validate visa") decoded_visa = validate_jwt( raw_visa, attempt_refresh=True, @@ -221,6 +222,9 @@ def validate_visa(raw_visa): issuers=config["GA4GH_VISA_ISSUER_ALLOWLIST"], options={"require_iat": True, "require_exp": True, "verify_aud": False}, ) + logger.info(f'Visa jti: "{decoded_visa.get("jti", "")}"') + logger.info(f'Visa txn: "{decoded_visa.get("txn", "")}"') + for claim in ["sub", "ga4gh_visa_v1"]: if claim not in decoded_visa: raise Exception(f'Visa does not contain REQUIRED "{claim}" claim') @@ -286,30 +290,33 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): (issuer, subject_id) ) if not iss_sub_pair_to_user: - logger.info( - "Creating a new Fence user with a username formed from subject " - "id and issuer. Mapping subject id and issuer combination to " - "said user" - ) username = subject_id + issuer[len("https://") :] - gen3_user = User(username=username) - idp_name = flask.current_app.issuer_to_idp.get(issuer) - if idp_name: - idp = ( - db_session.query(IdentityProvider) - .filter(IdentityProvider.name == idp_name) - .first() - ) - if not idp: - idp = IdentityProvider(name=idp_name) - gen3_user.identity_provider = idp - else: - logger.info( - "The user will be created without a linked identity " - "provider since it could not be determined based on " - "the issuer" - ) + user = query_for_user(session=db_session, username=username) + if not user: + logger.info(f'Creating a new Fence user with username "{username}"') + gen3_user = User(username=username) + idp_name = flask.current_app.issuer_to_idp.get(issuer) + if idp_name: + idp = ( + db_session.query(IdentityProvider) + .filter(IdentityProvider.name == idp_name) + .first() + ) + if not idp: + idp = IdentityProvider(name=idp_name) + gen3_user.identity_provider = idp + else: + logger.info( + "The user will be created without a linked identity " + "provider since it could not be determined based on " + "the issuer" + ) + logger.info( + f'Mapping subject id ("{subject_id}") and issuer ' + f'("{issuer}") combination to Fence user ' + f'"{gen3_user.username}"' + ) iss_sub_pair_to_user = IssSubPairToUser(iss=issuer, sub=subject_id) iss_sub_pair_to_user.user = gen3_user diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index fa44e1bfd..968b12b5b 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -181,8 +181,8 @@ def map_iss_sub_pair_to_user(self, issuer, subject_id, username, email): warning for more details. Args: - issuer (str): RAS issuer - subject_id (str): RAS subject + issuer (str): issuer + subject_id (str): subject username (str): username of the Fence user who is being mapped to email (str): email to populate the mapped Fence user with in cases when this function creates the mapped user or changes @@ -200,12 +200,12 @@ def map_iss_sub_pair_to_user(self, issuer, subject_id, username, email): user = query_for_user(db_session, username) if iss_sub_pair_to_user: if not user: - # TODO just say DRS, not DRS/data self.logger.info( - "Issuer and subject id have already been mapped to a " - "Fence user created from the DRS endpoint. " - "Changing said user's username to the username " - "returned from the RAS userinfo endpoint." + f'Issuer ("{issuer}") and subject id ("{subject_id}") ' + "have already been mapped to a Fence user " + f'("{iss_sub_pair_to_user.user.username}") created ' + "from the DRS endpoint. Changing said user's username" + f' to "{username}".' ) # TODO also change username in Arborist iss_sub_pair_to_user.user.username = username @@ -214,11 +214,12 @@ def map_iss_sub_pair_to_user(self, issuer, subject_id, username, email): elif iss_sub_pair_to_user.user.username != username: self.logger.warning( "Two users exist in the Fence database corresponding " - "to the RAS user who is currently trying to log in: one " - "created from an earlier login and one created from " - "the DRS endpoint. The one created from the " - "DRS endpoint will be logged in, rendering the " - "other one inaccessible." + "to the user who is currently trying to log in: one " + f'created from an earlier login ("{username}") and ' + f"one created from the DRS endpoint " + f'("{iss_sub_pair_to_user.user.username}"). ' + f'"{iss_sub_pair_to_user.user.username}" will be ' + f'logged in, rendering "{username}" inaccessible.' ) return iss_sub_pair_to_user.user.username From b639530ee0a62e3a684db6a13cf37746bc8f9791 Mon Sep 17 00:00:00 2001 From: John McCann Date: Mon, 15 Nov 2021 22:36:13 -0800 Subject: [PATCH 087/211] refactor(GA4GH): use create_user function --- fence/models.py | 34 ++++++++++++++++++++++++++++ fence/resources/ga4gh/passports.py | 22 +++++------------- fence/resources/openid/ras_oauth2.py | 19 ++++++---------- 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/fence/models.py b/fence/models.py index ca762d06d..b4d971742 100644 --- a/fence/models.py +++ b/fence/models.py @@ -66,6 +66,40 @@ def query_for_user(session, username): ) +def create_user(session, logger, username, email=None, idp_name=None): + """ + Create a new user in the database. + + Args: + session (sqlalchemy.orm.session.Session): database session + logger (logging.Logger): logger + username (str): username to save for the created user + email (str): email to save for the created user + idp_name (str): name of identity provider to link + + Return: + userdatamodel.user.User: the created user + """ + logger.info(f'Creating a new Fence user with username "{username}"') + + user = User(username=username) + if email: + user.email = email + if idp_name: + idp = ( + session.query(IdentityProvider) + .filter(IdentityProvider.name == idp_name) + .first() + ) + if not idp: + idp = IdentityProvider(name=idp_name) + user.identity_provider = idp + + session.add(user) + session.commit() + return user + + class ClientAuthType(Enum): """ List the possible types of OAuth client authentication, which are diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 379186f0b..cfffc4034 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -16,6 +16,7 @@ from fence.jwt.validate import validate_jwt from fence.config import config from fence.models import ( + create_user, query_for_user, GA4GHVisaV1, User, @@ -291,23 +292,13 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): ) if not iss_sub_pair_to_user: username = subject_id + issuer[len("https://") :] - user = query_for_user(session=db_session, username=username) - if not user: - logger.info(f'Creating a new Fence user with username "{username}"') - gen3_user = User(username=username) + gen3_user = query_for_user(session=db_session, username=username) + if not gen3_user: idp_name = flask.current_app.issuer_to_idp.get(issuer) - if idp_name: - idp = ( - db_session.query(IdentityProvider) - .filter(IdentityProvider.name == idp_name) - .first() - ) - if not idp: - idp = IdentityProvider(name=idp_name) - gen3_user.identity_provider = idp - else: + gen3_user = create_user(db_session, logger, username, idp_name=idp_name) + if not idp_name: logger.info( - "The user will be created without a linked identity " + "The user was created without a linked identity " "provider since it could not be determined based on " "the issuer" ) @@ -320,7 +311,6 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): iss_sub_pair_to_user = IssSubPairToUser(iss=issuer, sub=subject_id) iss_sub_pair_to_user.user = gen3_user - db_session.add(gen3_user) db_session.add(iss_sub_pair_to_user) db_session.commit() diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 968b12b5b..be0fdd2d0 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -19,6 +19,7 @@ User, IssSubPairToUser, query_for_user, + create_user, ) from fence.jwt.validate import validate_jwt from fence.utils import DEFAULT_BACKOFF_SETTINGS @@ -224,19 +225,13 @@ def map_iss_sub_pair_to_user(self, issuer, subject_id, username, email): return iss_sub_pair_to_user.user.username if not user: - self.logger.info( - "Creating a user in the Fence database before mapping issuer and subject id" + user = create_user( + db_session, + self.logger, + username, + email=email, + idp_name=IdentityProvider.ras, ) - user = User(username=username, email=email) - idp = ( - db_session.query(IdentityProvider) - .filter(IdentityProvider.name == IdentityProvider.ras) - .first() - ) - if not idp: - idp = IdentityProvider(name=IdentityProvider.ras) - user.identity_provider = idp - db_session.add(user) self.logger.info("Mapping issuer and subject id to Fence user") iss_sub_pair_to_user = IssSubPairToUser(iss=issuer, sub=subject_id) From 80b88385b05788cb1eaeba7567398eb82b7ce55d Mon Sep 17 00:00:00 2001 From: John McCann Date: Wed, 17 Nov 2021 10:31:25 -0800 Subject: [PATCH 088/211] Revert "refactor(ga4gh): return [users], not [usernames]" This reverts commit 4d06991a8541b013bc8ce797475d3edbf0f8b026. --- fence/resources/ga4gh/passports.py | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index cfffc4034..754555af1 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -45,14 +45,13 @@ def get_gen3_users_from_ga4gh_passports(passports): embedded within the passports passed in """ logger.info("Getting gen3 users from passports") - users_from_all_passports = [] - user_ids_from_all_passports = [] + usernames_from_all_passports = [] for passport in passports: try: # TODO check cache - cached_user_ids = get_gen3_user_ids_for_passport_from_cache(passport) - if cached_user_ids: - user_ids_from_all_passports.extend(cached_user_ids) + cached_usernames = get_gen3_usernames_for_passport_from_cache(passport) + if cached_usernames: + usernames_from_all_passports.extend(cached_usernames) # existence in the cache means that this passport was validated # previously continue @@ -99,7 +98,7 @@ def get_gen3_users_from_ga4gh_passports(passports): ) continue - users_from_current_passport = [] + usernames_from_current_passport = [] for (issuer, subject_id), visas in identity_to_visas.items(): gen3_user = get_or_create_gen3_user_from_iss_sub(issuer, subject_id) @@ -116,24 +115,17 @@ def get_gen3_users_from_ga4gh_passports(passports): ] # NOTE: does not validate, assumes validation occurs above. sync_visa_authorization(gen3_user, ga4gh_visas, min_visa_expiration) - users_from_current_passport.append(gen3_user) + usernames_from_current_passport.append(gen3_user.username) put_gen3_usernames_for_passport_into_cache( - passport, users_from_current_passport + passport, usernames_from_current_passport ) - users_from_all_passports.extend(users_from_current_passport) + usernames_from_all_passports.extend(usernames_from_current_passport) - # TODO use user_ids_from_all_passports that were returned from cache to - # query db for users and add those queried users to - # users_from_all_passports + return list(set(usernames_from_all_passports)) - # the same user could have been added to users_from_all_passports more - # than one time, making the dictionary comprehension below necessary to - # return a list of unique users - return list({u.username: u for u in users_from_all_passports}.values()) - -def get_gen3_user_ids_for_passport_from_cache(passport): +def get_gen3_usernames_for_passport_from_cache(passport): cached_user_ids = [] # TODO return cached_user_ids From 75b36f601f88b68cf70a47f1987ab83620b5592f Mon Sep 17 00:00:00 2001 From: John McCann Date: Wed, 17 Nov 2021 11:56:47 -0800 Subject: [PATCH 089/211] docs(map_iss_sub_pair_to_user): log map details --- fence/resources/openid/ras_oauth2.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index be0fdd2d0..0007a1989 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -233,7 +233,10 @@ def map_iss_sub_pair_to_user(self, issuer, subject_id, username, email): idp_name=IdentityProvider.ras, ) - self.logger.info("Mapping issuer and subject id to Fence user") + self.logger.info( + f'Mapping issuer ("{issuer}") and subject id ("{subject_id}") ' + f'combination to Fence user "{user.username}"' + ) iss_sub_pair_to_user = IssSubPairToUser(iss=issuer, sub=subject_id) iss_sub_pair_to_user.user = user db_session.add(iss_sub_pair_to_user) From 8a325268a654de557b7b2509fc484fd76628cb50 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Wed, 17 Nov 2021 16:20:33 -0600 Subject: [PATCH 090/211] feat(cfg): global cfg to disable allowing Passports->DRS as a method of authN/Z, also don't allow passport & Auth header --- fence/blueprints/data/indexd.py | 6 +++ fence/blueprints/ga4gh.py | 6 +++ fence/config-default.yaml | 2 + tests/test_drs.py | 69 +++++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index da1238965..56a364a5e 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -76,6 +76,12 @@ def get_signed_url_for_file( if no_force_sign_param and no_force_sign_param.lower() == "true": force_signed_url = False + if ga4gh_passports and not config["GA4GH_PASSPORTS_TO_DRS_ENABLED"]: + raise NotSupported( + "Using GA4GH Passports as a means of authentication and authorization " + "is not supported by this instance of Gen3." + ) + user_ids_from_passports = None if ga4gh_passports: # TODO change this to usernames diff --git a/fence/blueprints/ga4gh.py b/fence/blueprints/ga4gh.py index f4e9b6f45..4bb110bcc 100644 --- a/fence/blueprints/ga4gh.py +++ b/fence/blueprints/ga4gh.py @@ -30,6 +30,12 @@ def get_ga4gh_signed_url(object_id, access_id): config["GA4GH_DRS_POSTED_PASSPORT_FIELD"] ) + if ga4gh_passports and flask.request.headers.get("Authorization"): + raise UserError( + "You cannot supply both GA4GH passports and a token " + "in the Authorization header of a request." + ) + result = get_signed_url_for_file( "download", object_id, diff --git a/fence/config-default.yaml b/fence/config-default.yaml index 00c954d25..3c8631e03 100755 --- a/fence/config-default.yaml +++ b/fence/config-default.yaml @@ -871,6 +871,8 @@ SERVICE_ACCOUNT_LIMIT: 6 # ////////////////////////////////////////////////////////////////////////////////////// # GA4GH SUPPORT: DATA ACCESS AND AUTHORIZATION SYNCING # ////////////////////////////////////////////////////////////////////////////////////// +# whether or not to accept GA4GH Passports as a means of AuthN/Z to the DRS data access endpoint +GA4GH_PASSPORTS_TO_DRS_ENABLED: false # RAS refresh_tokens expire in 15 days RAS_REFRESH_EXPIRATION: 1296000 diff --git a/tests/test_drs.py b/tests/test_drs.py index 2769784de..df852dec6 100644 --- a/tests/test_drs.py +++ b/tests/test_drs.py @@ -5,6 +5,8 @@ import responses from tests import utils +from fence.config import config + def get_doc(has_version=True, urls=list(), drs_list=0): doc = { @@ -230,3 +232,70 @@ def test_get_presigned_url_with_query_params( headers=user, ) assert res.status_code == 200 + + +@responses.activate +@pytest.mark.parametrize("indexd_client", ["s3", "gs"], indirect=True) +def test_get_presigned_url_passports_disabled( + client, + indexd_client, + kid, + rsa_private_key, + google_proxy_group, + primary_google_service_account, + cloud_manager, + google_signed_url, + monkeypatch, +): + access_id = indexd_client["indexed_file_location"] + test_guid = "1" + passports_body = {"passports": ["i.am.passport"]} + + monkeypatch.setitem(config, "GA4GH_PASSPORTS_TO_DRS_ENABLED", False) + + res = client.post( + "/ga4gh/drs/v1/objects/" + test_guid + f"/access/{access_id}", + data=json.dumps(passports_body), + ) + + assert res.status_code == 400 + + +@responses.activate +@pytest.mark.parametrize("indexd_client", ["s3", "gs"], indirect=True) +def test_get_presigned_url_post_passport_and_send_header_token( + client, + user_client, + indexd_client, + kid, + rsa_private_key, + google_proxy_group, + primary_google_service_account, + cloud_manager, + google_signed_url, + monkeypatch, +): + access_id = indexd_client["indexed_file_location"] + test_guid = "1" + passports_body = {"passports": ["i.am.passport"]} + user = { + "Authorization": "Bearer " + + jwt.encode( + utils.authorized_download_context_claims( + user_client.username, user_client.user_id + ), + key=rsa_private_key, + headers={"kid": kid}, + algorithm="RS256", + ).decode("utf-8") + } + + monkeypatch.setitem(config, "GA4GH_PASSPORTS_TO_DRS_ENABLED", True) + + res = client.post( + "/ga4gh/drs/v1/objects/" + test_guid + f"/access/{access_id}", + headers=user, + data=json.dumps(passports_body), + ) + + assert res.status_code == 400 From ca9616a115addee0a92c4753714056b6c97743d7 Mon Sep 17 00:00:00 2001 From: BinamB Date: Thu, 18 Nov 2021 14:20:57 -0600 Subject: [PATCH 091/211] testing testing 123 --- fence/blueprints/data/indexd.py | 32 ++- gen3authz-1.2.1.tar.gz | Bin 0 -> 13121 bytes poetry.lock | 489 +++++++++++--------------------- pyproject.toml | 2 +- tests/test_drs.py | 161 +++++++++++ 5 files changed, 351 insertions(+), 333 deletions(-) create mode 100644 gen3authz-1.2.1.tar.gz diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index da1238965..4e9cd3a73 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -80,6 +80,8 @@ def get_signed_url_for_file( if ga4gh_passports: # TODO change this to usernames user_ids_from_passports = get_gen3_users_from_ga4gh_passports(ga4gh_passports) + print("------------user idss from passports---------------") + print(user_ids_from_passports) # add the user details to `flask.g.audit_data` first, so they are # included in the audit log if `IndexedFile(file_id)` raises a 404 @@ -436,11 +438,24 @@ def get_signed_url( if action is not None and action not in SUPPORTED_ACTIONS: raise NotSupported("action {} is not supported".format(action)) return self._get_signed_url( - protocol, action, expires_in, force_signed_url, r_pays_project, file_name + protocol, + action, + expires_in, + force_signed_url, + r_pays_project, + file_name, + user_ids_from_passports, ) def _get_signed_url( - self, protocol, action, expires_in, force_signed_url, r_pays_project, file_name + self, + protocol, + action, + expires_in, + force_signed_url, + r_pays_project, + file_name, + user_ids_from_passports=None, ): if action == "upload": # NOTE: self.index_document ensures the GUID exists in indexd and raises @@ -475,6 +490,7 @@ def _get_signed_url( public_data=self.public, force_signed_url=force_signed_url, r_pays_project=r_pays_project, + user_ids_from_passports=user_ids_from_passports, ) raise NotFound( @@ -687,6 +703,7 @@ def get_signed_url( expires_in, public_data=False, force_signed_url=True, + user_ids_from_passports=None, **kwargs, ): return self.url @@ -1050,10 +1067,11 @@ def get_signed_url( public_data=False, force_signed_url=True, r_pays_project=None, + user_ids_from_passports=None, ): resource_path = self.get_resource_path() - user_info = _get_user_info() + user_info = _get_user_info(user_ids_from_passports=user_ids_from_passports) if public_data and not force_signed_url: url = "https://storage.cloud.google.com/" + resource_path @@ -1428,13 +1446,19 @@ def delete(self, container, blob): # pylint: disable=R0201 return ("Failed to delete data file.", status_code) -def _get_user_info(sub_type=str): +def _get_user_info(sub_type=str, user_ids_from_passports=None): """ Attempt to parse the request for token to authenticate the user. fallback to populated information about an anonymous user. By default, cast `sub` to str. Use `sub_type` to override this behavior. """ # TODO Update to support POSTed passport + print("-----get user_infdo==========") + print(user_ids_from_passports) + # if user_ids_from_passports: + # try: + # #query idp table + # user_id = "" try: set_current_token( validate_request(scope={"user"}, audience=config.get("BASE_URL")) diff --git a/gen3authz-1.2.1.tar.gz b/gen3authz-1.2.1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..2f8ac00d0c462324b53efaf33b3afe4c4a0a0b56 GIT binary patch literal 13121 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

T8A3~ne& z2Ew?%p&Y6nL?E6p`tD$FVd`^bKWvhkR>402E2+CI=D6-06fPXj31K``?NDll?LJ4 z#9x(>mHD``d56U}TXfim#JP?fNWwH+%-fml5jZrQ2imhvPfd`zjhS&HAs)@x=#G?` zEYtCql1t1aUb$o}P16Sn$*LmPOb(eKX5=;2-V9SBEX}Kyn9ZgQzCN1QGyPtUthS;v zRxK|+u0Lu*Es<=regy0cQyTP6&idCvN-?j2(>3R663q;`7vojA(ROJ`a26O7ti}YR zm1{YLt)&p8`p1kJL3m>B5Tc2l^}2BSg|q#C%=5~_An#RH!kLLV=YRZIOunVP8(Bb+ zuIZb!G~jYI@hGYYi)K5+Z8NZ<9jI(-ko|k|+ASD4)@*$nyndkLTCeQ`UPpRABNZOfjFLH z%bFT%fEFUCg1mq1;$BFS0u-#t>t?hLu|v8+5r8fzFb9Yv>+G>AG&}@9Grb}V4$WOq??tMemM%8VQ3G+=b?19+V0sK5t*$ELc#vxbuuIwQ}UJ9ro6 zL9Nx}9|}hlp#FBx!m&05+CKqDem41f6Tc;uv!9YS1H#Qe9Ro~({1b!1SH|%VbF``# zN7FRg2KJ}uW%px9$FpdA(ZGUU*%L6y8#Q@TvM*}V1PE|I9XmET$?zrfu@dU5D2w#E z*c|sxGesYVISwPbq*<36`ZH%nbkwM*m-A&;0vJI0>G)~{+&l08seNtQ z+`bmzUz1uL`7!-O3Z}or^{d(p=-mDu4EiDpf@y025*{YJ~FALQquzIrpxzvwqCj3p`gKiyzsA(Y9uv1>^xj5tv-f@N)+$`m` z+jLeHJ+W}~ZJPiHO&)oL?6zT@Y?iQT-f@JljRCga13B*h`$>HsfHzn#hRZ-Y;5Pr( z<(0!%Z_Ll`UtrM%FgW%6X^=8AE~InOyimYwc7~NgvCk_(N_d_H1?uh|tLb-b zlW_J>TddYjxQ5U0U$G(8U!hiXdQ9*ik$F>DC6|V3_h|Glztg-iECu%hm23@-XM}46 zugN{;Nj72*nh!_c8&4>P9P5IW=2(7;D)H2?Aw*tORSaE z-6ntLOyecjMTPA~kMoxP?Hq+G^CGQU$9>Y%<{^P&-S;%32pD>#2|tIu698>`T#;6blot|JSAFkB^N&>2PKr zrn~wZjV!u0xD1Bjc;b71n9Dm>r8uE4+*XofoJT_MOMDt`8%g{~$NRG8OM0vS=Hl6Z z&9oNqUmsc4eLgvv0>448dpQGsoG+i2k?M#Re>Sf%Uw|GSh#a3lqSO9vT`pUP?-YbM zvR`fa_pnn*WRi~(q5_}Pt~ z$^CltYV&%}oYH&>U*iyQG(T68hd2&4;T>DFKh2#EUUi$=ofG{V?1-zi_paBtvs2Vv z`NgaY|J*Z2S{yJYwaTP3?QGJbu>m;3G{No9&8qgMSeHrLvFw0UAKTxi_PNuIUL3P0 Z>wH<3k9PmdB>DUZN;0e(g9w3w{0~brt&RWy literal 0 HcmV?d00001 diff --git a/poetry.lock b/poetry.lock index 1f8676514..83b370d66 100644 --- a/poetry.lock +++ b/poetry.lock @@ -79,22 +79,9 @@ xmltodict = ">=0.9,<1.0" flask = ["Flask (>=0.10.1)"] fastapi = ["fastapi (>=0.54.1,<0.55.0)"] -[[package]] -name = "aws-xray-sdk" -version = "0.95" -description = "The AWS X-Ray SDK for Python (the SDK) enables Python developers to record and emit information from within their applications to the AWS X-Ray service." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -jsonpickle = "*" -requests = "*" -wrapt = "*" - [[package]] name = "azure-core" -version = "1.19.0" +version = "1.20.1" description = "Microsoft Azure Core Library for Python" category = "main" optional = false @@ -396,23 +383,6 @@ idna = ["idna (>=2.1)"] curio = ["curio (>=1.2)", "sniffio (>=1.1)"] trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] -[[package]] -name = "docker" -version = "5.0.3" -description = "A Python library for the Docker Engine API." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pywin32 = {version = "227", markers = "sys_platform == \"win32\""} -requests = ">=2.14.2,<2.18.0 || >2.18.0" -websocket-client = ">=0.32.0" - -[package.extras] -ssh = ["paramiko (>=2.4.2)"] -tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=3.4.7)", "idna (>=2.0.0)"] - [[package]] name = "docopt" version = "0.6.2" @@ -543,7 +513,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "gen3authz" -version = "1.1.1" +version = "1.2.1" description = "Gen3 authz client" category = "main" optional = false @@ -555,6 +525,10 @@ cdiserrors = "<2.0.0" contextvars = {version = ">=2.4,<3.0", markers = "python_version < \"3.7\""} httpx = ">=0.20.0,<1.0.0" +[package.source] +type = "file" +url = "gen3authz-1.2.1.tar.gz" + [[package]] name = "gen3cirrus" version = "2.0.0" @@ -602,7 +576,7 @@ PyYAML = ">=5.1,<6.0" [[package]] name = "google-api-core" -version = "1.31.3" +version = "1.31.4" description = "Google API client core library" category = "main" optional = false @@ -612,7 +586,7 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" google-auth = ">=1.25.0,<2.0dev" googleapis-common-protos = ">=1.6.0,<2.0dev" packaging = ">=14.3" -protobuf = ">=3.12.0,<3.18.0" +protobuf = {version = ">=3.12.0", markers = "python_version > \"3\""} pytz = "*" requests = ">=2.18.0,<3.0.0dev" six = ">=1.13.0" @@ -672,7 +646,7 @@ six = "*" [[package]] name = "google-cloud-core" -version = "2.1.0" +version = "2.2.1" description = "Google Cloud API client core library" category = "main" optional = false @@ -687,7 +661,7 @@ grpc = ["grpcio (>=1.8.2,<2.0dev)"] [[package]] name = "google-cloud-storage" -version = "1.42.3" +version = "1.43.0" description = "Google Cloud Storage API client library" category = "main" optional = false @@ -767,14 +741,14 @@ http2 = ["h2 (>=3,<5)"] [[package]] name = "httplib2" -version = "0.20.1" +version = "0.20.2" description = "A comprehensive HTTP client library." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] -pyparsing = ">=2.4.2,<3" +pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} [[package]] name = "httpx" @@ -821,7 +795,7 @@ test = ["flake8 (>=3.8.4,<3.9.0)", "pycodestyle (>=2.6.0,<2.7.0)", "mypy (>=0.91 [[package]] name = "importlib-metadata" -version = "4.8.1" +version = "4.8.2" description = "Read metadata from Python packages" category = "main" optional = false @@ -834,7 +808,7 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] perf = ["ipython"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "isodate" @@ -877,51 +851,27 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "jsondiff" -version = "1.1.1" -description = "Diff JSON and JSON-like structures in Python" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "jsonpickle" -version = "2.0.0" -description = "Python library for serializing any arbitrary object graph into JSON" -category = "dev" -optional = false -python-versions = ">=2.7" - -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["coverage (<5)", "pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black-multipy", "pytest-cov", "ecdsa", "feedparser", "numpy", "pandas", "pymongo", "sklearn", "sqlalchemy", "enum34", "jsonlib"] -"testing.libs" = ["demjson", "simplejson", "ujson", "yajl"] - [[package]] name = "markdown" -version = "3.3.4" +version = "3.3.6" description = "Python implementation of Markdown." category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} [package.extras] testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" -version = "2.0.1" +version = "1.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" [[package]] name = "mock" @@ -941,7 +891,7 @@ test = ["unittest2 (>=1.1.0)"] [[package]] name = "more-itertools" -version = "8.10.0" +version = "8.11.0" description = "More routines for operating on iterables, beyond itertools" category = "dev" optional = false @@ -949,34 +899,41 @@ python-versions = ">=3.5" [[package]] name = "moto" -version = "1.3.7" +version = "1.3.15" description = "A library that allows your python tests to easily mock out the boto library" category = "dev" optional = false python-versions = "*" [package.dependencies] -aws-xray-sdk = ">=0.93,<0.96" boto = ">=2.36.0" -boto3 = ">=1.6.16" -botocore = ">=1.12.13" -cryptography = ">=2.3.0" -docker = ">=2.5.1" -Jinja2 = ">=2.7.3" -jsondiff = "1.1.1" +boto3 = ">=1.9.201" +botocore = ">=1.12.201" +Jinja2 = ">=2.10.1" +MarkupSafe = "<2.0" mock = "*" -pyaml = "*" +more-itertools = "*" python-dateutil = ">=2.1,<3.0.0" -python-jose = "<3.0.0" pytz = "*" requests = ">=2.5" responses = ">=0.9.0" six = ">1.9" werkzeug = "*" xmltodict = "*" +zipp = "*" [package.extras] +acm = ["cryptography (>=2.3.0)"] +all = ["cryptography (>=2.3.0)", "PyYAML (>=5.1)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)", "docker (>=2.5.1)", "jsondiff (>=1.1.2)", "aws-xray-sdk (>=0.93,!=0.96)", "idna (>=2.5,<3)", "cfn-lint (>=0.4.0)", "sshpubkeys (>=3.1.0,<4.0)", "sshpubkeys (>=3.1.0)"] +awslambda = ["docker (>=2.5.1)"] +batch = ["docker (>=2.5.1)"] +cloudformation = ["PyYAML (>=5.1)", "cfn-lint (>=0.4.0)"] +cognitoidp = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)"] +ec2 = ["cryptography (>=2.3.0)", "sshpubkeys (>=3.1.0,<4.0)", "sshpubkeys (>=3.1.0)"] +iam = ["cryptography (>=2.3.0)"] +iotdata = ["jsondiff (>=1.1.2)"] server = ["flask"] +xray = ["aws-xray-sdk (>=0.93,!=0.96)"] [[package]] name = "msrest" @@ -1025,14 +982,14 @@ signedtoken = ["cryptography (>=3.0.0,<4)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "packaging" -version = "21.0" +version = "21.3" description = "Core utilities for Python packages" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2" +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "paramiko" @@ -1089,7 +1046,7 @@ twisted = ["twisted"] [[package]] name = "prometheus-flask-exporter" -version = "0.18.4" +version = "0.18.5" description = "Prometheus metrics exporter for Flask" category = "main" optional = false @@ -1101,18 +1058,15 @@ prometheus-client = "*" [[package]] name = "protobuf" -version = "3.17.3" +version = "3.19.1" description = "Protocol Buffers" category = "main" optional = false -python-versions = "*" - -[package.dependencies] -six = ">=1.9" +python-versions = ">=3.5" [[package]] name = "psycopg2" -version = "2.9.1" +version = "2.9.2" description = "psycopg2 - Python-PostgreSQL Database Adapter" category = "main" optional = false @@ -1120,22 +1074,11 @@ python-versions = ">=3.6" [[package]] name = "py" -version = "1.10.0" +version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pyaml" -version = "21.10.1" -description = "PyYAML-based module to produce pretty and readable YAML-serialized data" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -PyYAML = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pyasn1" @@ -1158,7 +1101,7 @@ pyasn1 = ">=0.4.6,<0.5.0" [[package]] name = "pycparser" -version = "2.20" +version = "2.21" description = "C parser in Python" category = "main" optional = false @@ -1206,11 +1149,14 @@ tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] [[package]] name = "pyparsing" -version = "2.4.7" +version = "3.0.6" description = "Python parsing module" category = "main" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" @@ -1300,14 +1246,6 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "pywin32" -version = "227" -description = "Python for Window Extensions" -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "pyyaml" version = "5.4.1" @@ -1351,7 +1289,7 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] [[package]] name = "responses" -version = "0.15.0" +version = "0.16.0" description = "A utility library for mocking out the `requests` Python library." category = "dev" optional = false @@ -1481,11 +1419,11 @@ resolved_reference = "4d39265d6e478acd5e1afe6e5dc722418f887d78" [[package]] name = "typing-extensions" -version = "3.10.0.2" -description = "Backported and Experimental Type Hints for Python 3.5+" +version = "4.0.0" +description = "Backported and Experimental Type Hints for Python 3.6+" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "uritemplate" @@ -1520,18 +1458,6 @@ python-versions = "*" cdislogging = "*" sqlalchemy = ">=1.3.3,<1.4.0" -[[package]] -name = "websocket-client" -version = "1.2.1" -description = "WebSocket client for Python with low level API options" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - [[package]] name = "werkzeug" version = "1.0.1" @@ -1544,29 +1470,19 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] watchdog = ["watchdog"] -[[package]] -name = "wrapt" -version = "1.13.2" -description = "Module for decorators, wrappers and monkey patching." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - [[package]] name = "wtforms" -version = "2.3.3" -description = "A flexible forms validation and rendering library for Python web development." +version = "3.0.0" +description = "Form validation and rendering for Python web development." category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [package.dependencies] MarkupSafe = "*" [package.extras] email = ["email-validator"] -ipaddress = ["ipaddress"] -locale = ["Babel (>=1.3)"] [[package]] name = "xmltodict" @@ -1591,7 +1507,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "05fc2802e427c72c626874251df018ea3ceaccdc0297382ae97bbf274b664bb6" +content-hash = "acdd98018b0a3c640e5752ed99512ca6abbf30eb25fa7e143deeeee0ade8417f" [metadata.files] addict = [ @@ -1622,13 +1538,9 @@ authutils = [ {file = "authutils-6.1.0-py3-none-any.whl", hash = "sha256:682dba636694c36fb35af1d9ff576bb8436337c3899f0ef434cda5918d661db9"}, {file = "authutils-6.1.0.tar.gz", hash = "sha256:7263af0b2ce3a0db19236fd123b34f795d07e07111b7bd18a51808568ddfdc2e"}, ] -aws-xray-sdk = [ - {file = "aws-xray-sdk-0.95.tar.gz", hash = "sha256:9e7ba8dd08fd2939376c21423376206bff01d0deaea7d7721c6b35921fed1943"}, - {file = "aws_xray_sdk-0.95-py2.py3-none-any.whl", hash = "sha256:72791618feb22eaff2e628462b0d58f398ce8c1bacfa989b7679817ab1fad60c"}, -] azure-core = [ - {file = "azure-core-1.19.0.zip", hash = "sha256:18d2a6cd3b7391489f005775fe69e4d0870f9384b755e45185efd45c050e2306"}, - {file = "azure_core-1.19.0-py2.py3-none-any.whl", hash = "sha256:4fbbe8b867ef077df77614b86b7927e4d87aa7a0bd54e771d9ba14f48dae2c4b"}, + {file = "azure-core-1.20.1.zip", hash = "sha256:21d06311c9c373e394ed9f9db035306773334a0181932e265889eca34d778d17"}, + {file = "azure_core-1.20.1-py2.py3-none-any.whl", hash = "sha256:5e5df1850ef6eff2b481a4a5fefa7d73ec74b6a2e0b27b179341f73f655fa4bf"}, ] azure-storage-blob = [ {file = "azure-storage-blob-12.9.0.zip", hash = "sha256:cff66a115c73c90e496c8c8b3026898a3ce64100840276e9245434e28a864225"}, @@ -1846,10 +1758,6 @@ dnspython = [ {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, ] -docker = [ - {file = "docker-5.0.3-py2.py3-none-any.whl", hash = "sha256:7a79bb439e3df59d0a72621775d600bc8bc8b422d285824cb37103eab91d1ce0"}, - {file = "docker-5.0.3.tar.gz", hash = "sha256:d916a26b62970e7c2f554110ed6af04c7ccff8e9f81ad17d0d40c75637e227fb"}, -] docopt = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] @@ -1889,8 +1797,7 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] gen3authz = [ - {file = "gen3authz-1.1.1-py3-none-any.whl", hash = "sha256:ed34291683d8ebc8015a38f81917933dd64d3bef4c50a5158612e87f65d349a7"}, - {file = "gen3authz-1.1.1.tar.gz", hash = "sha256:0c32a6fd40c94c2f93c987f24e2953e6537e2fe98b029f9c85dde6e39818e014"}, + {file = "gen3authz-1.2.1.tar.gz", hash = "sha256:be3970b9e8b49b5468940be65835646fdbeaabffaf2b7b891e3a1011a99c660e"}, ] gen3cirrus = [ {file = "gen3cirrus-2.0.0.tar.gz", hash = "sha256:0bd590c407c42dad5f0b896da0fa30bd01ea6bef5ff7dd11324ec59f14a71793"}, @@ -1902,8 +1809,8 @@ gen3users = [ {file = "gen3users-0.6.0.tar.gz", hash = "sha256:3b9b56798a7d8b34712389dbbab93c00b0f92524f890513f899c31630ea986da"}, ] google-api-core = [ - {file = "google-api-core-1.31.3.tar.gz", hash = "sha256:4b7ad965865aef22afa4aded3318b8fa09b20bcc7e8dbb639a3753cf60af08ea"}, - {file = "google_api_core-1.31.3-py2.py3-none-any.whl", hash = "sha256:f52c708ab9fd958862dea9ac94d9db1a065608073fe583c3b9c18537b177f59a"}, + {file = "google-api-core-1.31.4.tar.gz", hash = "sha256:c77ffc8b4981b44efdb9d68431fd96d21dbd39545c29552bbe79b9b7dd2c3689"}, + {file = "google_api_core-1.31.4-py2.py3-none-any.whl", hash = "sha256:ed59c6a695a81f601e4ba0f37ca9dbde3c43b3309e161a1a8946f266db4a0c4e"}, ] google-api-python-client = [ {file = "google-api-python-client-1.11.0.tar.gz", hash = "sha256:caf4015800ef1a18d06d117f47f0219c0c0641f21978f6b1bb5ede7912fab97b"}, @@ -1918,12 +1825,12 @@ google-auth-httplib2 = [ {file = "google_auth_httplib2-0.1.0-py2.py3-none-any.whl", hash = "sha256:31e49c36c6b5643b57e82617cb3e021e3e1d2df9da63af67252c02fa9c1f4a10"}, ] google-cloud-core = [ - {file = "google-cloud-core-2.1.0.tar.gz", hash = "sha256:35a1f5f02a86e0fa2e28c669f0db4a76d928671a28fbbbb493ab59ba9d1cb9a9"}, - {file = "google_cloud_core-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d5fed11731dae8bc8656a2c9fa8ff17bdfdfd083cba97569324e35b94e7e002"}, + {file = "google-cloud-core-2.2.1.tar.gz", hash = "sha256:476d1f71ab78089e0638e0aaf34bfdc99bab4fce8f4170ba6321a5243d13c5c7"}, + {file = "google_cloud_core-2.2.1-py2.py3-none-any.whl", hash = "sha256:ab6cee07791afe4e210807ceeab749da6a076ab16d496ac734bf7e6ffea27486"}, ] google-cloud-storage = [ - {file = "google-cloud-storage-1.42.3.tar.gz", hash = "sha256:7754d4dcaa45975514b404ece0da2bb4292acbc67ca559a69e12a19d54fcdb06"}, - {file = "google_cloud_storage-1.42.3-py2.py3-none-any.whl", hash = "sha256:71ee3a0dcf2c139f034a054181cd7658f1ec8f12837d2769c450a8a00fcd4c6d"}, + {file = "google-cloud-storage-1.43.0.tar.gz", hash = "sha256:f3b4f4be5c8a1b5727a8f7136c94d3bacdd4b7bf11f9553f51ae4c1d876529d3"}, + {file = "google_cloud_storage-1.43.0-py2.py3-none-any.whl", hash = "sha256:bb3e4088054d50616bd57e4b81bb158db804c91faed39279d666e2fd07d2c118"}, ] google-crc32c = [ {file = "google-crc32c-1.3.0.tar.gz", hash = "sha256:276de6273eb074a35bc598f8efbc00c7869c5cf2e29c90748fccc8c898c244df"}, @@ -1987,8 +1894,8 @@ httpcore = [ {file = "httpcore-0.13.3.tar.gz", hash = "sha256:5d674b57a11275904d4fd0819ca02f960c538e4472533620f322fc7db1ea0edc"}, ] httplib2 = [ - {file = "httplib2-0.20.1-py3-none-any.whl", hash = "sha256:8fa4dbf2fbf839b71f8c7837a831e00fcdc860feca99b8bda58ceae4bc53d185"}, - {file = "httplib2-0.20.1.tar.gz", hash = "sha256:0efbcb8bfbfbc11578130d87d8afcc65c2274c6eb446e59fc674e4d7c972d327"}, + {file = "httplib2-0.20.2-py3-none-any.whl", hash = "sha256:6b937120e7d786482881b44b8eec230c1ee1c5c1d06bce8cc865f25abbbf713b"}, + {file = "httplib2-0.20.2.tar.gz", hash = "sha256:e404681d2fbcec7506bcb52c503f2b021e95bee0ef7d01e5c221468a2406d8dc"}, ] httpx = [ {file = "httpx-0.20.0-py3-none-any.whl", hash = "sha256:33af5aad9bdc82ef1fc89219c1e36f5693bf9cd0ebe330884df563445682c0f8"}, @@ -2028,8 +1935,8 @@ immutables = [ {file = "immutables-0.16.tar.gz", hash = "sha256:d67e86859598eed0d926562da33325dac7767b7b1eff84e232c22abea19f4360"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, - {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, + {file = "importlib_metadata-4.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"}, + {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"}, ] isodate = [ {file = "isodate-0.6.0-py2.py3-none-any.whl", hash = "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"}, @@ -2047,64 +1954,56 @@ jmespath = [ {file = "jmespath-0.9.2-py2.py3-none-any.whl", hash = "sha256:3f03b90ac8e0f3ba472e8ebff083e460c89501d8d41979771535efe9a343177e"}, {file = "jmespath-0.9.2.tar.gz", hash = "sha256:54c441e2e08b23f12d7fa7d8e6761768c47c969e6aed10eead57505ba760aee9"}, ] -jsondiff = [ - {file = "jsondiff-1.1.1.tar.gz", hash = "sha256:2d0437782de9418efa34e694aa59f43d7adb1899bd9a793f063867ddba8f7893"}, -] -jsonpickle = [ - {file = "jsonpickle-2.0.0-py2.py3-none-any.whl", hash = "sha256:c1010994c1fbda87a48f8a56698605b598cb0fc6bb7e7927559fc1100e69aeac"}, - {file = "jsonpickle-2.0.0.tar.gz", hash = "sha256:0be49cba80ea6f87a168aa8168d717d00c6ca07ba83df3cec32d3b30bfe6fb9a"}, -] markdown = [ - {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, - {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, + {file = "Markdown-3.3.6-py3-none-any.whl", hash = "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"}, + {file = "Markdown-3.3.6.tar.gz", hash = "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006"}, ] markupsafe = [ - {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, - {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mock = [ {file = "mock-2.0.0-py2.py3-none-any.whl", hash = "sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1"}, {file = "mock-2.0.0.tar.gz", hash = "sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba"}, ] more-itertools = [ - {file = "more-itertools-8.10.0.tar.gz", hash = "sha256:1debcabeb1df793814859d64a81ad7cb10504c24349368ccf214c664c474f41f"}, - {file = "more_itertools-8.10.0-py3-none-any.whl", hash = "sha256:56ddac45541718ba332db05f464bebfb0768110111affd27f66e0051f276fa43"}, + {file = "more-itertools-8.11.0.tar.gz", hash = "sha256:0a2fd25d343c08d7e7212071820e7e7ea2f41d8fb45d6bc8a00cd6ce3b7aab88"}, + {file = "more_itertools-8.11.0-py3-none-any.whl", hash = "sha256:88afff98d83d08fe5e4049b81e2b54c06ebb6b3871a600040865c7a592061cbb"}, ] moto = [ - {file = "moto-1.3.7-py2.py3-none-any.whl", hash = "sha256:4df37936ff8d6a4b8229aab347a7b412cd2ca4823ff47bd1362ddfbc6c5e4ecf"}, - {file = "moto-1.3.7.tar.gz", hash = "sha256:129de2e04cb250d9f8b2c722ec152ed1b5426ef179b4ebb03e9ec36e6eb3fcc5"}, + {file = "moto-1.3.15-py2.py3-none-any.whl", hash = "sha256:3be7e1f406ef7e9c222dbcbfd8cefa2cb1062200e26deae49b5df446e17be3df"}, + {file = "moto-1.3.15.tar.gz", hash = "sha256:fd98f7b219084ba8aadad263849c4dbe8be73979e035d8dc5c86e11a86f11b7f"}, ] msrest = [ {file = "msrest-0.6.21-py2.py3-none-any.whl", hash = "sha256:c840511c845330e96886011a236440fafc2c9aff7b2df9c0a92041ee2dee3782"}, @@ -2118,8 +2017,8 @@ oauthlib = [ {file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"}, ] packaging = [ - {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, - {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] paramiko = [ {file = "paramiko-2.8.0-py2.py3-none-any.whl", hash = "sha256:def3ec612399bab4e9f5eb66b0ae5983980db9dd9120d9e9c6ea3ff673865d1c"}, @@ -2138,52 +2037,51 @@ prometheus-client = [ {file = "prometheus_client-0.9.0.tar.gz", hash = "sha256:9da7b32f02439d8c04f7777021c304ed51d9ec180604700c1ba72a4d44dceb03"}, ] prometheus-flask-exporter = [ - {file = "prometheus_flask_exporter-0.18.4-py3-none-any.whl", hash = "sha256:dc3587a9890c86b9a9f0a489fa7db5c782f9e7f4616a2dec25d80350131d8c05"}, - {file = "prometheus_flask_exporter-0.18.4.tar.gz", hash = "sha256:66d6eb2124f2d97bc983ae7573322dce9104291a3052b436e6a7fb43a25abdbd"}, + {file = "prometheus_flask_exporter-0.18.5-py3-none-any.whl", hash = "sha256:38a3a1fdaf4fc98f988d33f551a8005d778d6b43ca0a2bc4aafb19d0449a48b9"}, + {file = "prometheus_flask_exporter-0.18.5.tar.gz", hash = "sha256:f9a03e88a8415fe96f785c31fc82bbd290a606aaab87bd244414637d55ef0ba4"}, ] protobuf = [ - {file = "protobuf-3.17.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ab6bb0e270c6c58e7ff4345b3a803cc59dbee19ddf77a4719c5b635f1d547aa8"}, - {file = "protobuf-3.17.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:13ee7be3c2d9a5d2b42a1030976f760f28755fcf5863c55b1460fd205e6cd637"}, - {file = "protobuf-3.17.3-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:1556a1049ccec58c7855a78d27e5c6e70e95103b32de9142bae0576e9200a1b0"}, - {file = "protobuf-3.17.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f0e59430ee953184a703a324b8ec52f571c6c4259d496a19d1cabcdc19dabc62"}, - {file = "protobuf-3.17.3-cp35-cp35m-win32.whl", hash = "sha256:a981222367fb4210a10a929ad5983ae93bd5a050a0824fc35d6371c07b78caf6"}, - {file = "protobuf-3.17.3-cp35-cp35m-win_amd64.whl", hash = "sha256:6d847c59963c03fd7a0cd7c488cadfa10cda4fff34d8bc8cba92935a91b7a037"}, - {file = "protobuf-3.17.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:145ce0af55c4259ca74993ddab3479c78af064002ec8227beb3d944405123c71"}, - {file = "protobuf-3.17.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6ce4d8bf0321e7b2d4395e253f8002a1a5ffbcfd7bcc0a6ba46712c07d47d0b4"}, - {file = "protobuf-3.17.3-cp36-cp36m-win32.whl", hash = "sha256:7a4c97961e9e5b03a56f9a6c82742ed55375c4a25f2692b625d4087d02ed31b9"}, - {file = "protobuf-3.17.3-cp36-cp36m-win_amd64.whl", hash = "sha256:a22b3a0dbac6544dacbafd4c5f6a29e389a50e3b193e2c70dae6bbf7930f651d"}, - {file = "protobuf-3.17.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ffea251f5cd3c0b9b43c7a7a912777e0bc86263436a87c2555242a348817221b"}, - {file = "protobuf-3.17.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:9b7a5c1022e0fa0dbde7fd03682d07d14624ad870ae52054849d8960f04bc764"}, - {file = "protobuf-3.17.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8727ee027157516e2c311f218ebf2260a18088ffb2d29473e82add217d196b1c"}, - {file = "protobuf-3.17.3-cp37-cp37m-win32.whl", hash = "sha256:14c1c9377a7ffbeaccd4722ab0aa900091f52b516ad89c4b0c3bb0a4af903ba5"}, - {file = "protobuf-3.17.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c56c050a947186ba51de4f94ab441d7f04fcd44c56df6e922369cc2e1a92d683"}, - {file = "protobuf-3.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ae692bb6d1992afb6b74348e7bb648a75bb0d3565a3f5eea5bec8f62bd06d87"}, - {file = "protobuf-3.17.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:99938f2a2d7ca6563c0ade0c5ca8982264c484fdecf418bd68e880a7ab5730b1"}, - {file = "protobuf-3.17.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6902a1e4b7a319ec611a7345ff81b6b004b36b0d2196ce7a748b3493da3d226d"}, - {file = "protobuf-3.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ffbd23640bb7403574f7aff8368e2aeb2ec9a5c6306580be48ac59a6bac8bde"}, - {file = "protobuf-3.17.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:26010f693b675ff5a1d0e1bdb17689b8b716a18709113288fead438703d45539"}, - {file = "protobuf-3.17.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e76d9686e088fece2450dbc7ee905f9be904e427341d289acbe9ad00b78ebd47"}, - {file = "protobuf-3.17.3-py2.py3-none-any.whl", hash = "sha256:2bfb815216a9cd9faec52b16fd2bfa68437a44b67c56bee59bc3926522ecb04e"}, - {file = "protobuf-3.17.3.tar.gz", hash = "sha256:72804ea5eaa9c22a090d2803813e280fb273b62d5ae497aaf3553d141c4fdd7b"}, + {file = "protobuf-3.19.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d80f80eb175bf5f1169139c2e0c5ada98b1c098e2b3c3736667f28cbbea39fc8"}, + {file = "protobuf-3.19.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a529e7df52204565bcd33738a7a5f288f3d2d37d86caa5d78c458fa5fabbd54d"}, + {file = "protobuf-3.19.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28ccea56d4dc38d35cd70c43c2da2f40ac0be0a355ef882242e8586c6d66666f"}, + {file = "protobuf-3.19.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b30a7de128c46b5ecb343917d9fa737612a6e8280f440874e5cc2ba0d79b8f6"}, + {file = "protobuf-3.19.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5935c8ce02e3d89c7900140a8a42b35bc037ec07a6aeb61cc108be8d3c9438a6"}, + {file = "protobuf-3.19.1-cp36-cp36m-win32.whl", hash = "sha256:74f33edeb4f3b7ed13d567881da8e5a92a72b36495d57d696c2ea1ae0cfee80c"}, + {file = "protobuf-3.19.1-cp36-cp36m-win_amd64.whl", hash = "sha256:038daf4fa38a7e818dd61f51f22588d61755160a98db087a046f80d66b855942"}, + {file = "protobuf-3.19.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e51561d72efd5bd5c91490af1f13e32bcba8dab4643761eb7de3ce18e64a853"}, + {file = "protobuf-3.19.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:6e8ea9173403219239cdfd8d946ed101f2ab6ecc025b0fda0c6c713c35c9981d"}, + {file = "protobuf-3.19.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db3532d9f7a6ebbe2392041350437953b6d7a792de10e629c1e4f5a6b1fe1ac6"}, + {file = "protobuf-3.19.1-cp37-cp37m-win32.whl", hash = "sha256:615b426a177780ce381ecd212edc1e0f70db8557ed72560b82096bd36b01bc04"}, + {file = "protobuf-3.19.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d8919368410110633717c406ab5c97e8df5ce93020cfcf3012834f28b1fab1ea"}, + {file = "protobuf-3.19.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:71b0250b0cfb738442d60cab68abc166de43411f2a4f791d31378590bfb71bd7"}, + {file = "protobuf-3.19.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3cd0458870ea7d1c58e948ac8078f6ba8a7ecc44a57e03032ed066c5bb318089"}, + {file = "protobuf-3.19.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:655264ed0d0efe47a523e2255fc1106a22f6faab7cc46cfe99b5bae085c2a13e"}, + {file = "protobuf-3.19.1-cp38-cp38-win32.whl", hash = "sha256:b691d996c6d0984947c4cf8b7ae2fe372d99b32821d0584f0b90277aa36982d3"}, + {file = "protobuf-3.19.1-cp38-cp38-win_amd64.whl", hash = "sha256:e7e8d2c20921f8da0dea277dfefc6abac05903ceac8e72839b2da519db69206b"}, + {file = "protobuf-3.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fd390367fc211cc0ffcf3a9e149dfeca78fecc62adb911371db0cec5c8b7472d"}, + {file = "protobuf-3.19.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d83e1ef8cb74009bebee3e61cc84b1c9cd04935b72bca0cbc83217d140424995"}, + {file = "protobuf-3.19.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36d90676d6f426718463fe382ec6274909337ca6319d375eebd2044e6c6ac560"}, + {file = "protobuf-3.19.1-cp39-cp39-win32.whl", hash = "sha256:e7b24c11df36ee8e0c085e5b0dc560289e4b58804746fb487287dda51410f1e2"}, + {file = "protobuf-3.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:77d2fadcf369b3f22859ab25bd12bb8e98fb11e05d9ff9b7cd45b711c719c002"}, + {file = "protobuf-3.19.1-py2.py3-none-any.whl", hash = "sha256:e813b1c9006b6399308e917ac5d298f345d95bb31f46f02b60cd92970a9afa17"}, + {file = "protobuf-3.19.1.tar.gz", hash = "sha256:62a8e4baa9cb9e064eb62d1002eca820857ab2138440cb4b3ea4243830f94ca7"}, ] psycopg2 = [ - {file = "psycopg2-2.9.1-cp36-cp36m-win32.whl", hash = "sha256:7f91312f065df517187134cce8e395ab37f5b601a42446bdc0f0d51773621854"}, - {file = "psycopg2-2.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:830c8e8dddab6b6716a4bf73a09910c7954a92f40cf1d1e702fb93c8a919cc56"}, - {file = "psycopg2-2.9.1-cp37-cp37m-win32.whl", hash = "sha256:89409d369f4882c47f7ea20c42c5046879ce22c1e4ea20ef3b00a4dfc0a7f188"}, - {file = "psycopg2-2.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7640e1e4d72444ef012e275e7b53204d7fab341fb22bc76057ede22fe6860b25"}, - {file = "psycopg2-2.9.1-cp38-cp38-win32.whl", hash = "sha256:079d97fc22de90da1d370c90583659a9f9a6ee4007355f5825e5f1c70dffc1fa"}, - {file = "psycopg2-2.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:2c992196719fadda59f72d44603ee1a2fdcc67de097eea38d41c7ad9ad246e62"}, - {file = "psycopg2-2.9.1-cp39-cp39-win32.whl", hash = "sha256:2087013c159a73e09713294a44d0c8008204d06326006b7f652bef5ace66eebb"}, - {file = "psycopg2-2.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:bf35a25f1aaa8a3781195595577fcbb59934856ee46b4f252f56ad12b8043bcf"}, - {file = "psycopg2-2.9.1.tar.gz", hash = "sha256:de5303a6f1d0a7a34b9d40e4d3bef684ccc44a49bbe3eb85e3c0bffb4a131b7c"}, + {file = "psycopg2-2.9.2-cp310-cp310-win32.whl", hash = "sha256:6796ac614412ce374587147150e56d03b7845c9e031b88aacdcadc880e81bb38"}, + {file = "psycopg2-2.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:dfc32db6ce9ecc35a131320888b547199f79822b028934bb5b332f4169393e15"}, + {file = "psycopg2-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:77d09a79f9739b97099d2952bbbf18eaa4eaf825362387acbb9552ec1b3fa228"}, + {file = "psycopg2-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f65cba7924363e0d2f416041b48ff69d559548f2cb168ff972c54e09e1e64db8"}, + {file = "psycopg2-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:b8816c6410fa08d2a022e4e38d128bae97c1855e176a00493d6ec62ccd606d57"}, + {file = "psycopg2-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:26322c3f114de1f60c1b0febf8fdd595c221b4f624524178f515d07350a71bd1"}, + {file = "psycopg2-2.9.2-cp38-cp38-win32.whl", hash = "sha256:77b9105ef37bc005b8ffbcb1ed6d8685bb0e8ce84773738aa56421a007ec5a7a"}, + {file = "psycopg2-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:91c7fd0fe9e6c118e8ff5b665bc3445781d3615fa78e131d0b4f8c85e8ca9ec8"}, + {file = "psycopg2-2.9.2-cp39-cp39-win32.whl", hash = "sha256:a761b60da0ecaf6a9866985bcde26327883ac3cdb90535ab68b8d784f02b05ef"}, + {file = "psycopg2-2.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:fd7ddab7d6afee4e21c03c648c8b667b197104713e57ec404d5b74097af21e31"}, + {file = "psycopg2-2.9.2.tar.gz", hash = "sha256:a84da9fa891848e0270e8e04dcca073bc9046441eeb47069f5c0e36783debbea"}, ] py = [ - {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, - {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, -] -pyaml = [ - {file = "pyaml-21.10.1-py2.py3-none-any.whl", hash = "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0"}, - {file = "pyaml-21.10.1.tar.gz", hash = "sha256:c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383"}, + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pyasn1 = [ {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, @@ -2216,8 +2114,8 @@ pyasn1-modules = [ {file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"}, ] pycparser = [ - {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, - {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] pycryptodome = [ {file = "pycryptodome-3.11.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ffd0cac13ff41f2d15ed39dc6ba1d2ad88dd2905d656c33d8235852f5d6151fd"}, @@ -2274,8 +2172,8 @@ pynacl = [ {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, ] pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, + {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, + {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, ] pytest = [ {file = "pytest-3.10.1-py2.py3-none-any.whl", hash = "sha256:3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec"}, @@ -2301,20 +2199,6 @@ pytz = [ {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, ] -pywin32 = [ - {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"}, - {file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"}, - {file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"}, - {file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"}, - {file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"}, - {file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"}, - {file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"}, - {file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"}, - {file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"}, - {file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"}, - {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"}, - {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"}, -] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, @@ -2348,8 +2232,8 @@ requests-oauthlib = [ {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, ] responses = [ - {file = "responses-0.15.0-py2.py3-none-any.whl", hash = "sha256:5955ad3468fe8eb5fb736cdab4943457b7768f8670fa3624b4e26ff52dfe20c0"}, - {file = "responses-0.15.0.tar.gz", hash = "sha256:866757987d1962aa908d9c8b3185739faefd72a359e95459de0c2e4e5369c9b2"}, + {file = "responses-0.16.0-py2.py3-none-any.whl", hash = "sha256:f358ef75e8bf431b0aa203cc62625c3a1c80a600dbe9de91b944bf4e9c600b92"}, + {file = "responses-0.16.0.tar.gz", hash = "sha256:a2e3aca2a8277e61257cd3b1c154b1dd0d782b1ae3d38b7fa37cbe3feb531791"}, ] retry = [ {file = "retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606"}, @@ -2413,9 +2297,8 @@ sqlalchemy = [ ] storageclient = [] typing-extensions = [ - {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, - {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, - {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, + {file = "typing_extensions-4.0.0-py3-none-any.whl", hash = "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9"}, + {file = "typing_extensions-4.0.0.tar.gz", hash = "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed"}, ] uritemplate = [ {file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"}, @@ -2428,63 +2311,13 @@ urllib3 = [ userdatamodel = [ {file = "userdatamodel-2.3.3.tar.gz", hash = "sha256:b846b7efd2d002a653474fa3bd7bf2a2c964277ff5f8d9bde8e9d975aca8d130"}, ] -websocket-client = [ - {file = "websocket-client-1.2.1.tar.gz", hash = "sha256:8dfb715d8a992f5712fff8c843adae94e22b22a99b2c5e6b0ec4a1a981cc4e0d"}, - {file = "websocket_client-1.2.1-py2.py3-none-any.whl", hash = "sha256:0133d2f784858e59959ce82ddac316634229da55b498aac311f1620567a710ec"}, -] werkzeug = [ {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, ] -wrapt = [ - {file = "wrapt-1.13.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3de7b4d3066cc610054e7aa2c005645e308df2f92be730aae3a47d42e910566a"}, - {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:8164069f775c698d15582bf6320a4f308c50d048c1c10cf7d7a341feaccf5df7"}, - {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9adee1891253670575028279de8365c3a02d3489a74a66d774c321472939a0b1"}, - {file = "wrapt-1.13.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a70d876c9aba12d3bd7f8f1b05b419322c6789beb717044eea2c8690d35cb91b"}, - {file = "wrapt-1.13.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3f87042623530bcffea038f824b63084180513c21e2e977291a9a7e65a66f13b"}, - {file = "wrapt-1.13.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e634136f700a21e1fcead0c137f433dde928979538c14907640607d43537d468"}, - {file = "wrapt-1.13.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3e33c138d1e3620b1e0cc6fd21e46c266393ed5dae0d595b7ed5a6b73ed57aa0"}, - {file = "wrapt-1.13.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:283e402e5357e104ac1e3fba5791220648e9af6fb14ad7d9cc059091af2b31d2"}, - {file = "wrapt-1.13.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:ccb34ce599cab7f36a4c90318697ead18312c67a9a76327b3f4f902af8f68ea1"}, - {file = "wrapt-1.13.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:fbad5ba74c46517e6488149514b2e2348d40df88cd6b52a83855b7a8bf04723f"}, - {file = "wrapt-1.13.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:724ed2bc9c91a2b9026e5adce310fa60c6e7c8760b03391445730b9789b9d108"}, - {file = "wrapt-1.13.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:83f2793ec6f3ef513ad8d5b9586f5ee6081cad132e6eae2ecb7eac1cc3decae0"}, - {file = "wrapt-1.13.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:0473d1558b93e314e84313cc611f6c86be779369f9d3734302bf185a4d2625b1"}, - {file = "wrapt-1.13.2-cp35-cp35m-win32.whl", hash = "sha256:15eee0e6fd07f48af2f66d0e6f2ff1916ffe9732d464d5e2390695296872cad9"}, - {file = "wrapt-1.13.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bc85d17d90201afd88e3d25421da805e4e135012b5d1f149e4de2981394b2a52"}, - {file = "wrapt-1.13.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6ee5f8734820c21b9b8bf705e99faba87f21566d20626568eeb0d62cbeaf23c"}, - {file = "wrapt-1.13.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:53c6706a1bcfb6436f1625511b95b812798a6d2ccc51359cd791e33722b5ea32"}, - {file = "wrapt-1.13.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fbe6aebc9559fed7ea27de51c2bf5c25ba2a4156cf0017556f72883f2496ee9a"}, - {file = "wrapt-1.13.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:0582180566e7a13030f896c2f1ac6a56134ab5f3c3f4c5538086f758b1caf3f2"}, - {file = "wrapt-1.13.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:bff0a59387a0a2951cb869251257b6553663329a1b5525b5226cab8c88dcbe7e"}, - {file = "wrapt-1.13.2-cp36-cp36m-win32.whl", hash = "sha256:df3eae297a5f1594d1feb790338120f717dac1fa7d6feed7b411f87e0f2401c7"}, - {file = "wrapt-1.13.2-cp36-cp36m-win_amd64.whl", hash = "sha256:1eb657ed84f4d3e6ad648483c8a80a0cf0a78922ef94caa87d327e2e1ad49b48"}, - {file = "wrapt-1.13.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0cdedf681db878416c05e1831ec69691b0e6577ac7dca9d4f815632e3549580"}, - {file = "wrapt-1.13.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:87ee3c73bdfb4367b26c57259995935501829f00c7b3eed373e2ad19ec21e4e4"}, - {file = "wrapt-1.13.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3e0d16eedc242d01a6f8cf0623e9cdc3b869329da3f97a15961d8864111d8cf0"}, - {file = "wrapt-1.13.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:8318088860968c07e741537030b1abdd8908ee2c71fbe4facdaade624a09e006"}, - {file = "wrapt-1.13.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d90520616fce71c05dedeac3a0fe9991605f0acacd276e5f821842e454485a70"}, - {file = "wrapt-1.13.2-cp37-cp37m-win32.whl", hash = "sha256:22142afab65daffc95863d78effcbd31c19a8003eca73de59f321ee77f73cadb"}, - {file = "wrapt-1.13.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d0d717e10f952df7ea41200c507cc7e24458f4c45b56c36ad418d2e79dacd1d4"}, - {file = "wrapt-1.13.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:593cb049ce1c391e0288523b30426c4430b26e74c7e6f6e2844bd99ac7ecc831"}, - {file = "wrapt-1.13.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8860c8011a6961a651b1b9f46fdbc589ab63b0a50d645f7d92659618a3655867"}, - {file = "wrapt-1.13.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ada5e29e59e2feb710589ca1c79fd989b1dd94d27079dc1d199ec954a6ecc724"}, - {file = "wrapt-1.13.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:fdede980273aeca591ad354608778365a3a310e0ecdd7a3587b38bc5be9b1808"}, - {file = "wrapt-1.13.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:af9480de8e63c5f959a092047aaf3d7077422ded84695b3398f5d49254af3e90"}, - {file = "wrapt-1.13.2-cp38-cp38-win32.whl", hash = "sha256:c65e623ea7556e39c4f0818200a046cbba7575a6b570ff36122c276fdd30ab0a"}, - {file = "wrapt-1.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:b20703356cae1799080d0ad15085dc3213c1ac3f45e95afb9f12769b98231528"}, - {file = "wrapt-1.13.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c5c4cf188b5643a97e87e2110bbd4f5bc491d54a5b90633837b34d5df6a03fe"}, - {file = "wrapt-1.13.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:82223f72eba6f63eafca87a0f614495ae5aa0126fe54947e2b8c023969e9f2d7"}, - {file = "wrapt-1.13.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:81a4cf257263b299263472d669692785f9c647e7dca01c18286b8f116dbf6b38"}, - {file = "wrapt-1.13.2-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:728e2d9b7a99dd955d3426f237b940fc74017c4a39b125fec913f575619ddfe9"}, - {file = "wrapt-1.13.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7574de567dcd4858a2ffdf403088d6df8738b0e1eabea220553abf7c9048f59e"}, - {file = "wrapt-1.13.2-cp39-cp39-win32.whl", hash = "sha256:c7ac2c7a8e34bd06710605b21dd1f3576764443d68e069d2afba9b116014d072"}, - {file = "wrapt-1.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e6d1a8eeef415d7fb29fe017de0e48f45e45efd2d1bfda28fc50b7b330859ef"}, - {file = "wrapt-1.13.2.tar.gz", hash = "sha256:dca56cc5963a5fd7c2aa8607017753f534ee514e09103a6c55d2db70b50e7447"}, -] wtforms = [ - {file = "WTForms-2.3.3-py2.py3-none-any.whl", hash = "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c"}, - {file = "WTForms-2.3.3.tar.gz", hash = "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"}, + {file = "WTForms-3.0.0-py3-none-any.whl", hash = "sha256:232dbb0094847dca2f45c72136b5ca1d5dca2a3e24ccd2229823b8b74b3c6698"}, + {file = "WTForms-3.0.0.tar.gz", hash = "sha256:4abfbaa1d529a1d0ac927d44af8dbb9833afd910e56448a103f1893b0b176886"}, ] xmltodict = [ {file = "xmltodict-0.12.0-py2.py3-none-any.whl", hash = "sha256:8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051"}, diff --git a/pyproject.toml b/pyproject.toml index 335f8988f..dc06c3896 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ flask-cors = "^3.0.3" flask-restful = "^0.3.6" flask_sqlalchemy_session = "^1.1" email_validator = "^1.1.1" -gen3authz = "^1.1.1" +gen3authz = {path = "./gen3authz-1.2.1.tar.gz"} gen3cirrus = "^2.0.0" gen3config = "^0.1.7" gen3users = "^0.6.0" diff --git a/tests/test_drs.py b/tests/test_drs.py index 2769784de..71924bfb0 100644 --- a/tests/test_drs.py +++ b/tests/test_drs.py @@ -1,9 +1,14 @@ +import flask import json import jwt import pytest import requests import responses from tests import utils +import time +from unittest.mock import MagicMock, patch + +from gen3authz.client.arborist.client import ArboristClient def get_doc(has_version=True, urls=list(), drs_list=0): @@ -230,3 +235,159 @@ def test_get_presigned_url_with_query_params( headers=user, ) assert res.status_code == 200 + + +@responses.activate +@pytest.mark.parametrize("indexd_client", ["s3", "gs"], indirect=True) +@patch("fence.resources.ga4gh.passports.ArboristClient") +def test_get_presigned_url_with_passport_for_non_public_acl( + mock_arborist, + client, + indexd_client, + kid, + rsa_private_key, + rsa_public_key, + indexd_client_accepting_record, + mock_arborist_requests, + google_proxy_group, + primary_google_service_account, + cloud_manager, + google_signed_url, +): + indexd_record_with_non_public_authz_and_public_acl_populated = { + "did": "1", + "baseid": "", + "rev": "", + "size": 10, + "file_name": "file1", + "urls": ["s3://bucket1/key", "gs://bucket1/key"], + "hashes": {}, + "metadata": {}, + "authz": ["/orgA/programs/phs000991.c1"], + "acl": ["*"], + "form": "", + "created_date": "", + "updated_date": "", + } + indexd_client_accepting_record( + indexd_record_with_non_public_authz_and_public_acl_populated + ) + mock_arborist_requests({"arborist/auth/request": {"POST": ({"auth": True}, 200)}}) + mock_arborist.return_value = MagicMock(ArboristClient) + + # Prepare Passport/Visa + headers = {"kid": kid} + decoded_visa = { + "iss": "https://stsstg.nih.gov", + "sub": "abcde12345aspdij", + "iat": int(time.time()), + "exp": int(time.time()) + 1000, + "scope": "openid ga4gh_passport_v1 email profile", + "jti": "jtiajoidasndokmasdl", + "txn": "sapidjspa.asipidja", + "name": "", + "ga4gh_visa_v1": { + "type": "https://ras.nih.gov/visas/v1.1", + "asserted": int(time.time()), + "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", + "source": "https://ncbi.nlm.nih.gov/gap", + }, + "ras_dbgap_permissions": [ + { + "consent_name": "Health/Medical/Biomedical", + "phs_id": "phs000991", + "version": "v1", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": int(time.time()) + 1001, + }, + { + "consent_name": "General Research Use (IRB, PUB)", + "phs_id": "phs000961", + "version": "v1", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": int(time.time()) + 1001, + }, + { + "consent_name": "Disease-Specific (Cardiovascular Disease)", + "phs_id": "phs000279", + "version": "v2", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": int(time.time()) + 1001, + }, + { + "consent_name": "Health/Medical/Biomedical (IRB)", + "phs_id": "phs000286", + "version": "v6", + "participant_set": "p2", + "consent_group": "c3", + "role": "designated user", + "expiration": int(time.time()) + 1001, + }, + { + "consent_name": "Disease-Specific (Focused Disease Only, IRB, NPU)", + "phs_id": "phs000289", + "version": "v6", + "participant_set": "p2", + "consent_group": "c2", + "role": "designated user", + "expiration": int(time.time()) + 1001, + }, + { + "consent_name": "Disease-Specific (Autism Spectrum Disorder)", + "phs_id": "phs000298", + "version": "v4", + "participant_set": "p3", + "consent_group": "c1", + "role": "designated user", + "expiration": int(time.time()) + 1001, + }, + ], + } + encoded_visa = jwt.encode( + decoded_visa, key=rsa_private_key, headers=headers, algorithm="RS256" + ).decode("utf-8") + + passport_header = { + "type": "JWT", + "alg": "RS256", + "kid": kid, + } + passport = { + "iss": "https://stsstg.nih.gov", + "sub": "abcde12345aspdij", + "iat": int(time.time()), + "scope": "openid ga4gh_passport_v1 email profile", + "exp": int(time.time()) + 1000, + "ga4gh_passport_v1": [encoded_visa], + } + encoded_passport = jwt.encode( + passport, key=rsa_private_key, headers=passport_header, algorithm="RS256" + ).decode("utf-8") + + access_id = indexd_client["indexed_file_location"] + test_guid = "1" + + passports = [encoded_passport] + + data = {"passports": passports} + + flask.current_app.jwt_public_keys = { + "https://stsstg.nih.gov": { + kid: rsa_public_key, + } + } + + res = client.post( + "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, + headers={ + "Content-Type": "application/json", + }, + data=json.dumps(data), + ) + assert res.status_code == 200 From e1a98f231c21d5fbc4718836ea5626b2a6e46155 Mon Sep 17 00:00:00 2001 From: BinamB Date: Thu, 18 Nov 2021 15:01:47 -0600 Subject: [PATCH 092/211] update poetry --- poetry.lock | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 83b370d66..4948d8a52 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1507,7 +1507,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "acdd98018b0a3c640e5752ed99512ca6abbf30eb25fa7e143deeeee0ade8417f" +content-hash = "5447e16540f4d7407a1dd854c4e565dfc5cfb1992af1c5393dc3559c6b1b9f78" [metadata.files] addict = [ diff --git a/pyproject.toml b/pyproject.toml index dc06c3896..22a52e4e7 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ flask-cors = "^3.0.3" flask-restful = "^0.3.6" flask_sqlalchemy_session = "^1.1" email_validator = "^1.1.1" -gen3authz = {path = "./gen3authz-1.2.1.tar.gz"} +gen3authz = { path = "./gen3authz-1.2.1.tar.gz", develop = true } gen3cirrus = "^2.0.0" gen3config = "^0.1.7" gen3users = "^0.6.0" From abbfef03c1547967ad772550c572d2ca0ba86658 Mon Sep 17 00:00:00 2001 From: BinamB Date: Thu, 18 Nov 2021 15:42:54 -0600 Subject: [PATCH 093/211] update gen3authz --- gen3authz-1.2.1.tar.gz | Bin 13121 -> 0 bytes gen3authz-1.3.0.tar.gz | Bin 0 -> 13085 bytes poetry.lock | 8 ++++---- pyproject.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 gen3authz-1.2.1.tar.gz create mode 100644 gen3authz-1.3.0.tar.gz diff --git a/gen3authz-1.2.1.tar.gz b/gen3authz-1.2.1.tar.gz deleted file mode 100644 index 2f8ac00d0c462324b53efaf33b3afe4c4a0a0b56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13121 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

T8A3~ne& z2Ew?%p&Y6nL?E6p`tD$FVd`^bKWvhkR>402E2+CI=D6-06fPXj31K``?NDll?LJ4 z#9x(>mHD``d56U}TXfim#JP?fNWwH+%-fml5jZrQ2imhvPfd`zjhS&HAs)@x=#G?` zEYtCql1t1aUb$o}P16Sn$*LmPOb(eKX5=;2-V9SBEX}Kyn9ZgQzCN1QGyPtUthS;v zRxK|+u0Lu*Es<=regy0cQyTP6&idCvN-?j2(>3R663q;`7vojA(ROJ`a26O7ti}YR zm1{YLt)&p8`p1kJL3m>B5Tc2l^}2BSg|q#C%=5~_An#RH!kLLV=YRZIOunVP8(Bb+ zuIZb!G~jYI@hGYYi)K5+Z8NZ<9jI(-ko|k|+ASD4)@*$nyndkLTCeQ`UPpRABNZOfjFLH z%bFT%fEFUCg1mq1;$BFS0u-#t>t?hLu|v8+5r8fzFb9Yv>+G>AG&}@9Grb}V4$WOq??tMemM%8VQ3G+=b?19+V0sK5t*$ELc#vxbuuIwQ}UJ9ro6 zL9Nx}9|}hlp#FBx!m&05+CKqDem41f6Tc;uv!9YS1H#Qe9Ro~({1b!1SH|%VbF``# zN7FRg2KJ}uW%px9$FpdA(ZGUU*%L6y8#Q@TvM*}V1PE|I9XmET$?zrfu@dU5D2w#E z*c|sxGesYVISwPbq*<36`ZH%nbkwM*m-A&;0vJI0>G)~{+&l08seNtQ z+`bmzUz1uL`7!-O3Z}or^{d(p=-mDu4EiDpf@y025*{YJ~FALQquzIrpxzvwqCj3p`gKiyzsA(Y9uv1>^xj5tv-f@N)+$`m` z+jLeHJ+W}~ZJPiHO&)oL?6zT@Y?iQT-f@JljRCga13B*h`$>HsfHzn#hRZ-Y;5Pr( z<(0!%Z_Ll`UtrM%FgW%6X^=8AE~InOyimYwc7~NgvCk_(N_d_H1?uh|tLb-b zlW_J>TddYjxQ5U0U$G(8U!hiXdQ9*ik$F>DC6|V3_h|Glztg-iECu%hm23@-XM}46 zugN{;Nj72*nh!_c8&4>P9P5IW=2(7;D)H2?Aw*tORSaE z-6ntLOyecjMTPA~kMoxP?Hq+G^CGQU$9>Y%<{^P&-S;%32pD>#2|tIu698>`T#;6blot|JSAFkB^N&>2PKr zrn~wZjV!u0xD1Bjc;b71n9Dm>r8uE4+*XofoJT_MOMDt`8%g{~$NRG8OM0vS=Hl6Z z&9oNqUmsc4eLgvv0>448dpQGsoG+i2k?M#Re>Sf%Uw|GSh#a3lqSO9vT`pUP?-YbM zvR`fa_pnn*WRi~(q5_}Pt~ z$^CltYV&%}oYH&>U*iyQG(T68hd2&4;T>DFKh2#EUUi$=ofG{V?1-zi_paBtvs2Vv z`NgaY|J*Z2S{yJYwaTP3?QGJbu>m;3G{No9&8qgMSeHrLvFw0UAKTxi_PNuIUL3P0 Z>wH<3k9PmdB>DUZN;0e(g9w3w{0~brt&RWy diff --git a/gen3authz-1.3.0.tar.gz b/gen3authz-1.3.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..a3c39d4f474169c4d1209752a3e7546b10bcc579 GIT binary patch literal 13085 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.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "5447e16540f4d7407a1dd854c4e565dfc5cfb1992af1c5393dc3559c6b1b9f78" +content-hash = "215bbee5a721704e974a40582871c7067264f290206b488fddca75d690d8ddc2" [metadata.files] addict = [ @@ -1797,7 +1797,7 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] gen3authz = [ - {file = "gen3authz-1.2.1.tar.gz", hash = "sha256:be3970b9e8b49b5468940be65835646fdbeaabffaf2b7b891e3a1011a99c660e"}, + {file = "gen3authz-1.3.0.tar.gz", hash = "sha256:b5efa0a030ca5e1ae5ae32e90303d0414567938ed9359233cd7d1caaa4463b66"}, ] gen3cirrus = [ {file = "gen3cirrus-2.0.0.tar.gz", hash = "sha256:0bd590c407c42dad5f0b896da0fa30bd01ea6bef5ff7dd11324ec59f14a71793"}, diff --git a/pyproject.toml b/pyproject.toml index 22a52e4e7..92deeba4b 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ flask-cors = "^3.0.3" flask-restful = "^0.3.6" flask_sqlalchemy_session = "^1.1" email_validator = "^1.1.1" -gen3authz = { path = "./gen3authz-1.2.1.tar.gz", develop = true } +gen3authz = { path = "./gen3authz-1.3.0.tar.gz", develop = true } gen3cirrus = "^2.0.0" gen3config = "^0.1.7" gen3users = "^0.6.0" From 02c80fbe2231eb99635c66ea94bcdb7b86377263 Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 19 Nov 2021 13:47:45 -0600 Subject: [PATCH 094/211] test aws download --- fence/blueprints/data/indexd.py | 53 ++++++++++++++++++--------------- fence/resources/google/utils.py | 13 +++++--- poetry.lock | 9 ++---- pyproject.toml | 2 +- 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index 4e9cd3a73..f55a1ef06 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -46,7 +46,7 @@ from fence.resources.ga4gh.passports import get_gen3_users_from_ga4gh_passports from fence.utils import get_valid_expiration_from_request from . import multipart_upload -from ...models import AssumeRoleCacheAWS +from ...models import AssumeRoleCacheAWS, query_for_user from ...models import AssumeRoleCacheGCP logger = get_logger(__name__) @@ -412,10 +412,11 @@ def get_signed_url( "upload": "write-storage", "download": "read-storage", } - if not self.check_authz( + authorized_user_id = self.check_authz( action_to_permission[action], user_ids_from_passports=user_ids_from_passports, - ): + ) + if not authorized_user_id: raise Unauthorized( f"Either you weren't logged in or you don't have " f"{action_to_permission[action]} permission " @@ -444,7 +445,7 @@ def get_signed_url( force_signed_url, r_pays_project, file_name, - user_ids_from_passports, + authorized_user_id, ) def _get_signed_url( @@ -455,7 +456,7 @@ def _get_signed_url( force_signed_url, r_pays_project, file_name, - user_ids_from_passports=None, + user_id=None, ): if action == "upload": # NOTE: self.index_document ensures the GUID exists in indexd and raises @@ -490,7 +491,7 @@ def _get_signed_url( public_data=self.public, force_signed_url=force_signed_url, r_pays_project=r_pays_project, - user_ids_from_passports=user_ids_from_passports, + user_id=user_id, ) raise NotFound( @@ -519,6 +520,7 @@ def check_authz(self, action, user_ids_from_passports=None): if user_ids_from_passports: for user_id in user_ids_from_passports: authorized = flask.current_app.arborist.auth_request( + jwt=None, user_id=user_id, service="fence", methods=action, @@ -526,7 +528,8 @@ def check_authz(self, action, user_ids_from_passports=None): ) # if any passport provides access, user is authorized if authorized: - return authorized + return user_id + return authorized else: try: token = get_jwt() @@ -1067,11 +1070,11 @@ def get_signed_url( public_data=False, force_signed_url=True, r_pays_project=None, - user_ids_from_passports=None, + user_id=None, ): resource_path = self.get_resource_path() - user_info = _get_user_info(user_ids_from_passports=user_ids_from_passports) + user_info = _get_user_info(user=user_id) if public_data and not force_signed_url: url = "https://storage.cloud.google.com/" + resource_path @@ -1138,7 +1141,7 @@ def _generate_google_storage_signed_url( r_pays_project=None, ): - proxy_group_id = get_or_create_proxy_group_id() + proxy_group_id = get_or_create_proxy_group_id(user_id=user_id) expiration_time = int(time.time()) + expires_in is_cached = False @@ -1446,27 +1449,29 @@ def delete(self, container, blob): # pylint: disable=R0201 return ("Failed to delete data file.", status_code) -def _get_user_info(sub_type=str, user_ids_from_passports=None): +def _get_user_info(sub_type=str, user=None): """ Attempt to parse the request for token to authenticate the user. fallback to populated information about an anonymous user. By default, cast `sub` to str. Use `sub_type` to override this behavior. """ # TODO Update to support POSTed passport - print("-----get user_infdo==========") - print(user_ids_from_passports) - # if user_ids_from_passports: - # try: - # #query idp table - # user_id = "" try: - set_current_token( - validate_request(scope={"user"}, audience=config.get("BASE_URL")) - ) - user_id = current_token["sub"] - if sub_type: - user_id = sub_type(user_id) - username = current_token["context"]["user"]["name"] + if user: + print("-------------user true-----------") + if hasattr(flask.current_app, "db"): + with flask.current_app.db.session as session: + result = query_for_user(session, user) + username = result.username + user_id = result.id + else: + set_current_token( + validate_request(scope={"user"}, audience=config.get("BASE_URL")) + ) + user_id = current_token["sub"] + if sub_type: + user_id = sub_type(user_id) + username = current_token["context"]["user"]["name"] except JWTError: # this is fine b/c it might be public data, sign with anonymous username/id user_id = None diff --git a/fence/resources/google/utils.py b/fence/resources/google/utils.py index 27dacb479..a891c1fec 100644 --- a/fence/resources/google/utils.py +++ b/fence/resources/google/utils.py @@ -515,7 +515,7 @@ def _update_service_account_db_entry( return service_account_db_entry -def get_or_create_proxy_group_id(expires=None): +def get_or_create_proxy_group_id(expires=None, user_id=None): """ If no username returned from token or database, create a new proxy group for the give user. Also, add the access privileges. @@ -523,7 +523,7 @@ def get_or_create_proxy_group_id(expires=None): Returns: int: id of (possibly newly created) proxy group associated with user """ - proxy_group_id = _get_proxy_group_id() + proxy_group_id = _get_proxy_group_id(user_id=user_id) if not proxy_group_id: user_id = current_token["sub"] username = current_token.get("context", {}).get("user", {}).get("name", "") @@ -557,7 +557,7 @@ def get_or_create_proxy_group_id(expires=None): return proxy_group_id -def _get_proxy_group_id(): +def _get_proxy_group_id(user_id=None): """ Get users proxy group id from the current token, if possible. Otherwise, check the database for it. @@ -568,10 +568,15 @@ def _get_proxy_group_id(): proxy_group_id = get_users_proxy_group_from_token() if not proxy_group_id: + user_id = user_id or current_token["sub"] user = ( - current_session.query(User).filter(User.id == current_token["sub"]).first() + current_session.query(User).filter(User.id == user_id).first() ) + print("-----------userid-----------") + print(user) proxy_group_id = user.google_proxy_group_id + print("-----proxy----") + print(proxy_group_id) return proxy_group_id diff --git a/poetry.lock b/poetry.lock index bfb35e52f..e9ff86b53 100644 --- a/poetry.lock +++ b/poetry.lock @@ -525,10 +525,6 @@ cdiserrors = "<2.0.0" contextvars = {version = ">=2.4,<3.0", markers = "python_version < \"3.7\""} httpx = ">=0.20.0,<1.0.0" -[package.source] -type = "file" -url = "gen3authz-1.3.0.tar.gz" - [[package]] name = "gen3cirrus" version = "2.0.0" @@ -1507,7 +1503,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "215bbee5a721704e974a40582871c7067264f290206b488fddca75d690d8ddc2" +content-hash = "6ab75ef8133cee334806d5da321202a3589a72020a6b9afff22b23596cc16d6b" [metadata.files] addict = [ @@ -1797,7 +1793,8 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] gen3authz = [ - {file = "gen3authz-1.3.0.tar.gz", hash = "sha256:b5efa0a030ca5e1ae5ae32e90303d0414567938ed9359233cd7d1caaa4463b66"}, + {file = "gen3authz-1.3.0-py3-none-any.whl", hash = "sha256:b7dd8e4e81d72e42bfc70406e8dd44e473b424439da33bd80991c45d6753b927"}, + {file = "gen3authz-1.3.0.tar.gz", hash = "sha256:3fb88dd7dc0fa76edf9e3c4bbd1a7303509686cb22d86f48d1c1734092dc5682"}, ] gen3cirrus = [ {file = "gen3cirrus-2.0.0.tar.gz", hash = "sha256:0bd590c407c42dad5f0b896da0fa30bd01ea6bef5ff7dd11324ec59f14a71793"}, diff --git a/pyproject.toml b/pyproject.toml index 92deeba4b..01685302c 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ flask-cors = "^3.0.3" flask-restful = "^0.3.6" flask_sqlalchemy_session = "^1.1" email_validator = "^1.1.1" -gen3authz = { path = "./gen3authz-1.3.0.tar.gz", develop = true } +gen3authz = "^1.3.0" gen3cirrus = "^2.0.0" gen3config = "^0.1.7" gen3users = "^0.6.0" From 6d4632e424f0334a47ac095827e048a1c8beae21 Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 19 Nov 2021 14:58:54 -0600 Subject: [PATCH 095/211] install vim --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 121833779..4d23a139a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ RUN pip install --upgrade pip RUN pip install --upgrade poetry RUN apt-get update \ && apt-get install -y --no-install-recommends curl bash git \ + && apt-get isntall -y vim \ libmcrypt4 libmhash2 mcrypt \ && apt-get clean \ && rm -rf /var/lib/apt/lists/ From 40eab5c3de5f22bdc31c8886bcb3d6d995e60172 Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 19 Nov 2021 15:17:33 -0600 Subject: [PATCH 096/211] install vim --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4d23a139a..8362569e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN pip install --upgrade pip RUN pip install --upgrade poetry RUN apt-get update \ && apt-get install -y --no-install-recommends curl bash git \ - && apt-get isntall -y vim \ + apt-get isntall -y vim \ libmcrypt4 libmhash2 mcrypt \ && apt-get clean \ && rm -rf /var/lib/apt/lists/ From b7af4a2d06313c06112a288273e035f40fb707f9 Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 19 Nov 2021 15:21:06 -0600 Subject: [PATCH 097/211] install vim --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8362569e3..1a5594d0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,8 @@ ENV appname=fence RUN pip install --upgrade pip RUN pip install --upgrade poetry RUN apt-get update \ - && apt-get install -y --no-install-recommends curl bash git \ apt-get isntall -y vim \ + && apt-get install -y --no-install-recommends curl bash git \ libmcrypt4 libmhash2 mcrypt \ && apt-get clean \ && rm -rf /var/lib/apt/lists/ From d5fd0f238dba7854e444135a69018fcb3c1175bd Mon Sep 17 00:00:00 2001 From: BinamB Date: Fri, 19 Nov 2021 15:32:02 -0600 Subject: [PATCH 098/211] fix vim install --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1a5594d0a..cb4c27733 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,8 @@ ENV appname=fence RUN pip install --upgrade pip RUN pip install --upgrade poetry RUN apt-get update \ - apt-get isntall -y vim \ && apt-get install -y --no-install-recommends curl bash git \ + && apt-get install -y vim \ libmcrypt4 libmhash2 mcrypt \ && apt-get clean \ && rm -rf /var/lib/apt/lists/ From dcaf3dee362972490e9c9f7718111b718c16a5b5 Mon Sep 17 00:00:00 2001 From: BinamB Date: Mon, 22 Nov 2021 15:13:25 -0600 Subject: [PATCH 099/211] google proxy stuff --- fence/blueprints/data/indexd.py | 9 ++++++--- fence/resources/google/utils.py | 18 ++++++++++++------ tests/test_drs.py | 2 ++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index f55a1ef06..4728ecddc 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -5,6 +5,8 @@ from datetime import datetime, timedelta +from sqlalchemy.sql.functions import user + from cached_property import cached_property import cirrus from cirrus import GoogleCloudManager @@ -19,6 +21,7 @@ AccountSasPermissions, generate_blob_sas, ) +from fence import auth from fence.auth import ( get_jwt, @@ -99,7 +102,6 @@ def get_signed_url_for_file( ) prepare_presigned_url_audit_log(requested_protocol, indexed_file) - signed_url = indexed_file.get_signed_url( requested_protocol, action, @@ -407,6 +409,7 @@ def get_signed_url( file_name=None, user_ids_from_passports=None, ): + authorized_user_id = None if self.index_document.get("authz"): action_to_permission = { "upload": "write-storage", @@ -422,6 +425,7 @@ def get_signed_url( f"{action_to_permission[action]} permission " f"on authz resource: {self.index_document['authz']}" ) + authorized_user_id = authorized_user_id if isinstance(authorized_user_id, str) else None else: if self.public_acl and action == "upload": raise Unauthorized( @@ -435,7 +439,6 @@ def get_signed_url( raise Unauthorized( f"You don't have access permission on this file: {self.file_id}" ) - if action is not None and action not in SUPPORTED_ACTIONS: raise NotSupported("action {} is not supported".format(action)) return self._get_signed_url( @@ -517,6 +520,7 @@ def check_authz(self, action, user_ids_from_passports=None): ) # handle multiple GA4GH passports as a means of authn/z + if user_ids_from_passports: for user_id in user_ids_from_passports: authorized = flask.current_app.arborist.auth_request( @@ -1458,7 +1462,6 @@ def _get_user_info(sub_type=str, user=None): # TODO Update to support POSTed passport try: if user: - print("-------------user true-----------") if hasattr(flask.current_app, "db"): with flask.current_app.db.session as session: result = query_for_user(session, user) diff --git a/fence/resources/google/utils.py b/fence/resources/google/utils.py index a891c1fec..7db104885 100644 --- a/fence/resources/google/utils.py +++ b/fence/resources/google/utils.py @@ -3,6 +3,7 @@ import os from cryptography.fernet import Fernet import flask +from sqlalchemy.sql.functions import user from flask_sqlalchemy_session import current_session from sqlalchemy import desc, func @@ -525,8 +526,13 @@ def get_or_create_proxy_group_id(expires=None, user_id=None): """ proxy_group_id = _get_proxy_group_id(user_id=user_id) if not proxy_group_id: - user_id = current_token["sub"] - username = current_token.get("context", {}).get("user", {}).get("name", "") + if user_id: + user = current_session.query(User).filter_by(id=int(user_id)).first() + user_id = user_id + username = user.username + else: + user_id = current_token["sub"] + username = current_token.get("context", {}).get("user", {}).get("name", "") proxy_group_id = _create_proxy_group(user_id, username).id privileges = current_session.query(AccessPrivilege).filter( @@ -572,11 +578,7 @@ def _get_proxy_group_id(user_id=None): user = ( current_session.query(User).filter(User.id == user_id).first() ) - print("-----------userid-----------") - print(user) proxy_group_id = user.google_proxy_group_id - print("-----proxy----") - print(proxy_group_id) return proxy_group_id @@ -605,6 +607,10 @@ def _create_proxy_group(user_id, username): # link proxy group to user user = current_session.query(User).filter_by(id=user_id).first() + print("----------proxy grou id---------------- ") + print(proxy_group) + print(proxy_group.id) + print(user) user.google_proxy_group_id = proxy_group.id current_session.add(proxy_group) diff --git a/tests/test_drs.py b/tests/test_drs.py index 71924bfb0..cd01d0a83 100644 --- a/tests/test_drs.py +++ b/tests/test_drs.py @@ -391,3 +391,5 @@ def test_get_presigned_url_with_passport_for_non_public_acl( data=json.dumps(data), ) assert res.status_code == 200 + + flask.current_app.jwt_public_keys = {} From cbea9e980497ef69632ad683bcbe7780f50531f4 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 23 Nov 2021 11:41:37 -0600 Subject: [PATCH 100/211] fix(google): pass storage creds into sync so Google Storage backend can be updated --- fence/blueprints/login/ras.py | 3 ++- fence/resources/ga4gh/passports.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index cd5aa58cc..c61c3c81e 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -169,10 +169,11 @@ def post_login(self, user=None, token_result=None, id_from_idp=None): dbGaP = os.environ.get("dbGaP") or config.get("dbGaP") if not isinstance(dbGaP, list): dbGaP = [dbGaP] + storage_creds = config["STORAGE_CREDENTIALS"] sync = fence.scripting.fence_create.init_syncer( dbGaP, - None, + storage_creds, DB, arborist=arborist, ) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 754555af1..bba9bd532 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -339,8 +339,9 @@ def sync_visa_authorization(gen3_user, ga4gh_visas, expiration): from fence.settings import DB except ImportError: pass + storage_creds = config["STORAGE_CREDENTIALS"] syncer = fence.scripting.fence_create.init_syncer( - dbgap_config, None, DB, arborist=arborist_client + dbgap_config, storage_creds, DB, arborist=arborist_client ) with flask.current_app.db.session as db_session: From 2cd126309276f4be49f75262edea6e03cb47f60c Mon Sep 17 00:00:00 2001 From: BinamB Date: Tue, 23 Nov 2021 12:33:40 -0600 Subject: [PATCH 101/211] fix tests --- fence/blueprints/data/indexd.py | 8 ++++---- fence/resources/google/utils.py | 8 +------- tests/test_drs.py | 15 ++++++++------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index 4728ecddc..7a5b0e37b 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -83,8 +83,6 @@ def get_signed_url_for_file( if ga4gh_passports: # TODO change this to usernames user_ids_from_passports = get_gen3_users_from_ga4gh_passports(ga4gh_passports) - print("------------user idss from passports---------------") - print(user_ids_from_passports) # add the user details to `flask.g.audit_data` first, so they are # included in the audit log if `IndexedFile(file_id)` raises a 404 @@ -425,7 +423,9 @@ def get_signed_url( f"{action_to_permission[action]} permission " f"on authz resource: {self.index_document['authz']}" ) - authorized_user_id = authorized_user_id if isinstance(authorized_user_id, str) else None + authorized_user_id = ( + authorized_user_id if isinstance(authorized_user_id, str) else None + ) else: if self.public_acl and action == "upload": raise Unauthorized( @@ -520,7 +520,7 @@ def check_authz(self, action, user_ids_from_passports=None): ) # handle multiple GA4GH passports as a means of authn/z - + if user_ids_from_passports: for user_id in user_ids_from_passports: authorized = flask.current_app.arborist.auth_request( diff --git a/fence/resources/google/utils.py b/fence/resources/google/utils.py index 7db104885..8a936f0bc 100644 --- a/fence/resources/google/utils.py +++ b/fence/resources/google/utils.py @@ -575,9 +575,7 @@ def _get_proxy_group_id(user_id=None): if not proxy_group_id: user_id = user_id or current_token["sub"] - user = ( - current_session.query(User).filter(User.id == user_id).first() - ) + user = current_session.query(User).filter(User.id == user_id).first() proxy_group_id = user.google_proxy_group_id return proxy_group_id @@ -607,10 +605,6 @@ def _create_proxy_group(user_id, username): # link proxy group to user user = current_session.query(User).filter_by(id=user_id).first() - print("----------proxy grou id---------------- ") - print(proxy_group) - print(proxy_group.id) - print(user) user.google_proxy_group_id = proxy_group.id current_session.add(proxy_group) diff --git a/tests/test_drs.py b/tests/test_drs.py index cd01d0a83..e197c6380 100644 --- a/tests/test_drs.py +++ b/tests/test_drs.py @@ -1,4 +1,5 @@ import flask +import httpx import json import jwt import pytest @@ -239,9 +240,13 @@ def test_get_presigned_url_with_query_params( @responses.activate @pytest.mark.parametrize("indexd_client", ["s3", "gs"], indirect=True) +@patch("httpx.get") +@patch("fence.resources.google.utils._create_proxy_group") @patch("fence.resources.ga4gh.passports.ArboristClient") def test_get_presigned_url_with_passport_for_non_public_acl( mock_arborist, + mock_google_proxy_group, + mock_httpx_get, client, indexd_client, kid, @@ -274,6 +279,7 @@ def test_get_presigned_url_with_passport_for_non_public_acl( ) mock_arborist_requests({"arborist/auth/request": {"POST": ({"auth": True}, 200)}}) mock_arborist.return_value = MagicMock(ArboristClient) + mock_google_proxy_group.return_value = google_proxy_group # Prepare Passport/Visa headers = {"kid": kid} @@ -377,11 +383,8 @@ def test_get_presigned_url_with_passport_for_non_public_acl( data = {"passports": passports} - flask.current_app.jwt_public_keys = { - "https://stsstg.nih.gov": { - kid: rsa_public_key, - } - } + keys = [keypair.public_key_to_jwk() for keypair in flask.current_app.keypairs] + mock_httpx_get.return_value = httpx.Response(200, json={"keys": keys}) res = client.post( "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, @@ -391,5 +394,3 @@ def test_get_presigned_url_with_passport_for_non_public_acl( data=json.dumps(data), ) assert res.status_code == 200 - - flask.current_app.jwt_public_keys = {} From 820537e05b5c9aca4ce0efb478eddafb49e6885c Mon Sep 17 00:00:00 2001 From: John McCann Date: Tue, 23 Nov 2021 11:41:45 -0800 Subject: [PATCH 102/211] chore(grant_user_policy): supply expires_at --- fence/sync/sync_users.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 90bb62d20..1d6aa5920 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -5,6 +5,7 @@ import subprocess as sp import yaml import copy +import datetime from contextlib import contextmanager from collections import defaultdict @@ -1612,7 +1613,8 @@ def _update_authz_in_arborist( Args: user_projects (dict) user_yaml (UserYAML) optional, if there are policies for users in a user.yaml - expiration (datetime.datetime): expiration time + TODO update this + expires (datetime.datetime): expiration time Return: bool: success @@ -1731,9 +1733,12 @@ def _update_authz_in_arborist( "not creating policy in arborist; {}".format(str(e)) ) self._created_policies.add(policy_id) - self.arborist_client.grant_user_policy(username, policy_id) - # TODO need to add expiration to this function in gen3authz - # self.arborist_client.grant_user_policy(username, policy_id, expiration=expiration) + + self.arborist_client.grant_user_policy( + username, + policy_id, + expires_at=datetime.datetime.fromtimestamp(expires), + ) # TODO As of 10-11-2021, there's no endpoint yet in Arborist to # support the creation of policies in bulk. When syncing RAS From cc6494f296b1ba6ea1667ce02de5876004a29cf0 Mon Sep 17 00:00:00 2001 From: John McCann Date: Tue, 23 Nov 2021 11:58:29 -0800 Subject: [PATCH 103/211] chore(grant_user_policy): expire in user_yaml case --- fence/sync/sync_users.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 1d6aa5920..3cea11602 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -1666,6 +1666,9 @@ def _update_authz_in_arborist( policy_id_list = [] policies = [] + if expires: + expires = datetime.datetime.fromtimestamp(expires) + for username, user_project_info in user_projects.items(): self.logger.info("processing user `{}`".format(username)) user = query_for_user(session=session, username=username) @@ -1737,7 +1740,7 @@ def _update_authz_in_arborist( self.arborist_client.grant_user_policy( username, policy_id, - expires_at=datetime.datetime.fromtimestamp(expires), + expires_at=expires, ) # TODO As of 10-11-2021, there's no endpoint yet in Arborist to @@ -1771,7 +1774,11 @@ def _update_authz_in_arborist( # if user_yaml: for policy in user_yaml.policies.get(username, []): - self.arborist_client.grant_user_policy(username, policy) + self.arborist_client.grant_user_policy( + username, + policy, + expires_at=expires, + ) if user_yaml: for client_name, client_details in user_yaml.clients.items(): From b754d30e7d3138344c7b904936cd75afe684556f Mon Sep 17 00:00:00 2001 From: John McCann Date: Tue, 23 Nov 2021 12:04:10 -0800 Subject: [PATCH 104/211] fix(authz expiry): allow expires=0 --- fence/sync/sync_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 3cea11602..a830ea649 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -1666,7 +1666,7 @@ def _update_authz_in_arborist( policy_id_list = [] policies = [] - if expires: + if expires is not None: expires = datetime.datetime.fromtimestamp(expires) for username, user_project_info in user_projects.items(): From 3ab4776acb3cde32f3071a0325ddb7c18f3c7034 Mon Sep 17 00:00:00 2001 From: BinamB Date: Tue, 23 Nov 2021 14:23:56 -0600 Subject: [PATCH 105/211] add unit tests --- tests/test_drs.py | 280 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 279 insertions(+), 1 deletion(-) diff --git a/tests/test_drs.py b/tests/test_drs.py index e197c6380..4c32b94b3 100644 --- a/tests/test_drs.py +++ b/tests/test_drs.py @@ -243,7 +243,7 @@ def test_get_presigned_url_with_query_params( @patch("httpx.get") @patch("fence.resources.google.utils._create_proxy_group") @patch("fence.resources.ga4gh.passports.ArboristClient") -def test_get_presigned_url_with_passport_for_non_public_acl( +def test_get_presigned_url_for_non_public_data_with_passport( mock_arborist, mock_google_proxy_group, mock_httpx_get, @@ -394,3 +394,281 @@ def test_get_presigned_url_with_passport_for_non_public_acl( data=json.dumps(data), ) assert res.status_code == 200 + + +@responses.activate +@pytest.mark.parametrize("indexd_client", ["s3", "gs"], indirect=True) +@patch("httpx.get") +@patch("fence.resources.google.utils._create_proxy_group") +@patch("fence.resources.ga4gh.passports.ArboristClient") +def test_get_presigned_url_with_passport_with_incorrect_authz( + mock_arborist, + mock_google_proxy_group, + mock_httpx_get, + client, + indexd_client, + kid, + rsa_private_key, + rsa_public_key, + indexd_client_accepting_record, + mock_arborist_requests, + google_proxy_group, + primary_google_service_account, + cloud_manager, + google_signed_url, +): + indexd_record_with_non_public_authz_and_public_acl_populated = { + "did": "1", + "baseid": "", + "rev": "", + "size": 10, + "file_name": "file1", + "urls": ["s3://bucket1/key", "gs://bucket1/key"], + "hashes": {}, + "metadata": {}, + "authz": ["/orgA/programs/phs000991.c1"], + "acl": ["*"], + "form": "", + "created_date": "", + "updated_date": "", + } + indexd_client_accepting_record( + indexd_record_with_non_public_authz_and_public_acl_populated + ) + mock_arborist_requests({"arborist/auth/request": {"POST": ({"auth": False}, 200)}}) + mock_arborist.return_value = MagicMock(ArboristClient) + mock_google_proxy_group.return_value = google_proxy_group + + # Prepare Passport/Visa + headers = {"kid": kid} + decoded_visa = { + "iss": "https://stsstg.nih.gov", + "sub": "abcde12345aspdij", + "iat": int(time.time()), + "exp": int(time.time()) + 1000, + "scope": "openid ga4gh_passport_v1 email profile", + "jti": "jtiajoidasndokmasdl", + "txn": "sapidjspa.asipidja", + "name": "", + "ga4gh_visa_v1": { + "type": "https://ras.nih.gov/visas/v1.1", + "asserted": int(time.time()), + "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", + "source": "https://ncbi.nlm.nih.gov/gap", + }, + "ras_dbgap_permissions": [ + { + "consent_name": "Health/Medical/Biomedical", + "phs_id": "phs000991", + "version": "v1", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": int(time.time()) + 1001, + }, + { + "consent_name": "General Research Use (IRB, PUB)", + "phs_id": "phs000961", + "version": "v1", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": int(time.time()) + 1001, + }, + { + "consent_name": "Disease-Specific (Cardiovascular Disease)", + "phs_id": "phs000279", + "version": "v2", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": int(time.time()) + 1001, + }, + { + "consent_name": "Health/Medical/Biomedical (IRB)", + "phs_id": "phs000286", + "version": "v6", + "participant_set": "p2", + "consent_group": "c3", + "role": "designated user", + "expiration": int(time.time()) + 1001, + }, + { + "consent_name": "Disease-Specific (Focused Disease Only, IRB, NPU)", + "phs_id": "phs000289", + "version": "v6", + "participant_set": "p2", + "consent_group": "c2", + "role": "designated user", + "expiration": int(time.time()) + 1001, + }, + { + "consent_name": "Disease-Specific (Autism Spectrum Disorder)", + "phs_id": "phs000298", + "version": "v4", + "participant_set": "p3", + "consent_group": "c1", + "role": "designated user", + "expiration": int(time.time()) + 1001, + }, + ], + } + encoded_visa = jwt.encode( + decoded_visa, key=rsa_private_key, headers=headers, algorithm="RS256" + ).decode("utf-8") + + passport_header = { + "type": "JWT", + "alg": "RS256", + "kid": kid, + } + passport = { + "iss": "https://stsstg.nih.gov", + "sub": "abcde12345aspdij", + "iat": int(time.time()), + "scope": "openid ga4gh_passport_v1 email profile", + "exp": int(time.time()) + 1000, + "ga4gh_passport_v1": [encoded_visa], + } + encoded_passport = jwt.encode( + passport, key=rsa_private_key, headers=passport_header, algorithm="RS256" + ).decode("utf-8") + + access_id = indexd_client["indexed_file_location"] + test_guid = "1" + + passports = [encoded_passport] + + data = {"passports": passports} + + keys = [keypair.public_key_to_jwk() for keypair in flask.current_app.keypairs] + mock_httpx_get.return_value = httpx.Response(200, json={"keys": keys}) + + res = client.post( + "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, + headers={ + "Content-Type": "application/json", + }, + data=json.dumps(data), + ) + assert res.status_code == 401 + + +@responses.activate +@pytest.mark.parametrize("indexd_client", ["s3", "gs"], indirect=True) +@patch("httpx.get") +@patch("fence.resources.google.utils._create_proxy_group") +@patch("fence.resources.ga4gh.passports.ArboristClient") +def test_get_presigned_url_for_public_data_with_no_passport( + mock_arborist, + mock_google_proxy_group, + mock_httpx_get, + client, + indexd_client, + kid, + rsa_private_key, + rsa_public_key, + indexd_client_accepting_record, + mock_arborist_requests, + google_proxy_group, + primary_google_service_account, + cloud_manager, + google_signed_url, +): + indexd_record_with_public_authz_and_public_acl_populated = { + "did": "1", + "baseid": "", + "rev": "", + "size": 10, + "file_name": "file1", + "urls": ["s3://bucket1/key", "gs://bucket1/key"], + "hashes": {}, + "metadata": {}, + "authz": ["/open"], + "acl": ["*"], + "form": "", + "created_date": "", + "updated_date": "", + } + indexd_client_accepting_record( + indexd_record_with_public_authz_and_public_acl_populated + ) + mock_arborist_requests({"arborist/auth/request": {"POST": ({"auth": True}, 200)}}) + mock_arborist.return_value = MagicMock(ArboristClient) + mock_google_proxy_group.return_value = google_proxy_group + + access_id = indexd_client["indexed_file_location"] + test_guid = "1" + + passports = [] + + data = {"passports": passports} + + res = client.post( + "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, + headers={ + "Content-Type": "application/json", + }, + data=json.dumps(data), + ) + assert res.status_code == 200 + + +@responses.activate +@pytest.mark.parametrize("indexd_client", ["s3", "gs"], indirect=True) +@patch("httpx.get") +@patch("fence.resources.google.utils._create_proxy_group") +@patch("fence.resources.ga4gh.passports.ArboristClient") +def test_get_presigned_url_for_non_public_data_with_no_passport( + mock_arborist, + mock_google_proxy_group, + mock_httpx_get, + client, + indexd_client, + kid, + rsa_private_key, + rsa_public_key, + indexd_client_accepting_record, + mock_arborist_requests, + google_proxy_group, + primary_google_service_account, + cloud_manager, + google_signed_url, +): + indexd_record_with_public_authz_and_non_public_acl_populated = { + "did": "1", + "baseid": "", + "rev": "", + "size": 10, + "file_name": "file1", + "urls": ["s3://bucket1/key", "gs://bucket1/key"], + "hashes": {}, + "metadata": {}, + "authz": ["/orgA/programs/phs000991.c1"], + "acl": ["*"], + "form": "", + "created_date": "", + "updated_date": "", + } + indexd_client_accepting_record( + indexd_record_with_public_authz_and_non_public_acl_populated + ) + mock_arborist_requests({"arborist/auth/request": {"POST": ({"auth": False}, 200)}}) + mock_arborist.return_value = MagicMock(ArboristClient) + mock_google_proxy_group.return_value = google_proxy_group + + access_id = indexd_client["indexed_file_location"] + test_guid = "1" + + passports = [] + + data = {"passports": passports} + + res = client.post( + "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, + headers={ + "Content-Type": "application/json", + }, + data=json.dumps(data), + ) + assert res.status_code == 401 From 2339756fc4f693199b01ae6b2bf6bfd70c01726a Mon Sep 17 00:00:00 2001 From: BinamB Date: Tue, 23 Nov 2021 14:58:57 -0600 Subject: [PATCH 106/211] add comment --- fence/blueprints/data/indexd.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index 7a5b0e37b..2b5c2b16c 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -532,6 +532,7 @@ def check_authz(self, action, user_ids_from_passports=None): ) # if any passport provides access, user is authorized if authorized: + # for google proxy groups we need to know which user_id gave access return user_id return authorized else: From ffd0e605c08cf8163f5461e2ba4e8f41efc4b73f Mon Sep 17 00:00:00 2001 From: BinamB Date: Tue, 23 Nov 2021 15:47:11 -0600 Subject: [PATCH 107/211] remove unused import --- fence/resources/google/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fence/resources/google/utils.py b/fence/resources/google/utils.py index 8a936f0bc..efa2da2cd 100644 --- a/fence/resources/google/utils.py +++ b/fence/resources/google/utils.py @@ -3,7 +3,6 @@ import os from cryptography.fernet import Fernet import flask -from sqlalchemy.sql.functions import user from flask_sqlalchemy_session import current_session from sqlalchemy import desc, func From 0f62ec569f31f358f7112fb05c93886e8b41160b Mon Sep 17 00:00:00 2001 From: John McCann Date: Sun, 28 Nov 2021 13:39:53 -0800 Subject: [PATCH 108/211] chore(ras_oauth2.py): update Arborist username --- fence/resources/openid/ras_oauth2.py | 6 +++++- fence/sync/sync_users.py | 6 +++--- tests/ras/test_ras.py | 6 +++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 1850e03d7..ac10f50bf 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -206,7 +206,11 @@ def map_iss_sub_pair_to_user(self, issuer, subject_id, username, email): "from the DRS endpoint. Changing said user's username" f' to "{username}".' ) - # TODO also change username in Arborist + flask.current_app.arborist.update_user( + iss_sub_pair_to_user.user.username, + new_username=username, + new_email=email, + ) iss_sub_pair_to_user.user.username = username iss_sub_pair_to_user.user.email = email db_session.commit() diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index a830ea649..551bc1fd5 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -1613,8 +1613,8 @@ def _update_authz_in_arborist( Args: user_projects (dict) user_yaml (UserYAML) optional, if there are policies for users in a user.yaml - TODO update this - expires (datetime.datetime): expiration time + single_user_sync (bool) whether authz update is for a single user + expires (int) time at which authz info in Arborist should expire Return: bool: success @@ -1667,7 +1667,7 @@ def _update_authz_in_arborist( policies = [] if expires is not None: - expires = datetime.datetime.fromtimestamp(expires) + expires = datetime.datetime.utcfromtimestamp(expires) for username, user_project_info in user_projects.items(): self.logger.info("processing user `{}`".format(username)) diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index 6f3faee62..47d09fd66 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -677,7 +677,9 @@ def test_map_iss_sub_pair_to_user_with_no_prior_DRS_access(db_session): assert len(iss_sub_pair_to_user_records) == 1 -def test_map_iss_sub_pair_to_user_with_prior_DRS_access(db_session): +def test_map_iss_sub_pair_to_user_with_prior_DRS_access( + db_session, mock_arborist_requests +): """ Test RASOauth2Client.map_iss_sub_pair_to_user when the username passed in (e.g. eRA username) does not already exist in the Fence database but that @@ -686,6 +688,8 @@ def test_map_iss_sub_pair_to_user_with_prior_DRS_access(db_session): existing user's username is changed from sub+iss to the username passed in. """ + mock_arborist_requests({"arborist/user/123_abcdomain.tld": {"PATCH": (None, 204)}}) + iss = "https://domain.tld" sub = "123_abc" username = "johnsmith" From e673cdfbfa4b79e9a15d63c80979ab3361429900 Mon Sep 17 00:00:00 2001 From: John McCann Date: Sun, 28 Nov 2021 13:45:06 -0800 Subject: [PATCH 109/211] chore(dependencies): use local gen3authz --- gen3authz-1.4.0.tar.gz | Bin 0 -> 13815 bytes poetry.lock | 38 ++++++++++++++++++++++++++++++++++---- pyproject.toml | 2 +- 3 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 gen3authz-1.4.0.tar.gz diff --git a/gen3authz-1.4.0.tar.gz b/gen3authz-1.4.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..009f9d16b35d8330cdcc4bbd98b925ea7b2d6e75 GIT binary patch literal 13815 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.4,<3.0", markers = "python_version < \"3.7\""} httpx = ">=0.20.0,<1.0.0" +six = ">=1.16.0,<2.0.0" + +[package.source] +type = "file" +url = "gen3authz-1.4.0.tar.gz" [[package]] name = "gen3cirrus" @@ -1591,7 +1596,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "05fc2802e427c72c626874251df018ea3ceaccdc0297382ae97bbf274b664bb6" +content-hash = "f885d062949ca66eb4110fc3491d8b7b5bcb4202fd839530ec143af232776034" [metadata.files] addict = [ @@ -1889,8 +1894,7 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] gen3authz = [ - {file = "gen3authz-1.1.1-py3-none-any.whl", hash = "sha256:ed34291683d8ebc8015a38f81917933dd64d3bef4c50a5158612e87f65d349a7"}, - {file = "gen3authz-1.1.1.tar.gz", hash = "sha256:0c32a6fd40c94c2f93c987f24e2953e6537e2fe98b029f9c85dde6e39818e014"}, + {file = "gen3authz-1.4.0.tar.gz", hash = "sha256:7983834bcc5710fd9e915297f57098d47ffb12b73ebab0d9abcfc05b7676ce19"}, ] gen3cirrus = [ {file = "gen3cirrus-2.0.0.tar.gz", hash = "sha256:0bd590c407c42dad5f0b896da0fa30bd01ea6bef5ff7dd11324ec59f14a71793"}, @@ -2059,12 +2063,22 @@ markdown = [ {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -2073,14 +2087,21 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -2090,6 +2111,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -2160,9 +2184,13 @@ protobuf = [ {file = "protobuf-3.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ae692bb6d1992afb6b74348e7bb648a75bb0d3565a3f5eea5bec8f62bd06d87"}, {file = "protobuf-3.17.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:99938f2a2d7ca6563c0ade0c5ca8982264c484fdecf418bd68e880a7ab5730b1"}, {file = "protobuf-3.17.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6902a1e4b7a319ec611a7345ff81b6b004b36b0d2196ce7a748b3493da3d226d"}, + {file = "protobuf-3.17.3-cp38-cp38-win32.whl", hash = "sha256:59e5cf6b737c3a376932fbfb869043415f7c16a0cf176ab30a5bbc419cd709c1"}, + {file = "protobuf-3.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ebcb546f10069b56dc2e3da35e003a02076aaa377caf8530fe9789570984a8d2"}, {file = "protobuf-3.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ffbd23640bb7403574f7aff8368e2aeb2ec9a5c6306580be48ac59a6bac8bde"}, {file = "protobuf-3.17.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:26010f693b675ff5a1d0e1bdb17689b8b716a18709113288fead438703d45539"}, {file = "protobuf-3.17.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e76d9686e088fece2450dbc7ee905f9be904e427341d289acbe9ad00b78ebd47"}, + {file = "protobuf-3.17.3-cp39-cp39-win32.whl", hash = "sha256:a38bac25f51c93e4be4092c88b2568b9f407c27217d3dd23c7a57fa522a17554"}, + {file = "protobuf-3.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:85d6303e4adade2827e43c2b54114d9a6ea547b671cb63fafd5011dc47d0e13d"}, {file = "protobuf-3.17.3-py2.py3-none-any.whl", hash = "sha256:2bfb815216a9cd9faec52b16fd2bfa68437a44b67c56bee59bc3926522ecb04e"}, {file = "protobuf-3.17.3.tar.gz", hash = "sha256:72804ea5eaa9c22a090d2803813e280fb273b62d5ae497aaf3553d141c4fdd7b"}, ] @@ -2263,6 +2291,8 @@ pynacl = [ {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, diff --git a/pyproject.toml b/pyproject.toml index 335f8988f..b2f65061d 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ flask-cors = "^3.0.3" flask-restful = "^0.3.6" flask_sqlalchemy_session = "^1.1" email_validator = "^1.1.1" -gen3authz = "^1.1.1" +gen3authz = {path = "./gen3authz-1.4.0.tar.gz"} gen3cirrus = "^2.0.0" gen3config = "^0.1.7" gen3users = "^0.6.0" From 557ef26521703afa35c2c83ce617ee568d1a7beb Mon Sep 17 00:00:00 2001 From: John McCann Date: Sun, 28 Nov 2021 19:12:51 -0800 Subject: [PATCH 110/211] chore(Dockerfile): copy gen3authz-1.4.0.tar.gz --- Dockerfile | 2 ++ pyproject.toml | 1 + 2 files changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 121833779..84abfdc0f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,8 @@ RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2 WORKDIR /$appname +# TODO TAKE OUT: ONLY FOR DEVELOPMENT AND TESTING PURPOSES +COPY gen3authz-1.4.0.tar.gz /$appname/ # copy ONLY poetry artifact and install # this will make sure than the dependencies is cached COPY poetry.lock pyproject.toml /$appname/ diff --git a/pyproject.toml b/pyproject.toml index b2f65061d..3da06d65f 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ flask-cors = "^3.0.3" flask-restful = "^0.3.6" flask_sqlalchemy_session = "^1.1" email_validator = "^1.1.1" +# TODO USE ^1.4.0 WHEN RELEASED. LOCAL COPY ONLY FOR DEVELOPMENT AND TESTING PURPOSES gen3authz = {path = "./gen3authz-1.4.0.tar.gz"} gen3cirrus = "^2.0.0" gen3config = "^0.1.7" From da5a6122bcaad9cdf679c52e081b15110cbcc56f Mon Sep 17 00:00:00 2001 From: BinamB Date: Mon, 29 Nov 2021 08:26:46 -0600 Subject: [PATCH 111/211] remove gen3authz + todo --- fence/blueprints/data/indexd.py | 1 - gen3authz-1.3.0.tar.gz | Bin 13085 -> 0 bytes 2 files changed, 1 deletion(-) delete mode 100644 gen3authz-1.3.0.tar.gz diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index 84a5b2250..690f0e80c 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -1466,7 +1466,6 @@ def _get_user_info(sub_type=str, user=None): populated information about an anonymous user. By default, cast `sub` to str. Use `sub_type` to override this behavior. """ - # TODO Update to support POSTed passport try: if user: if hasattr(flask.current_app, "db"): diff --git a/gen3authz-1.3.0.tar.gz b/gen3authz-1.3.0.tar.gz deleted file mode 100644 index a3c39d4f474169c4d1209752a3e7546b10bcc579..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13085 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 Date: Mon, 29 Nov 2021 07:28:49 -0800 Subject: [PATCH 112/211] chore(Jenkins): trigger integration tests From e48fde3201269c7467ee647ef1884117748422da Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Mon, 29 Nov 2021 14:35:37 -0600 Subject: [PATCH 113/211] feat(authz): persist authz project to resource mapping in database so it can be accessed w/o user.yaml --- fence/models.py | 21 +++++++++++++ fence/sync/sync_users.py | 49 +++++++++++++++++++++++++---- poetry.lock | 67 ++++++++++++++++++++++++++++------------ pyproject.toml | 4 +-- 4 files changed, 114 insertions(+), 27 deletions(-) diff --git a/fence/models.py b/fence/models.py index b4d971742..3a09830b5 100644 --- a/fence/models.py +++ b/fence/models.py @@ -100,6 +100,27 @@ def create_user(session, logger, username, email=None, idp_name=None): return user +def get_project_to_authz_mapping(session): + """ + Get the mappings for Project.auth_id to authorization resource (Project.authz) + from the database if a mapping exists. e.g. will only return if Project.authz is + populated. + + Args: + session (sqlalchemy.orm.session.Session): database session + + Returns: + dict{str:str}: Mapping from Project.auth_id to Project.authz + """ + output = {} + + query_results = session.query(Project.auth_id, Project.authz) + if query_results: + output = {item.auth_id: item.authz for item in query_results if item.authz} + + return output + + class ClientAuthType(Enum): """ List the possible types of OAuth client authentication, which are diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 90bb62d20..35633df3a 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -31,6 +31,7 @@ User, query_for_user, Client, + get_project_to_authz_mapping, ) from fence.resources.storage import StorageManager from fence.resources.google.access_utils import bulk_update_google_groups @@ -277,6 +278,24 @@ def from_file(cls, filepath, encrypted=True, key=None, logger=None): logger=logger, ) + def persist_project_to_resource(self, db_session): + """ + Store the mappings from Project.auth_id to authorization resource (Project.authz) + + The mapping comes from an external source, this function persists what was parsed + into memory into the database for future use. + """ + for auth_id, authz_resource in self.project_to_resource.items(): + project = ( + db_session.query(Project).filter(Project.auth_id == auth_id).first() + ) + if project: + project.authz = authz_resource + else: + project = Project(name=auth_id, auth_id=auth_id, authz=authz_resource) + db_session.add(project) + db_session.commit() + class UserSyncer(object): def __init__( @@ -1406,6 +1425,11 @@ def _sync(self, sess): for u, s in self.auth_source.items(): self.logger.info("Access for user {} from {}".format(u, s)) + self.logger.info( + f"Persisting authz mapping to database: {user_yaml.project_to_resource}" + ) + user_yaml.persist_project_to_resource(db_session=sess) + def _grant_all_consents_to_c999_users( self, user_projects, user_yaml_project_to_resources ): @@ -1664,6 +1688,20 @@ def _update_authz_in_arborist( policy_id_list = [] policies = [] + # prefer in-memory if available from user_yaml, if not, get from database + if user_yaml and user_yaml.project_to_resource: + project_to_authz_mapping = user_yaml.project_to_resource + self.logger.debug( + f"using in-memory project to authz resource mapping from " + f"user.yaml (instead of database): {project_to_authz_mapping}" + ) + else: + project_to_authz_mapping = get_project_to_authz_mapping(session) + self.logger.debug( + f"using persisted project to authz resource mapping from database " + f"(instead of user.yaml - as it may not be available): {project_to_authz_mapping}" + ) + for username, user_project_info in user_projects.items(): self.logger.info("processing user `{}`".format(username)) user = query_for_user(session=session, username=username) @@ -1680,12 +1718,11 @@ def _update_authz_in_arborist( # resource path, otherwise just use given project as path paths = self._dbgap_study_to_resources.get(project, [project]) - if user_yaml: - try: - # check if project is in mapping and convert accordingly - paths = [user_yaml.project_to_resource[project]] - except KeyError: - pass + try: + # check if project is in mapping and convert accordingly + paths = [project_to_authz_mapping[project]] + except KeyError: + pass self.logger.info( "resource paths for project {}: {}".format(project, paths) diff --git a/poetry.lock b/poetry.lock index e9ff86b53..1e1967ed8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -263,7 +263,7 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "2.0.7" +version = "2.0.8" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -887,7 +887,7 @@ test = ["unittest2 (>=1.1.0)"] [[package]] name = "more-itertools" -version = "8.11.0" +version = "8.12.0" description = "More routines for operating on iterables, beyond itertools" category = "dev" optional = false @@ -989,7 +989,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "paramiko" -version = "2.8.0" +version = "2.8.1" description = "SSH2 protocol library" category = "main" optional = false @@ -1042,7 +1042,7 @@ twisted = ["twisted"] [[package]] name = "prometheus-flask-exporter" -version = "0.18.5" +version = "0.18.6" description = "Prometheus metrics exporter for Flask" category = "main" optional = false @@ -1327,11 +1327,11 @@ idna2008 = ["idna"] [[package]] name = "rsa" -version = "4.7.2" +version = "4.8" description = "Pure-Python RSA implementation" category = "main" optional = false -python-versions = ">=3.5, <4" +python-versions = ">=3.6,<4" [package.dependencies] pyasn1 = ">=0.1.3" @@ -1444,7 +1444,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "userdatamodel" -version = "2.3.3" +version = "2.4.0" description = "" category = "main" optional = false @@ -1503,7 +1503,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "6ab75ef8133cee334806d5da321202a3589a72020a6b9afff22b23596cc16d6b" +content-hash = "5fb6eadf724e09b8309645a21e2b9c16a7b8e8b464f56f55f664a747b529e3ca" [metadata.files] addict = [ @@ -1647,8 +1647,8 @@ cffi = [ {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"}, - {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"}, + {file = "charset-normalizer-2.0.8.tar.gz", hash = "sha256:735e240d9a8506778cd7a453d97e817e536bb1fc29f4f6961ce297b9c7a917b0"}, + {file = "charset_normalizer-2.0.8-py3-none-any.whl", hash = "sha256:83fcdeb225499d6344c8f7f34684c2981270beacc32ede2e669e94f7fa544405"}, ] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, @@ -1974,20 +1974,39 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mock = [ @@ -1995,8 +2014,8 @@ mock = [ {file = "mock-2.0.0.tar.gz", hash = "sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba"}, ] more-itertools = [ - {file = "more-itertools-8.11.0.tar.gz", hash = "sha256:0a2fd25d343c08d7e7212071820e7e7ea2f41d8fb45d6bc8a00cd6ce3b7aab88"}, - {file = "more_itertools-8.11.0-py3-none-any.whl", hash = "sha256:88afff98d83d08fe5e4049b81e2b54c06ebb6b3871a600040865c7a592061cbb"}, + {file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"}, + {file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"}, ] moto = [ {file = "moto-1.3.15-py2.py3-none-any.whl", hash = "sha256:3be7e1f406ef7e9c222dbcbfd8cefa2cb1062200e26deae49b5df446e17be3df"}, @@ -2018,8 +2037,8 @@ packaging = [ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] paramiko = [ - {file = "paramiko-2.8.0-py2.py3-none-any.whl", hash = "sha256:def3ec612399bab4e9f5eb66b0ae5983980db9dd9120d9e9c6ea3ff673865d1c"}, - {file = "paramiko-2.8.0.tar.gz", hash = "sha256:e673b10ee0f1c80d46182d3af7751d033d9b573dd7054d2d0aa46be186c3c1d2"}, + {file = "paramiko-2.8.1-py2.py3-none-any.whl", hash = "sha256:7b5910f5815a00405af55da7abcc8a9e0d9657f57fcdd9a89894fdbba1c6b8a8"}, + {file = "paramiko-2.8.1.tar.gz", hash = "sha256:85b1245054e5d7592b9088cc6d08da22445417912d3a3e48138675c7a8616438"}, ] pbr = [ {file = "pbr-2.0.0-py2.py3-none-any.whl", hash = "sha256:d9b69a26a5cb4e3898eb3c5cea54d2ab3332382167f04e30db5e1f54e1945e45"}, @@ -2034,8 +2053,8 @@ prometheus-client = [ {file = "prometheus_client-0.9.0.tar.gz", hash = "sha256:9da7b32f02439d8c04f7777021c304ed51d9ec180604700c1ba72a4d44dceb03"}, ] prometheus-flask-exporter = [ - {file = "prometheus_flask_exporter-0.18.5-py3-none-any.whl", hash = "sha256:38a3a1fdaf4fc98f988d33f551a8005d778d6b43ca0a2bc4aafb19d0449a48b9"}, - {file = "prometheus_flask_exporter-0.18.5.tar.gz", hash = "sha256:f9a03e88a8415fe96f785c31fc82bbd290a606aaab87bd244414637d55ef0ba4"}, + {file = "prometheus_flask_exporter-0.18.6-py3-none-any.whl", hash = "sha256:02717c9d15c0956fe54e76bdde7c4116e1a1bddd12be7bc8538bc5b6af431ef1"}, + {file = "prometheus_flask_exporter-0.18.6.tar.gz", hash = "sha256:9a2af3d4ba014e3da6387b3b7cebaed291ccd6f474e240be13f50f8a5af671ca"}, ] protobuf = [ {file = "protobuf-3.19.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d80f80eb175bf5f1169139c2e0c5ada98b1c098e2b3c3736667f28cbbea39fc8"}, @@ -2158,6 +2177,8 @@ pynacl = [ {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, @@ -2203,18 +2224,26 @@ pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, @@ -2241,8 +2270,8 @@ rfc3986 = [ {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] rsa = [ - {file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"}, - {file = "rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"}, + {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, + {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, ] s3transfer = [ {file = "s3transfer-0.2.1-py2.py3-none-any.whl", hash = "sha256:b780f2411b824cb541dbcd2c713d0cb61c7d1bcadae204cdddda2b35cef493ba"}, @@ -2306,7 +2335,7 @@ urllib3 = [ {file = "urllib3-1.25.11.tar.gz", hash = "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"}, ] userdatamodel = [ - {file = "userdatamodel-2.3.3.tar.gz", hash = "sha256:b846b7efd2d002a653474fa3bd7bf2a2c964277ff5f8d9bde8e9d975aca8d130"}, + {file = "userdatamodel-2.4.0.tar.gz", hash = "sha256:11c3faf2c4a855e51305a02341123442bb6722bc518548e1da023b7a65136457"}, ] werkzeug = [ {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, diff --git a/pyproject.toml b/pyproject.toml index 01685302c..1a3c1344f 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ flask-cors = "^3.0.3" flask-restful = "^0.3.6" flask_sqlalchemy_session = "^1.1" email_validator = "^1.1.1" -gen3authz = "^1.3.0" +gen3authz = "^1.3.0" gen3cirrus = "^2.0.0" gen3config = "^0.1.7" gen3users = "^0.6.0" @@ -45,7 +45,7 @@ requests = "^2.18.0" retry = "^0.9.2" sqlalchemy = "^1.3.3" storageclient = {git = "https://github.com/uc-cdis/storage-client", rev = "1.0.2"} -userdatamodel = "^2.3.3" +userdatamodel = "^2.4.0" werkzeug = "^1.0.0" cachelib = "^0.2.0" azure-storage-blob = "^12.6.0" From f4069ca43a3716c14fd0cf0d011b231026b1a07e Mon Sep 17 00:00:00 2001 From: John McCann Date: Tue, 30 Nov 2021 18:23:14 -0800 Subject: [PATCH 114/211] chore(dependencies): use local gen3authz --- gen3authz-1.4.0.tar.gz | Bin 13815 -> 13826 bytes poetry.lock | 14 ++++++++++---- pyproject.toml | 3 ++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/gen3authz-1.4.0.tar.gz b/gen3authz-1.4.0.tar.gz index 009f9d16b35d8330cdcc4bbd98b925ea7b2d6e75..7edbabf13701a9cb4090af40e436c8bcc73d7566 100644 GIT binary patch literal 13826 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 literal 13815 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.4,<3.0", markers = "python_version < \"3.7\""} httpx = ">=0.20.0,<1.0.0" +six = ">=1.16.0,<2.0.0" + +[package.source] +type = "file" +url = "gen3authz-1.4.0.tar.gz" [[package]] name = "gen3cirrus" @@ -1503,7 +1508,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "6ab75ef8133cee334806d5da321202a3589a72020a6b9afff22b23596cc16d6b" +content-hash = "f885d062949ca66eb4110fc3491d8b7b5bcb4202fd839530ec143af232776034" [metadata.files] addict = [ @@ -1793,8 +1798,7 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] gen3authz = [ - {file = "gen3authz-1.3.0-py3-none-any.whl", hash = "sha256:b7dd8e4e81d72e42bfc70406e8dd44e473b424439da33bd80991c45d6753b927"}, - {file = "gen3authz-1.3.0.tar.gz", hash = "sha256:3fb88dd7dc0fa76edf9e3c4bbd1a7303509686cb22d86f48d1c1734092dc5682"}, + {file = "gen3authz-1.4.0.tar.gz", hash = "sha256:9b5a3a881374cab91a71cd54bf9cefbedc17f3c6d4e25d1c7e1e862c237f8888"}, ] gen3cirrus = [ {file = "gen3cirrus-2.0.0.tar.gz", hash = "sha256:0bd590c407c42dad5f0b896da0fa30bd01ea6bef5ff7dd11324ec59f14a71793"}, @@ -2158,6 +2162,8 @@ pynacl = [ {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, diff --git a/pyproject.toml b/pyproject.toml index ffd895afc..3da06d65f 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,8 @@ flask-cors = "^3.0.3" flask-restful = "^0.3.6" flask_sqlalchemy_session = "^1.1" email_validator = "^1.1.1" -gen3authz = "^1.3.0" +# TODO USE ^1.4.0 WHEN RELEASED. LOCAL COPY ONLY FOR DEVELOPMENT AND TESTING PURPOSES +gen3authz = {path = "./gen3authz-1.4.0.tar.gz"} gen3cirrus = "^2.0.0" gen3config = "^0.1.7" gen3users = "^0.6.0" From 71c07a1ebe06e1a50d8d3c1b183d6165617d6d96 Mon Sep 17 00:00:00 2001 From: John McCann Date: Tue, 30 Nov 2021 18:25:07 -0800 Subject: [PATCH 115/211] chore(authz expiry): pass timestamp to gen3authz --- fence/sync/sync_users.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 551bc1fd5..2c6016723 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -1666,9 +1666,6 @@ def _update_authz_in_arborist( policy_id_list = [] policies = [] - if expires is not None: - expires = datetime.datetime.utcfromtimestamp(expires) - for username, user_project_info in user_projects.items(): self.logger.info("processing user `{}`".format(username)) user = query_for_user(session=session, username=username) From b4d200e6af331afe4232515e95bbd2210eda3db5 Mon Sep 17 00:00:00 2001 From: John McCann Date: Tue, 30 Nov 2021 18:30:39 -0800 Subject: [PATCH 116/211] chore(ras_oauth2.py): retry failed Arborist update --- .secrets.baseline | 5 ++-- fence/resources/openid/ras_oauth2.py | 36 +++++++++++++++++++++++----- tests/ras/test_ras.py | 33 ++++++++++++++++++++++--- 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 7323a1723..481543680 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -250,7 +250,7 @@ "filename": "tests/ras/test_ras.py", "hashed_secret": "d9db6fe5c14dc55edd34115cdf3958845ac30882", "is_verified": false, - "line_number": 103 + "line_number": 105 } ], "tests/test-fence-config.yaml": [ @@ -260,7 +260,6 @@ "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_verified": false, "line_number": 31 - }, { "type": "Secret Keyword", @@ -271,5 +270,5 @@ } ] }, - "generated_at": "2021-11-15T23:28:25Z" + "generated_at": "2021-12-01T02:28:44Z" } diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index ac10f50bf..3d3e2ad8a 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -10,6 +10,7 @@ from authutils.errors import JWTError from authutils.token.core import get_iss, get_kid +from gen3authz.client.arborist.errors import ArboristError from fence.config import config @@ -23,6 +24,7 @@ ) from fence.jwt.validate import validate_jwt from fence.utils import DEFAULT_BACKOFF_SETTINGS +from fence.errors import InternalError from .idp_oauth2 import Oauth2ClientBase @@ -160,6 +162,8 @@ def get_user_id(self, code): flask.g.tokens = token flask.g.keys = keys + except InternalError: + raise except Exception as e: self.logger.exception("{}: {}".format(err_msg, e)) return {"error": err_msg} @@ -206,13 +210,33 @@ def map_iss_sub_pair_to_user(self, issuer, subject_id, username, email): "from the DRS endpoint. Changing said user's username" f' to "{username}".' ) - flask.current_app.arborist.update_user( - iss_sub_pair_to_user.user.username, - new_username=username, - new_email=email, - ) + + tries = 2 + for i in range(tries): + try: + flask.current_app.arborist.update_user( + iss_sub_pair_to_user.user.username, + new_username=username, + new_email=email, + ) + except ArboristError as e: + self.logger.warning( + f"Try {i+1}: could not update user's username in Arborist: {e}" + ) + if i == tries - 1: + err_msg = f"Failed to update user's username in Arborist after {tries} tries" + self.logger.exception(err_msg) + raise InternalError(err_msg) + else: + self.logger.info( + "Successfully changed Arborist user's username from " + f'"{iss_sub_pair_to_user.user.username}" to "{username}"' + ) + break + iss_sub_pair_to_user.user.username = username - iss_sub_pair_to_user.user.email = email + if email: + iss_sub_pair_to_user.user.email = email db_session.commit() elif iss_sub_pair_to_user.user.username != username: self.logger.warning( diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index 47d09fd66..08bcef22a 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -4,6 +4,7 @@ import time import mock import jwt +import pytest from cdislogging import get_logger @@ -19,6 +20,7 @@ ) from fence.resources.openid.ras_oauth2 import RASOauth2Client as RASClient from fence.resources.ga4gh.passports import get_or_create_gen3_user_from_iss_sub +from fence.errors import InternalError from tests.dbgap_sync.conftest import add_visa_manually from fence.job.visa_update_cronjob import Visa_Token_Update @@ -650,7 +652,7 @@ def test_map_iss_sub_pair_to_user_with_no_prior_DRS_access(db_session): Test RASOauth2Client.map_iss_sub_pair_to_user when the username passed in (e.g. eRA username) does not already exist in the Fence database and that user's combination has not already been mapped through a prior - DRS/data access request. + DRS access request. """ iss = "https://domain.tld" sub = "123_abc" @@ -684,7 +686,7 @@ def test_map_iss_sub_pair_to_user_with_prior_DRS_access( Test RASOauth2Client.map_iss_sub_pair_to_user when the username passed in (e.g. eRA username) does not already exist in the Fence database but that user's combination has already been mapped to an existing user - created during a prior DRS/data access request. In this case, that + created during a prior DRS access request. In this case, that existing user's username is changed from sub+iss to the username passed in. """ @@ -717,6 +719,31 @@ def test_map_iss_sub_pair_to_user_with_prior_DRS_access( assert iss_sub_pair_to_user.user.email == email +def test_map_iss_sub_pair_to_user_with_prior_DRS_access_and_arborist_error( + db_session, mock_arborist_requests +): + """ + Test that RASOauth2Client.map_iss_sub_pair_to_user raises an internal error + when Arborist fails to return a successful response. + """ + mock_arborist_requests({"arborist/user/123_abcdomain.tld": {"PATCH": (None, 500)}}) + + iss = "https://domain.tld" + sub = "123_abc" + username = "johnsmith" + email = "johnsmith@domain.tld" + oidc = config.get("OPENID_CONNECT", {}) + ras_client = RASClient( + oidc["ras"], + HTTP_PROXY=config.get("HTTP_PROXY"), + logger=logger, + ) + get_or_create_gen3_user_from_iss_sub(iss, sub) + + with pytest.raises(InternalError): + ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) + + def test_map_iss_sub_pair_to_user_with_prior_login_and_prior_DRS_access( db_session, ): @@ -724,7 +751,7 @@ def test_map_iss_sub_pair_to_user_with_prior_login_and_prior_DRS_access( Test RASOauth2Client.map_iss_sub_pair_to_user when the username passed in (e.g. eRA username) already exists in the Fence database and that user's combination has already been mapped to a separate user - created during a prior DRS/data access request. In this case, + created during a prior DRS access request. In this case, map_iss_sub_pair_to_user returns the user created from prior DRS/data access, rendering the other user (e.g. the eRA one) inaccessible. """ From 11439fd0974c915f2a2f5f9d8491e1905763634c Mon Sep 17 00:00:00 2001 From: John McCann Date: Wed, 1 Dec 2021 08:25:33 -0800 Subject: [PATCH 117/211] chore(dependencies): use nonlocal gen3authz ^1.4.0 --- Dockerfile | 2 -- gen3authz-1.4.0.tar.gz | Bin 13826 -> 0 bytes poetry.lock | 9 +++------ pyproject.toml | 3 +-- 4 files changed, 4 insertions(+), 10 deletions(-) delete mode 100644 gen3authz-1.4.0.tar.gz diff --git a/Dockerfile b/Dockerfile index af64b385c..cb4c27733 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,8 +30,6 @@ RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2 WORKDIR /$appname -# TODO TAKE OUT: ONLY FOR DEVELOPMENT AND TESTING PURPOSES -COPY gen3authz-1.4.0.tar.gz /$appname/ # copy ONLY poetry artifact and install # this will make sure than the dependencies is cached COPY poetry.lock pyproject.toml /$appname/ diff --git a/gen3authz-1.4.0.tar.gz b/gen3authz-1.4.0.tar.gz deleted file mode 100644 index 7edbabf13701a9cb4090af40e436c8bcc73d7566..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13826 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 diff --git a/poetry.lock b/poetry.lock index 5c9b1c7b7..0408d5d30 100644 --- a/poetry.lock +++ b/poetry.lock @@ -526,10 +526,6 @@ contextvars = {version = ">=2.4,<3.0", markers = "python_version < \"3.7\""} httpx = ">=0.20.0,<1.0.0" six = ">=1.16.0,<2.0.0" -[package.source] -type = "file" -url = "gen3authz-1.4.0.tar.gz" - [[package]] name = "gen3cirrus" version = "2.0.0" @@ -1508,7 +1504,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "f885d062949ca66eb4110fc3491d8b7b5bcb4202fd839530ec143af232776034" +content-hash = "056f7db41eb2405bb0583ee0e5f813366c5e1d76cd72f033af31745f4b6ca5aa" [metadata.files] addict = [ @@ -1798,7 +1794,8 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] gen3authz = [ - {file = "gen3authz-1.4.0.tar.gz", hash = "sha256:9b5a3a881374cab91a71cd54bf9cefbedc17f3c6d4e25d1c7e1e862c237f8888"}, + {file = "gen3authz-1.4.0-py3-none-any.whl", hash = "sha256:82471a104376fc74c05fb735b1129941f27eaf9e01d57de4a9a87031a78d14ca"}, + {file = "gen3authz-1.4.0.tar.gz", hash = "sha256:9cc679f16253b04a9c08eaa840a3db2ed5824b32f3e75cc311f350572e111697"}, ] gen3cirrus = [ {file = "gen3cirrus-2.0.0.tar.gz", hash = "sha256:0bd590c407c42dad5f0b896da0fa30bd01ea6bef5ff7dd11324ec59f14a71793"}, diff --git a/pyproject.toml b/pyproject.toml index 3da06d65f..683c2ed22 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,8 +27,7 @@ flask-cors = "^3.0.3" flask-restful = "^0.3.6" flask_sqlalchemy_session = "^1.1" email_validator = "^1.1.1" -# TODO USE ^1.4.0 WHEN RELEASED. LOCAL COPY ONLY FOR DEVELOPMENT AND TESTING PURPOSES -gen3authz = {path = "./gen3authz-1.4.0.tar.gz"} +gen3authz = "^1.4.0" gen3cirrus = "^2.0.0" gen3config = "^0.1.7" gen3users = "^0.6.0" From 5a1be31b6207853405e417d61c7fdca206d7a33e Mon Sep 17 00:00:00 2001 From: John McCann Date: Wed, 1 Dec 2021 17:58:00 -0800 Subject: [PATCH 118/211] chore(GA4GH DRS): prefix policies --- fence/resources/ga4gh/passports.py | 8 ++++++-- fence/sync/sync_users.py | 19 +++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index bba9bd532..4c8186695 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -327,7 +327,7 @@ def sync_visa_authorization(gen3_user, ga4gh_visas, expiration): None """ arborist_client = ArboristClient( - arborist_base_url=config["ARBORIST"], logger=logger, authz_provider="GA4GH" + arborist_base_url=config["ARBORIST"], logger=logger, authz_provider="GA4GH.DRS" ) dbgap_config = os.environ.get("dbGaP") or config["dbGaP"] @@ -346,7 +346,11 @@ def sync_visa_authorization(gen3_user, ga4gh_visas, expiration): with flask.current_app.db.session as db_session: syncer.sync_single_user_visas( - gen3_user, ga4gh_visas, db_session, expires=expiration + gen3_user, + ga4gh_visas, + db_session, + expires=expiration, + policy_prefix="GA4GH.DRS", ) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 2c6016723..bc4e00286 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -39,9 +39,12 @@ from fence.sync.passport_sync.ras_sync import RASVisa -def _format_policy_id(path, privilege): +def _format_policy_id(path, privilege, prefix=None): resource = ".".join(name for name in path.split("/") if name) - return "{}-{}".format(resource, privilege) + parts = [resource, privilege] + if prefix: + parts.insert(0, prefix) + return "-".join(parts) def download_dir(sftp, remote_dir, local_dir): @@ -1601,6 +1604,7 @@ def _update_authz_in_arborist( user_yaml=None, single_user_sync=False, expires=None, + policy_prefix=None, ): """ Assign users policies in arborist from the information in @@ -1615,6 +1619,7 @@ def _update_authz_in_arborist( user_yaml (UserYAML) optional, if there are policies for users in a user.yaml single_user_sync (bool) whether authz update is for a single user expires (int) time at which authz info in Arborist should expire + policy_prefix (str) prefix to prepend policy names with Return: bool: success @@ -1715,7 +1720,9 @@ def _update_authz_in_arborist( # format project '/x/y/z' -> 'x.y.z' # so the policy id will be something like 'x.y.z-create' - policy_id = _format_policy_id(path, permission) + policy_id = _format_policy_id( + path, permission, prefix=policy_prefix + ) if policy_id not in self._created_policies: try: @@ -2129,7 +2136,9 @@ def sync_visas(self): self._sync_visas(s) # if returns with some failure use telemetry file - def sync_single_user_visas(self, user, ga4gh_visas, sess=None, expires=None): + def sync_single_user_visas( + self, user, ga4gh_visas, sess=None, expires=None, policy_prefix=None + ): """ Sync a single user's visas during login or DRS/data access @@ -2141,6 +2150,7 @@ def sync_single_user_visas(self, user, ga4gh_visas, sess=None, expires=None): sess (sqlalchemy.orm.session.Session): database session expires (int): time at which synced Arborist policies and inclusion in any GBAG are set to expire + policy_prefix (str): prefix to prepend policy names with Return: None @@ -2221,6 +2231,7 @@ def sync_single_user_visas(self, user, ga4gh_visas, sess=None, expires=None): user_yaml=user_yaml, single_user_sync=True, expires=expires, + policy_prefix=policy_prefix, ) if success: self.logger.info( From 9ce5d25c29cab010b160723e245aa1aaeeb9deab Mon Sep 17 00:00:00 2001 From: John McCann Date: Thu, 2 Dec 2021 11:56:37 -0800 Subject: [PATCH 119/211] fix(usersync): recreate existing resources --- fence/sync/sync_users.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index bc4e00286..f3253d8e5 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -1493,8 +1493,21 @@ def _update_arborist(self, session, user_yaml): self.logger.debug("user_yaml resources: {}".format(resources)) self.logger.debug("dbgap resource paths: {}".format(dbgap_resource_paths)) + # existing resources are also added to prevent resources created from + # DRS endpoint from being overwritten + try: + existing_resources = self.arborist_client.list_resources() + except ArboristError as e: + self.logger.error("could not list Arborist resources: {}".format(e)) + # intentionally fail; avoid overwriting of resources + raise + + existing_paths = [r["path"] for r in existing_resources.get("resources", [])] + combined_resources = utils.combine_provided_and_dbgap_resources( + resources, existing_paths + ) combined_resources = utils.combine_provided_and_dbgap_resources( - resources, dbgap_resource_paths + combined_resources, dbgap_resource_paths ) for resource in combined_resources: From e1fb6429e9dbd7ed03ee81fe21886ae6ff5f84c7 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Fri, 3 Dec 2021 15:40:42 -0600 Subject: [PATCH 120/211] fix(passports): refactoring/cleanup, improve session handling, start to fix up unit tests --- .secrets.baseline | 11 +- fence/blueprints/data/indexd.py | 63 ++++--- fence/blueprints/login/ras.py | 23 +-- fence/config-default.yaml | 3 +- fence/resources/ga4gh/passports.py | 112 +++++++------ fence/resources/openid/ras_oauth2.py | 6 +- fence/sync/passport_sync/ras_sync.py | 4 +- fence/sync/sync_users.py | 8 +- poetry.lock | 27 +++ pyproject.toml | 2 +- tests/admin/test_admin_projects.py | 7 + tests/data/test_indexed_file.py | 6 +- tests/dbgap_sync/conftest.py | 6 +- tests/ga4gh/test_ga4gh.py | 24 ++- tests/ras/test_ras.py | 242 ++++++++++++++++++++------- tests/test-fence-config.yaml | 34 +++- 16 files changed, 399 insertions(+), 179 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 481543680..8afc279c5 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -250,7 +250,7 @@ "filename": "tests/ras/test_ras.py", "hashed_secret": "d9db6fe5c14dc55edd34115cdf3958845ac30882", "is_verified": false, - "line_number": 105 + "line_number": 134 } ], "tests/test-fence-config.yaml": [ @@ -260,15 +260,8 @@ "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_verified": false, "line_number": 31 - }, - { - "type": "Secret Keyword", - "filename": "tests/test-fence-config.yaml", - "hashed_secret": "1627df13b5cd8b3521d02bd8eb2ca31334b3aef2", - "is_verified": false, - "line_number": 471 } ] }, - "generated_at": "2021-12-01T02:28:44Z" + "generated_at": "2021-12-03T21:40:38Z" } diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index 1bb709e39..ea7bd79c0 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -85,12 +85,13 @@ def get_signed_url_for_file( "is not supported by this instance of Gen3." ) - user_ids_from_passports = None + user_ids_from_passports = [] if ga4gh_passports: - # TODO change this to usernames - user_ids_from_passports = sync_gen3_users_authz_from_ga4gh_passports( - ga4gh_passports + users_from_passports = sync_gen3_users_authz_from_ga4gh_passports( + ga4gh_passports, db_session=db_session ) + user_ids_from_passports = [user.id for user in users_from_passports] + logger.debug(f"user_ids_from_passports: {user_ids_from_passports}") # add the user details to `flask.g.audit_data` first, so they are # included in the audit log if `IndexedFile(file_id)` raises a 404 @@ -421,19 +422,20 @@ def get_signed_url( "upload": "write-storage", "download": "read-storage", } - authorized_user_id = self.check_authz( + is_authorized, authorized_user_id = self.get_authorized_and_user_id( action_to_permission[action], user_ids_from_passports=user_ids_from_passports, ) - if not authorized_user_id: - raise Unauthorized( - f"Either you weren't logged in or you don't have " + if not is_authorized: + msg = ( + f"Either you weren't authenticated successfully or you don't have " f"{action_to_permission[action]} permission " - f"on authz resource: {self.index_document['authz']}" + f"on authorization resource: {self.index_document['authz']}." ) - authorized_user_id = ( - authorized_user_id if isinstance(authorized_user_id, str) else None - ) + logger.debug( + f"denied. authorized_user_id: {authorized_user_id}\nmsg:\n{msg}" + ) + raise Unauthorized(msg) else: if self.public_acl and action == "upload": raise Unauthorized( @@ -519,12 +521,28 @@ def set_acls(self): else: raise Unauthorized("This file is not accessible") - def check_authz(self, action, user_ids_from_passports=None): + def get_authorized_and_user_id(self, action, user_ids_from_passports=None): + """ + Return a tuple of (boolean, str) which represents whether they're authorized + and their user_id. user_id is only returned if `user_ids_from_passports` + is provided and one of the ids from the passports is authorized. + + Args: + action (str): Authorization action being performed + user_ids_from_passports (list[str], optional): List of user ids parsed + from validated passports + + Returns: + tuple of (boolean, str): which represents whether they're authorized + and their user_id. user_id is only returned if `user_ids_from_passports` + is provided and one of the ids from the passports is authorized. + """ if not self.index_document.get("authz"): raise ValueError("index record missing `authz`") logger.debug( - f"authz check can user {action} on {self.index_document['authz']} for fence?" + f"authz check can user {action} on {self.index_document['authz']} for fence? " + f"if passport provided, IDs parsed: {user_ids_from_passports}" ) # handle multiple GA4GH passports as a means of authn/z @@ -541,8 +559,8 @@ def check_authz(self, action, user_ids_from_passports=None): # if any passport provides access, user is authorized if authorized: # for google proxy groups we need to know which user_id gave access - return user_id - return authorized + return authorized, user_id + return authorized, None else: try: token = get_jwt() @@ -552,11 +570,14 @@ def check_authz(self, action, user_ids_from_passports=None): # public data, we still make the request to Arborist token = None - return flask.current_app.arborist.auth_request( - jwt=token, - service="fence", - methods=action, - resources=self.index_document["authz"], + return ( + flask.current_app.arborist.auth_request( + jwt=token, + service="fence", + methods=action, + resources=self.index_document["authz"], + ), + None, ) @cached_property diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index 991067cc8..cf820291e 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -39,16 +39,8 @@ def __init__(self): ) def post_login(self, user=None, token_result=None, id_from_idp=None): - # TODO: I'm not convinced this code should be in post_login. - # Just putting it in here for now, but might refactor later. - # This saves us a call to RAS /userinfo, but will not make sense - # when there is more than one visa issuer. - - # Clear all of user's visas, to avoid having duplicate visas - # where only iss/exp/jti differ - # TODO: This is not IdP-specific and will need a rethink when - # we have multiple IdPs - current_session.commit() + parsed_url = urlparse(flask.session.get("redirect")) + query_params = parse_qs(parsed_url.query) userinfo = flask.g.userinfo @@ -65,7 +57,8 @@ def post_login(self, user=None, token_result=None, id_from_idp=None): # do an on-the-fly usersync for this user to give them instant access after logging in through RAS # if GLOBAL_PARSE_VISAS_ON_LOGIN is true then we want to run it regardless of whether or not the client sent parse_visas on request if parse_visas: - # Close previous db sessions. Leaving it open causes a race condition where we're viewing user.project_access while trying to update it in usersync + # Close previous db sessions. Leaving it open causes a race condition where we're + # viewing user.project_access while trying to update it in usersync # not closing leads to partially updated records current_session.close() @@ -82,9 +75,11 @@ def post_login(self, user=None, token_result=None, id_from_idp=None): raise # now sync authz updates - user_ids_from_passports = fence.resources.ga4gh.passports.sync_gen3_users_authz_from_ga4gh_passports( - [passport], pkey_cache=pkey_cache + users_from_passports = fence.resources.ga4gh.passports.sync_gen3_users_authz_from_ga4gh_passports( + [passport], pkey_cache=pkey_cache, db_session=current_session ) + user_ids_from_passports = [user.id for user in users_from_passports] + logger.debug(f"user_ids_from_passports: {user_ids_from_passports}") # TODO? # put_gen3_usernames_for_passport_into_cache( @@ -104,8 +99,6 @@ def post_login(self, user=None, token_result=None, id_from_idp=None): expires = config["RAS_REFRESH_EXPIRATION"] # User definied RAS refresh token expiration time - parsed_url = urlparse(flask.session.get("redirect")) - query_params = parse_qs(parsed_url.query) if query_params.get("upstream_expires_in"): custom_refresh_expiration = query_params.get("upstream_expires_in")[0] expires = get_valid_expiration( diff --git a/fence/config-default.yaml b/fence/config-default.yaml index 3c8631e03..c132664b1 100755 --- a/fence/config-default.yaml +++ b/fence/config-default.yaml @@ -885,6 +885,7 @@ GA4GH_VISA_V1_CLAIM_REQUIRED_FIELDS: type: - "https://ras.nih.gov/visas/v1.1" value: + - "https://sts.nih.gov/passport/dbgap/v1.1" - "https://stsstg.nih.gov/passport/dbgap/v1.1" source: - "https://ncbi.nlm.nih.gov/gap" @@ -900,5 +901,5 @@ USERSYNC: # fallback to dbgap sftp when there are no valid visas for a user i.e. if they're expired or if they're malformed fallback_to_dbgap_sftp: false visa_types: - ras: [https://ras.nih.gov/visas/v1, https://ras.nih.gov/visas/v1.1] + ras: ["https://ras.nih.gov/visas/v1", "https://ras.nih.gov/visas/v1.1"] RAS_USERINFO_ENDPOINT: '/openid/connect/v1.1/userinfo' diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 0b7bf7bfa..6b7be964e 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -12,6 +12,7 @@ from authutils.token.core import get_iss, get_kid from cdislogging import get_logger from gen3authz.client.arborist.client import ArboristClient +from flask_sqlalchemy_session import current_session from fence.jwt.validate import validate_jwt from fence.config import config @@ -27,7 +28,9 @@ logger = get_logger(__name__) -def sync_gen3_users_authz_from_ga4gh_passports(passports, pkey_cache=None): +def sync_gen3_users_authz_from_ga4gh_passports( + passports, pkey_cache=None, db_session=None +): """ Validate passports and embedded visas, using each valid visa's identity established by combination to possibly create and definitely @@ -44,14 +47,15 @@ def sync_gen3_users_authz_from_ga4gh_passports(passports, pkey_cache=None): list: a list of users, each corresponding to a valid visa identity embedded within the passports passed in """ + db_session = db_session or current_session logger.info("Getting gen3 users from passports") - usernames_from_all_passports = [] + users_from_all_passports = [] for passport in passports: try: # TODO check cache cached_usernames = get_gen3_usernames_for_passport_from_cache(passport) if cached_usernames: - usernames_from_all_passports.extend(cached_usernames) + users_from_all_passports.extend(cached_usernames) # existence in the cache means that this passport was validated # previously continue @@ -74,7 +78,7 @@ def sync_gen3_users_authz_from_ga4gh_passports(passports, pkey_cache=None): min_visa_expiration = int(time.time()) + datetime.timedelta(hours=1).seconds for raw_visa in raw_visas: try: - validated_decoded_visa = validate_visa(raw_visa) + validated_decoded_visa = validate_visa(raw_visa, pkey_cache=pkey_cache) identity_to_visas[ ( validated_decoded_visa.get("iss"), @@ -100,9 +104,11 @@ def sync_gen3_users_authz_from_ga4gh_passports(passports, pkey_cache=None): ) continue - usernames_from_current_passport = [] + users_from_current_passport = [] for (issuer, subject_id), visas in identity_to_visas.items(): - gen3_user = get_or_create_gen3_user_from_iss_sub(issuer, subject_id) + gen3_user = get_or_create_gen3_user_from_iss_sub( + issuer, subject_id, db_session=db_session + ) ga4gh_visas = [ GA4GHVisaV1( @@ -117,16 +123,17 @@ def sync_gen3_users_authz_from_ga4gh_passports(passports, pkey_cache=None): ] # NOTE: does not validate, assumes validation occurs above. sync_validated_visa_authorization( - gen3_user, ga4gh_visas, min_visa_expiration + gen3_user, ga4gh_visas, min_visa_expiration, db_session=db_session ) - usernames_from_current_passport.append(gen3_user) + users_from_current_passport.append(gen3_user) put_gen3_usernames_for_passport_into_cache( - passport, usernames_from_current_passport + passport, users_from_current_passport ) - usernames_from_all_passports.extend(usernames_from_current_passport) + users_from_all_passports.extend(users_from_current_passport) - return list(set(usernames_from_all_passports)) + db_session.commit() + return list(set(users_from_all_passports)) def get_gen3_usernames_for_passport_from_cache(passport): @@ -182,7 +189,7 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): ) if "sub" not in decoded_passport: - raise JWTError("Visa is missing the 'sub' claim.") + raise JWTError(f"Passport is missing the 'sub' claim") except Exception as e: logger.error("Passport failed validation: {}. Discarding passport.".format(e)) # ignore malformed/invalid passports @@ -191,7 +198,7 @@ def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): return decoded_passport.get("ga4gh_passport_v1", []) -def validate_visa(raw_visa): +def validate_visa(raw_visa, pkey_cache=None): """ Validate a raw visa in accordance with: - GA4GH AAI spec (https://github.com/ga4gh/data-security/blob/master/AAI/AAIConnectProfile.md) @@ -211,6 +218,7 @@ def validate_visa(raw_visa): ) logger.info("Attempting to validate visa") + decoded_visa = validate_jwt( raw_visa, attempt_refresh=True, @@ -218,6 +226,7 @@ def validate_visa(raw_visa): require_purpose=False, issuers=config["GA4GH_VISA_ISSUER_ALLOWLIST"], options={"require_iat": True, "require_exp": True, "verify_aud": False}, + pkey_cache=pkey_cache, ) logger.info(f'Visa jti: "{decoded_visa.get("jti", "")}"') logger.info(f'Visa txn: "{decoded_visa.get("txn", "")}"') @@ -237,7 +246,7 @@ def validate_visa(raw_visa): ) if decoded_visa["ga4gh_visa_v1"][field] not in allowed_values: raise Exception( - f'"{field}" field in "ga4gh_visa_v1" is not equal to one of the allowed_values: {allowed_values}' + f'{field}={decoded_visa["ga4gh_visa_v1"][field]} field in "ga4gh_visa_v1" is not equal to one of the allowed_values: {allowed_values}' ) if "asserted" not in decoded_visa["ga4gh_visa_v1"]: @@ -268,7 +277,7 @@ def validate_visa(raw_visa): return decoded_visa -def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): +def get_or_create_gen3_user_from_iss_sub(issuer, subject_id, db_session=None): """ Get a user from the Fence database corresponding to the visa identity indicated by the combination. If a Fence user has @@ -282,38 +291,39 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): Return: userdatamodel.user.User: the Fence user corresponding to issuer and subject_id """ - with flask.current_app.db.session as db_session: - iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get( - (issuer, subject_id) - ) - if not iss_sub_pair_to_user: - username = subject_id + issuer[len("https://") :] - gen3_user = query_for_user(session=db_session, username=username) - if not gen3_user: - idp_name = flask.current_app.issuer_to_idp.get(issuer) - gen3_user = create_user(db_session, logger, username, idp_name=idp_name) - if not idp_name: - logger.info( - "The user was created without a linked identity " - "provider since it could not be determined based on " - "the issuer" - ) + db_session = db_session or current_session + iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((issuer, subject_id)) + if not iss_sub_pair_to_user: + username = subject_id + issuer[len("https://") :] + gen3_user = query_for_user(session=db_session, username=username) + if not gen3_user: + idp_name = flask.current_app.issuer_to_idp.get(issuer) + logger.debug(f"issuer_to_idp: {flask.current_app.issuer_to_idp}") + gen3_user = create_user(db_session, logger, username, idp_name=idp_name) + if not idp_name: + logger.info( + f"The user (id:{gen3_user.id}) was created without a linked identity " + f"provider since it could not be determined based on " + f"the issuer {issuer}" + ) - logger.info( - f'Mapping subject id ("{subject_id}") and issuer ' - f'("{issuer}") combination to Fence user ' - f'"{gen3_user.username}"' - ) - iss_sub_pair_to_user = IssSubPairToUser(iss=issuer, sub=subject_id) - iss_sub_pair_to_user.user = gen3_user + logger.info( + f'Mapping subject id ("{subject_id}") and issuer ' + f'("{issuer}") combination to Fence user ' + f'"{gen3_user.username}"' + ) + iss_sub_pair_to_user = IssSubPairToUser(iss=issuer, sub=subject_id) + iss_sub_pair_to_user.user = gen3_user - db_session.add(iss_sub_pair_to_user) - db_session.commit() + db_session.add(iss_sub_pair_to_user) + db_session.commit() - return iss_sub_pair_to_user.user + return iss_sub_pair_to_user.user -def sync_validated_visa_authorization(gen3_user, ga4gh_visas, expiration): +def sync_validated_visa_authorization( + gen3_user, ga4gh_visas, expiration, db_session=None +): """ Wrapper around UserSyncer.sync_single_user_visas method, which parses authorization information from the provided visas, persists it in Fence, @@ -326,25 +336,27 @@ def sync_validated_visa_authorization(gen3_user, ga4gh_visas, expiration): gen3_user (userdatamodel.user.User): the Fence user whose visas' authz info is being synced ga4gh_visas (list): a list of fence.models.GA4GHVisaV1 objects - that are parsed and synced + that are parsed expiration (int): time at which synced Arborist policies and inclusion in any GBAG are set to expire Return: None """ + db_session = db_session or current_session default_args = fence.scripting.fence_create.get_default_init_syncer_inputs() syncer = fence.scripting.fence_create.init_syncer(**default_args) - with flask.current_app.db.session as db_session: - synced_visas = syncer.sync_single_user_visas( - gen3_user, ga4gh_visas, db_session, expires=expiration - ) + synced_visas = syncer.sync_single_user_visas( + gen3_user, ga4gh_visas, db_session, expires=expiration + ) - # after syncing authorization, perist the visas that were parsed successfully - for visa in synced_visas: + # after syncing authorization, perist the visas that were parsed successfully. + for visa in ga4gh_visas: + if visa not in synced_visas: + db_session.remove(visa) + else: db_session.add(visa) - db_session.commit() def put_gen3_usernames_for_passport_into_cache(passport, usernames_from_passports): diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index a489f746a..3825e8610 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -301,11 +301,13 @@ def update_user_authorization(self, user, pkey_cache, db_session=current_session raise # now sync authz updates - user_ids_from_passports = ( + users_from_passports = ( fence.resources.ga4gh.passports.sync_gen3_users_authz_from_ga4gh_passports( - [passport], pkey_cache=pkey_cache + [passport], pkey_cache=pkey_cache, db_session=db_session ) ) + user_ids_from_passports = [user.id for user in users_from_passports] + self.logger.debug(f"user_ids_from_passports:{user_ids_from_passports}") # TODO? # put_gen3_usernames_for_passport_into_cache( diff --git a/fence/sync/passport_sync/ras_sync.py b/fence/sync/passport_sync/ras_sync.py index f734b6fd3..08569197c 100644 --- a/fence/sync/passport_sync/ras_sync.py +++ b/fence/sync/passport_sync/ras_sync.py @@ -14,9 +14,7 @@ def __init__(self, logger): logger=logger, ) - def _parse_single_visa( - self, user, encoded_visa, expires, parse_consent_code, db_session - ): + def _parse_single_visa(self, user, encoded_visa, expires, parse_consent_code): """ Return user information from the visa. diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 422a03c98..10692845f 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -1055,7 +1055,9 @@ def _init_projects(self, user_project, sess): project = self._get_or_create(sess, Project, **data) except IntegrityError as e: sess.rollback() - self.logger.error(str(e)) + self.logger.error( + f"Project {auth_id} already exists. Detail {str(e)}" + ) raise Exception( "Project {} already exists. Detail {}. Please contact your system administrator.".format( auth_id, str(e) @@ -1941,7 +1943,6 @@ def parse_user_visas(self, db_session): encoded_visa, visa.expires, self.parse_consent_code, - db_session, ) projects = {**projects, **project} if projects: @@ -2005,7 +2006,6 @@ def sync_single_user_visas(self, user, ga4gh_visas, sess=None, expires=None): encoded_visa, visa.expires, self.parse_consent_code, - sess, ) except Exception: logging.warning( @@ -2040,7 +2040,7 @@ def sync_single_user_visas(self, user, ga4gh_visas, sess=None, expires=None): ) if user_projects: - self.logger.info("Sync to db and storage backend") + self.logger.info("Sync to db and storage backend [sync_single_user_visas]") self.sync_to_db_and_storage_backend( user_projects, user_info, sess, single_visa_sync=True, expires=expires ) diff --git a/poetry.lock b/poetry.lock index 0408d5d30..8c51060f2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1975,20 +1975,39 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mock = [ @@ -2206,18 +2225,26 @@ pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, diff --git a/pyproject.toml b/pyproject.toml index 683c2ed22..564b68d52 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ include = [ [tool.poetry.dependencies] python = "^3.6" authlib = "^0.11" -authutils = "^6.1.0" bcrypt = "^3.1.4" boto3 = "~1.9.91" botocore = "^1.12.253" @@ -50,6 +49,7 @@ werkzeug = "^1.0.0" cachelib = "^0.2.0" azure-storage-blob = "^12.6.0" Flask-WTF = "^0.14.3" +authutils = "^6.1.0" [tool.poetry.dev-dependencies] diff --git a/tests/admin/test_admin_projects.py b/tests/admin/test_admin_projects.py index 458dadba0..032ab3da6 100644 --- a/tests/admin/test_admin_projects.py +++ b/tests/admin/test_admin_projects.py @@ -1,12 +1,19 @@ import fence.resources.admin as adm from fence.models import Project, Bucket, ProjectToBucket, CloudProvider, StorageAccess +import pytest +@pytest.mark.skip( + reason="The AWG/admin/fence_as_authz support is deprecated in favor of authorization being handled fully by the policy engine" +) def test_get_project(db_session, awg_users): info = adm.get_project_info(db_session, "test_project_1") assert info["name"] == "test_project_1" +@pytest.mark.skip( + reason="The AWG/admin/fence_as_authz support is deprecated in favor of authorization being handled fully by the policy engine" +) def test_get_all_projects(db_session, awg_users): projects = adm.get_all_projects(db_session)["projects"] info = { diff --git a/tests/data/test_indexed_file.py b/tests/data/test_indexed_file.py index 531abecfc..52dea317b 100755 --- a/tests/data/test_indexed_file.py +++ b/tests/data/test_indexed_file.py @@ -402,11 +402,11 @@ def test_set_acl_missing_unauthorized( indexed_file.set_acls -def test_check_authz_missing_value_error( +def test_get_authorized_and_user_id_missing_value_error( app, supported_action, supported_protocol, indexd_client_accepting_record ): """ - Test fence.blueprints.data.indexd.IndexedFile check_authz without authz in indexd record + Test fence.blueprints.data.indexd.IndexedFile get_authorized_and_user_id without authz in indexd record """ indexd_record_with_no_authz_and_public_acl_populated = { "urls": [f"{supported_protocol}://some/location"], @@ -418,7 +418,7 @@ def test_check_authz_missing_value_error( with patch("fence.blueprints.data.indexd.flask.current_app", return_value=app): indexed_file = IndexedFile(file_id="some id") with pytest.raises(ValueError): - indexed_file.check_authz(supported_action) + indexed_file.get_authorized_and_user_id(supported_action) @pytest.mark.parametrize( diff --git a/tests/dbgap_sync/conftest.py b/tests/dbgap_sync/conftest.py index c9243910b..55166fd2d 100644 --- a/tests/dbgap_sync/conftest.py +++ b/tests/dbgap_sync/conftest.py @@ -221,8 +221,8 @@ def add_visa_manually(db_session, user, rsa_private_key, kid): "ga4gh_visa_v1": { "type": "https://ras.nih.gov/visas/v1", "asserted": int(time.time()), - "value": "https://nig/passport/dbgap", - "source": "https://ncbi/gap", + "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", + "source": "https://ncbi.nlm.nih.gov/gap", }, "ras_dbgap_permissions": [ { @@ -305,3 +305,5 @@ def add_visa_manually(db_session, user, rsa_private_key, kid): db_session.add(visa) db_session.commit() + + return encoded_visa diff --git a/tests/ga4gh/test_ga4gh.py b/tests/ga4gh/test_ga4gh.py index b84a31950..0d22b7477 100644 --- a/tests/ga4gh/test_ga4gh.py +++ b/tests/ga4gh/test_ga4gh.py @@ -11,31 +11,39 @@ logger = get_logger(__name__, log_level="debug") -def test_get_or_create_gen3_user_from_iss_sub_without_prior_login(db_session): +def test_get_or_create_gen3_user_from_iss_sub_without_prior_login( + db_session, mock_arborist_requests +): """ Test get_or_create_gen3_user_from_iss_sub when the visa's combination are not present in the mapping table beforehand (i.e. the user has not previously logged in) """ - iss = "https://sts.nih.gov" + mock_arborist_requests({"arborist/user/": {"PATCH": (None, 204)}}) + + iss = "https://stsstg.nih.gov" sub = "123_abc" - user = get_or_create_gen3_user_from_iss_sub(iss, sub) + user = get_or_create_gen3_user_from_iss_sub(iss, sub, db_session=db_session) - assert user.username == "123_abcsts.nih.gov" + assert user.username == "123_abcstsstg.nih.gov" assert user.identity_provider.name == IdentityProvider.ras iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() assert len(iss_sub_pair_to_user_records) == 1 - assert iss_sub_pair_to_user_records[0].user.username == "123_abcsts.nih.gov" + assert iss_sub_pair_to_user_records[0].user.username == "123_abcstsstg.nih.gov" -def test_get_or_create_gen3_user_from_iss_sub_after_prior_login(db_session): +def test_get_or_create_gen3_user_from_iss_sub_after_prior_login( + db_session, mock_arborist_requests +): """ Test get_or_create_gen3_user_from_iss_sub when the visa's combination are present in the mapping table beforehand (i.e. the user has previously logged in) """ - iss = "https://sts.nih.gov" + mock_arborist_requests({"arborist/user/": {"PATCH": (None, 204)}}) + + iss = "https://stsstg.nih.gov" sub = "123_abc" username = "johnsmith" email = "johnsmith@domain.tld" @@ -50,7 +58,7 @@ def test_get_or_create_gen3_user_from_iss_sub_after_prior_login(db_session): assert len(iss_sub_pair_to_user_records) == 1 assert iss_sub_pair_to_user_records[0].user.username == username - user = get_or_create_gen3_user_from_iss_sub(iss, sub) + user = get_or_create_gen3_user_from_iss_sub(iss, sub, db_session=db_session) iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() assert len(iss_sub_pair_to_user_records) == 1 diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index 3733fd65c..9c7be218e 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -18,6 +18,7 @@ IdentityProvider, IssSubPairToUser, ) +from fence.jwt.validate import validate_jwt from fence.resources.openid.ras_oauth2 import RASOauth2Client as RASClient from fence.resources.ga4gh.passports import get_or_create_gen3_user_from_iss_sub from fence.errors import InternalError @@ -28,14 +29,19 @@ logger = get_logger(__name__, log_level="debug") +TEST_USERNAME = "admin_user" +TEST_RAS_SUB = "abcd-asdj-sajpiasj12iojd-asnoin" -def add_test_user(db_session, username="admin_user", id="5678", is_admin=True): - test_user = User(username=username, id=id, is_admin=is_admin) - # id is part of primary key - check_user_exists = db_session.query(User).filter_by(id=id).first() - if not check_user_exists: - db_session.add(test_user) - db_session.commit() + +def add_test_user(db_session, username=TEST_USERNAME, is_admin=True): + # pre-populate mapping table, as login would do + test_user = get_or_create_gen3_user_from_iss_sub( + issuer="https://stsstg.nih.gov", subject_id=TEST_RAS_SUB, db_session=db_session + ) + test_user.username = username + test_user.is_admin = is_admin + db_session.add(test_user) + db_session.commit() return test_user @@ -73,7 +79,9 @@ def test_store_refresh_token(db_session): logger=logger, ) - ras_client.store_refresh_token(test_user, new_refresh_token, new_expire) + ras_client.store_refresh_token( + test_user, new_refresh_token, new_expire, db_session=db_session + ) final_query = db_session.query(UpstreamRefreshToken).first() assert final_query.refresh_token == new_refresh_token @@ -85,7 +93,9 @@ def test_store_refresh_token(db_session): @mock.patch( "fence.resources.openid.ras_oauth2.RASOauth2Client.get_value_from_discovery_doc" ) +@mock.patch("fence.resources.ga4gh.passports.validate_jwt") def test_update_visa_token( + mock_validate_jwt, mock_discovery, mock_get_token, mock_userinfo, @@ -94,10 +104,29 @@ def test_update_visa_token( rsa_private_key, rsa_public_key, kid, + mock_arborist_requests, ): """ Test to check visa table is updated when getting new visa """ + # ensure we don't actually try to reach out to external sites to refresh public keys + def validate_jwt_no_key_refresh(*args, **kwargs): + kwargs.update({"attempt_refresh": False}) + return validate_jwt(*args, **kwargs) + + mock_validate_jwt.side_effect = validate_jwt_no_key_refresh + + # ensure there is no application context or cached keys + temp_stored_public_keys = flask.current_app.jwt_public_keys + temp_app_context = flask.has_app_context + del flask.current_app.jwt_public_keys + + def return_false(): + return False + + flask.has_app_context = return_false + + mock_arborist_requests({f"arborist/user/{TEST_USERNAME}": {"PATCH": (None, 204)}}) mock_discovery.return_value = "https://ras/token_endpoint" new_token = "refresh12345abcdefg" @@ -109,16 +138,18 @@ def test_update_visa_token( mock_get_token.return_value = token_response userinfo_response = { - "sub": "abcd-asdj-sajpiasj12iojd-asnoin", + "sub": TEST_RAS_SUB, "name": "", "preferred_username": "someuser@era.com", "UID": "", - "UserID": "admin_user", + "UserID": TEST_USERNAME, "email": "", } test_user = add_test_user(db_session) - add_visa_manually(db_session, test_user, rsa_private_key, kid) + existing_encoded_visa = add_visa_manually( + db_session, test_user, rsa_private_key, kid + ) add_refresh_token(db_session, test_user) visa_query = db_session.query(GA4GHVisaV1).filter_by(user=test_user).first() @@ -134,7 +165,7 @@ def test_update_visa_token( new_visa = { "iss": "https://stsstg.nih.gov", - "sub": "abcde12345aspdij", + "sub": TEST_RAS_SUB, "iat": int(time.time()), "exp": int(time.time()) + 1000, "scope": "openid ga4gh_passport_v1 email profile", @@ -144,8 +175,8 @@ def test_update_visa_token( "ga4gh_visa_v1": { "type": "https://ras.nih.gov/visas/v1", "asserted": int(time.time()), - "value": "https://nig/passport/dbgap", - "source": "https://ncbi/gap", + "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", + "source": "https://ncbi.nlm.nih.gov/gap", }, } @@ -162,7 +193,7 @@ def test_update_visa_token( } new_passport = { "iss": "https://stsstg.nih.gov", - "sub": "abcde12345aspdij", + "sub": TEST_RAS_SUB, "iat": int(time.time()), "scope": "openid ga4gh_passport_v1 email profile", "exp": int(time.time()) + 1000, @@ -181,11 +212,24 @@ def test_update_visa_token( kid: rsa_public_key, } } - ras_client.update_user_authorization(test_user, pkey_cache=pkey_cache) + ras_client.update_user_authorization( + test_user, pkey_cache=pkey_cache, db_session=db_session + ) - query_visa = db_session.query(GA4GHVisaV1).first() - assert query_visa.ga4gh_visa - assert query_visa.ga4gh_visa == encoded_visa + # restore public keys and context + flask.current_app.jwt_public_keys = temp_stored_public_keys + flask.has_app_context = temp_app_context + + query_visas = [ + item.ga4gh_visa + for item in db_session.query(GA4GHVisaV1).filter_by(user=test_user) + ] + + # at this point we expect the existing visa to stay around (since it hasn't expired) + # and the new visa should also show up + assert len(query_visas) == 2 + assert existing_encoded_visa in query_visas + assert encoded_visa in query_visas @mock.patch("fence.resources.openid.ras_oauth2.RASOauth2Client.get_userinfo") @@ -202,10 +246,13 @@ def test_update_visa_empty_passport_returned( rsa_private_key, rsa_public_key, kid, + mock_arborist_requests, ): """ Test to handle empty passport sent from RAS """ + mock_arborist_requests({f"arborist/user/{TEST_USERNAME}": {"PATCH": (None, 204)}}) + mock_discovery.return_value = "https://ras/token_endpoint" new_token = "refresh12345abcdefg" token_response = { @@ -216,18 +263,20 @@ def test_update_visa_empty_passport_returned( mock_get_token.return_value = token_response userinfo_response = { - "sub": "abcd-asdj-sajpiasj12iojd-asnoin", + "sub": TEST_RAS_SUB, "name": "", "preferred_username": "someuser@era.com", "UID": "", - "UserID": "admin_user", + "UserID": TEST_USERNAME, "email": "", "passport_jwt_v11": "", } mock_userinfo.return_value = userinfo_response test_user = add_test_user(db_session) - add_visa_manually(db_session, test_user, rsa_private_key, kid) + existing_encoded_visa = add_visa_manually( + db_session, test_user, rsa_private_key, kid + ) add_refresh_token(db_session, test_user) visa_query = db_session.query(GA4GHVisaV1).filter_by(user=test_user).first() @@ -246,10 +295,18 @@ def test_update_visa_empty_passport_returned( kid: rsa_public_key, } } - ras_client.update_user_authorization(test_user, pkey_cache=pkey_cache) + ras_client.update_user_authorization( + test_user, pkey_cache=pkey_cache, db_session=db_session + ) - query_visa = db_session.query(GA4GHVisaV1).first() - assert query_visa == None + # at this point we expect the existing visa to stay around (since it hasn't expired) + # but no new visas + query_visas = [ + item.ga4gh_visa + for item in db_session.query(GA4GHVisaV1).filter_by(user=test_user) + ] + assert len(query_visas) == 1 + assert existing_encoded_visa in query_visas @mock.patch("fence.resources.openid.ras_oauth2.RASOauth2Client.get_userinfo") @@ -265,10 +322,12 @@ def test_update_visa_empty_visa_returned( db_session, rsa_private_key, kid, + mock_arborist_requests, ): """ Test to check if the db is emptied if the ras userinfo sends back an empty visa """ + mock_arborist_requests({f"arborist/user/{TEST_USERNAME}": {"PATCH": (None, 204)}}) mock_discovery.return_value = "https://ras/token_endpoint" new_token = "refresh12345abcdefg" @@ -280,11 +339,11 @@ def test_update_visa_empty_visa_returned( mock_get_token.return_value = token_response userinfo_response = { - "sub": "abcd-asdj-sajpiasj12iojd-asnoin", + "sub": TEST_RAS_SUB, "name": "", "preferred_username": "someuser@era.com", "UID": "", - "UserID": "admin_user", + "UserID": TEST_USERNAME, "email": "", } @@ -295,7 +354,7 @@ def test_update_visa_empty_visa_returned( } new_passport = { "iss": "https://stsstg.nih.gov", - "sub": "abcde12345aspdij", + "sub": TEST_RAS_SUB, "iat": int(time.time()), "scope": "openid ga4gh_passport_v1 email profile", "exp": int(time.time()) + 1000, @@ -309,7 +368,9 @@ def test_update_visa_empty_visa_returned( mock_userinfo.return_value = userinfo_response test_user = add_test_user(db_session) - add_visa_manually(db_session, test_user, rsa_private_key, kid) + existing_encoded_visa = add_visa_manually( + db_session, test_user, rsa_private_key, kid + ) add_refresh_token(db_session, test_user) visa_query = db_session.query(GA4GHVisaV1).filter_by(user=test_user).first() @@ -323,10 +384,18 @@ def test_update_visa_empty_visa_returned( logger=logger, ) - ras_client.update_user_authorization(test_user, pkey_cache={}) + ras_client.update_user_authorization( + test_user, pkey_cache={}, db_session=db_session + ) - query_visa = db_session.query(GA4GHVisaV1).first() - assert query_visa == None + # at this point we expect the existing visa to stay around (since it hasn't expired) + # but no new visas + query_visas = [ + item.ga4gh_visa + for item in db_session.query(GA4GHVisaV1).filter_by(user=test_user) + ] + assert len(query_visas) == 1 + assert existing_encoded_visa in query_visas @mock.patch("fence.resources.openid.ras_oauth2.RASOauth2Client.get_userinfo") @@ -334,7 +403,9 @@ def test_update_visa_empty_visa_returned( @mock.patch( "fence.resources.openid.ras_oauth2.RASOauth2Client.get_value_from_discovery_doc" ) +@mock.patch("fence.resources.ga4gh.passports.validate_jwt") def test_update_visa_token_with_invalid_visa( + mock_validate_jwt, mock_discovery, mock_get_token, mock_userinfo, @@ -343,12 +414,31 @@ def test_update_visa_token_with_invalid_visa( rsa_private_key, rsa_public_key, kid, + mock_arborist_requests, ): """ Test to check the following case: Received visa: [good1, bad2, good3] Processed/stored visa: [good1, good3] """ + # ensure we don't actually try to reach out to external sites to refresh public keys + def validate_jwt_no_key_refresh(*args, **kwargs): + kwargs.update({"attempt_refresh": False}) + return validate_jwt(*args, **kwargs) + + mock_validate_jwt.side_effect = validate_jwt_no_key_refresh + + # ensure there is no application context or cached keys + temp_stored_public_keys = flask.current_app.jwt_public_keys + temp_app_context = flask.has_app_context + del flask.current_app.jwt_public_keys + + def return_false(): + return False + + flask.has_app_context = return_false + + mock_arborist_requests({f"arborist/user/{TEST_USERNAME}": {"PATCH": (None, 204)}}) mock_discovery.return_value = "https://ras/token_endpoint" new_token = "refresh12345abcdefg" @@ -360,16 +450,18 @@ def test_update_visa_token_with_invalid_visa( mock_get_token.return_value = token_response userinfo_response = { - "sub": "abcd-asdj-sajpiasj12iojd-asnoin", + "sub": TEST_RAS_SUB, "name": "", "preferred_username": "someuser@era.com", "UID": "", - "UserID": "admin_user", + "UserID": TEST_USERNAME, "email": "", } test_user = add_test_user(db_session) - add_visa_manually(db_session, test_user, rsa_private_key, kid) + existing_encoded_visa = add_visa_manually( + db_session, test_user, rsa_private_key, kid + ) add_refresh_token(db_session, test_user) visa_query = db_session.query(GA4GHVisaV1).filter_by(user=test_user).first() @@ -385,7 +477,7 @@ def test_update_visa_token_with_invalid_visa( new_visa = { "iss": "https://stsstg.nih.gov", - "sub": "abcde12345aspdij", + "sub": TEST_RAS_SUB, "iat": int(time.time()), "exp": int(time.time()) + 1000, "scope": "openid ga4gh_passport_v1 email profile", @@ -395,8 +487,8 @@ def test_update_visa_token_with_invalid_visa( "ga4gh_visa_v1": { "type": "https://ras.nih.gov/visas/v1", "asserted": int(time.time()), - "value": "https://nig/passport/dbgap", - "source": "https://ncbi/gap", + "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", + "source": "https://ncbi.nlm.nih.gov/gap", }, } @@ -413,7 +505,7 @@ def test_update_visa_token_with_invalid_visa( } new_passport = { "iss": "https://stsstg.nih.gov", - "sub": "abcde12345aspdij", + "sub": TEST_RAS_SUB, "iat": int(time.time()), "scope": "openid ga4gh_passport_v1 email profile", "exp": int(time.time()) + 1000, @@ -432,13 +524,24 @@ def test_update_visa_token_with_invalid_visa( kid: rsa_public_key, } } - ras_client.update_user_authorization(test_user, pkey_cache=pkey_cache) - query_visas = db_session.query(GA4GHVisaV1).filter_by(user=test_user).all() - assert len(query_visas) == 2 + ras_client.update_user_authorization( + test_user, pkey_cache=pkey_cache, db_session=db_session + ) + + # restore public keys and context + flask.current_app.jwt_public_keys = temp_stored_public_keys + flask.has_app_context = temp_app_context + + # at this point we expect the existing visa to stay around (since it hasn't expired) + # and 2 new good visas + query_visas = [ + item.ga4gh_visa + for item in db_session.query(GA4GHVisaV1).filter_by(user=test_user) + ] + assert len(query_visas) == 3 for query_visa in query_visas: - assert query_visa.ga4gh_visa - assert query_visa.ga4gh_visa == encoded_visa + assert query_visa == existing_encoded_visa or query_visa == encoded_visa @mock.patch("httpx.get") @@ -455,12 +558,25 @@ def test_update_visa_fetch_pkey( db_session, rsa_private_key, kid, + mock_arborist_requests, ): """ Test that when the RAS client's pkey cache is empty, the client's update_user_authorization can fetch and serialize the visa issuer's public keys and validate a visa using the correct key. """ + # ensure there is no application context or cached keys + temp_stored_public_keys = flask.current_app.jwt_public_keys + temp_app_context = flask.has_app_context + del flask.current_app.jwt_public_keys + + def return_false(): + return False + + flask.has_app_context = return_false + + mock_arborist_requests({f"arborist/user/{TEST_USERNAME}": {"PATCH": (None, 204)}}) + mock_discovery.return_value = "https://ras/token_endpoint" mock_get_token.return_value = { "access_token": "abcdef12345", @@ -470,7 +586,7 @@ def test_update_visa_fetch_pkey( # New visa that will be returned by userinfo new_visa = { "iss": "https://stsstg.nih.gov", - "sub": "abcde12345aspdij", + "sub": TEST_RAS_SUB, "iat": int(time.time()), "exp": int(time.time()) + 1000, "scope": "openid ga4gh_passport_v1 email profile", @@ -480,8 +596,8 @@ def test_update_visa_fetch_pkey( "ga4gh_visa_v1": { "type": "https://ras.nih.gov/visas/v1", "asserted": int(time.time()), - "value": "https://nig/passport/dbgap", - "source": "https://ncbi/gap", + "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", + "source": "https://ncbi.nlm.nih.gov/gap", }, } headers = {"kid": kid} @@ -496,7 +612,7 @@ def test_update_visa_fetch_pkey( } new_passport = { "iss": "https://stsstg.nih.gov", - "sub": "abcde12345aspdij", + "sub": TEST_RAS_SUB, "iat": int(time.time()), "scope": "openid ga4gh_passport_v1 email profile", "exp": int(time.time()) + 1000, @@ -526,11 +642,17 @@ def test_update_visa_fetch_pkey( test_user = add_test_user(db_session) # Pass in an empty pkey cache so that the client will have to hit the jwks endpoint. - ras_client.update_user_authorization(test_user, pkey_cache={}) + ras_client.update_user_authorization( + test_user, pkey_cache={}, db_session=db_session + ) + + # restore public keys and context + flask.current_app.jwt_public_keys = temp_stored_public_keys + flask.has_app_context = temp_app_context # Check that the new visa passed validation, indicating a successful pkey fetch query_visa = db_session.query(GA4GHVisaV1).first() - assert query_visa.ga4gh_visa == encoded_visa + assert query_visa and query_visa.ga4gh_visa == encoded_visa @mock.patch("fence.resources.openid.ras_oauth2.RASOauth2Client.get_userinfo") @@ -546,10 +668,12 @@ def dont_test_visa_update_cronjob( rsa_private_key, rsa_public_key, kid, + mock_arborist_requests, ): """ Test to check visa table is updated when updating visas using cronjob """ + mock_arborist_requests({f"arborist/user/{TEST_USERNAME}": {"PATCH": (None, 204)}}) n_users = 20 n_users_no_visa = 15 @@ -564,11 +688,11 @@ def dont_test_visa_update_cronjob( mock_get_token.return_value = token_response userinfo_response = { - "sub": "abcd-asdj-sajpiasj12iojd-asnoin", + "sub": TEST_RAS_SUB, "name": "", "preferred_username": "someuser@era.com", "UID": "", - "UserID": "admin_user", + "UserID": TEST_USERNAME, "email": "", } @@ -587,7 +711,7 @@ def dont_test_visa_update_cronjob( new_visa = { "iss": "https://stsstg.nih.gov", - "sub": "abcde12345aspdij", + "sub": TEST_RAS_SUB, "iat": int(time.time()), "exp": int(time.time()) + 1000, "scope": "openid ga4gh_passport_v1 email profile", @@ -597,8 +721,8 @@ def dont_test_visa_update_cronjob( "ga4gh_visa_v1": { "type": "https://ras.nih.gov/visas/v1", "asserted": int(time.time()), - "value": "https://nig/passport/dbgap", - "source": "https://ncbi/gap", + "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", + "source": "https://ncbi.nlm.nih.gov/gap", }, } @@ -615,7 +739,7 @@ def dont_test_visa_update_cronjob( } new_passport = { "iss": "https://stsstg.nih.gov", - "sub": "abcde12345aspdij", + "sub": TEST_RAS_SUB, "iat": int(time.time()), "scope": "openid ga4gh_passport_v1 email profile", "exp": int(time.time()) + 1000, @@ -703,7 +827,7 @@ def test_map_iss_sub_pair_to_user_with_prior_DRS_access( logger=logger, ) - get_or_create_gen3_user_from_iss_sub(iss, sub) + get_or_create_gen3_user_from_iss_sub(iss, sub, db_session=db_session) iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() assert len(iss_sub_pair_to_user_records) == 1 iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((iss, sub)) @@ -738,7 +862,7 @@ def test_map_iss_sub_pair_to_user_with_prior_DRS_access_and_arborist_error( HTTP_PROXY=config.get("HTTP_PROXY"), logger=logger, ) - get_or_create_gen3_user_from_iss_sub(iss, sub) + get_or_create_gen3_user_from_iss_sub(iss, sub, db_session=db_session) with pytest.raises(InternalError): ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) @@ -769,7 +893,7 @@ def test_map_iss_sub_pair_to_user_with_prior_login_and_prior_DRS_access( db_session.add(user) db_session.commit() - get_or_create_gen3_user_from_iss_sub(iss, sub) + get_or_create_gen3_user_from_iss_sub(iss, sub, db_session=db_session) username_to_log_in = ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) assert username_to_log_in == "123_abcdomain.tld" iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((iss, sub)) diff --git a/tests/test-fence-config.yaml b/tests/test-fence-config.yaml index baee39f07..a478847b4 100755 --- a/tests/test-fence-config.yaml +++ b/tests/test-fence-config.yaml @@ -97,7 +97,7 @@ OPENID_CONNECT: client_id: '' client_secret: '' redirect_url: '{{BASE_URL}}/login/ras/callback' - discovery_url: 'https://sts.nih.gov/.well-known/openid-configuration' + discovery_url: 'https://stsstg.nih.gov/.well-known/openid-configuration' microsoft: client_id: '' client_secret: '' @@ -612,7 +612,39 @@ GOOGLE_MANAGED_SERVICE_ACCOUNT_DOMAINS: MAX_ROLE_SESSION_INCREASE: true ASSUME_ROLE_CACHE_SECONDS: 1800 +# ////////////////////////////////////////////////////////////////////////////////////// +# GA4GH SUPPORT: DATA ACCESS AND AUTHORIZATION SYNCING +# ////////////////////////////////////////////////////////////////////////////////////// +# whether or not to accept GA4GH Passports as a means of AuthN/Z to the DRS data access endpoint +GA4GH_PASSPORTS_TO_DRS_ENABLED: true + +# RAS refresh_tokens expire in 15 days +RAS_REFRESH_EXPIRATION: 1296000 # List of JWT issuers from which Fence will accept GA4GH visas GA4GH_VISA_ISSUER_ALLOWLIST: + - '{{BASE_URL}}' - 'https://sts.nih.gov' - 'https://stsstg.nih.gov' +GA4GH_VISA_V1_CLAIM_REQUIRED_FIELDS: + type: + - "https://ras.nih.gov/visas/v1.1" + - "https://ras.nih.gov/visas/v1" + value: + - "https://sts.nih.gov/passport/dbgap/v1.1" + - "https://stsstg.nih.gov/passport/dbgap/v1.1" + source: + - "https://ncbi.nlm.nih.gov/gap" +EXPIRED_AUTHZ_REMOVAL_JOB_FREQ_IN_SECONDS: 300 +# Global sync visas during login +# None(Default): Allow per client i.e. a fence client can pick whether or not to sync their visas during login with parse_visas param in /authorization endpoint +# True: Parse for all clients i.e. a fence client will always sync their visas during login +# False: Parse for no clients i.e. a fence client will not be able to sync visas during login even with parse_visas param +GLOBAL_PARSE_VISAS_ON_LOGIN: +# Settings for usersync with visas +USERSYNC: + sync_from_visas: true + # fallback to dbgap sftp when there are no valid visas for a user i.e. if they're expired or if they're malformed + fallback_to_dbgap_sftp: false + visa_types: + ras: ["https://ras.nih.gov/visas/v1", "https://ras.nih.gov/visas/v1.1"] +RAS_USERINFO_ENDPOINT: '/openid/connect/v1.1/userinfo' From 1134c01eff1f41dc316cffe5aca469f867fd0698 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Fri, 3 Dec 2021 16:03:12 -0600 Subject: [PATCH 121/211] fix(google): allow looking up proxy group information by user.id or user.username, improve error handling --- .secrets.baseline | 4 +-- fence/models.py | 6 ++++- fence/resources/google/utils.py | 47 ++++++++++++++++++++++++++------- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 8afc279c5..8edac82ad 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -152,7 +152,7 @@ "filename": "fence/resources/google/utils.py", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 125 + "line_number": 127 } ], "fence/utils.py": [ @@ -263,5 +263,5 @@ } ] }, - "generated_at": "2021-12-03T21:40:38Z" + "generated_at": "2021-12-03T22:03:05Z" } diff --git a/fence/models.py b/fence/models.py index b4d971742..6731ea98d 100644 --- a/fence/models.py +++ b/fence/models.py @@ -61,11 +61,15 @@ def query_for_user(session, username): return ( session.query(User) - .filter(func.lower(User.username) == username.lower()) + .filter(func.lower(User.username) == str(username).lower()) .first() ) +def query_for_user_by_id(session, user_id): + return session.query(User).filter(User.id == user_id).first() + + def create_user(session, logger, username, email=None, idp_name=None): """ Create a new user in the database. diff --git a/fence/resources/google/utils.py b/fence/resources/google/utils.py index efa2da2cd..00e4eb2f1 100644 --- a/fence/resources/google/utils.py +++ b/fence/resources/google/utils.py @@ -28,6 +28,8 @@ UserServiceAccount, ServiceAccountAccessPrivilege, ServiceAccountToGoogleBucketAccessGroup, + query_for_user, + query_for_user_by_id, ) from fence.resources.google import STORAGE_ACCESS_PROVIDER_NAME from fence.errors import NotSupported, NotFound @@ -515,23 +517,40 @@ def _update_service_account_db_entry( return service_account_db_entry -def get_or_create_proxy_group_id(expires=None, user_id=None): +def get_or_create_proxy_group_id(expires=None, user_id=None, username=None): """ If no username returned from token or database, create a new proxy group - for the give user. Also, add the access privileges. + for the given user. Also, add the access privileges. Returns: int: id of (possibly newly created) proxy group associated with user """ - proxy_group_id = _get_proxy_group_id(user_id=user_id) + proxy_group_id = _get_proxy_group_id(user_id=user_id, username=username) if not proxy_group_id: - if user_id: - user = current_session.query(User).filter_by(id=int(user_id)).first() + try: + user_by_id = query_for_user_by_id(current_session, user_id) + user_by_username = query_for_user( + session=current_session, username=username + ) + except Exception: + user_by_id = None + user_by_username = None + + if user_by_id: user_id = user_id - username = user.username - else: + username = user_by_id.username + elif user_by_username: + user_id = user_by_username.id + username = username + elif current_token: user_id = current_token["sub"] username = current_token.get("context", {}).get("user", {}).get("name", "") + else: + raise Exception( + f"could not find user given input user_id={user_id} or " + f"username={username}, nor was there a current_token" + ) + proxy_group_id = _create_proxy_group(user_id, username).id privileges = current_session.query(AccessPrivilege).filter( @@ -562,7 +581,7 @@ def get_or_create_proxy_group_id(expires=None, user_id=None): return proxy_group_id -def _get_proxy_group_id(user_id=None): +def _get_proxy_group_id(user_id=None, username=None): """ Get users proxy group id from the current token, if possible. Otherwise, check the database for it. @@ -574,8 +593,16 @@ def _get_proxy_group_id(user_id=None): if not proxy_group_id: user_id = user_id or current_token["sub"] - user = current_session.query(User).filter(User.id == user_id).first() - proxy_group_id = user.google_proxy_group_id + + try: + user = query_for_user_by_id(current_session, user_id) + if not user: + user = query_for_user(current_session, username) + except Exception: + user = None + + if user: + proxy_group_id = user.google_proxy_group_id return proxy_group_id From 8814c785a75edb10dd028a48d28368547e4fc24a Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Fri, 3 Dec 2021 16:09:55 -0600 Subject: [PATCH 122/211] fix(google): allow looking up proxy group information by user.id or user.username, improve error handling --- fence/blueprints/data/indexd.py | 58 ++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index ea7bd79c0..17d47afca 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -2,11 +2,9 @@ import time import json from urllib.parse import urlparse, ParseResult, urlunparse - from datetime import datetime, timedelta from sqlalchemy.sql.functions import user - from cached_property import cached_property import cirrus from cirrus import GoogleCloudManager @@ -14,6 +12,7 @@ from cdispyutils.config import get_value from cdispyutils.hmac4 import generate_aws_presigned_url import flask +from flask_sqlalchemy_session import current_session import requests from azure.storage.blob import ( BlobServiceClient, @@ -49,7 +48,7 @@ from fence.resources.ga4gh.passports import sync_gen3_users_authz_from_ga4gh_passports from fence.utils import get_valid_expiration_from_request from . import multipart_upload -from ...models import AssumeRoleCacheAWS, query_for_user +from ...models import AssumeRoleCacheAWS, query_for_user, query_for_user_by_id from ...models import AssumeRoleCacheGCP logger = get_logger(__name__) @@ -62,15 +61,21 @@ SUPPORTED_PROTOCOLS = ["s3", "http", "ftp", "https", "gs", "az"] SUPPORTED_ACTIONS = ["upload", "download"] -ANONYMOUS_USER_ID = "anonymous" +ANONYMOUS_USER_ID = "-1" ANONYMOUS_USERNAME = "anonymous" def get_signed_url_for_file( - action, file_id, file_name=None, requested_protocol=None, ga4gh_passports=None + action, + file_id, + file_name=None, + requested_protocol=None, + ga4gh_passports=None, + db_session=None, ): requested_protocol = requested_protocol or flask.request.args.get("protocol", None) r_pays_project = flask.request.args.get("userProject", None) + db_session = db_session or current_session # default to signing the url even if it's a public object # this will work so long as we're provided a user token @@ -1174,8 +1179,9 @@ def _generate_google_storage_signed_url( username, r_pays_project=None, ): - - proxy_group_id = get_or_create_proxy_group_id(user_id=user_id) + proxy_group_id = get_or_create_proxy_group_id( + user_id=user_id, username=username + ) expiration_time = int(time.time()) + expires_in is_cached = False @@ -1483,35 +1489,43 @@ def delete(self, container, blob): # pylint: disable=R0201 return ("Failed to delete data file.", status_code) -def _get_user_info(sub_type=str, user=None): +def _get_user_info(sub_type=str, user_id=None): """ - Attempt to parse the request for token to authenticate the user. fallback to + Attempt to parse the request to get information about user. fallback to populated information about an anonymous user. By default, cast `sub` to str. Use `sub_type` to override this behavior. + + WARNING: This does NOT actually check authorization information and always falls + back on anonymous user information. DO NOT USE THIS AS A MEANS TO AUTHORIZE, + IT WILL ALWAYS GIVE YOU BACK ANONYMOUS USER INFO. Only use this + after you've authorized the access to the data via other means. """ try: - if user: + if user_id: if hasattr(flask.current_app, "db"): with flask.current_app.db.session as session: - result = query_for_user(session, user) - username = result.username - user_id = result.id + result = query_for_user_by_id(session, user_id) + final_username = result.username + final_user_id = result.id else: set_current_token( validate_request(scope={"user"}, audience=config.get("BASE_URL")) ) - user_id = current_token["sub"] + final_user_id = current_token["sub"] if sub_type: - user_id = sub_type(user_id) - username = current_token["context"]["user"]["name"] - except JWTError: + final_user_id = sub_type(final_user_id) + final_username = current_token["context"]["user"]["name"] + except Exception as exc: + logger.info( + "could not determine user info from request. setting anonymous user information." + ) # this is fine b/c it might be public data, sign with anonymous username/id - user_id = None + final_user_id = None if sub_type == str: - user_id = ANONYMOUS_USER_ID - username = ANONYMOUS_USERNAME + final_user_id = sub_type(ANONYMOUS_USER_ID) + final_username = ANONYMOUS_USERNAME - return {"user_id": user_id, "username": username} + return {"user_id": final_user_id, "username": final_username} def _is_anonymous_user(user_info): @@ -1519,7 +1533,7 @@ def _is_anonymous_user(user_info): Check if there's a current user authenticated or if request is anonymous """ user_info = user_info or _get_user_info() - return user_info.get("user_id") == ANONYMOUS_USER_ID + return str(user_info.get("user_id")) == ANONYMOUS_USER_ID def filter_auth_ids(action, list_auth_ids): From b85dc76d4f8108c1f2cc53d816e3b2421d554a36 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Fri, 3 Dec 2021 16:11:15 -0600 Subject: [PATCH 123/211] fix(indexd): pass correct param --- fence/blueprints/data/indexd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index 17d47afca..294a7e6b4 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -1113,7 +1113,7 @@ def get_signed_url( ): resource_path = self.get_resource_path() - user_info = _get_user_info(user=user_id) + user_info = _get_user_info(user_id=user_id) if public_data and not force_signed_url: url = "https://storage.cloud.google.com/" + resource_path From 13226d8fb568712ae8fb613bdaee4eae485f15ad Mon Sep 17 00:00:00 2001 From: John McCann Date: Sun, 5 Dec 2021 16:21:35 -0800 Subject: [PATCH 124/211] chore(uWSGI): increase http and socket timeouts --- deployment/uwsgi/uwsgi.ini | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/deployment/uwsgi/uwsgi.ini b/deployment/uwsgi/uwsgi.ini index a2f7741c4..ee17c2c8d 100644 --- a/deployment/uwsgi/uwsgi.ini +++ b/deployment/uwsgi/uwsgi.ini @@ -11,8 +11,10 @@ harakiri-verbose = true # No global HARAKIRI, using only user HARAKIRI, because export overwrites it # Cannot overwrite global HARAKIRI with user's: https://git.io/fjYuD # harakiri = 45 -http-timeout = 45 -socket-timeout = 45 +# TODO reduce http and socket timeouts after performance improvements +# to DRS endpoint +http-timeout = 300 +socket-timeout = 300 worker-reload-mercy = 45 reload-mercy = 45 mule-reload-mercy = 45 From 0fd9c97cae6e8c4ab3422c0a24ad04750ddbd957 Mon Sep 17 00:00:00 2001 From: John McCann Date: Mon, 6 Dec 2021 07:57:31 -0800 Subject: [PATCH 125/211] chore(uWSGI): change timeouts to 60s --- deployment/uwsgi/uwsgi.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment/uwsgi/uwsgi.ini b/deployment/uwsgi/uwsgi.ini index ee17c2c8d..63334161a 100644 --- a/deployment/uwsgi/uwsgi.ini +++ b/deployment/uwsgi/uwsgi.ini @@ -13,8 +13,8 @@ harakiri-verbose = true # harakiri = 45 # TODO reduce http and socket timeouts after performance improvements # to DRS endpoint -http-timeout = 300 -socket-timeout = 300 +http-timeout = 60 +socket-timeout = 60 worker-reload-mercy = 45 reload-mercy = 45 mule-reload-mercy = 45 From 4940ff8f6fde81f29b88c78859688900f63e025a Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Mon, 6 Dec 2021 14:54:07 -0600 Subject: [PATCH 126/211] fix(tests): clear out user-related tables between tests --- .secrets.baseline | 6 +++--- tests/conftest.py | 8 +++++++- tests/ras/test_ras.py | 8 ++++++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 8edac82ad..2b70b5e7c 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -207,14 +207,14 @@ "filename": "tests/conftest.py", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 1174 + "line_number": 1363 }, { "type": "Base64 High Entropy String", "filename": "tests/conftest.py", "hashed_secret": "227dea087477346785aefd575f91dd13ab86c108", "is_verified": false, - "line_number": 1197 + "line_number": 1386 } ], "tests/credentials/google/test_credentials.py": [ @@ -263,5 +263,5 @@ } ] }, - "generated_at": "2021-12-03T22:03:05Z" + "generated_at": "2021-12-06T20:53:55Z" } diff --git a/tests/conftest.py b/tests/conftest.py index 5cf2e6369..68f896337 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -157,7 +157,7 @@ class FakeAzureCredential: """ def __init__(self): - self.account_key = "FakefakeAccountKey" + self.account_key = "FakefakeAccountKey" # pragma: allowlist secret class FakeBlobServiceClient: @@ -455,6 +455,12 @@ def db_session(db, patch_app_db_session): yield session + # clear out user and project tables upon function close in case unit test didn't + session.query(models.User).delete() + session.query(models.IssSubPairToUser).delete() + session.query(models.Project).delete() + session.commit() + session.close() transaction.rollback() connection.close() diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index 9c7be218e..fe395a9ba 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -651,8 +651,12 @@ def return_false(): flask.has_app_context = temp_app_context # Check that the new visa passed validation, indicating a successful pkey fetch - query_visa = db_session.query(GA4GHVisaV1).first() - assert query_visa and query_visa.ga4gh_visa == encoded_visa + query_visas = [ + item.ga4gh_visa + for item in db_session.query(GA4GHVisaV1).filter_by(user=test_user) + ] + for visa in query_visas: + assert visa == encoded_visa @mock.patch("fence.resources.openid.ras_oauth2.RASOauth2Client.get_userinfo") From e89221b25901a3d2851a7d057e3bb70a23a79a62 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Mon, 6 Dec 2021 16:29:14 -0600 Subject: [PATCH 127/211] feat(ga4gh): add fence-create job for cleaning up expired visas --- .secrets.baseline | 4 +- fence/scripting/fence_create.py | 43 ++++++++++++++++ tests/dbgap_sync/conftest.py | 25 +++++----- tests/ras/test_ras.py | 74 ++++++++++++++-------------- tests/scripting/test_fence-create.py | 67 ++++++++++++++++++++++++- tests/utils/__init__.py | 17 ++++++- 6 files changed, 176 insertions(+), 54 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 2b70b5e7c..60385ca7f 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -250,7 +250,7 @@ "filename": "tests/ras/test_ras.py", "hashed_secret": "d9db6fe5c14dc55edd34115cdf3958845ac30882", "is_verified": false, - "line_number": 134 + "line_number": 122 } ], "tests/test-fence-config.yaml": [ @@ -263,5 +263,5 @@ } ] }, - "generated_at": "2021-12-06T20:53:55Z" + "generated_at": "2021-12-06T22:29:08Z" } diff --git a/fence/scripting/fence_create.py b/fence/scripting/fence_create.py index 020866f80..2db2604d0 100644 --- a/fence/scripting/fence_create.py +++ b/fence/scripting/fence_create.py @@ -48,6 +48,7 @@ ServiceAccountToGoogleBucketAccessGroup, query_for_user, migrate, + GA4GHVisaV1, ) from fence.scripting.google_monitor import email_users_without_access, validation_check from fence.config import config @@ -696,6 +697,48 @@ def delete_users(DB, usernames): session.commit() +def cleanup_expired_ga4gh_information(DB): + """ + Remove any expired passports/visas from the database if they're expired. + + IMPORTANT NOTE: This DOES NOT actually remove authorization, it assumes that the + same expiration was set and honored in the authorization system. + """ + driver = SQLAlchemyDriver(DB) + with driver.session as session: + current_time = int(time.time()) + + # Get expires field from db, if None default to NOT expired + records_to_delete = ( + session.query(GA4GHVisaV1) + .filter( + and_( + GA4GHVisaV1.expires.isnot(None), + GA4GHVisaV1.expires < current_time, + ) + ) + .all() + ) + num_deleted_records = 0 + if records_to_delete: + for record in records_to_delete: + try: + session.delete(record) + session.commit() + + num_deleted_records += 1 + except Exception as e: + logger.error( + "ERROR: Could not remove GA4GHVisaV1 with id={}. Detail {}".format( + record.id, e + ) + ) + + logger.info( + f"Removed {num_deleted_records} expired GA4GHVisaV1 records from db." + ) + + def delete_expired_google_access(DB): """ Delete all expired Google data access (e.g. remove proxy groups from Google Bucket diff --git a/tests/dbgap_sync/conftest.py b/tests/dbgap_sync/conftest.py index 55166fd2d..f0d4861c6 100644 --- a/tests/dbgap_sync/conftest.py +++ b/tests/dbgap_sync/conftest.py @@ -205,18 +205,19 @@ def mocked_get(path, **kwargs): return syncer_obj -def add_visa_manually(db_session, user, rsa_private_key, kid): - +def add_visa_manually(db_session, user, rsa_private_key, kid, expires=None): + expires = expires or int(time.time()) + 1000 headers = {"kid": kid} decoded_visa = { "iss": "https://stsstg.nih.gov", "sub": "abcde12345aspdij", "iat": int(time.time()), - "exp": int(time.time()) + 1000, + "exp": expires, "scope": "openid ga4gh_passport_v1 email profile", - "jti": "jtiajoidasndokmasdl", - "txn": "sapidjspa.asipidja", + "jti": "jtiajoidasndokmasdl" + + str(expires), # expires to make unique from others + "txn": "sapidjspa.asipidja" + str(expires), "name": "", "ga4gh_visa_v1": { "type": "https://ras.nih.gov/visas/v1", @@ -232,7 +233,7 @@ def add_visa_manually(db_session, user, rsa_private_key, kid): "participant_set": "p1", "consent_group": "c1", "role": "designated user", - "expiration": int(time.time()) + 1001, + "expiration": expires, }, { "consent_name": "General Research Use (IRB, PUB)", @@ -241,7 +242,7 @@ def add_visa_manually(db_session, user, rsa_private_key, kid): "participant_set": "p1", "consent_group": "c1", "role": "designated user", - "expiration": int(time.time()) + 1001, + "expiration": expires, }, { "consent_name": "Disease-Specific (Cardiovascular Disease)", @@ -250,7 +251,7 @@ def add_visa_manually(db_session, user, rsa_private_key, kid): "participant_set": "p1", "consent_group": "c1", "role": "designated user", - "expiration": int(time.time()) + 1001, + "expiration": expires, }, { "consent_name": "Health/Medical/Biomedical (IRB)", @@ -259,7 +260,7 @@ def add_visa_manually(db_session, user, rsa_private_key, kid): "participant_set": "p2", "consent_group": "c3", "role": "designated user", - "expiration": int(time.time()) + 1001, + "expiration": expires, }, { "consent_name": "Disease-Specific (Focused Disease Only, IRB, NPU)", @@ -268,7 +269,7 @@ def add_visa_manually(db_session, user, rsa_private_key, kid): "participant_set": "p2", "consent_group": "c2", "role": "designated user", - "expiration": int(time.time()) + 1001, + "expiration": expires, }, { "consent_name": "Disease-Specific (Autism Spectrum Disorder)", @@ -277,7 +278,7 @@ def add_visa_manually(db_session, user, rsa_private_key, kid): "participant_set": "p3", "consent_group": "c1", "role": "designated user", - "expiration": int(time.time()) + 1001, + "expiration": expires, }, ], } @@ -306,4 +307,4 @@ def add_visa_manually(db_session, user, rsa_private_key, kid): db_session.add(visa) db_session.commit() - return encoded_visa + return encoded_visa, visa diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index fe395a9ba..a13a189b2 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -23,27 +23,13 @@ from fence.resources.ga4gh.passports import get_or_create_gen3_user_from_iss_sub from fence.errors import InternalError +from tests.utils import add_test_ras_user, TEST_RAS_USERNAME, TEST_RAS_SUB from tests.dbgap_sync.conftest import add_visa_manually from fence.job.visa_update_cronjob import Visa_Token_Update import tests.utils logger = get_logger(__name__, log_level="debug") -TEST_USERNAME = "admin_user" -TEST_RAS_SUB = "abcd-asdj-sajpiasj12iojd-asnoin" - - -def add_test_user(db_session, username=TEST_USERNAME, is_admin=True): - # pre-populate mapping table, as login would do - test_user = get_or_create_gen3_user_from_iss_sub( - issuer="https://stsstg.nih.gov", subject_id=TEST_RAS_SUB, db_session=db_session - ) - test_user.username = username - test_user.is_admin = is_admin - db_session.add(test_user) - db_session.commit() - return test_user - def add_refresh_token(db_session, user): refresh_token = "abcde1234567kposjdas" @@ -64,7 +50,7 @@ def test_store_refresh_token(db_session): Test to check if store_refresh_token replaces the existing token with a new one in the db """ - test_user = add_test_user(db_session) + test_user = add_test_ras_user(db_session) add_refresh_token(db_session, test_user) initial_query = db_session.query(UpstreamRefreshToken).first() assert initial_query.refresh_token @@ -126,7 +112,9 @@ def return_false(): flask.has_app_context = return_false - mock_arborist_requests({f"arborist/user/{TEST_USERNAME}": {"PATCH": (None, 204)}}) + mock_arborist_requests( + {f"arborist/user/{TEST_RAS_USERNAME}": {"PATCH": (None, 204)}} + ) mock_discovery.return_value = "https://ras/token_endpoint" new_token = "refresh12345abcdefg" @@ -142,12 +130,12 @@ def return_false(): "name": "", "preferred_username": "someuser@era.com", "UID": "", - "UserID": TEST_USERNAME, + "UserID": TEST_RAS_USERNAME, "email": "", } - test_user = add_test_user(db_session) - existing_encoded_visa = add_visa_manually( + test_user = add_test_ras_user(db_session) + existing_encoded_visa, _ = add_visa_manually( db_session, test_user, rsa_private_key, kid ) add_refresh_token(db_session, test_user) @@ -251,7 +239,9 @@ def test_update_visa_empty_passport_returned( """ Test to handle empty passport sent from RAS """ - mock_arborist_requests({f"arborist/user/{TEST_USERNAME}": {"PATCH": (None, 204)}}) + mock_arborist_requests( + {f"arborist/user/{TEST_RAS_USERNAME}": {"PATCH": (None, 204)}} + ) mock_discovery.return_value = "https://ras/token_endpoint" new_token = "refresh12345abcdefg" @@ -267,14 +257,14 @@ def test_update_visa_empty_passport_returned( "name": "", "preferred_username": "someuser@era.com", "UID": "", - "UserID": TEST_USERNAME, + "UserID": TEST_RAS_USERNAME, "email": "", "passport_jwt_v11": "", } mock_userinfo.return_value = userinfo_response - test_user = add_test_user(db_session) - existing_encoded_visa = add_visa_manually( + test_user = add_test_ras_user(db_session) + existing_encoded_visa, _ = add_visa_manually( db_session, test_user, rsa_private_key, kid ) add_refresh_token(db_session, test_user) @@ -327,7 +317,9 @@ def test_update_visa_empty_visa_returned( """ Test to check if the db is emptied if the ras userinfo sends back an empty visa """ - mock_arborist_requests({f"arborist/user/{TEST_USERNAME}": {"PATCH": (None, 204)}}) + mock_arborist_requests( + {f"arborist/user/{TEST_RAS_USERNAME}": {"PATCH": (None, 204)}} + ) mock_discovery.return_value = "https://ras/token_endpoint" new_token = "refresh12345abcdefg" @@ -343,7 +335,7 @@ def test_update_visa_empty_visa_returned( "name": "", "preferred_username": "someuser@era.com", "UID": "", - "UserID": TEST_USERNAME, + "UserID": TEST_RAS_USERNAME, "email": "", } @@ -367,8 +359,8 @@ def test_update_visa_empty_visa_returned( userinfo_response["passport_jwt_v11"] = encoded_passport mock_userinfo.return_value = userinfo_response - test_user = add_test_user(db_session) - existing_encoded_visa = add_visa_manually( + test_user = add_test_ras_user(db_session) + existing_encoded_visa, _ = add_visa_manually( db_session, test_user, rsa_private_key, kid ) add_refresh_token(db_session, test_user) @@ -438,7 +430,9 @@ def return_false(): flask.has_app_context = return_false - mock_arborist_requests({f"arborist/user/{TEST_USERNAME}": {"PATCH": (None, 204)}}) + mock_arborist_requests( + {f"arborist/user/{TEST_RAS_USERNAME}": {"PATCH": (None, 204)}} + ) mock_discovery.return_value = "https://ras/token_endpoint" new_token = "refresh12345abcdefg" @@ -454,12 +448,12 @@ def return_false(): "name": "", "preferred_username": "someuser@era.com", "UID": "", - "UserID": TEST_USERNAME, + "UserID": TEST_RAS_USERNAME, "email": "", } - test_user = add_test_user(db_session) - existing_encoded_visa = add_visa_manually( + test_user = add_test_ras_user(db_session) + existing_encoded_visa, _ = add_visa_manually( db_session, test_user, rsa_private_key, kid ) add_refresh_token(db_session, test_user) @@ -575,7 +569,9 @@ def return_false(): flask.has_app_context = return_false - mock_arborist_requests({f"arborist/user/{TEST_USERNAME}": {"PATCH": (None, 204)}}) + mock_arborist_requests( + {f"arborist/user/{TEST_RAS_USERNAME}": {"PATCH": (None, 204)}} + ) mock_discovery.return_value = "https://ras/token_endpoint" mock_get_token.return_value = { @@ -639,7 +635,7 @@ def return_false(): HTTP_PROXY=config.get("HTTP_PROXY"), logger=logger, ) - test_user = add_test_user(db_session) + test_user = add_test_ras_user(db_session) # Pass in an empty pkey cache so that the client will have to hit the jwks endpoint. ras_client.update_user_authorization( @@ -677,7 +673,9 @@ def dont_test_visa_update_cronjob( """ Test to check visa table is updated when updating visas using cronjob """ - mock_arborist_requests({f"arborist/user/{TEST_USERNAME}": {"PATCH": (None, 204)}}) + mock_arborist_requests( + {f"arborist/user/{TEST_RAS_USERNAME}": {"PATCH": (None, 204)}} + ) n_users = 20 n_users_no_visa = 15 @@ -696,18 +694,18 @@ def dont_test_visa_update_cronjob( "name": "", "preferred_username": "someuser@era.com", "UID": "", - "UserID": TEST_USERNAME, + "UserID": TEST_RAS_USERNAME, "email": "", } for i in range(n_users): username = "user_{}".format(i) - test_user = add_test_user(db_session, username, i) + test_user = add_test_ras_user(db_session, username, i) add_visa_manually(db_session, test_user, rsa_private_key, kid) add_refresh_token(db_session, test_user) for j in range(n_users_no_visa): username = "no_visa_{}".format(j) - test_user = add_test_user(db_session, username, j + n_users) + test_user = add_test_ras_user(db_session, username, j + n_users) query_visas = db_session.query(GA4GHVisaV1).all() diff --git a/tests/scripting/test_fence-create.py b/tests/scripting/test_fence-create.py index 28cc37d6f..2cd5911a1 100644 --- a/tests/scripting/test_fence-create.py +++ b/tests/scripting/test_fence-create.py @@ -28,6 +28,7 @@ GoogleProxyGroupToGoogleBucketAccessGroup, GoogleServiceAccountKey, StorageAccess, + GA4GHVisaV1, ) from fence.scripting.fence_create import ( delete_users, @@ -45,8 +46,10 @@ modify_client_action, create_projects, create_group, + cleanup_expired_ga4gh_information, ) - +from tests.dbgap_sync.conftest import add_visa_manually +from tests.utils import add_test_ras_user ROOT_DIR = "./" @@ -387,6 +390,29 @@ def test_create_refresh_token_with_found_user( assert db_token is not None +def _setup_ga4gh_info( + db_session, rsa_private_key, kid, access_1_expires=None, access_2_expires=None +): + """ + Setup some testing data. + + Args: + access_1_expires (str, optional): expiration for the Proxy Group -> + Google Bucket Access Group for user 1, defaults to None + access_2_expires (str, optional): expiration for the Proxy Group -> + Google Bucket Access Group for user 2, defaults to None + """ + test_user = add_test_ras_user(db_session) + _, visa1 = add_visa_manually( + db_session, test_user, rsa_private_key, kid, expires=access_1_expires + ) + _, visa2 = add_visa_manually( + db_session, test_user, rsa_private_key, kid, expires=access_2_expires + ) + + return {"ga4gh_visas": {"1": visa1.id, "2": visa2.id, "test_user": test_user}} + + def _setup_google_access(db_session, access_1_expires=None, access_2_expires=None): """ Setup some testing data. @@ -818,6 +844,45 @@ def test_delete_expired_google_access_with_one_fail_first( assert len(google_bucket_access_grps) == pre_deletion_google_bucket_access_grps_size +def test_cleanup_expired_ga4gh_information(app, db_session, rsa_private_key, kid): + """ + Test removal of expired ga4gh info + """ + import fence + + current_time = int(time.time()) + # 1 expired, 2 not expired + access_1_expires = current_time - 3600 + access_2_expires = current_time + 3600 + setup_results = _setup_ga4gh_info( + db_session, + rsa_private_key, + kid, + access_1_expires=access_1_expires, + access_2_expires=access_2_expires, + ) + + ga4gh_visas = db_session.query(GA4GHVisaV1).all() + + # check database to make sure all the service accounts exist + pre_deletion_ga4gh_visas_size = len(ga4gh_visas) + + # call function to delete expired service account + cleanup_expired_ga4gh_information(config["DB"]) + + ga4gh_visas = db_session.query(GA4GHVisaV1).all() + + # check database again. Expect 1 access is deleted - proxy group and gbag should be intact + assert len(ga4gh_visas) == pre_deletion_ga4gh_visas_size - 1 + remaining_ids = [str(item.id) for item in ga4gh_visas] + + # b/c expired + assert str(setup_results["ga4gh_visas"]["1"]) not in remaining_ids + + # b/c not expired + assert str(setup_results["ga4gh_visas"]["2"]) in remaining_ids + + def test_verify_bucket_access_group_no_interested_accounts( app, cloud_manager, db_session, setup_test_data ): diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index c7193a4f3..3d41749de 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -6,7 +6,7 @@ from flask import current_app from fence.config import config - +from fence.resources.ga4gh.passports import get_or_create_gen3_user_from_iss_sub from fence.models import ( User, Project, @@ -24,6 +24,21 @@ import tests import tests.utils.oauth2 +TEST_RAS_USERNAME = "admin_user" +TEST_RAS_SUB = "abcd-asdj-sajpiasj12iojd-asnoin" + + +def add_test_ras_user(db_session, username=TEST_RAS_USERNAME, is_admin=True): + # pre-populate mapping table, as login would do + test_user = get_or_create_gen3_user_from_iss_sub( + issuer="https://stsstg.nih.gov", subject_id=TEST_RAS_SUB, db_session=db_session + ) + test_user.username = username + test_user.is_admin = is_admin + db_session.add(test_user) + db_session.commit() + return test_user + def read_file(filename): """Read the contents of a file in the tests directory.""" From 363ba3e4777927c25bf5b470ffe6e46d983718f1 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 7 Dec 2021 12:44:18 -0600 Subject: [PATCH 128/211] feat(fence-create): add command to run cleanup for ga4gh info --- bin/fence_create.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/fence_create.py b/bin/fence_create.py index 9ed8e50e5..d68b24d9a 100755 --- a/bin/fence_create.py +++ b/bin/fence_create.py @@ -462,6 +462,8 @@ def main(): verify_bucket_access_group(DB) elif args.action == "delete-expired-google-access": delete_expired_google_access(DB) + elif args.action == "cleanup-expired-ga4gh-information": + cleanup_expired_ga4gh_information(DB) elif args.action == "sync": sync_users( dbGaP, From 011721c695bbe9b944426e4d3a46dc75ddd70de9 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Wed, 8 Dec 2021 12:09:34 -0600 Subject: [PATCH 129/211] chore(sync): remove unused features (fallback to dbgap and visa sync in usersync), ensure single visa sync is only updating storage backend and arborist --- bin/fence_create.py | 2 - fence/config-default.yaml | 3 - fence/scripting/fence_create.py | 4 - fence/sync/sync_users.py | 88 ++++++++++++++++-- tests/dbgap_sync/test_user_sync.py | 139 ++++++++++------------------- tests/test-fence-config.yaml | 3 - 6 files changed, 126 insertions(+), 113 deletions(-) diff --git a/bin/fence_create.py b/bin/fence_create.py index d68b24d9a..4ef76461f 100755 --- a/bin/fence_create.py +++ b/bin/fence_create.py @@ -408,7 +408,6 @@ def main(): "STORAGE_CREDENTIALS" ) usersync = config.get("USERSYNC", {}) - fallback_to_dbgap_sftp = usersync.get("fallback_to_dbgap_sftp", False) arborist = None if args.arborist: @@ -475,7 +474,6 @@ def main(): sync_from_local_yaml_file=args.yaml, folder=args.folder, arborist=arborist, - fallback_to_dbgap_sftp=fallback_to_dbgap_sftp, ) elif args.action == "dbgap-download-access-files": download_dbgap_files( diff --git a/fence/config-default.yaml b/fence/config-default.yaml index c132664b1..463afcbba 100755 --- a/fence/config-default.yaml +++ b/fence/config-default.yaml @@ -897,9 +897,6 @@ EXPIRED_AUTHZ_REMOVAL_JOB_FREQ_IN_SECONDS: 300 GLOBAL_PARSE_VISAS_ON_LOGIN: # Settings for usersync with visas USERSYNC: - sync_from_visas: false - # fallback to dbgap sftp when there are no valid visas for a user i.e. if they're expired or if they're malformed - fallback_to_dbgap_sftp: false visa_types: ras: ["https://ras.nih.gov/visas/v1", "https://ras.nih.gov/visas/v1.1"] RAS_USERINFO_ENDPOINT: '/openid/connect/v1.1/userinfo' diff --git a/fence/scripting/fence_create.py b/fence/scripting/fence_create.py index 2db2604d0..8dbf61d5b 100644 --- a/fence/scripting/fence_create.py +++ b/fence/scripting/fence_create.py @@ -240,7 +240,6 @@ def init_syncer( sync_from_local_yaml_file=None, arborist=None, folder=None, - fallback_to_dbgap_sftp=False, ): """ sync ACL files from dbGap to auth db and storage backends @@ -299,7 +298,6 @@ def init_syncer( sync_from_local_yaml_file=sync_from_local_yaml_file, arborist=arborist, folder=folder, - fallback_to_dbgap_sftp=fallback_to_dbgap_sftp, ) @@ -341,7 +339,6 @@ def sync_users( sync_from_local_yaml_file=None, arborist=None, folder=None, - fallback_to_dbgap_sftp=False, ): syncer = init_syncer( dbGaP, @@ -353,7 +350,6 @@ def sync_users( sync_from_local_yaml_file, arborist, folder, - fallback_to_dbgap_sftp, ) if not syncer: exit(1) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 10692845f..8be5536e0 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -292,7 +292,6 @@ def __init__( sync_from_local_yaml_file=None, arborist=None, folder=None, - fallback_to_dbgap_sftp=False, ): """ Syncs ACL files from dbGap to auth database and storage backends @@ -307,7 +306,6 @@ def __init__( ArboristClient instance if the syncer should also create resources in arborist folder: a local folder where dbgap telemetry files will sync to - fallback_to_dbgap_sftp: fallback to telemetry files when visa sync fails """ self.sync_from_local_csv_dir = sync_from_local_csv_dir self.sync_from_local_yaml_file = sync_from_local_yaml_file @@ -326,7 +324,6 @@ def __init__( ) self.arborist_client = arborist self.folder = folder - self.fallback_to_dbgap_sftp = fallback_to_dbgap_sftp self.auth_source = defaultdict(set) # auth_source used for logging. username : [source1, source2] @@ -686,7 +683,12 @@ def sync_two_phsids_dict( self.auth_source[user].add(source2) def sync_to_db_and_storage_backend( - self, user_project, user_info, sess, single_visa_sync=False, expires=None + self, + user_project, + user_info, + sess, + do_not_revoke_from_db_and_storage=False, + expires=None, ): """ sync user access control to database and storage backend @@ -746,7 +748,7 @@ def sync_to_db_and_storage_backend( # pass the original, non-lowered user_info dict self._upsert_userinfo(sess, user_info) - if not single_visa_sync: + if not do_not_revoke_from_db_and_storage: self._revoke_from_storage( to_delete, sess, google_bulk_mapping=google_bulk_mapping ) @@ -778,7 +780,7 @@ def sync_to_db_and_storage_backend( ) self._update_from_db(sess, to_update, user_project_lowercase) - if not single_visa_sync: + if not do_not_revoke_from_db_and_storage: self._validate_and_update_user_admin(sess, user_info_lowercase) if config["GOOGLE_BULK_UPDATES"]: @@ -788,6 +790,74 @@ def sync_to_db_and_storage_backend( sess.commit() + def sync_to_storage_backend(self, user_project, user_info, sess, expires): + """ + sync user access control to storage backend with given expiration + + Args: + user_project (dict): a dictionary of + + { + username: { + 'project1': {'read-storage','write-storage'}, + 'project2': {'read-storage'} + } + } + + user_info (dict): a dictionary of {username: user_info{}} + sess: a sqlalchemy session + + Return: + None + """ + if not expires: + raise Exception( + f"sync to storage backend requires an expiration. you provided: {expires}" + ) + + google_bulk_mapping = None + if config["GOOGLE_BULK_UPDATES"]: + google_bulk_mapping = {} + + # TODO: eventually it'd be nice to remove this step but it's required + # so that grant_from_storage can determine what storage backends + # are needed for a project. + self._init_projects(user_project, sess) + + # we need to compare db -> whitelist case-insensitively for username. + # db stores case-sensitively, but we need to query case-insensitively + user_project_lowercase = {} + syncing_user_project_list = set() + for username, projects in user_project.items(): + user_project_lowercase[username.lower()] = projects + for project, _ in projects.items(): + syncing_user_project_list.add((username.lower(), project)) + + user_info_lowercase = { + username.lower(): info for username, info in user_info.items() + } + + to_add = set(syncing_user_project_list) + + # when updating users we want to maintain case sesitivity in the username so + # pass the original, non-lowered user_info dict + self._upsert_userinfo(sess, user_info) + + self._grant_from_storage( + to_add, + user_project_lowercase, + sess, + google_bulk_mapping=google_bulk_mapping, + expires=expires, + ) + + if config["GOOGLE_BULK_UPDATES"]: + self.logger.info("Doing bulk Google update...") + bulk_update_google_groups(google_bulk_mapping) + self.logger.info("Bulk Google update done!") + + sess.commit() + def _revoke_from_db(self, sess, to_delete): """ Revoke user access to projects in the auth database @@ -2040,9 +2110,9 @@ def sync_single_user_visas(self, user, ga4gh_visas, sess=None, expires=None): ) if user_projects: - self.logger.info("Sync to db and storage backend [sync_single_user_visas]") - self.sync_to_db_and_storage_backend( - user_projects, user_info, sess, single_visa_sync=True, expires=expires + self.logger.info("Sync to storage backend [sync_single_user_visas]") + self.sync_to_storage_backend( + user_projects, user_info, sess, expires=expires ) else: self.logger.info("No users for syncing") diff --git a/tests/dbgap_sync/test_user_sync.py b/tests/dbgap_sync/test_user_sync.py index b4e07aebb..3f97ce907 100644 --- a/tests/dbgap_sync/test_user_sync.py +++ b/tests/dbgap_sync/test_user_sync.py @@ -615,13 +615,11 @@ def mock_merge(dbgap_servers, sess): # @pytest.mark.parametrize("syncer", ["cleversafe", "google"], indirect=True) # @pytest.mark.parametrize("parse_consent_code_config", [False, True]) -# @pytest.mark.parametrize("fallback_to_dbgap_sftp", [False, True]) # def test_user_sync_with_visas( # syncer, # db_session, # storage_client, # parse_consent_code_config, -# fallback_to_dbgap_sftp, # monkeypatch, # ): # # patch the sync to use the parameterized config value @@ -629,8 +627,6 @@ def mock_merge(dbgap_servers, sess): # syncer.dbGaP[0], "parse_consent_code", parse_consent_code_config # ) # monkeypatch.setattr(syncer, "parse_consent_code", parse_consent_code_config) -# monkeypatch.setattr(syncer, "fallback_to_dbgap_sftp", fallback_to_dbgap_sftp) -# monkeypatch.setattr(syncer, "sync_from_visas", True) # syncer.sync_visas() @@ -657,93 +653,52 @@ def mock_merge(dbgap_servers, sess): # assert len(invalid_user.ga4gh_visas_v1) == 0 # assert len(expired_user.ga4gh_visas_v1) == 0 - -# if fallback_to_dbgap_sftp: -# assert len(users) == 14 - -# if parse_consent_code_config: -# assert equal_project_access( -# user.project_access, -# { -# "phs000991.c1": ["read", "read-storage"], -# "phs000961.c1": ["read", "read-storage"], -# "phs000279.c1": ["read", "read-storage"], -# "phs000286.c3": ["read", "read-storage"], -# "phs000289.c2": ["read", "read-storage"], -# "phs000298.c1": ["read", "read-storage"], -# }, -# ) -# assert equal_project_access( -# backup_user.project_access, -# { -# "phs000179.c1": ["read", "read-storage"], -# }, -# ) -# else: -# assert equal_project_access( -# user.project_access, -# { -# "phs000991": ["read", "read-storage"], -# "phs000961": ["read", "read-storage"], -# "phs000279": ["read", "read-storage"], -# "phs000286": ["read", "read-storage"], -# "phs000289": ["read", "read-storage"], -# "phs000298": ["read", "read-storage"], -# }, -# ) -# assert equal_project_access( -# backup_user.project_access, -# { -# "phs000179": ["read", "read-storage"], -# }, -# ) - -# else: -# assert len(users) == 12 -# assert len(backup_user.project_access) == 0 -# if parse_consent_code_config: -# assert equal_project_access( -# user.project_access, -# { -# "phs000991.c1": ["read", "read-storage"], -# "phs000961.c1": ["read", "read-storage"], -# "phs000279.c1": ["read", "read-storage"], -# "phs000286.c3": ["read", "read-storage"], -# "phs000289.c2": ["read", "read-storage"], -# "phs000298.c1": ["read", "read-storage"], -# }, -# ) -# else: -# assert equal_project_access( -# user.project_access, -# { -# "phs000991": ["read", "read-storage"], -# "phs000961": ["read", "read-storage"], -# "phs000279": ["read", "read-storage"], -# "phs000286": ["read", "read-storage"], -# "phs000289": ["read", "read-storage"], -# "phs000298": ["read", "read-storage"], -# }, -# ) +# +# assert len(users) == 12 +# assert len(backup_user.project_access) == 0 +# if parse_consent_code_config: +# assert equal_project_access( +# user.project_access, +# { +# "phs000991.c1": ["read", "read-storage"], +# "phs000961.c1": ["read", "read-storage"], +# "phs000279.c1": ["read", "read-storage"], +# "phs000286.c3": ["read", "read-storage"], +# "phs000289.c2": ["read", "read-storage"], +# "phs000298.c1": ["read", "read-storage"], +# }, +# ) +# else: +# assert equal_project_access( +# user.project_access, +# { +# "phs000991": ["read", "read-storage"], +# "phs000961": ["read", "read-storage"], +# "phs000279": ["read", "read-storage"], +# "phs000286": ["read", "read-storage"], +# "phs000289": ["read", "read-storage"], +# "phs000298": ["read", "read-storage"], +# }, +# ) -@pytest.mark.parametrize("syncer", ["google"], indirect=True) -def test_sync_in_login( - syncer, - db_session, - storage_client, - rsa_private_key, - kid, - monkeypatch, -): - user = models.query_for_user( - session=db_session, username="TESTUSERB" - ) # contains no information - synced_visas = syncer.sync_single_user_visas(user, user.ga4gh_visas_v1, db_session) - user = models.query_for_user( - session=db_session, username="TESTUSERB" - ) # contains only visa information - user1 = models.query_for_user(session=db_session, username="USER_1") - assert len(user1.project_access) == 0 # other users are not affected - assert len(user.project_access) == 6 - assert len(synced_visas) +# @pytest.mark.parametrize("syncer", ["google"], indirect=True) +# def test_sync_in_login( +# syncer, +# db_session, +# storage_client, +# rsa_private_key, +# kid, +# monkeypatch, +# ): +# user = models.query_for_user( +# session=db_session, username="TESTUSERB" +# ) # contains no information +# synced_visas = syncer.sync_single_user_visas(user, user.ga4gh_visas_v1, db_session, expires=99999999) +# user = models.query_for_user( +# session=db_session, username="TESTUSERB" +# ) # contains only visa information +# user1 = models.query_for_user(session=db_session, username="USER_1") +# assert len(user1.project_access) == 0 # other users are not affected +# assert len(user.project_access) == 6 +# assert len(synced_visas) diff --git a/tests/test-fence-config.yaml b/tests/test-fence-config.yaml index a478847b4..690bd70f4 100755 --- a/tests/test-fence-config.yaml +++ b/tests/test-fence-config.yaml @@ -642,9 +642,6 @@ EXPIRED_AUTHZ_REMOVAL_JOB_FREQ_IN_SECONDS: 300 GLOBAL_PARSE_VISAS_ON_LOGIN: # Settings for usersync with visas USERSYNC: - sync_from_visas: true - # fallback to dbgap sftp when there are no valid visas for a user i.e. if they're expired or if they're malformed - fallback_to_dbgap_sftp: false visa_types: ras: ["https://ras.nih.gov/visas/v1", "https://ras.nih.gov/visas/v1.1"] RAS_USERINFO_ENDPOINT: '/openid/connect/v1.1/userinfo' From 8c5dc0e9ca83b5d62e9c236cc378f80118214fe9 Mon Sep 17 00:00:00 2001 From: John McCann Date: Mon, 13 Dec 2021 08:07:40 -0800 Subject: [PATCH 130/211] chore(iss_sub_pair_to_user): populate after_create --- fence/models.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/fence/models.py b/fence/models.py index b4d971742..7b615f2c8 100644 --- a/fence/models.py +++ b/fence/models.py @@ -23,6 +23,7 @@ MetaData, Table, text, + event, ) from sqlalchemy.dialects.postgresql import ARRAY, JSONB from sqlalchemy.orm import relationship, backref @@ -652,6 +653,28 @@ class IssSubPairToUser(Base): extra_info = Column(JSONB(), server_default=text("'{}'")) +@event.listens_for(IssSubPairToUser.__table__, "after_create") +def populate_iss_sub_pair_to_user_table(target, connection, **kw): + idp_name = IdentityProvider.ras + issuer = "https://stsstg.nih.gov" + + transaction = connection.begin() + result = connection.execute( + text( + """ + WITH identity_provider_id AS (SELECT id FROM identity_provider WHERE name=:idp_name) + INSERT INTO iss_sub_pair_to_user (iss, sub, "fk_to_User", extra_info) + SELECT :iss, id_from_idp, id, additional_info + FROM "User" + WHERE idp_id IN (SELECT * FROM identity_provider_id) AND id_from_idp IS NOT NULL; + """ + ), + idp_name=idp_name, + iss=issuer, + ) + transaction.commit() + + to_timestamp = ( "CREATE OR REPLACE FUNCTION pc_datetime_to_timestamp(datetoconvert timestamp) " "RETURNS BIGINT AS " From 6c25eeb7bfade7027f9f4769965466117e617c5b Mon Sep 17 00:00:00 2001 From: John McCann Date: Thu, 16 Dec 2021 00:07:43 -0800 Subject: [PATCH 131/211] chore(IssSubPairToUser): add ISSUER_TO_IDP --- fence/models.py | 53 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/fence/models.py b/fence/models.py index 7b615f2c8..a1a50a3d5 100644 --- a/fence/models.py +++ b/fence/models.py @@ -652,27 +652,44 @@ class IssSubPairToUser(Base): # dump whatever idp provides in here extra_info = Column(JSONB(), server_default=text("'{}'")) + def _get_issuer_to_idp(): + possibly_matching_idps = [IdentityProvider.ras] + issuer_to_idp = {} + + oidc = config.get("OPENID_CONNECT", {}) + for idp in possibly_matching_idps: + discovery_url = oidc.get(idp, {}).get("discovery_url") + if discovery_url: + for allowed_issuer in config["GA4GH_VISA_ISSUER_ALLOWLIST"]: + if discovery_url.startswith(allowed_issuer): + issuer_to_idp[allowed_issuer] = idp + break + + return issuer_to_idp + + ISSUER_TO_IDP = _get_issuer_to_idp() + + del _get_issuer_to_idp + @event.listens_for(IssSubPairToUser.__table__, "after_create") def populate_iss_sub_pair_to_user_table(target, connection, **kw): - idp_name = IdentityProvider.ras - issuer = "https://stsstg.nih.gov" - - transaction = connection.begin() - result = connection.execute( - text( - """ - WITH identity_provider_id AS (SELECT id FROM identity_provider WHERE name=:idp_name) - INSERT INTO iss_sub_pair_to_user (iss, sub, "fk_to_User", extra_info) - SELECT :iss, id_from_idp, id, additional_info - FROM "User" - WHERE idp_id IN (SELECT * FROM identity_provider_id) AND id_from_idp IS NOT NULL; - """ - ), - idp_name=idp_name, - iss=issuer, - ) - transaction.commit() + for issuer, idp_name in IssSubPairToUser.ISSUER_TO_IDP.items(): + transaction = connection.begin() + result = connection.execute( + text( + """ + WITH identity_provider_id AS (SELECT id FROM identity_provider WHERE name=:idp_name) + INSERT INTO iss_sub_pair_to_user (iss, sub, "fk_to_User", extra_info) + SELECT :iss, id_from_idp, id, additional_info + FROM "User" + WHERE idp_id IN (SELECT * FROM identity_provider_id) AND id_from_idp IS NOT NULL; + """ + ), + idp_name=idp_name, + iss=issuer, + ) + transaction.commit() to_timestamp = ( From 6cbb5d21e49e780ba8eaa07d44c3695370a8f16f Mon Sep 17 00:00:00 2001 From: John McCann Date: Thu, 16 Dec 2021 01:01:29 -0800 Subject: [PATCH 132/211] chore(GA4GH): use IssSubPairToUser.ISSUER_TO_IDP --- fence/__init__.py | 7 ------- fence/models.py | 3 +++ fence/resources/ga4gh/passports.py | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/fence/__init__.py b/fence/__init__.py index 3e17e2f98..26eeba07c 100755 --- a/fence/__init__.py +++ b/fence/__init__.py @@ -401,7 +401,6 @@ def _set_authlib_cfgs(app): def _setup_oidc_clients(app): oidc = config.get("OPENID_CONNECT", {}) - app.issuer_to_idp = {} # Add OIDC client for Google if configured. if "google" in oidc: @@ -426,12 +425,6 @@ def _setup_oidc_clients(app): HTTP_PROXY=config.get("HTTP_PROXY"), logger=logger, ) - for allowed_issuer in config["GA4GH_VISA_ISSUER_ALLOWLIST"]: - if app.ras_client.discovery_url.startswith(allowed_issuer): - app.issuer_to_idp[allowed_issuer] = IdentityProvider.ras - break - else: - logger.warn("Could not determine issuer for the RAS OIDC client") # Add OIDC client for Synapse if configured. if "synapse" in oidc: diff --git a/fence/models.py b/fence/models.py index a1a50a3d5..ee5651479 100644 --- a/fence/models.py +++ b/fence/models.py @@ -57,6 +57,9 @@ import warnings from fence.config import config +from fence.settings import CONFIG_SEARCH_FOLDERS + +config.load(search_folders=CONFIG_SEARCH_FOLDERS) def query_for_user(session, username): diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 4c8186695..5071f1a35 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -286,7 +286,7 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id): username = subject_id + issuer[len("https://") :] gen3_user = query_for_user(session=db_session, username=username) if not gen3_user: - idp_name = flask.current_app.issuer_to_idp.get(issuer) + idp_name = IssSubPairToUser.ISSUER_TO_IDP.get(issuer) gen3_user = create_user(db_session, logger, username, idp_name=idp_name) if not idp_name: logger.info( From 8989badadc10b2de3bac1aeba15713a4506a14f6 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Mon, 20 Dec 2021 17:09:46 -0600 Subject: [PATCH 133/211] chore(deps): update dependencies --- poetry.lock | 486 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 340 insertions(+), 146 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8c51060f2..8816b01e2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -61,7 +61,7 @@ requests = "*" [[package]] name = "authutils" -version = "6.1.0" +version = "6.1.1" description = "Gen3 auth utility functions" category = "main" optional = false @@ -79,9 +79,22 @@ xmltodict = ">=0.9,<1.0" flask = ["Flask (>=0.10.1)"] fastapi = ["fastapi (>=0.54.1,<0.55.0)"] +[[package]] +name = "aws-xray-sdk" +version = "0.95" +description = "The AWS X-Ray SDK for Python (the SDK) enables Python developers to record and emit information from within their applications to the AWS X-Ray service." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +jsonpickle = "*" +requests = "*" +wrapt = "*" + [[package]] name = "azure-core" -version = "1.20.1" +version = "1.21.1" description = "Microsoft Azure Core Library for Python" category = "main" optional = false @@ -263,7 +276,7 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "2.0.7" +version = "2.0.9" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -383,6 +396,23 @@ idna = ["idna (>=2.1)"] curio = ["curio (>=1.2)", "sniffio (>=1.1)"] trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] +[[package]] +name = "docker" +version = "5.0.3" +description = "A Python library for the Docker Engine API." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pywin32 = {version = "227", markers = "sys_platform == \"win32\""} +requests = ">=2.14.2,<2.18.0 || >2.18.0" +websocket-client = ">=0.32.0" + +[package.extras] +ssh = ["paramiko (>=2.4.2)"] +tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=3.4.7)", "idna (>=2.0.0)"] + [[package]] name = "docopt" version = "0.6.2" @@ -513,7 +543,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "gen3authz" -version = "1.4.0" +version = "1.4.1" description = "Gen3 authz client" category = "main" optional = false @@ -573,7 +603,7 @@ PyYAML = ">=5.1,<6.0" [[package]] name = "google-api-core" -version = "1.31.4" +version = "1.31.5" description = "Google API client core library" category = "main" optional = false @@ -701,7 +731,7 @@ requests = ["requests (>=2.18.0,<3.0.0dev)"] [[package]] name = "googleapis-common-protos" -version = "1.53.0" +version = "1.54.0" description = "Common protobufs used in Google APIs" category = "main" optional = false @@ -792,7 +822,7 @@ test = ["flake8 (>=3.8.4,<3.9.0)", "pycodestyle (>=2.6.0,<2.7.0)", "mypy (>=0.91 [[package]] name = "importlib-metadata" -version = "4.8.2" +version = "4.8.3" description = "Read metadata from Python packages" category = "main" optional = false @@ -809,7 +839,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [[package]] name = "isodate" -version = "0.6.0" +version = "0.6.1" description = "An ISO 8601 date/time/duration parser and formatter" category = "main" optional = false @@ -848,6 +878,30 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "jsondiff" +version = "1.1.1" +description = "Diff JSON and JSON-like structures in Python" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "jsonpickle" +version = "2.0.0" +description = "Python library for serializing any arbitrary object graph into JSON" +category = "dev" +optional = false +python-versions = ">=2.7" + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["coverage (<5)", "pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black-multipy", "pytest-cov", "ecdsa", "feedparser", "numpy", "pandas", "pymongo", "sklearn", "sqlalchemy", "enum34", "jsonlib"] +"testing.libs" = ["demjson", "simplejson", "ujson", "yajl"] + [[package]] name = "markdown" version = "3.3.6" @@ -864,11 +918,11 @@ testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" -version = "1.1.1" +version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +python-versions = ">=3.6" [[package]] name = "mock" @@ -888,7 +942,7 @@ test = ["unittest2 (>=1.1.0)"] [[package]] name = "more-itertools" -version = "8.11.0" +version = "8.12.0" description = "More routines for operating on iterables, beyond itertools" category = "dev" optional = false @@ -896,41 +950,34 @@ python-versions = ">=3.5" [[package]] name = "moto" -version = "1.3.15" +version = "1.3.7" description = "A library that allows your python tests to easily mock out the boto library" category = "dev" optional = false python-versions = "*" [package.dependencies] +aws-xray-sdk = ">=0.93,<0.96" boto = ">=2.36.0" -boto3 = ">=1.9.201" -botocore = ">=1.12.201" -Jinja2 = ">=2.10.1" -MarkupSafe = "<2.0" +boto3 = ">=1.6.16" +botocore = ">=1.12.13" +cryptography = ">=2.3.0" +docker = ">=2.5.1" +Jinja2 = ">=2.7.3" +jsondiff = "1.1.1" mock = "*" -more-itertools = "*" +pyaml = "*" python-dateutil = ">=2.1,<3.0.0" +python-jose = "<3.0.0" pytz = "*" requests = ">=2.5" responses = ">=0.9.0" six = ">1.9" werkzeug = "*" xmltodict = "*" -zipp = "*" [package.extras] -acm = ["cryptography (>=2.3.0)"] -all = ["cryptography (>=2.3.0)", "PyYAML (>=5.1)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)", "docker (>=2.5.1)", "jsondiff (>=1.1.2)", "aws-xray-sdk (>=0.93,!=0.96)", "idna (>=2.5,<3)", "cfn-lint (>=0.4.0)", "sshpubkeys (>=3.1.0,<4.0)", "sshpubkeys (>=3.1.0)"] -awslambda = ["docker (>=2.5.1)"] -batch = ["docker (>=2.5.1)"] -cloudformation = ["PyYAML (>=5.1)", "cfn-lint (>=0.4.0)"] -cognitoidp = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)"] -ec2 = ["cryptography (>=2.3.0)", "sshpubkeys (>=3.1.0,<4.0)", "sshpubkeys (>=3.1.0)"] -iam = ["cryptography (>=2.3.0)"] -iotdata = ["jsondiff (>=1.1.2)"] server = ["flask"] -xray = ["aws-xray-sdk (>=0.93,!=0.96)"] [[package]] name = "msrest" @@ -990,7 +1037,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "paramiko" -version = "2.8.0" +version = "2.8.1" description = "SSH2 protocol library" category = "main" optional = false @@ -1043,7 +1090,7 @@ twisted = ["twisted"] [[package]] name = "prometheus-flask-exporter" -version = "0.18.5" +version = "0.18.6" description = "Prometheus metrics exporter for Flask" category = "main" optional = false @@ -1077,6 +1124,17 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pyaml" +version = "21.10.1" +description = "PyYAML-based module to produce pretty and readable YAML-serialized data" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +PyYAML = "*" + [[package]] name = "pyasn1" version = "0.4.8" @@ -1106,7 +1164,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pycryptodome" -version = "3.11.0" +version = "3.12.0" description = "Cryptographic library for Python" category = "main" optional = false @@ -1243,6 +1301,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "pywin32" +version = "227" +description = "Python for Window Extensions" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "pyyaml" version = "5.4.1" @@ -1328,11 +1394,11 @@ idna2008 = ["idna"] [[package]] name = "rsa" -version = "4.7.2" +version = "4.8" description = "Pure-Python RSA implementation" category = "main" optional = false -python-versions = ">=3.5, <4" +python-versions = ">=3.6,<4" [package.dependencies] pyasn1 = ">=0.1.3" @@ -1416,7 +1482,7 @@ resolved_reference = "4d39265d6e478acd5e1afe6e5dc722418f887d78" [[package]] name = "typing-extensions" -version = "4.0.0" +version = "4.0.1" description = "Backported and Experimental Type Hints for Python 3.6+" category = "main" optional = false @@ -1445,7 +1511,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "userdatamodel" -version = "2.3.3" +version = "2.4.0" description = "" category = "main" optional = false @@ -1455,6 +1521,19 @@ python-versions = "*" cdislogging = "*" sqlalchemy = ">=1.3.3,<1.4.0" +[[package]] +name = "websocket-client" +version = "1.2.3" +description = "WebSocket client for Python with low level API options" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + [[package]] name = "werkzeug" version = "1.0.1" @@ -1467,6 +1546,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] watchdog = ["watchdog"] +[[package]] +name = "wrapt" +version = "1.13.3" +description = "Module for decorators, wrappers and monkey patching." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + [[package]] name = "wtforms" version = "3.0.0" @@ -1532,12 +1619,16 @@ authlib = [ {file = "Authlib-0.11.tar.gz", hash = "sha256:9741db6de2950a0a5cefbdb72ec7ab12f7e9fd530ff47219f1530e79183cbaaf"}, ] authutils = [ - {file = "authutils-6.1.0-py3-none-any.whl", hash = "sha256:682dba636694c36fb35af1d9ff576bb8436337c3899f0ef434cda5918d661db9"}, - {file = "authutils-6.1.0.tar.gz", hash = "sha256:7263af0b2ce3a0db19236fd123b34f795d07e07111b7bd18a51808568ddfdc2e"}, + {file = "authutils-6.1.1-py3-none-any.whl", hash = "sha256:9c61c563e19781807f354dee8a2720769b18689c2972cf57178cfcb197c7ab6d"}, + {file = "authutils-6.1.1.tar.gz", hash = "sha256:a9cc6ccd9a2ab97bda77ae52a20fe9515dfe06b0621442e41675f4796d1a21ff"}, +] +aws-xray-sdk = [ + {file = "aws-xray-sdk-0.95.tar.gz", hash = "sha256:9e7ba8dd08fd2939376c21423376206bff01d0deaea7d7721c6b35921fed1943"}, + {file = "aws_xray_sdk-0.95-py2.py3-none-any.whl", hash = "sha256:72791618feb22eaff2e628462b0d58f398ce8c1bacfa989b7679817ab1fad60c"}, ] azure-core = [ - {file = "azure-core-1.20.1.zip", hash = "sha256:21d06311c9c373e394ed9f9db035306773334a0181932e265889eca34d778d17"}, - {file = "azure_core-1.20.1-py2.py3-none-any.whl", hash = "sha256:5e5df1850ef6eff2b481a4a5fefa7d73ec74b6a2e0b27b179341f73f655fa4bf"}, + {file = "azure-core-1.21.1.zip", hash = "sha256:88d2db5cf9a135a7287dc45fdde6b96f9ca62c9567512a3bb3e20e322ce7deb2"}, + {file = "azure_core-1.21.1-py2.py3-none-any.whl", hash = "sha256:3d70e9ec64de92dfae330c15bc69085caceb2d83813ef6c01cc45326f2a4be83"}, ] azure-storage-blob = [ {file = "azure-storage-blob-12.9.0.zip", hash = "sha256:cff66a115c73c90e496c8c8b3026898a3ce64100840276e9245434e28a864225"}, @@ -1648,8 +1739,8 @@ cffi = [ {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"}, - {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"}, + {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, + {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, ] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, @@ -1755,6 +1846,10 @@ dnspython = [ {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, ] +docker = [ + {file = "docker-5.0.3-py2.py3-none-any.whl", hash = "sha256:7a79bb439e3df59d0a72621775d600bc8bc8b422d285824cb37103eab91d1ce0"}, + {file = "docker-5.0.3.tar.gz", hash = "sha256:d916a26b62970e7c2f554110ed6af04c7ccff8e9f81ad17d0d40c75637e227fb"}, +] docopt = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] @@ -1794,8 +1889,8 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] gen3authz = [ - {file = "gen3authz-1.4.0-py3-none-any.whl", hash = "sha256:82471a104376fc74c05fb735b1129941f27eaf9e01d57de4a9a87031a78d14ca"}, - {file = "gen3authz-1.4.0.tar.gz", hash = "sha256:9cc679f16253b04a9c08eaa840a3db2ed5824b32f3e75cc311f350572e111697"}, + {file = "gen3authz-1.4.1-py3-none-any.whl", hash = "sha256:a1eac6a314a12216b47518155100d766905be2fa96210565f0f35239391f4f75"}, + {file = "gen3authz-1.4.1.tar.gz", hash = "sha256:354fa107fd7f15fe318d79f410095f51c6225e1ced04b506e0f3a906ad683ff6"}, ] gen3cirrus = [ {file = "gen3cirrus-2.0.0.tar.gz", hash = "sha256:0bd590c407c42dad5f0b896da0fa30bd01ea6bef5ff7dd11324ec59f14a71793"}, @@ -1807,8 +1902,8 @@ gen3users = [ {file = "gen3users-0.6.0.tar.gz", hash = "sha256:3b9b56798a7d8b34712389dbbab93c00b0f92524f890513f899c31630ea986da"}, ] google-api-core = [ - {file = "google-api-core-1.31.4.tar.gz", hash = "sha256:c77ffc8b4981b44efdb9d68431fd96d21dbd39545c29552bbe79b9b7dd2c3689"}, - {file = "google_api_core-1.31.4-py2.py3-none-any.whl", hash = "sha256:ed59c6a695a81f601e4ba0f37ca9dbde3c43b3309e161a1a8946f266db4a0c4e"}, + {file = "google-api-core-1.31.5.tar.gz", hash = "sha256:85d2074f2c8f9c07e614d7f978767d71ceb7d40647814ef4236d3a0ef671ee75"}, + {file = "google_api_core-1.31.5-py2.py3-none-any.whl", hash = "sha256:6815207a8b422e9da42c200681603f304b25f98c98b675a9db9fdc3717e44280"}, ] google-api-python-client = [ {file = "google-api-python-client-1.11.0.tar.gz", hash = "sha256:caf4015800ef1a18d06d117f47f0219c0c0641f21978f6b1bb5ede7912fab97b"}, @@ -1880,8 +1975,8 @@ google-resumable-media = [ {file = "google_resumable_media-2.1.0-py2.py3-none-any.whl", hash = "sha256:cdc75ea0361e39704dc7df7da59fbd419e73c8bc92eac94d8a020d36baa9944b"}, ] googleapis-common-protos = [ - {file = "googleapis-common-protos-1.53.0.tar.gz", hash = "sha256:a88ee8903aa0a81f6c3cec2d5cf62d3c8aa67c06439b0496b49048fb1854ebf4"}, - {file = "googleapis_common_protos-1.53.0-py2.py3-none-any.whl", hash = "sha256:f6d561ab8fb16b30020b940e2dd01cd80082f4762fa9f3ee670f4419b4b8dbd0"}, + {file = "googleapis-common-protos-1.54.0.tar.gz", hash = "sha256:a4031d6ec6c2b1b6dc3e0be7e10a1bd72fb0b18b07ef9be7b51f2c1004ce2437"}, + {file = "googleapis_common_protos-1.54.0-py2.py3-none-any.whl", hash = "sha256:e54345a2add15dc5e1a7891c27731ff347b4c33765d79b5ed7026a6c0c7cbcae"}, ] h11 = [ {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, @@ -1933,12 +2028,12 @@ immutables = [ {file = "immutables-0.16.tar.gz", hash = "sha256:d67e86859598eed0d926562da33325dac7767b7b1eff84e232c22abea19f4360"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"}, - {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"}, + {file = "importlib_metadata-4.8.3-py3-none-any.whl", hash = "sha256:65a9576a5b2d58ca44d133c42a241905cc45e34d2c06fd5ba2bafa221e5d7b5e"}, + {file = "importlib_metadata-4.8.3.tar.gz", hash = "sha256:766abffff765960fcc18003801f7044eb6755ffae4521c8e8ce8e83b9c9b0668"}, ] isodate = [ - {file = "isodate-0.6.0-py2.py3-none-any.whl", hash = "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"}, - {file = "isodate-0.6.0.tar.gz", hash = "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8"}, + {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, + {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, ] itsdangerous = [ {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, @@ -1952,75 +2047,99 @@ jmespath = [ {file = "jmespath-0.9.2-py2.py3-none-any.whl", hash = "sha256:3f03b90ac8e0f3ba472e8ebff083e460c89501d8d41979771535efe9a343177e"}, {file = "jmespath-0.9.2.tar.gz", hash = "sha256:54c441e2e08b23f12d7fa7d8e6761768c47c969e6aed10eead57505ba760aee9"}, ] +jsondiff = [ + {file = "jsondiff-1.1.1.tar.gz", hash = "sha256:2d0437782de9418efa34e694aa59f43d7adb1899bd9a793f063867ddba8f7893"}, +] +jsonpickle = [ + {file = "jsonpickle-2.0.0-py2.py3-none-any.whl", hash = "sha256:c1010994c1fbda87a48f8a56698605b598cb0fc6bb7e7927559fc1100e69aeac"}, + {file = "jsonpickle-2.0.0.tar.gz", hash = "sha256:0be49cba80ea6f87a168aa8168d717d00c6ca07ba83df3cec32d3b30bfe6fb9a"}, +] markdown = [ {file = "Markdown-3.3.6-py3-none-any.whl", hash = "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"}, {file = "Markdown-3.3.6.tar.gz", hash = "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006"}, ] markupsafe = [ - {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, - {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, ] mock = [ {file = "mock-2.0.0-py2.py3-none-any.whl", hash = "sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1"}, {file = "mock-2.0.0.tar.gz", hash = "sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba"}, ] more-itertools = [ - {file = "more-itertools-8.11.0.tar.gz", hash = "sha256:0a2fd25d343c08d7e7212071820e7e7ea2f41d8fb45d6bc8a00cd6ce3b7aab88"}, - {file = "more_itertools-8.11.0-py3-none-any.whl", hash = "sha256:88afff98d83d08fe5e4049b81e2b54c06ebb6b3871a600040865c7a592061cbb"}, + {file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"}, + {file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"}, ] moto = [ - {file = "moto-1.3.15-py2.py3-none-any.whl", hash = "sha256:3be7e1f406ef7e9c222dbcbfd8cefa2cb1062200e26deae49b5df446e17be3df"}, - {file = "moto-1.3.15.tar.gz", hash = "sha256:fd98f7b219084ba8aadad263849c4dbe8be73979e035d8dc5c86e11a86f11b7f"}, + {file = "moto-1.3.7-py2.py3-none-any.whl", hash = "sha256:4df37936ff8d6a4b8229aab347a7b412cd2ca4823ff47bd1362ddfbc6c5e4ecf"}, + {file = "moto-1.3.7.tar.gz", hash = "sha256:129de2e04cb250d9f8b2c722ec152ed1b5426ef179b4ebb03e9ec36e6eb3fcc5"}, ] msrest = [ {file = "msrest-0.6.21-py2.py3-none-any.whl", hash = "sha256:c840511c845330e96886011a236440fafc2c9aff7b2df9c0a92041ee2dee3782"}, @@ -2038,8 +2157,8 @@ packaging = [ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] paramiko = [ - {file = "paramiko-2.8.0-py2.py3-none-any.whl", hash = "sha256:def3ec612399bab4e9f5eb66b0ae5983980db9dd9120d9e9c6ea3ff673865d1c"}, - {file = "paramiko-2.8.0.tar.gz", hash = "sha256:e673b10ee0f1c80d46182d3af7751d033d9b573dd7054d2d0aa46be186c3c1d2"}, + {file = "paramiko-2.8.1-py2.py3-none-any.whl", hash = "sha256:7b5910f5815a00405af55da7abcc8a9e0d9657f57fcdd9a89894fdbba1c6b8a8"}, + {file = "paramiko-2.8.1.tar.gz", hash = "sha256:85b1245054e5d7592b9088cc6d08da22445417912d3a3e48138675c7a8616438"}, ] pbr = [ {file = "pbr-2.0.0-py2.py3-none-any.whl", hash = "sha256:d9b69a26a5cb4e3898eb3c5cea54d2ab3332382167f04e30db5e1f54e1945e45"}, @@ -2054,8 +2173,8 @@ prometheus-client = [ {file = "prometheus_client-0.9.0.tar.gz", hash = "sha256:9da7b32f02439d8c04f7777021c304ed51d9ec180604700c1ba72a4d44dceb03"}, ] prometheus-flask-exporter = [ - {file = "prometheus_flask_exporter-0.18.5-py3-none-any.whl", hash = "sha256:38a3a1fdaf4fc98f988d33f551a8005d778d6b43ca0a2bc4aafb19d0449a48b9"}, - {file = "prometheus_flask_exporter-0.18.5.tar.gz", hash = "sha256:f9a03e88a8415fe96f785c31fc82bbd290a606aaab87bd244414637d55ef0ba4"}, + {file = "prometheus_flask_exporter-0.18.6-py3-none-any.whl", hash = "sha256:02717c9d15c0956fe54e76bdde7c4116e1a1bddd12be7bc8538bc5b6af431ef1"}, + {file = "prometheus_flask_exporter-0.18.6.tar.gz", hash = "sha256:9a2af3d4ba014e3da6387b3b7cebaed291ccd6f474e240be13f50f8a5af671ca"}, ] protobuf = [ {file = "protobuf-3.19.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d80f80eb175bf5f1169139c2e0c5ada98b1c098e2b3c3736667f28cbbea39fc8"}, @@ -2100,6 +2219,10 @@ py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] +pyaml = [ + {file = "pyaml-21.10.1-py2.py3-none-any.whl", hash = "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0"}, + {file = "pyaml-21.10.1.tar.gz", hash = "sha256:c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383"}, +] pyasn1 = [ {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, @@ -2135,36 +2258,36 @@ pycparser = [ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] pycryptodome = [ - {file = "pycryptodome-3.11.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ffd0cac13ff41f2d15ed39dc6ba1d2ad88dd2905d656c33d8235852f5d6151fd"}, - {file = "pycryptodome-3.11.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:ead516e03dfe062aefeafe4a29445a6449b0fc43bc8cb30194b2754917a63798"}, - {file = "pycryptodome-3.11.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4ce6b09547bf2c7cede3a017f79502eaed3e819c13cdb3cb357aea1b004e4cc6"}, - {file = "pycryptodome-3.11.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:014c758af7fa38cab85b357a496b76f4fc9dda1f731eb28358d66fef7ad4a3e1"}, - {file = "pycryptodome-3.11.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a843350d08c3d22f6c09c2f17f020d8dcfa59496165d7425a3fba0045543dda7"}, - {file = "pycryptodome-3.11.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:53989477044be41fa4a63da09d5038c2a34b2f4554cfea2e3933b17186ee9e19"}, - {file = "pycryptodome-3.11.0-cp27-cp27m-win32.whl", hash = "sha256:f9bad2220b80b4ed74f089db012ab5ab5419143a33fad6c8aedcc2a9341eac70"}, - {file = "pycryptodome-3.11.0-cp27-cp27m-win_amd64.whl", hash = "sha256:3c7ed5b07274535979c730daf5817db5e983ea80b04c22579eee8da4ca3ae4f8"}, - {file = "pycryptodome-3.11.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:8f3a60926be78422e662b0d0b18351b426ce27657101c8a50bad80300de6a701"}, - {file = "pycryptodome-3.11.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:fce7e22d96030b35345637c563246c24d4513bd3b413e1c40293114837ab8912"}, - {file = "pycryptodome-3.11.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:bc3c61ff92efdcc14af4a7b81da71d849c9acee51d8fd8ac9841a7620140d6c6"}, - {file = "pycryptodome-3.11.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:b33c9b3d1327d821e28e9cc3a6512c14f8b17570ddb4cfb9a52247ed0fcc5d8b"}, - {file = "pycryptodome-3.11.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:75e78360d1dd6d02eb288fd8275bb4d147d6e3f5337935c096d11dba1fa84748"}, - {file = "pycryptodome-3.11.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:621a90147a5e255fdc2a0fec2d56626b76b5d72ea9e60164c9a5a8976d45b0c9"}, - {file = "pycryptodome-3.11.0-cp35-abi3-manylinux1_i686.whl", hash = "sha256:0ca7a6b4fc1f9fafe990b95c8cda89099797e2cfbf40e55607f2f2f5a3355dcb"}, - {file = "pycryptodome-3.11.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:b59bf823cfafde8ef1105d8984f26d1694dff165adb7198b12e3e068d7999b15"}, - {file = "pycryptodome-3.11.0-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:ce81b9c6aaa0f920e2ab05eb2b9f4ccd102e3016b2f37125593b16a83a4b0cc2"}, - {file = "pycryptodome-3.11.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:ae29fcd56152f417bfba50a36a56a7a5f9fb74ff80bab98704cac704de6568ab"}, - {file = "pycryptodome-3.11.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:ae31cb874f6f0cedbed457c6374e7e54d7ed45c1a4e11a65a9c80968da90a650"}, - {file = "pycryptodome-3.11.0-cp35-abi3-win32.whl", hash = "sha256:6db1f9fa1f52226621905f004278ce7bd90c8f5363ffd5d7ab3755363d98549a"}, - {file = "pycryptodome-3.11.0-cp35-abi3-win_amd64.whl", hash = "sha256:d7e5f6f692421e5219aa3b545eb0cffd832cd589a4b9dcd4a5eb4260e2c0d68a"}, - {file = "pycryptodome-3.11.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:da796e9221dda61a0019d01742337eb8a322de8598b678a4344ca0a436380315"}, - {file = "pycryptodome-3.11.0-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:ed45ef92d21db33685b789de2c015e9d9a18a74760a8df1fc152faee88cdf741"}, - {file = "pycryptodome-3.11.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4169ed515742425ff21e4bd3fabbb6994ffb64434472fb72230019bdfa36b939"}, - {file = "pycryptodome-3.11.0-pp27-pypy_73-win32.whl", hash = "sha256:f19edd42368e9057c39492947bb99570dc927123e210008f2af7cf9b505c6892"}, - {file = "pycryptodome-3.11.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:06162fcfed2f9deee8383fd59eaeabc7b7ffc3af50d3fad4000032deb8f700b0"}, - {file = "pycryptodome-3.11.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:6eda8a3157c91ba60b26a07bedd6c44ab8bda6cd79b6b5ea9744ba62c39b7b1e"}, - {file = "pycryptodome-3.11.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ff701fc283412e651eaab4319b3cd4eaa0827e94569cd37ee9075d5c05fe655"}, - {file = "pycryptodome-3.11.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:2a4bcc8a9977fee0979079cd33a9e9f0d3ddba5660d35ffe874cf84f1dd399d2"}, - {file = "pycryptodome-3.11.0.tar.gz", hash = "sha256:428096bbf7a77e207f418dfd4d7c284df8ade81d2dc80f010e92753a3e406ad0"}, + {file = "pycryptodome-3.12.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:90ad3381ccdc6a24cc2841e295706a168f32abefe64c679695712acac71fd5da"}, + {file = "pycryptodome-3.12.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e80f7469b0b3ea0f694230477d8501dc5a30a717e94fddd4821e6721f3053eae"}, + {file = "pycryptodome-3.12.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:b91404611767a7485837a6f1fd20cf9a5ae0ad362040a022cd65827ecb1b0d00"}, + {file = "pycryptodome-3.12.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:db66ccda65d5d20c17b00768e462a86f6f540f9aea8419a7f76cc7d9effd82cd"}, + {file = "pycryptodome-3.12.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:dc88355c4b261ed259268e65705b28b44d99570337694d593f06e3b1698eaaf3"}, + {file = "pycryptodome-3.12.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:6f8f5b7b53516da7511951910ab458e799173722c91fea54e2ba2f56d102e4aa"}, + {file = "pycryptodome-3.12.0-cp27-cp27m-win32.whl", hash = "sha256:93acad54a72d81253242eb0a15064be559ec9d989e5173286dc21cad19f01765"}, + {file = "pycryptodome-3.12.0-cp27-cp27m-win_amd64.whl", hash = "sha256:5a8c24d39d4a237dbfe181ea6593792bf9b5582c7fcfa7b8e0e12fda5eec07af"}, + {file = "pycryptodome-3.12.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:32d15da81959faea6cbed95df2bb44f7f796211c110cf90b5ad3b2aeeb97fc8e"}, + {file = "pycryptodome-3.12.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:aed7eb4b64c600fbc5e6d4238991ad1b4179a558401f203d1fcbd24883748982"}, + {file = "pycryptodome-3.12.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:341c6bbf932c406b4f3ee2372e8589b67ac0cf4e99e7dc081440f43a3cde9f0f"}, + {file = "pycryptodome-3.12.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:de0b711d673904dd6c65307ead36cb76622365a393569bf880895cba21195b7a"}, + {file = "pycryptodome-3.12.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:3558616f45d8584aee3eba27559bc6fd0ba9be6c076610ed3cc62bd5229ffdc3"}, + {file = "pycryptodome-3.12.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:a78e4324e566b5fbc2b51e9240950d82fa9e1c7eb77acdf27f58712f65622c1d"}, + {file = "pycryptodome-3.12.0-cp35-abi3-manylinux1_i686.whl", hash = "sha256:3f2f3dd596c6128d91314e60a6bcf4344610ef0e97f4ae4dd1770f86dd0748d8"}, + {file = "pycryptodome-3.12.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:e05f994f30f1cda3cbe57441f41220d16731cf99d868bb02a8f6484c454c206b"}, + {file = "pycryptodome-3.12.0-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:4cded12e13785bbdf4ba1ff5fb9d261cd98162145f869e4fbc4a4b9083392f0b"}, + {file = "pycryptodome-3.12.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:1181c90d1a6aee68a84826825548d0db1b58d8541101f908d779d601d1690586"}, + {file = "pycryptodome-3.12.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:6bb0d340c93bcb674ea8899e2f6408ec64c6c21731a59481332b4b2a8143cc60"}, + {file = "pycryptodome-3.12.0-cp35-abi3-win32.whl", hash = "sha256:39da5807aa1ff820799c928f745f89432908bf6624b9e981d2d7f9e55d91b860"}, + {file = "pycryptodome-3.12.0-cp35-abi3-win_amd64.whl", hash = "sha256:212c7f7fe11cad9275fbcff50ca977f1c6643f13560d081e7b0f70596df447b8"}, + {file = "pycryptodome-3.12.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:b07a4238465eb8c65dd5df2ab8ba6df127e412293c0ed7656c003336f557a100"}, + {file = "pycryptodome-3.12.0-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:a6e1bcd9d5855f1a3c0f8d585f44c81b08f39a02754007f374fb8db9605ba29c"}, + {file = "pycryptodome-3.12.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:aceb1d217c3a025fb963849071446cf3aca1353282fe1c3cb7bd7339a4d47947"}, + {file = "pycryptodome-3.12.0-pp27-pypy_73-win32.whl", hash = "sha256:f699360ae285fcae9c8f53ca6acf33796025a82bb0ccd7c1c551b04c1726def3"}, + {file = "pycryptodome-3.12.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d845c587ceb82ac7cbac7d0bf8c62a1a0fe7190b028b322da5ca65f6e5a18b9e"}, + {file = "pycryptodome-3.12.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:d8083de50f6dec56c3c6f270fb193590999583a1b27c9c75bc0b5cac22d438cc"}, + {file = "pycryptodome-3.12.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:9ea2f6674c803602a7c0437fccdc2ea036707e60456974fe26ca263bd501ec45"}, + {file = "pycryptodome-3.12.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:5d4264039a2087977f50072aaff2346d1c1c101cb359f9444cf92e3d1f42b4cd"}, + {file = "pycryptodome-3.12.0.zip", hash = "sha256:12c7343aec5a3b3df5c47265281b12b611f26ec9367b6129199d67da54b768c1"}, ] pyjwt = [ {file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"}, @@ -2218,6 +2341,20 @@ pytz = [ {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, ] +pywin32 = [ + {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"}, + {file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"}, + {file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"}, + {file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"}, + {file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"}, + {file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"}, + {file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"}, + {file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"}, + {file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"}, + {file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"}, + {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"}, + {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"}, +] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, @@ -2271,8 +2408,8 @@ rfc3986 = [ {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] rsa = [ - {file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"}, - {file = "rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"}, + {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, + {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, ] s3transfer = [ {file = "s3transfer-0.2.1-py2.py3-none-any.whl", hash = "sha256:b780f2411b824cb541dbcd2c713d0cb61c7d1bcadae204cdddda2b35cef493ba"}, @@ -2324,8 +2461,8 @@ sqlalchemy = [ ] storageclient = [] typing-extensions = [ - {file = "typing_extensions-4.0.0-py3-none-any.whl", hash = "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9"}, - {file = "typing_extensions-4.0.0.tar.gz", hash = "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed"}, + {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, + {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, ] uritemplate = [ {file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"}, @@ -2336,12 +2473,69 @@ urllib3 = [ {file = "urllib3-1.25.11.tar.gz", hash = "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"}, ] userdatamodel = [ - {file = "userdatamodel-2.3.3.tar.gz", hash = "sha256:b846b7efd2d002a653474fa3bd7bf2a2c964277ff5f8d9bde8e9d975aca8d130"}, + {file = "userdatamodel-2.4.0.tar.gz", hash = "sha256:11c3faf2c4a855e51305a02341123442bb6722bc518548e1da023b7a65136457"}, +] +websocket-client = [ + {file = "websocket-client-1.2.3.tar.gz", hash = "sha256:1315816c0acc508997eb3ae03b9d3ff619c9d12d544c9a9b553704b1cc4f6af5"}, + {file = "websocket_client-1.2.3-py3-none-any.whl", hash = "sha256:2eed4cc58e4d65613ed6114af2f380f7910ff416fc8c46947f6e76b6815f56c0"}, ] werkzeug = [ {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, ] +wrapt = [ + {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, + {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, + {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, + {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, + {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, + {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, + {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, + {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, + {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, + {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, + {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, + {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, + {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, + {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, + {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, + {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, + {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, + {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, + {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, +] wtforms = [ {file = "WTForms-3.0.0-py3-none-any.whl", hash = "sha256:232dbb0094847dca2f45c72136b5ca1d5dca2a3e24ccd2229823b8b74b3c6698"}, {file = "WTForms-3.0.0.tar.gz", hash = "sha256:4abfbaa1d529a1d0ac927d44af8dbb9833afd910e56448a103f1893b0b176886"}, From f1f70f9f50dfc9e87debf8bf03fa7f6ccb8203b0 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Mon, 20 Dec 2021 17:11:57 -0600 Subject: [PATCH 134/211] feat(visa_sync): choose client based on identity_provider for user, improve handling of IdPs throughout --- fence/job/visa_update_cronjob.py | 43 +++++--- fence/resources/ga4gh/passports.py | 20 +++- fence/resources/openid/ras_oauth2.py | 142 ++++++++++++++------------- fence/sync/sync_users.py | 14 ++- 4 files changed, 131 insertions(+), 88 deletions(-) diff --git a/fence/job/visa_update_cronjob.py b/fence/job/visa_update_cronjob.py index 78b1e4e49..ebbfa1916 100644 --- a/fence/job/visa_update_cronjob.py +++ b/fence/job/visa_update_cronjob.py @@ -145,30 +145,43 @@ async def updater(self, name, updater_queue, db_session): """ while True: user = await updater_queue.get() - # IF WE NEED TO UPDATE THEIR VISAS, DETERMINE WHICH CLIENT TO USE - # if idp is RAS then update visas? use that info to determine client? - if user.ga4gh_visas_v1: - for visa in user.ga4gh_visas_v1: - client = self._pick_client(visa) + try: + client = self._pick_client(user) + if client: self.logger.info( - "Updater {} updating visa for user {}".format( + "Updater {} updating authorization for user {}".format( name, user.username ) ) + # when getting access token, this persists new refresh token client.update_user_authorization(user, self.pkey_cache, db_session) - else: - # clear expired refresh tokens - if user.upstream_refresh_tokens: - user.upstream_refresh_tokens = [] - db_session.commit() - - self.logger.info( - "User {} doesn't have visa. Skipping . . .".format(user.username) + else: + self.logger.debug( + f"Updater {name} NOT updating authorization for " + f"user {user.username} because no client was found for IdP: {user.identity_provider}" + ) + except Exception as exc: + self.logger.error( + f"Updater {name} could not update authorization " + f"for {user.username}. Error: {exc}. Continuing." ) + pass updater_queue.task_done() - def _pick_client(self, visa): + def _pick_client(self, user): + """ + Pick oidc client according to the identity provider + """ + client = None + if ( + user.identity_provider + and getattr(user.identity_provider, "name") == self.ras_client.idp + ): + client = self.ras_client + return client + + def _pick_client_from_visa(self, visa): """ Pick oidc client according to the visa provider """ diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index bfc25e81d..5efea07b8 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -292,13 +292,16 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id, db_session=None): userdatamodel.user.User: the Fence user corresponding to issuer and subject_id """ db_session = db_session or current_session + logger.debug( + f"get_or_create_gen3_user_from_iss_sub: issuer: {issuer} & subject_id: {subject_id}" + ) iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((issuer, subject_id)) if not iss_sub_pair_to_user: username = subject_id + issuer[len("https://") :] gen3_user = query_for_user(session=db_session, username=username) + idp_name = flask.current_app.issuer_to_idp.get(issuer) + logger.debug(f"issuer_to_idp: {flask.current_app.issuer_to_idp}") if not gen3_user: - idp_name = flask.current_app.issuer_to_idp.get(issuer) - logger.debug(f"issuer_to_idp: {flask.current_app.issuer_to_idp}") gen3_user = create_user(db_session, logger, username, idp_name=idp_name) if not idp_name: logger.info( @@ -307,10 +310,21 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id, db_session=None): f"the issuer {issuer}" ) + # ensure user has an associated identity provider + if not gen3_user.identity_provider: + idp = ( + db_session.query(IdentityProvider) + .filter(IdentityProvider.name == idp_name) + .first() + ) + if not idp: + idp = IdentityProvider(name=idp_name) + gen3_user.identity_provider = idp + logger.info( f'Mapping subject id ("{subject_id}") and issuer ' f'("{issuer}") combination to Fence user ' - f'"{gen3_user.username}"' + f'"{gen3_user.username}" with IdP = "{idp_name}"' ) iss_sub_pair_to_user = IssSubPairToUser(iss=issuer, sub=subject_id) iss_sub_pair_to_user.user = gen3_user diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index b759918cb..78727e8b0 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -188,7 +188,9 @@ def get_user_id(self, code): "sub": userinfo.get("sub"), } - def map_iss_sub_pair_to_user(self, issuer, subject_id, username, email): + def map_iss_sub_pair_to_user( + self, issuer, subject_id, username, email, db_session=None + ): """ Map combination to a Fence user whose username equals the username argument passed into this function. @@ -210,79 +212,79 @@ def map_iss_sub_pair_to_user(self, issuer, subject_id, username, email): username that was passed in in all cases except for the exception noted above """ - with flask.current_app.db.session as db_session: - iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get( - (issuer, subject_id) - ) - user = query_for_user(db_session, username) - if iss_sub_pair_to_user: - if not user: - self.logger.info( - f'Issuer ("{issuer}") and subject id ("{subject_id}") ' - "have already been mapped to a Fence user " - f'("{iss_sub_pair_to_user.user.username}") created ' - "from the DRS endpoint. Changing said user's username" - f' to "{username}".' - ) - - tries = 2 - for i in range(tries): - try: - flask.current_app.arborist.update_user( - iss_sub_pair_to_user.user.username, - new_username=username, - new_email=email, - ) - except ArboristError as e: - self.logger.warning( - f"Try {i+1}: could not update user's username in Arborist: {e}" - ) - if i == tries - 1: - err_msg = f"Failed to update user's username in Arborist after {tries} tries" - self.logger.exception(err_msg) - raise InternalError(err_msg) - else: - self.logger.info( - "Successfully changed Arborist user's username from " - f'"{iss_sub_pair_to_user.user.username}" to "{username}"' - ) - break - - iss_sub_pair_to_user.user.username = username - if email: - iss_sub_pair_to_user.user.email = email - db_session.commit() - elif iss_sub_pair_to_user.user.username != username: - self.logger.warning( - "Two users exist in the Fence database corresponding " - "to the user who is currently trying to log in: one " - f'created from an earlier login ("{username}") and ' - f"one created from the DRS endpoint " - f'("{iss_sub_pair_to_user.user.username}"). ' - f'"{iss_sub_pair_to_user.user.username}" will be ' - f'logged in, rendering "{username}" inaccessible.' - ) - return iss_sub_pair_to_user.user.username - + db_session = db_session or current_session + iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get( + (issuer, subject_id) + ) + user = query_for_user(db_session, username) + if iss_sub_pair_to_user: if not user: - user = create_user( - db_session, - self.logger, - username, - email=email, - idp_name=IdentityProvider.ras, + self.logger.info( + f'Issuer ("{issuer}") and subject id ("{subject_id}") ' + "have already been mapped to a Fence user " + f'("{iss_sub_pair_to_user.user.username}") created ' + "from the DRS endpoint. Changing said user's username" + f' to "{username}".' ) - self.logger.info( - f'Mapping issuer ("{issuer}") and subject id ("{subject_id}") ' - f'combination to Fence user "{user.username}"' - ) - iss_sub_pair_to_user = IssSubPairToUser(iss=issuer, sub=subject_id) - iss_sub_pair_to_user.user = user - db_session.add(iss_sub_pair_to_user) - db_session.commit() + tries = 2 + for i in range(tries): + try: + flask.current_app.arborist.update_user( + iss_sub_pair_to_user.user.username, + new_username=username, + new_email=email, + ) + except ArboristError as e: + self.logger.warning( + f"Try {i+1}: could not update user's username in Arborist: {e}" + ) + if i == tries - 1: + err_msg = f"Failed to update user's username in Arborist after {tries} tries" + self.logger.exception(err_msg) + raise InternalError(err_msg) + else: + self.logger.info( + "Successfully changed Arborist user's username from " + f'"{iss_sub_pair_to_user.user.username}" to "{username}"' + ) + break + + iss_sub_pair_to_user.user.username = username + if email: + iss_sub_pair_to_user.user.email = email + db_session.commit() + elif iss_sub_pair_to_user.user.username != username: + self.logger.warning( + "Two users exist in the Fence database corresponding " + "to the user who is currently trying to log in: one " + f'created from an earlier login ("{username}") and ' + f"one created from the DRS endpoint " + f'("{iss_sub_pair_to_user.user.username}"). ' + f'"{iss_sub_pair_to_user.user.username}" will be ' + f'logged in, rendering "{username}" inaccessible.' + ) return iss_sub_pair_to_user.user.username + if not user: + user = create_user( + db_session, + self.logger, + username, + email=email, + idp_name=IdentityProvider.ras, + ) + + self.logger.info( + f'Mapping issuer ("{issuer}") and subject id ("{subject_id}") ' + f'combination to Fence user "{user.username}"' + ) + iss_sub_pair_to_user = IssSubPairToUser(iss=issuer, sub=subject_id) + iss_sub_pair_to_user.user = user + db_session.add(iss_sub_pair_to_user) + db_session.commit() + return iss_sub_pair_to_user.user.username + @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) def update_user_authorization(self, user, pkey_cache, db_session=current_session): """ @@ -291,6 +293,8 @@ def update_user_authorization(self, user, pkey_cache, db_session=current_session """ try: token_endpoint = self.get_value_from_discovery_doc("token_endpoint", "") + + # this get_access_token also persists the refresh token in the db token = self.get_access_token(user, token_endpoint, db_session) userinfo = self.get_userinfo(token) passport = self.get_encoded_passport_v11_userinfo(userinfo) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 2345703a9..662ca80b7 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -32,6 +32,7 @@ User, query_for_user, Client, + IdentityProvider, ) from fence.resources.storage import StorageManager from fence.resources.google.access_utils import bulk_update_google_groups @@ -1002,6 +1003,17 @@ def _upsert_userinfo(self, sess, user_info): u.phone_number = user_info[username].get("phone_number", "") u.is_admin = user_info[username].get("admin", False) + idp_name = user_info[username].get("idp_name", "") + if idp_name and not u.identity_provider: + idp = ( + sess.query(IdentityProvider) + .filter(IdentityProvider.name == idp_name) + .first() + ) + if not idp: + idp = IdentityProvider(name=idp_name) + u.identity_provider = idp + # do not update if there is no tag if not user_info[username].get("tags"): continue @@ -2101,7 +2113,7 @@ def sync_single_user_visas( self.parse_consent_code, ) except Exception: - logging.warning( + self.logger.warning( f"ignoring unsuccessfully parsed or expired visa: {encoded_visa}" ) continue From 3f66abed16b25fe1b7a7874351d5626788e2d711 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Mon, 20 Dec 2021 17:12:44 -0600 Subject: [PATCH 135/211] chore(logging): add more details --- fence/auth.py | 1 + fence/models.py | 5 ++++- fence/resources/openid/idp_oauth2.py | 5 ++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/fence/auth.py b/fence/auth.py index 37eb9a221..f05794442 100644 --- a/fence/auth.py +++ b/fence/auth.py @@ -114,6 +114,7 @@ def set_flask_session_values(user): if id_from_idp: user.id_from_idp = id_from_idp + # TODO Update iss_sub mapping table? # setup idp connection for new user (or existing user w/o it setup) idp = ( diff --git a/fence/models.py b/fence/models.py index 6731ea98d..88310b6f1 100644 --- a/fence/models.py +++ b/fence/models.py @@ -84,7 +84,10 @@ def create_user(session, logger, username, email=None, idp_name=None): Return: userdatamodel.user.User: the created user """ - logger.info(f'Creating a new Fence user with username "{username}"') + logger.info( + f"Creating a new user with username: {username}, " + f"email: {email}, and idp_name: {idp_name}" + ) user = User(username=username) if email: diff --git a/fence/resources/openid/idp_oauth2.py b/fence/resources/openid/idp_oauth2.py index 35077c170..99e9046d2 100644 --- a/fence/resources/openid/idp_oauth2.py +++ b/fence/resources/openid/idp_oauth2.py @@ -147,9 +147,8 @@ def get_user_id(self, code): raise NotImplementedError() def get_access_token(self, user, token_endpoint, db_session=None): - """ - Get access_token using a refresh_token and store it in upstream_refresh_token table. + Get access_token using a refresh_token and store new refresh in upstream_refresh_token table. """ refresh_token = None expires = None @@ -160,7 +159,7 @@ def get_access_token(self, user, token_endpoint, db_session=None): expires = row.expires if not refresh_token: - raise AuthError("User doesnt have a refresh token") + raise AuthError("User doesn't have a refresh token") if time.time() > expires: raise AuthError("Refresh token expired. Please login again.") From 139c3d3f66996eec8e402a8ca1e18aafb8c6dd81 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Mon, 20 Dec 2021 17:13:54 -0600 Subject: [PATCH 136/211] chore(tests): refactor/rewrite RAS and usersync tests to reflect new expected behavior --- .secrets.baseline | 8 +- tests/conftest.py | 110 ++++++++- tests/dbgap_sync/conftest.py | 129 +++++++++-- tests/dbgap_sync/test_user_sync.py | 346 +++++++++++++++++++++-------- tests/ras/test_ras.py | 123 ++++------ 5 files changed, 522 insertions(+), 194 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index c4608fe23..f3b460935 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -207,14 +207,14 @@ "filename": "tests/conftest.py", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 1364 + "line_number": 1472 }, { "type": "Base64 High Entropy String", "filename": "tests/conftest.py", "hashed_secret": "227dea087477346785aefd575f91dd13ab86c108", "is_verified": false, - "line_number": 1387 + "line_number": 1495 } ], "tests/credentials/google/test_credentials.py": [ @@ -250,7 +250,7 @@ "filename": "tests/ras/test_ras.py", "hashed_secret": "d9db6fe5c14dc55edd34115cdf3958845ac30882", "is_verified": false, - "line_number": 122 + "line_number": 120 } ], "tests/test-fence-config.yaml": [ @@ -263,5 +263,5 @@ } ] }, - "generated_at": "2021-12-08T20:43:31Z" + "generated_at": "2021-12-20T23:13:45Z" } diff --git a/tests/conftest.py b/tests/conftest.py index f011c1fd0..b3422a6a7 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ import os import copy import time +import flask from datetime import datetime import mock @@ -38,10 +39,12 @@ from fence.config import config from fence.errors import NotFound from fence.resources.openid.microsoft_oauth2 import MicrosoftOauth2Client +from fence.jwt.validate import validate_jwt import tests from tests import test_settings from tests import utils +from tests.utils import TEST_RAS_USERNAME, TEST_RAS_SUB from tests.utils.oauth2.client import OAuth2TestClient @@ -236,6 +239,111 @@ def kid_2(): return "test-keypair-2" +def get_subjects_to_passports( + subject_to_encoded_visas=None, passport_exp=None, kid=None, rsa_private_key=None +): + subject_to_encoded_visas = subject_to_encoded_visas or {} + passport_exp = passport_exp or int(time.time()) + 1000 + subjects = subject_to_encoded_visas.keys() or [TEST_RAS_SUB] + + output = {} + for subject in subjects: + visas = [] + encoded_visas = subject_to_encoded_visas.get(subject) + if not encoded_visas: + visas = [ + { + "iss": "https://stsstg.nih.gov", + "sub": subject, + "iat": int(time.time()), + "exp": int(time.time()) + 1000, + "scope": "openid ga4gh_passport_v1 email profile", + "jti": "jtiajoidasndokmasdl", + "txn": "sapidjspa.asipidja", + "name": "", + "ga4gh_visa_v1": { + "type": "https://ras.nih.gov/visas/v1", + "asserted": int(time.time()), + "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", + "source": "https://ncbi.nlm.nih.gov/gap", + }, + } + ] + + headers = {"kid": kid} + encoded_visas = [] + + for visa in visas: + encoded_visa = jwt.encode( + visa, key=rsa_private_key, headers=headers, algorithm="RS256" + ).decode("utf-8") + encoded_visas.append(encoded_visa) + + passport_header = { + "type": "JWT", + "alg": "RS256", + "kid": kid, + } + new_passport = { + "iss": "https://stsstg.nih.gov", + "sub": subject, + "iat": int(time.time()), + "scope": "openid ga4gh_passport_v1 email profile", + "exp": int(time.time()) + 1000, + "ga4gh_passport_v1": encoded_visas, + } + + encoded_passport = jwt.encode( + new_passport, + key=rsa_private_key, + headers=passport_header, + algorithm="RS256", + ).decode("utf-8") + + output[subject] = { + "visas": visas, + "encoded_visas": encoded_visas, + "new_passport": new_passport, + "encoded_passport": encoded_passport, + } + return output + + +@pytest.fixture(scope="function") +def no_app_context_no_public_keys(): + mock_validate_jwt = MagicMock()() + + # ensure we don't actually try to reach out to external sites to refresh public keys + def validate_jwt_no_key_refresh(*args, **kwargs): + kwargs.update({"attempt_refresh": False}) + return validate_jwt(*args, **kwargs) + + mock_validate_jwt.side_effect = validate_jwt_no_key_refresh + + # ensure there is no application context or cached keys + if flask.current_app: + temp_stored_public_keys = flask.current_app.jwt_public_keys + temp_app_context = flask.has_app_context + flask.current_app.jwt_public_keys = {} + + def return_false(): + return False + + flask.has_app_context = return_false + + patcher = patch("fence.resources.ga4gh.passports.validate_jwt", mock_validate_jwt) + patcher.start() + + yield mock_validate_jwt + + patcher.stop() + + # restore public keys and context + if flask.current_app: + flask.current_app.jwt_public_keys = temp_stored_public_keys + flask.has_app_context = temp_app_context + + @pytest.fixture(scope="function") def mock_arborist_requests(request): """ @@ -459,10 +567,10 @@ def db_session(db, patch_app_db_session): session.query(models.User).delete() session.query(models.IssSubPairToUser).delete() session.query(models.Project).delete() + session.query(models.GA4GHVisaV1).delete() session.commit() session.close() - transaction.rollback() connection.close() diff --git a/tests/dbgap_sync/conftest.py b/tests/dbgap_sync/conftest.py index f0d4861c6..02c5b650a 100644 --- a/tests/dbgap_sync/conftest.py +++ b/tests/dbgap_sync/conftest.py @@ -5,19 +5,30 @@ from unittest.mock import MagicMock, patch from yaml import safe_load as yaml_load +from cdislogging import get_logger from cirrus import GoogleCloudManager from cdisutilstest.code.storage_client_mock import get_client, StorageClientMocker import pytest from userdatamodel import Base from userdatamodel.models import * from userdatamodel.driver import SQLAlchemyDriver +from gen3authz.client.arborist.client import ArboristClient +from fence.config import config +from fence.resources.openid.ras_oauth2 import RASOauth2Client +from fence.auth import login_user from fence.sync.sync_users import UserSyncer from fence.resources import userdatamodel as udm +from fence.models import ( + AccessPrivilege, + AuthorizationProvider, + User, + GA4GHVisaV1, + create_user, + User, +) -from fence.models import AccessPrivilege, AuthorizationProvider, User, GA4GHVisaV1 - -from gen3authz.client.arborist.client import ArboristClient +logger = get_logger(__name__) LOCAL_CSV_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data/csv") @@ -72,6 +83,11 @@ def storage_client(): @pytest.fixture def syncer(db_session, request, rsa_private_key, kid): + # reset GA4GH visas and users table + db_session.query(User).delete() + db_session.query(GA4GHVisaV1).delete() + db_session.commit() + if request.param == "google": backend = "google" else: @@ -82,28 +98,47 @@ def syncer(db_session, request, rsa_private_key, kid): provider = [{"name": backend_name, "backend": backend}] users = [ - {"username": "TESTUSERB", "is_admin": True, "email": "userA@gmail.com"}, - {"username": "USER_1", "is_admin": True, "email": "user1@gmail.com"}, + { + "username": "TESTUSERB", + "is_admin": True, + "email": "userA@gmail.com", + "idp_name": "ras", + }, + { + "username": "USER_1", + "is_admin": True, + "email": "user1@gmail.com", + "idp_name": "ras", + }, { "username": "test_user1@gmail.com", "is_admin": False, "email": "test_user1@gmail.com", + "idp_name": "ras", }, { "username": "deleted_user@gmail.com", "is_admin": True, "email": "deleted_user@gmail.com", + "idp_name": "ras", + }, + { + "username": "TESTUSERD", + "is_admin": True, + "email": "userD@gmail.com", + "idp_name": "ras", }, - {"username": "TESTUSERD", "is_admin": True, "email": "userD@gmail.com"}, { "username": "expired_visa_user", "is_admin": False, "email": "expired@expired.com", + "idp_name": "ras", }, { "username": "invalid_visa_user", "is_admin": False, "email": "invalid@invalid.com", + "idp_name": "ras", }, ] @@ -195,23 +230,30 @@ def mocked_get(path, **kwargs): sa["name"], db_session, bucket, p ) - for user in users: - user = User(**user) - db_session.add(user) - add_visa_manually(db_session, user, rsa_private_key, kid) - db_session.commit() return syncer_obj -def add_visa_manually(db_session, user, rsa_private_key, kid, expires=None): +def get_test_encoded_decoded_visa_and_exp( + db_session, + user, + rsa_private_key, + kid, + expires=None, + sub=None, + make_invalid=False, +): + """ + user can be a db user object or just a username + """ expires = expires or int(time.time()) + 1000 headers = {"kid": kid} + sub = sub or "abcde12345aspdij" decoded_visa = { "iss": "https://stsstg.nih.gov", - "sub": "abcde12345aspdij", + "sub": sub, "iat": int(time.time()), "exp": expires, "scope": "openid ga4gh_passport_v1 email profile", @@ -288,13 +330,33 @@ def add_visa_manually(db_session, user, rsa_private_key, kid, expires=None): ).decode("utf-8") expires = int(decoded_visa["exp"]) - if user.username == "expired_visa_user": - expires -= 100000 - if user.username == "invalid_visa_user": - encoded_visa = encoded_visa[: len(encoded_visa) // 2] - if user.username == "TESTUSERD": + + if make_invalid: encoded_visa = encoded_visa[: len(encoded_visa) // 2] + return encoded_visa, decoded_visa, expires + + +def add_visa_manually(db_session, user, rsa_private_key, kid, expires=None): + expires = expires or int(time.time()) + 1000 + make_invalid = False + + if getattr(user, "username", user) == "expired_visa_user": + expires -= 100000 + if getattr(user, "username", user) == "invalid_visa_user": + make_invalid = True + if getattr(user, "username", user) == "TESTUSERD": + make_invalid = True + + encoded_visa, decoded_visa, expires = get_test_encoded_decoded_visa_and_exp( + db_session, + user, + rsa_private_key, + kid, + expires=expires, + make_invalid=make_invalid, + ) + visa = GA4GHVisaV1( user=user, source=decoded_visa["ga4gh_visa_v1"]["source"], @@ -308,3 +370,34 @@ def add_visa_manually(db_session, user, rsa_private_key, kid, expires=None): db_session.commit() return encoded_visa, visa + + +def fake_ras_login(username, subject, email=None, db_session=None): + """ + Mock a login by creating a sub/iss mapping in the db and logging them into a + session. + + Args: + username (str): Username from IdP + subject (str): sub id in tokens from IdP + email (None, optional): email if provided + db_session (None, optional): db session to use + """ + ras_client = RASOauth2Client( + config["OPENID_CONNECT"]["ras"], + HTTP_PROXY=config["HTTP_PROXY"], + logger=logger, + ) + actual_username = ras_client.map_iss_sub_pair_to_user( + issuer="https://stsstg.nih.gov", + subject_id=subject, + username=username, + email=email, + db_session=db_session, + ) + logger.debug( + f"subject: {subject}, username: {username}, actual_username: {actual_username}" + ) + login_user(actual_username, provider="ras", email=None, id_from_idp=subject) + + # todo sub to iss table diff --git a/tests/dbgap_sync/test_user_sync.py b/tests/dbgap_sync/test_user_sync.py index 3f97ce907..4d51baf2d 100644 --- a/tests/dbgap_sync/test_user_sync.py +++ b/tests/dbgap_sync/test_user_sync.py @@ -2,13 +2,24 @@ import pytest import yaml +import asyncio +import flask from unittest.mock import MagicMock +import mock from fence import models from fence.sync.sync_users import _format_policy_id from fence.config import config +from fence.job.visa_update_cronjob import Visa_Token_Update from tests.dbgap_sync.conftest import LOCAL_YAML_DIR +from tests.utils import TEST_RAS_USERNAME, TEST_RAS_SUB +from tests.dbgap_sync.conftest import ( + get_test_encoded_decoded_visa_and_exp, + fake_ras_login, +) +from tests.conftest import get_subjects_to_passports + def equal_project_access(d1, d2): """ @@ -76,7 +87,9 @@ def test_sync( syncer.sync() users = db_session.query(models.User).all() - assert len(users) == 14 + + # 5 from user.yaml, 4 from fake dbgap SFTP + assert len(users) == 9 if parse_consent_code_config: user = models.query_for_user(session=db_session, username="USERC") @@ -152,12 +165,8 @@ def test_sync( } assert len(user_access) == 1 - # TODO: check user policy access (add in user sync changes) - user = models.query_for_user(session=db_session, username="deleted_user@gmail.com") - assert not user.is_admin - user_access = db_session.query(models.AccessPrivilege).filter_by(user=user).all() - assert not user_access + assert not user @pytest.mark.parametrize("syncer", ["google"], indirect=True) @@ -613,92 +622,239 @@ def mock_merge(dbgap_servers, sess): assert syncer._process_dbgap_files.call_count == 2 -# @pytest.mark.parametrize("syncer", ["cleversafe", "google"], indirect=True) -# @pytest.mark.parametrize("parse_consent_code_config", [False, True]) -# def test_user_sync_with_visas( -# syncer, -# db_session, -# storage_client, -# parse_consent_code_config, -# monkeypatch, -# ): -# # patch the sync to use the parameterized config value -# monkeypatch.setitem( -# syncer.dbGaP[0], "parse_consent_code", parse_consent_code_config -# ) -# monkeypatch.setattr(syncer, "parse_consent_code", parse_consent_code_config) - -# syncer.sync_visas() - -# users = db_session.query(models.User).all() - -# user = models.query_for_user( -# session=db_session, username="TESTUSERB" -# ) # contains only visa information - -# backup_user = models.query_for_user( -# session=db_session, username="TESTUSERD" -# ) # Contains invalid visa and also in telemetry file - -# expired_user = models.query_for_user( -# session=db_session, -# username="expired_visa_user", -# ) -# invalid_user = models.query_for_user( -# session=db_session, username="invalid_visa_user" -# ) - -# assert len(invalid_user.project_access) == 0 -# assert len(expired_user.project_access) == 0 - -# assert len(invalid_user.ga4gh_visas_v1) == 0 -# assert len(expired_user.ga4gh_visas_v1) == 0 -# -# assert len(users) == 12 -# assert len(backup_user.project_access) == 0 -# if parse_consent_code_config: -# assert equal_project_access( -# user.project_access, -# { -# "phs000991.c1": ["read", "read-storage"], -# "phs000961.c1": ["read", "read-storage"], -# "phs000279.c1": ["read", "read-storage"], -# "phs000286.c3": ["read", "read-storage"], -# "phs000289.c2": ["read", "read-storage"], -# "phs000298.c1": ["read", "read-storage"], -# }, -# ) -# else: -# assert equal_project_access( -# user.project_access, -# { -# "phs000991": ["read", "read-storage"], -# "phs000961": ["read", "read-storage"], -# "phs000279": ["read", "read-storage"], -# "phs000286": ["read", "read-storage"], -# "phs000289": ["read", "read-storage"], -# "phs000298": ["read", "read-storage"], -# }, -# ) - - -# @pytest.mark.parametrize("syncer", ["google"], indirect=True) -# def test_sync_in_login( -# syncer, -# db_session, -# storage_client, -# rsa_private_key, -# kid, -# monkeypatch, -# ): -# user = models.query_for_user( -# session=db_session, username="TESTUSERB" -# ) # contains no information -# synced_visas = syncer.sync_single_user_visas(user, user.ga4gh_visas_v1, db_session, expires=99999999) -# user = models.query_for_user( -# session=db_session, username="TESTUSERB" -# ) # contains only visa information -# user1 = models.query_for_user(session=db_session, username="USER_1") -# assert len(user1.project_access) == 0 # other users are not affected -# assert len(user.project_access) == 6 -# assert len(synced_visas) +def setup_ras_sync_testing( + mock_discovery, + mock_get_token, + db_session, + rsa_private_key, + kid, + mock_userinfo, + mock_arborist_requests, +): + """ + BEGIN Setup + - make sure no app context + - setup mock RAS responses for various users + - setup fake access tokens + - make userinfo respond with passport and visas (some valid, some expired, some invalid) + """ + setup_info = {} + + mock_arborist_requests({"arborist/user/TESTUSERB": {"PATCH": (None, 204)}}) + mock_arborist_requests( + {"arborist/user/test_user1@gmail.com": {"PATCH": (None, 204)}} + ) + mock_arborist_requests({"arborist/user/TESTUSERD": {"PATCH": (None, 204)}}) + mock_arborist_requests({"arborist/user/USERF": {"PATCH": (None, 204)}}) + + mock_discovery.return_value = "https://ras/token_endpoint" + + def get_token_response_for_user(*args, **kwargs): + token_response = { + "access_token": f"{args[0].username}", + "id_token": f"{args[0].username}-id12345abcdef", + "refresh_token": f"{args[0].username}-refresh12345abcdefg", + } + return token_response + + mock_get_token.side_effect = get_token_response_for_user + + usernames_to_ras_subjects = { + "TESTUSERB": "sub-TESTUSERB-1234", + "test_user1@gmail.com": "sub-test_user1@gmail.com-1234", + "TESTUSERD": "sub-TESTUSERD-1234", + "USERF": "sub-USERF-1234", + } + + setup_info["usernames_to_ras_subjects"] = usernames_to_ras_subjects + + subjects_to_encoded_visas = { + usernames_to_ras_subjects["TESTUSERB"]: [ + get_test_encoded_decoded_visa_and_exp( + db_session, + "TESTUSERB", + rsa_private_key, + kid, + sub=usernames_to_ras_subjects["TESTUSERB"], + )[0] + ], + usernames_to_ras_subjects["test_user1@gmail.com"]: [ + get_test_encoded_decoded_visa_and_exp( + db_session, + "test_user1@gmail.com", + rsa_private_key, + kid, + expires=1, + sub=usernames_to_ras_subjects["test_user1@gmail.com"], + )[0] + ], + # note: get_test_encoded_decoded_visa_and_exp makes the visas for the next 2 users completely invalid + usernames_to_ras_subjects["TESTUSERD"]: [ + get_test_encoded_decoded_visa_and_exp( + db_session, + "TESTUSERD", + rsa_private_key, + kid, + sub=usernames_to_ras_subjects["TESTUSERD"], + make_invalid=True, + )[0] + ], + usernames_to_ras_subjects["USERF"]: [ + get_test_encoded_decoded_visa_and_exp( + db_session, + "USERF", + rsa_private_key, + kid, + sub=usernames_to_ras_subjects["USERF"], + make_invalid=True, + )[0] + ], + } + + setup_info["subjects_to_encoded_visas"] = subjects_to_encoded_visas + + subjects_to_passports = get_subjects_to_passports( + subjects_to_encoded_visas, kid=kid, rsa_private_key=rsa_private_key + ) + + setup_info["subjects_to_passports"] = subjects_to_passports + + def get_userinfo_for_user(*args, **kwargs): + # username is the access token only b/c of the way the mocks are setup + username = args[0]["access_token"] + + # sub is likely different than username + sub = f"sub-{username}-1234" + userinfo_response = { + "sub": sub, + "name": "", + "preferred_username": "someuser@era.com", + "UID": "", + "UserID": username, + "email": "", + } + subject_to_passports = subjects_to_passports.get(sub) or {} + userinfo_response["passport_jwt_v11"] = subject_to_passports.get( + "encoded_passport" + ) + return userinfo_response + + mock_userinfo.side_effect = get_userinfo_for_user + return setup_info + + +@pytest.mark.parametrize("syncer", ["cleversafe", "google"], indirect=True) +@mock.patch("fence.resources.openid.ras_oauth2.RASOauth2Client.get_userinfo") +@mock.patch("fence.resources.openid.ras_oauth2.RASOauth2Client.get_access_token") +@mock.patch( + "fence.resources.openid.ras_oauth2.RASOauth2Client.get_value_from_discovery_doc" +) +def test_user_sync_with_visa_sync_job( + mock_discovery, + mock_get_token, + mock_userinfo, + syncer, + db_session, + storage_client, + monkeypatch, + kid, + rsa_public_key, + rsa_private_key, + mock_arborist_requests, + no_app_context_no_public_keys, +): + """ + Test that visas and authorization from them only get added to the database + after visa sync job and not by usersync alone. Ensure usersync does not + alter visa information. + + NOTE: syncer above creates users as if they already exist before this usersync + and they have a specified IdP == RAS (e.g. they should get visas synced) + """ + setup_info = setup_ras_sync_testing( + mock_discovery, + mock_get_token, + db_session, + rsa_private_key, + kid, + mock_userinfo, + mock_arborist_requests, + ) + + # reset GA4GH visas table + # db_session.query(models.User).delete() + # db_session.query(models.GA4GHVisaV1).delete() + # db_session.commit() + + # Usersync + syncer.sync() + + users_after = db_session.query(models.User).all() + + # 5 from user.yaml, 4 from fake dbgap SFTP + assert len(users_after) == 9 + + for user in users_after: + if user.username in setup_info["usernames_to_ras_subjects"]: + # at this point, we will mock a login event by the user (at which point we'd get + # a refresh token we can update visas with later) + fake_ras_login( + user.username, + setup_info["usernames_to_ras_subjects"][user.username], + db_session=db_session, + ) + + # make sure no one has visas yet + assert not user.ga4gh_visas_v1 + + # use refresh tokens from users to call access token polling "fence-create update-visa" + # and sync authorization from visas + job = Visa_Token_Update() + job.pkey_cache = { + "https://stsstg.nih.gov": { + kid: rsa_public_key, + } + } + loop = asyncio.get_event_loop() + loop.run_until_complete(job.update_tokens(db_session)) + + users_after_visas_sync = db_session.query(models.User).all() + + # now let's check that actual authorization / visas got added as expected + valid_user = models.query_for_user(session=db_session, username="TESTUSERB") + + user_with_invalid_visa_also_in_telemetry_file = models.query_for_user( + session=db_session, username="TESTUSERD" + ) + + user_with_invalid_visa_also_in_telemetry_file_2 = models.query_for_user( + session=db_session, username="USERF" + ) + + user_with_expired_visa_also_in_telemetry_file = models.query_for_user( + session=db_session, + username="test_user1@gmail.com", + ) + + # make sure no access or visas for users not expected to have any + assert ( + user_with_invalid_visa_also_in_telemetry_file + and len(user_with_invalid_visa_also_in_telemetry_file.ga4gh_visas_v1) == 0 + ) + assert ( + user_with_invalid_visa_also_in_telemetry_file_2 + and len(user_with_invalid_visa_also_in_telemetry_file_2.ga4gh_visas_v1) == 0 + ) + assert ( + user_with_expired_visa_also_in_telemetry_file + and len(user_with_expired_visa_also_in_telemetry_file.ga4gh_visas_v1) == 0 + ) + + assert valid_user and valid_user.ga4gh_visas_v1 + assert len(valid_user.ga4gh_visas_v1) == 1 + assert ( + valid_user.ga4gh_visas_v1[0].ga4gh_visa + in setup_info["subjects_to_encoded_visas"][ + setup_info["usernames_to_ras_subjects"][valid_user.username] + ] + ) diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index a13a189b2..68e7fd784 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -27,6 +27,7 @@ from tests.dbgap_sync.conftest import add_visa_manually from fence.job.visa_update_cronjob import Visa_Token_Update import tests.utils +from tests.conftest import get_subjects_to_passports logger = get_logger(__name__, log_level="debug") @@ -79,9 +80,7 @@ def test_store_refresh_token(db_session): @mock.patch( "fence.resources.openid.ras_oauth2.RASOauth2Client.get_value_from_discovery_doc" ) -@mock.patch("fence.resources.ga4gh.passports.validate_jwt") def test_update_visa_token( - mock_validate_jwt, mock_discovery, mock_get_token, mock_userinfo, @@ -91,6 +90,7 @@ def test_update_visa_token( rsa_public_key, kid, mock_arborist_requests, + no_app_context_no_public_keys, ): """ Test to check visa table is updated when getting new visa @@ -100,8 +100,6 @@ def validate_jwt_no_key_refresh(*args, **kwargs): kwargs.update({"attempt_refresh": False}) return validate_jwt(*args, **kwargs) - mock_validate_jwt.side_effect = validate_jwt_no_key_refresh - # ensure there is no application context or cached keys temp_stored_public_keys = flask.current_app.jwt_public_keys temp_app_context = flask.has_app_context @@ -151,48 +149,14 @@ def return_false(): logger=logger, ) - new_visa = { - "iss": "https://stsstg.nih.gov", - "sub": TEST_RAS_SUB, - "iat": int(time.time()), - "exp": int(time.time()) + 1000, - "scope": "openid ga4gh_passport_v1 email profile", - "jti": "jtiajoidasndokmasdl", - "txn": "sapidjspa.asipidja", - "name": "", - "ga4gh_visa_v1": { - "type": "https://ras.nih.gov/visas/v1", - "asserted": int(time.time()), - "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", - "source": "https://ncbi.nlm.nih.gov/gap", - }, - } - - headers = {"kid": kid} - - encoded_visa = jwt.encode( - new_visa, key=rsa_private_key, headers=headers, algorithm="RS256" - ).decode("utf-8") - - passport_header = { - "type": "JWT", - "alg": "RS256", - "kid": kid, - } - new_passport = { - "iss": "https://stsstg.nih.gov", - "sub": TEST_RAS_SUB, - "iat": int(time.time()), - "scope": "openid ga4gh_passport_v1 email profile", - "exp": int(time.time()) + 1000, - "ga4gh_passport_v1": [encoded_visa], - } - - encoded_passport = jwt.encode( - new_passport, key=rsa_private_key, headers=passport_header, algorithm="RS256" - ).decode("utf-8") + # use default user and passport + subjects_to_passports = get_subjects_to_passports( + kid=kid, rsa_private_key=rsa_private_key + ) - userinfo_response["passport_jwt_v11"] = encoded_passport + userinfo_response["passport_jwt_v11"] = subjects_to_passports[TEST_RAS_SUB][ + "encoded_passport" + ] mock_userinfo.return_value = userinfo_response pkey_cache = { @@ -217,7 +181,8 @@ def return_false(): # and the new visa should also show up assert len(query_visas) == 2 assert existing_encoded_visa in query_visas - assert encoded_visa in query_visas + for visa in subjects_to_passports[TEST_RAS_SUB]["encoded_visas"]: + assert visa in query_visas @mock.patch("fence.resources.openid.ras_oauth2.RASOauth2Client.get_userinfo") @@ -397,7 +362,6 @@ def test_update_visa_empty_visa_returned( ) @mock.patch("fence.resources.ga4gh.passports.validate_jwt") def test_update_visa_token_with_invalid_visa( - mock_validate_jwt, mock_discovery, mock_get_token, mock_userinfo, @@ -407,29 +371,13 @@ def test_update_visa_token_with_invalid_visa( rsa_public_key, kid, mock_arborist_requests, + no_app_context_no_public_keys, ): """ Test to check the following case: Received visa: [good1, bad2, good3] Processed/stored visa: [good1, good3] """ - # ensure we don't actually try to reach out to external sites to refresh public keys - def validate_jwt_no_key_refresh(*args, **kwargs): - kwargs.update({"attempt_refresh": False}) - return validate_jwt(*args, **kwargs) - - mock_validate_jwt.side_effect = validate_jwt_no_key_refresh - - # ensure there is no application context or cached keys - temp_stored_public_keys = flask.current_app.jwt_public_keys - temp_app_context = flask.has_app_context - del flask.current_app.jwt_public_keys - - def return_false(): - return False - - flask.has_app_context = return_false - mock_arborist_requests( {f"arborist/user/{TEST_RAS_USERNAME}": {"PATCH": (None, 204)}} ) @@ -522,11 +470,6 @@ def return_false(): ras_client.update_user_authorization( test_user, pkey_cache=pkey_cache, db_session=db_session ) - - # restore public keys and context - flask.current_app.jwt_public_keys = temp_stored_public_keys - flask.has_app_context = temp_app_context - # at this point we expect the existing visa to stay around (since it hasn't expired) # and 2 new good visas query_visas = [ @@ -660,7 +603,7 @@ def return_false(): @mock.patch( "fence.resources.openid.ras_oauth2.RASOauth2Client.get_value_from_discovery_doc" ) -def dont_test_visa_update_cronjob( +def test_visa_update_cronjob( mock_discovery, mock_get_token, mock_userinfo, @@ -676,6 +619,9 @@ def dont_test_visa_update_cronjob( mock_arborist_requests( {f"arborist/user/{TEST_RAS_USERNAME}": {"PATCH": (None, 204)}} ) + # reset users table + db_session.query(User).delete() + db_session.commit() n_users = 20 n_users_no_visa = 15 @@ -700,12 +646,12 @@ def dont_test_visa_update_cronjob( for i in range(n_users): username = "user_{}".format(i) - test_user = add_test_ras_user(db_session, username, i) + test_user = add_test_ras_user(db_session, username) add_visa_manually(db_session, test_user, rsa_private_key, kid) add_refresh_token(db_session, test_user) for j in range(n_users_no_visa): username = "no_visa_{}".format(j) - test_user = add_test_ras_user(db_session, username, j + n_users) + test_user = add_test_ras_user(db_session, username) query_visas = db_session.query(GA4GHVisaV1).all() @@ -780,6 +726,10 @@ def test_map_iss_sub_pair_to_user_with_no_prior_DRS_access(db_session): user's combination has not already been mapped through a prior DRS access request. """ + # reset users table + db_session.query(User).delete() + db_session.commit() + iss = "https://domain.tld" sub = "123_abc" username = "johnsmith" @@ -795,7 +745,9 @@ def test_map_iss_sub_pair_to_user_with_no_prior_DRS_access(db_session): iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() assert len(iss_sub_pair_to_user_records) == 0 - username_to_log_in = ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) + username_to_log_in = ras_client.map_iss_sub_pair_to_user( + iss, sub, username, email, db_session=db_session + ) assert username_to_log_in == username iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((iss, sub)) @@ -818,6 +770,10 @@ def test_map_iss_sub_pair_to_user_with_prior_DRS_access( """ mock_arborist_requests({"arborist/user/123_abcdomain.tld": {"PATCH": (None, 204)}}) + # reset users table + db_session.query(User).delete() + db_session.commit() + iss = "https://domain.tld" sub = "123_abc" username = "johnsmith" @@ -835,7 +791,9 @@ def test_map_iss_sub_pair_to_user_with_prior_DRS_access( iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((iss, sub)) assert iss_sub_pair_to_user.user.username == "123_abcdomain.tld" - username_to_log_in = ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) + username_to_log_in = ras_client.map_iss_sub_pair_to_user( + iss, sub, username, email, db_session=db_session + ) assert username_to_log_in == username iss_sub_pair_to_user_records = db_session.query(IssSubPairToUser).all() @@ -854,6 +812,10 @@ def test_map_iss_sub_pair_to_user_with_prior_DRS_access_and_arborist_error( """ mock_arborist_requests({"arborist/user/123_abcdomain.tld": {"PATCH": (None, 500)}}) + # reset users table + db_session.query(User).delete() + db_session.commit() + iss = "https://domain.tld" sub = "123_abc" username = "johnsmith" @@ -867,7 +829,9 @@ def test_map_iss_sub_pair_to_user_with_prior_DRS_access_and_arborist_error( get_or_create_gen3_user_from_iss_sub(iss, sub, db_session=db_session) with pytest.raises(InternalError): - ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) + ras_client.map_iss_sub_pair_to_user( + iss, sub, username, email, db_session=db_session + ) def test_map_iss_sub_pair_to_user_with_prior_login_and_prior_DRS_access( @@ -891,12 +855,19 @@ def test_map_iss_sub_pair_to_user_with_prior_login_and_prior_DRS_access( HTTP_PROXY=config.get("HTTP_PROXY"), logger=logger, ) + + # reset users table + db_session.query(User).delete() + db_session.commit() + user = User(username=username, email=email) db_session.add(user) db_session.commit() get_or_create_gen3_user_from_iss_sub(iss, sub, db_session=db_session) - username_to_log_in = ras_client.map_iss_sub_pair_to_user(iss, sub, username, email) + username_to_log_in = ras_client.map_iss_sub_pair_to_user( + iss, sub, username, email, db_session=db_session + ) assert username_to_log_in == "123_abcdomain.tld" iss_sub_pair_to_user = db_session.query(IssSubPairToUser).get((iss, sub)) assert iss_sub_pair_to_user.user.username == "123_abcdomain.tld" From e0c8e58397fc70d27919b5f4505fa1aa521de497 Mon Sep 17 00:00:00 2001 From: John McCann Date: Sun, 2 Jan 2022 15:43:47 -0800 Subject: [PATCH 137/211] chore(fence/__init__.py): init b/f imports --- .secrets.baseline | 132 ++++++++++++++++++++++++++-------------------- fence/__init__.py | 23 +++++--- fence/models.py | 3 -- tests/conftest.py | 4 ++ 4 files changed, 96 insertions(+), 66 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 481543680..e6942596b 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -40,6 +40,9 @@ { "name": "MailchimpDetector" }, + { + "name": "NpmDetector" + }, { "name": "PrivateKeyDetector" }, @@ -49,6 +52,9 @@ { "name": "SoftlayerDetector" }, + { + "name": "SquareOAuthDetector" + }, { "name": "StripeDetector" }, @@ -103,13 +109,24 @@ } ], "results": { + "deployment/scripts/postgresql/postgresql_init.sql": [ + { + "type": "Secret Keyword", + "filename": "deployment/scripts/postgresql/postgresql_init.sql", + "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", + "is_verified": false, + "line_number": 7, + "is_secret": false + } + ], "fence/blueprints/storage_creds/google.py": [ { "type": "Private Key", "filename": "fence/blueprints/storage_creds/google.py", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 139 + "line_number": 139, + "is_secret": false } ], "fence/blueprints/storage_creds/other.py": [ @@ -118,7 +135,16 @@ "filename": "fence/blueprints/storage_creds/other.py", "hashed_secret": "98c144f5ecbb4dbe575147a39698b6be1a5649dd", "is_verified": false, - "line_number": 66 + "line_number": 66, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "fence/blueprints/storage_creds/other.py", + "hashed_secret": "98c144f5ecbb4dbe575147a39698b6be1a5649dd", + "is_verified": false, + "line_number": 66, + "is_secret": false } ], "fence/config-default.yaml": [ @@ -127,7 +153,8 @@ "filename": "fence/config-default.yaml", "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", "is_verified": false, - "line_number": 31 + "line_number": 31, + "is_secret": false } ], "fence/local_settings.example.py": [ @@ -136,14 +163,16 @@ "filename": "fence/local_settings.example.py", "hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", "is_verified": false, - "line_number": 6 + "line_number": 6, + "is_secret": false }, { "type": "Secret Keyword", "filename": "fence/local_settings.example.py", "hashed_secret": "5d07e1b80e448a213b392049888111e1779a52db", "is_verified": false, - "line_number": 63 + "line_number": 63, + "is_secret": false } ], "fence/resources/google/utils.py": [ @@ -152,7 +181,8 @@ "filename": "fence/resources/google/utils.py", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 125 + "line_number": 125, + "is_secret": false } ], "fence/utils.py": [ @@ -161,69 +191,60 @@ "filename": "fence/utils.py", "hashed_secret": "8318df9ecda039deac9868adf1944a29a95c7114", "is_verified": false, - "line_number": 105 + "line_number": 105, + "is_secret": false } ], - "openapis/swagger.yaml": [ - { - "type": "Private Key", - "filename": "openapis/swagger.yaml", - "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", - "is_verified": false, - "line_number": 1927 - }, - { - "type": "Secret Keyword", - "filename": "openapis/swagger.yaml", - "hashed_secret": "bb8e48bd1e73662027a0f0b876b695d4c18f5ed4", - "is_verified": false, - "line_number": 1927 - }, + "tests/conftest.py": [ { "type": "Secret Keyword", - "filename": "openapis/swagger.yaml", - "hashed_secret": "7861ab65194de92776ab9cd06d4d7e7e1ec2c36d", - "is_verified": false, - "line_number": 2007 - }, - { - "type": "JSON Web Token", - "filename": "openapis/swagger.yaml", - "hashed_secret": "d6b66ddd9ea7dbe760114bfe9a97352a5e139134", + "filename": "tests/conftest.py", + "hashed_secret": "9801ff058ba790388c9efc095cb3e89a819d5ed6", "is_verified": false, - "line_number": 2029 + "line_number": 164, + "is_secret": false }, - { - "type": "Base64 High Entropy String", - "filename": "openapis/swagger.yaml", - "hashed_secret": "98c144f5ecbb4dbe575147a39698b6be1a5649dd", - "is_verified": false, - "line_number": 2041 - } - ], - "tests/conftest.py": [ { "type": "Private Key", "filename": "tests/conftest.py", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 1174 + "line_number": 1361, + "is_secret": false }, { "type": "Base64 High Entropy String", "filename": "tests/conftest.py", "hashed_secret": "227dea087477346785aefd575f91dd13ab86c108", "is_verified": false, - "line_number": 1197 + "line_number": 1384, + "is_secret": false } ], "tests/credentials/google/test_credentials.py": [ { "type": "Secret Keyword", "filename": "tests/credentials/google/test_credentials.py", - "hashed_secret": "22afbfecd4124e2eb0e2a79fafdf62b207a8f8c7", + "hashed_secret": "a06bdb09c0106ab559bd6acab2f1935e19f7e939", + "is_verified": false, + "line_number": 381, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "tests/credentials/google/test_credentials.py", + "hashed_secret": "93aa43c580f5347782e17fba5091f944767b15f0", "is_verified": false, - "line_number": 579 + "line_number": 474, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "tests/credentials/google/test_credentials.py", + "hashed_secret": "768b7fe00de4fd233c0c72375d12f87ce9670144", + "is_verified": false, + "line_number": 476, + "is_secret": false } ], "tests/keys/2018-05-01T21:29:02Z/jwt_private_key.pem": [ @@ -232,7 +253,8 @@ "filename": "tests/keys/2018-05-01T21:29:02Z/jwt_private_key.pem", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 1 + "line_number": 1, + "is_secret": false } ], "tests/login/test_fence_login.py": [ @@ -241,7 +263,8 @@ "filename": "tests/login/test_fence_login.py", "hashed_secret": "d300421e208bfd0d432294de15169fd9b8975def", "is_verified": false, - "line_number": 48 + "line_number": 48, + "is_secret": false } ], "tests/ras/test_ras.py": [ @@ -250,7 +273,8 @@ "filename": "tests/ras/test_ras.py", "hashed_secret": "d9db6fe5c14dc55edd34115cdf3958845ac30882", "is_verified": false, - "line_number": 105 + "line_number": 105, + "is_secret": false } ], "tests/test-fence-config.yaml": [ @@ -259,16 +283,10 @@ "filename": "tests/test-fence-config.yaml", "hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3", "is_verified": false, - "line_number": 31 - }, - { - "type": "Secret Keyword", - "filename": "tests/test-fence-config.yaml", - "hashed_secret": "1627df13b5cd8b3521d02bd8eb2ca31334b3aef2", - "is_verified": false, - "line_number": 471 + "line_number": 31, + "is_secret": false } ] }, - "generated_at": "2021-12-01T02:28:44Z" + "generated_at": "2022-01-02T23:43:11Z" } diff --git a/fence/__init__.py b/fence/__init__.py index 26eeba07c..413bc3b1e 100755 --- a/fence/__init__.py +++ b/fence/__init__.py @@ -16,6 +16,23 @@ from azure.core.exceptions import ResourceNotFoundError from urllib.parse import urlparse +# Can't read config yet. Just set to debug for now, else no handlers. +# Later, in app_config(), will actually set level based on config +logger = get_logger(__name__, log_level="debug") + +# Load the configuration *before* importing modules that rely on it +from fence.config import config, DEFAULT_CFG_PATH +from fence.settings import CONFIG_SEARCH_FOLDERS + +try: + if os.environ.get("FENCE_CONFIG_PATH"): + config.load(config_path=os.environ["FENCE_CONFIG_PATH"]) + else: + config.load(search_folders=CONFIG_SEARCH_FOLDERS) +except: + logger.warning("Unable to load config, using default config...", exc_info=True) + config.load(config_path=DEFAULT_CFG_PATH) + from fence.auth import logout, build_redirect_url from fence.blueprints.data.indexd import S3IndexedFileLocation from fence.blueprints.login.utils import allowed_login_redirects, domain @@ -40,8 +57,6 @@ from fence.resources.user.user_session import UserSessionInterface from fence.error_handler import get_error_response from fence.utils import random_str -from fence.config import config -from fence.settings import CONFIG_SEARCH_FOLDERS import fence.blueprints.admin import fence.blueprints.data import fence.blueprints.login @@ -62,10 +77,6 @@ PROMETHEUS_TMP_COUNTER_DIR = tempfile.TemporaryDirectory() -# Can't read config yet. Just set to debug for now, else no handlers. -# Later, in app_config(), will actually set level based on config -logger = get_logger(__name__, log_level="debug") - app = flask.Flask(__name__) CORS(app=app, headers=["content-type", "accept"], expose_headers="*") diff --git a/fence/models.py b/fence/models.py index ee5651479..a1a50a3d5 100644 --- a/fence/models.py +++ b/fence/models.py @@ -57,9 +57,6 @@ import warnings from fence.config import config -from fence.settings import CONFIG_SEARCH_FOLDERS - -config.load(search_folders=CONFIG_SEARCH_FOLDERS) def query_for_user(session, username): diff --git a/tests/conftest.py b/tests/conftest.py index 5cf2e6369..e43a79c53 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,6 +31,10 @@ from sqlalchemy.ext.compiler import compiles from sqlalchemy.schema import DropTable +# Set FENCE_CONFIG_PATH *before* loading the configuration +CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) +os.environ["FENCE_CONFIG_PATH"] = os.path.join(CURRENT_DIR, "test-fence-config.yaml") + import fence from fence import app_init from fence import models From e70aceb378d3f48809ef749a97d868218106e549 Mon Sep 17 00:00:00 2001 From: John McCann Date: Sun, 2 Jan 2022 21:14:35 -0800 Subject: [PATCH 138/211] chore(iss_sub_pair_to_user): populate within try --- fence/models.py | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/fence/models.py b/fence/models.py index a1a50a3d5..5d525a34b 100644 --- a/fence/models.py +++ b/fence/models.py @@ -56,6 +56,7 @@ ) import warnings +from fence import logger from fence.config import config @@ -675,21 +676,34 @@ def _get_issuer_to_idp(): @event.listens_for(IssSubPairToUser.__table__, "after_create") def populate_iss_sub_pair_to_user_table(target, connection, **kw): for issuer, idp_name in IssSubPairToUser.ISSUER_TO_IDP.items(): - transaction = connection.begin() - result = connection.execute( - text( - """ - WITH identity_provider_id AS (SELECT id FROM identity_provider WHERE name=:idp_name) - INSERT INTO iss_sub_pair_to_user (iss, sub, "fk_to_User", extra_info) - SELECT :iss, id_from_idp, id, additional_info - FROM "User" - WHERE idp_id IN (SELECT * FROM identity_provider_id) AND id_from_idp IS NOT NULL; - """ - ), - idp_name=idp_name, - iss=issuer, + logger.info( + 'Attempting to populate iss_sub_pair_to_user table for users with "{}" idp and "{}" issuer'.format( + idp_name, issuer + ) ) - transaction.commit() + transaction = connection.begin() + try: + connection.execute( + text( + """ + WITH identity_provider_id AS (SELECT id FROM identity_provider WHERE name=:idp_name) + INSERT INTO iss_sub_pair_to_user (iss, sub, "fk_to_User", extra_info) + SELECT :iss, id_from_idp, id, additional_info + FROM "User" + WHERE idp_id IN (SELECT * FROM identity_provider_id) AND id_from_idp IS NOT NULL; + """ + ), + idp_name=idp_name, + iss=issuer, + ) + except Exception as e: + transaction.rollback() + logger.warning( + "Could not populate iss_sub_pair_to_user table: {}".format(e) + ) + else: + transaction.commit() + logger.info("Population was successful") to_timestamp = ( From c9b94a4ac870ef51d0e5c9c00d21624889db7f50 Mon Sep 17 00:00:00 2001 From: John McCann Date: Sun, 2 Jan 2022 22:40:53 -0800 Subject: [PATCH 139/211] docs(iss_sub_pair_to_user): add docstring --- fence/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fence/models.py b/fence/models.py index 5d525a34b..0e12216e5 100644 --- a/fence/models.py +++ b/fence/models.py @@ -675,6 +675,10 @@ def _get_issuer_to_idp(): @event.listens_for(IssSubPairToUser.__table__, "after_create") def populate_iss_sub_pair_to_user_table(target, connection, **kw): + """ + Populate iss_sub_pair_to_user table using User table's id_from_idp + column. + """ for issuer, idp_name in IssSubPairToUser.ISSUER_TO_IDP.items(): logger.info( 'Attempting to populate iss_sub_pair_to_user table for users with "{}" idp and "{}" issuer'.format( From 0385f8e1e1d58c00cbf5a0253a8d3bd0bf879077 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Mon, 3 Jan 2022 16:01:25 -0600 Subject: [PATCH 140/211] fix(tests): update visa sync cronjob test to reflect new behavior, add some misc comments for clarity --- fence/job/visa_update_cronjob.py | 3 +- fence/resources/openid/ras_oauth2.py | 3 +- tests/dbgap_sync/conftest.py | 3 +- tests/dbgap_sync/test_user_sync.py | 5 - tests/ras/test_ras.py | 141 ++++++++++++++------------- tests/utils/__init__.py | 6 +- 6 files changed, 85 insertions(+), 76 deletions(-) diff --git a/fence/job/visa_update_cronjob.py b/fence/job/visa_update_cronjob.py index ebbfa1916..7f7945316 100644 --- a/fence/job/visa_update_cronjob.py +++ b/fence/job/visa_update_cronjob.py @@ -153,7 +153,8 @@ async def updater(self, name, updater_queue, db_session): name, user.username ) ) - # when getting access token, this persists new refresh token + # when getting access token, this persists new refresh token, + # it also persists validated visa(s) in the database client.update_user_authorization(user, self.pkey_cache, db_session) else: self.logger.debug( diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index 78727e8b0..d506f4fe8 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -303,7 +303,8 @@ def update_user_authorization(self, user, pkey_cache, db_session=current_session self.logger.exception("{}: {}".format(err_msg, e)) raise - # now sync authz updates + # now sync authz updates (this includes persisting new valid visas into the + # database) users_from_passports = ( fence.resources.ga4gh.passports.sync_gen3_users_authz_from_ga4gh_passports( [passport], pkey_cache=pkey_cache, db_session=db_session diff --git a/tests/dbgap_sync/conftest.py b/tests/dbgap_sync/conftest.py index 02c5b650a..ae72fc5dc 100644 --- a/tests/dbgap_sync/conftest.py +++ b/tests/dbgap_sync/conftest.py @@ -337,7 +337,7 @@ def get_test_encoded_decoded_visa_and_exp( return encoded_visa, decoded_visa, expires -def add_visa_manually(db_session, user, rsa_private_key, kid, expires=None): +def add_visa_manually(db_session, user, rsa_private_key, kid, expires=None, sub=None): expires = expires or int(time.time()) + 1000 make_invalid = False @@ -355,6 +355,7 @@ def add_visa_manually(db_session, user, rsa_private_key, kid, expires=None): kid, expires=expires, make_invalid=make_invalid, + sub=sub, ) visa = GA4GHVisaV1( diff --git a/tests/dbgap_sync/test_user_sync.py b/tests/dbgap_sync/test_user_sync.py index 4d51baf2d..bcb99727c 100644 --- a/tests/dbgap_sync/test_user_sync.py +++ b/tests/dbgap_sync/test_user_sync.py @@ -781,11 +781,6 @@ def test_user_sync_with_visa_sync_job( mock_arborist_requests, ) - # reset GA4GH visas table - # db_session.query(models.User).delete() - # db_session.query(models.GA4GHVisaV1).delete() - # db_session.commit() - # Usersync syncer.sync() diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index 68e7fd784..a109f0311 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -360,7 +360,6 @@ def test_update_visa_empty_visa_returned( @mock.patch( "fence.resources.openid.ras_oauth2.RASOauth2Client.get_value_from_discovery_doc" ) -@mock.patch("fence.resources.ga4gh.passports.validate_jwt") def test_update_visa_token_with_invalid_visa( mock_discovery, mock_get_token, @@ -612,6 +611,7 @@ def test_visa_update_cronjob( rsa_public_key, kid, mock_arborist_requests, + no_app_context_no_public_keys, ): """ Test to check visa table is updated when updating visas using cronjob @@ -621,85 +621,90 @@ def test_visa_update_cronjob( ) # reset users table db_session.query(User).delete() + db_session.query(GA4GHVisaV1).delete() db_session.commit() - n_users = 20 - n_users_no_visa = 15 + n_users = 3 + n_users_no_visas = 2 mock_discovery.return_value = "https://ras/token_endpoint" new_token = "refresh12345abcdefg" - token_response = { - "access_token": "abcdef12345", - "id_token": "id12345abcdef", - "refresh_token": new_token, - } - mock_get_token.return_value = token_response - userinfo_response = { - "sub": TEST_RAS_SUB, - "name": "", - "preferred_username": "someuser@era.com", - "UID": "", - "UserID": TEST_RAS_USERNAME, - "email": "", - } + def _get_token_response_for_user(*args, **kwargs): + token_response = { + "access_token": f"{args[0].id}", + "id_token": f"{args[0].id}-id12345abcdef", + "refresh_token": f"{args[0].id}-refresh12345abcdefg", + } + return token_response + + mock_get_token.side_effect = _get_token_response_for_user - for i in range(n_users): + user_id_to_ga4gh_info = {} + + for i in range(1, n_users + 1): username = "user_{}".format(i) - test_user = add_test_ras_user(db_session, username) - add_visa_manually(db_session, test_user, rsa_private_key, kid) - add_refresh_token(db_session, test_user) - for j in range(n_users_no_visa): - username = "no_visa_{}".format(j) - test_user = add_test_ras_user(db_session, username) + test_user = add_test_ras_user(db_session, username, subject_id=username) + encoded_visa, visa = add_visa_manually( + db_session, test_user, rsa_private_key, kid, sub=username + ) + user_id_to_ga4gh_info[str(test_user.id)] = {"encoded_visa": encoded_visa} - query_visas = db_session.query(GA4GHVisaV1).all() + passport_header = { + "type": "JWT", + "alg": "RS256", + "kid": kid, + } + new_passport = { + "iss": "https://stsstg.nih.gov", + "sub": username, + "iat": int(time.time()), + "scope": "openid ga4gh_passport_v1 email profile", + "exp": int(time.time()) + 1000, + "ga4gh_passport_v1": [ + user_id_to_ga4gh_info[str(test_user.id)]["encoded_visa"] + ], + } - assert len(query_visas) == n_users + userinfo_response = { + "sub": username, + "name": "", + "preferred_username": "someuser@era.com", + "UID": "", + "UserID": username + "_USERNAME", + "email": "", + } + encoded_passport = jwt.encode( + new_passport, + key=rsa_private_key, + headers=passport_header, + algorithm="RS256", + ).decode("utf-8") + user_id_to_ga4gh_info[str(test_user.id)]["encoded_passport"] = encoded_passport + + userinfo_response["passport_jwt_v11"] = encoded_passport + user_id_to_ga4gh_info[str(test_user.id)][ + "userinfo_response" + ] = userinfo_response - new_visa = { - "iss": "https://stsstg.nih.gov", - "sub": TEST_RAS_SUB, - "iat": int(time.time()), - "exp": int(time.time()) + 1000, - "scope": "openid ga4gh_passport_v1 email profile", - "jti": "jtiajoidasndokmasdl", - "txn": "sapidjspa.asipidja", - "name": "", - "ga4gh_visa_v1": { - "type": "https://ras.nih.gov/visas/v1", - "asserted": int(time.time()), - "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", - "source": "https://ncbi.nlm.nih.gov/gap", - }, - } + add_refresh_token(db_session, test_user) - headers = {"kid": kid} + for j in range(1, n_users_no_visas + 1): + username = "no_existing_visa_{}".format(j) + test_user = add_test_ras_user(db_session, username, subject_id=username) - encoded_visa = jwt.encode( - new_visa, key=rsa_private_key, headers=headers, algorithm="RS256" - ).decode("utf-8") + query_visas = db_session.query(GA4GHVisaV1).all() - passport_header = { - "type": "JWT", - "alg": "RS256", - "kid": kid, - } - new_passport = { - "iss": "https://stsstg.nih.gov", - "sub": TEST_RAS_SUB, - "iat": int(time.time()), - "scope": "openid ga4gh_passport_v1 email profile", - "exp": int(time.time()) + 1000, - "ga4gh_passport_v1": [encoded_visa], - } + assert len(query_visas) == n_users - encoded_passport = jwt.encode( - new_passport, key=rsa_private_key, headers=passport_header, algorithm="RS256" - ).decode("utf-8") + def _get_userinfo(*args, **kwargs): + # b/c of the setup in _get_token_response_for_user we know the + # access token will be the user.id + return user_id_to_ga4gh_info.get(str(args[0].get("access_token", {})), {})[ + "userinfo_response" + ] - userinfo_response["passport_jwt_v11"] = encoded_passport - mock_userinfo.return_value = userinfo_response + mock_userinfo.side_effect = _get_userinfo # test "fence-create update-visa" job = Visa_Token_Update() @@ -713,10 +718,14 @@ def test_visa_update_cronjob( query_visas = db_session.query(GA4GHVisaV1).all() - assert len(query_visas) == n_users + # this should not disturb previous manually added visas + # and should add a new visa per user (including users without existing visas) + assert len(query_visas) == n_users * 2 for visa in query_visas: - assert visa.ga4gh_visa == encoded_visa + assert ( + visa.ga4gh_visa == user_id_to_ga4gh_info[str(visa.user.id)]["encoded_visa"] + ) def test_map_iss_sub_pair_to_user_with_no_prior_DRS_access(db_session): diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index 3d41749de..adebec098 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -28,10 +28,12 @@ TEST_RAS_SUB = "abcd-asdj-sajpiasj12iojd-asnoin" -def add_test_ras_user(db_session, username=TEST_RAS_USERNAME, is_admin=True): +def add_test_ras_user( + db_session, username=TEST_RAS_USERNAME, is_admin=True, subject_id=TEST_RAS_SUB +): # pre-populate mapping table, as login would do test_user = get_or_create_gen3_user_from_iss_sub( - issuer="https://stsstg.nih.gov", subject_id=TEST_RAS_SUB, db_session=db_session + issuer="https://stsstg.nih.gov", subject_id=subject_id, db_session=db_session ) test_user.username = username test_user.is_admin = is_admin From 57672f27cabcbe551c49716ef6c0d1adfc7da308 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Mon, 3 Jan 2022 16:29:51 -0600 Subject: [PATCH 141/211] fix(dependencies): pin authutils b/c of breaking change in newer version --- poetry.lock | 402 +++++++++++++------------------------------------ pyproject.toml | 2 +- 2 files changed, 105 insertions(+), 299 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8816b01e2..29412587a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -35,17 +35,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.2.0" +version = "21.4.0" description = "Classes Without Boilerplate" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] [[package]] name = "authlib" @@ -61,7 +61,7 @@ requests = "*" [[package]] name = "authutils" -version = "6.1.1" +version = "6.1.0" description = "Gen3 auth utility functions" category = "main" optional = false @@ -79,19 +79,6 @@ xmltodict = ">=0.9,<1.0" flask = ["Flask (>=0.10.1)"] fastapi = ["fastapi (>=0.54.1,<0.55.0)"] -[[package]] -name = "aws-xray-sdk" -version = "0.95" -description = "The AWS X-Ray SDK for Python (the SDK) enables Python developers to record and emit information from within their applications to the AWS X-Ray service." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -jsonpickle = "*" -requests = "*" -wrapt = "*" - [[package]] name = "azure-core" version = "1.21.1" @@ -396,23 +383,6 @@ idna = ["idna (>=2.1)"] curio = ["curio (>=1.2)", "sniffio (>=1.1)"] trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] -[[package]] -name = "docker" -version = "5.0.3" -description = "A Python library for the Docker Engine API." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pywin32 = {version = "227", markers = "sys_platform == \"win32\""} -requests = ">=2.14.2,<2.18.0 || >2.18.0" -websocket-client = ">=0.32.0" - -[package.extras] -ssh = ["paramiko (>=2.4.2)"] -tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=3.4.7)", "idna (>=2.0.0)"] - [[package]] name = "docopt" version = "0.6.2" @@ -878,30 +848,6 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "jsondiff" -version = "1.1.1" -description = "Diff JSON and JSON-like structures in Python" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "jsonpickle" -version = "2.0.0" -description = "Python library for serializing any arbitrary object graph into JSON" -category = "dev" -optional = false -python-versions = ">=2.7" - -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["coverage (<5)", "pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black-multipy", "pytest-cov", "ecdsa", "feedparser", "numpy", "pandas", "pymongo", "sklearn", "sqlalchemy", "enum34", "jsonlib"] -"testing.libs" = ["demjson", "simplejson", "ujson", "yajl"] - [[package]] name = "markdown" version = "3.3.6" @@ -918,11 +864,11 @@ testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" -version = "2.0.1" +version = "1.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" [[package]] name = "mock" @@ -950,34 +896,41 @@ python-versions = ">=3.5" [[package]] name = "moto" -version = "1.3.7" +version = "1.3.15" description = "A library that allows your python tests to easily mock out the boto library" category = "dev" optional = false python-versions = "*" [package.dependencies] -aws-xray-sdk = ">=0.93,<0.96" boto = ">=2.36.0" -boto3 = ">=1.6.16" -botocore = ">=1.12.13" -cryptography = ">=2.3.0" -docker = ">=2.5.1" -Jinja2 = ">=2.7.3" -jsondiff = "1.1.1" +boto3 = ">=1.9.201" +botocore = ">=1.12.201" +Jinja2 = ">=2.10.1" +MarkupSafe = "<2.0" mock = "*" -pyaml = "*" +more-itertools = "*" python-dateutil = ">=2.1,<3.0.0" -python-jose = "<3.0.0" pytz = "*" requests = ">=2.5" responses = ">=0.9.0" six = ">1.9" werkzeug = "*" xmltodict = "*" +zipp = "*" [package.extras] +acm = ["cryptography (>=2.3.0)"] +all = ["cryptography (>=2.3.0)", "PyYAML (>=5.1)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)", "docker (>=2.5.1)", "jsondiff (>=1.1.2)", "aws-xray-sdk (>=0.93,!=0.96)", "idna (>=2.5,<3)", "cfn-lint (>=0.4.0)", "sshpubkeys (>=3.1.0,<4.0)", "sshpubkeys (>=3.1.0)"] +awslambda = ["docker (>=2.5.1)"] +batch = ["docker (>=2.5.1)"] +cloudformation = ["PyYAML (>=5.1)", "cfn-lint (>=0.4.0)"] +cognitoidp = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)"] +ec2 = ["cryptography (>=2.3.0)", "sshpubkeys (>=3.1.0,<4.0)", "sshpubkeys (>=3.1.0)"] +iam = ["cryptography (>=2.3.0)"] +iotdata = ["jsondiff (>=1.1.2)"] server = ["flask"] +xray = ["aws-xray-sdk (>=0.93,!=0.96)"] [[package]] name = "msrest" @@ -1037,7 +990,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "paramiko" -version = "2.8.1" +version = "2.9.1" description = "SSH2 protocol library" category = "main" optional = false @@ -1090,7 +1043,7 @@ twisted = ["twisted"] [[package]] name = "prometheus-flask-exporter" -version = "0.18.6" +version = "0.18.7" description = "Prometheus metrics exporter for Flask" category = "main" optional = false @@ -1110,7 +1063,7 @@ python-versions = ">=3.5" [[package]] name = "psycopg2" -version = "2.9.2" +version = "2.9.3" description = "psycopg2 - Python-PostgreSQL Database Adapter" category = "main" optional = false @@ -1124,17 +1077,6 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -[[package]] -name = "pyaml" -version = "21.10.1" -description = "PyYAML-based module to produce pretty and readable YAML-serialized data" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -PyYAML = "*" - [[package]] name = "pyasn1" version = "0.4.8" @@ -1301,14 +1243,6 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "pywin32" -version = "227" -description = "Python for Window Extensions" -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "pyyaml" version = "5.4.1" @@ -1319,7 +1253,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "requests" -version = "2.26.0" +version = "2.27.0" description = "Python HTTP for Humans." category = "main" optional = false @@ -1521,19 +1455,6 @@ python-versions = "*" cdislogging = "*" sqlalchemy = ">=1.3.3,<1.4.0" -[[package]] -name = "websocket-client" -version = "1.2.3" -description = "WebSocket client for Python with low level API options" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - [[package]] name = "werkzeug" version = "1.0.1" @@ -1546,14 +1467,6 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] watchdog = ["watchdog"] -[[package]] -name = "wrapt" -version = "1.13.3" -description = "Module for decorators, wrappers and monkey patching." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - [[package]] name = "wtforms" version = "3.0.0" @@ -1591,7 +1504,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "056f7db41eb2405bb0583ee0e5f813366c5e1d76cd72f033af31745f4b6ca5aa" +content-hash = "43f3be30215be01158c35d88488b77683c2353f3f230ab0fb3ec2ee6c9c0652a" [metadata.files] addict = [ @@ -1611,20 +1524,16 @@ atomicwrites = [ {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, - {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] authlib = [ {file = "Authlib-0.11-py2.py3-none-any.whl", hash = "sha256:3a226f231e962a16dd5f6fcf0c113235805ba206e294717a64fa8e04ae3ad9c4"}, {file = "Authlib-0.11.tar.gz", hash = "sha256:9741db6de2950a0a5cefbdb72ec7ab12f7e9fd530ff47219f1530e79183cbaaf"}, ] authutils = [ - {file = "authutils-6.1.1-py3-none-any.whl", hash = "sha256:9c61c563e19781807f354dee8a2720769b18689c2972cf57178cfcb197c7ab6d"}, - {file = "authutils-6.1.1.tar.gz", hash = "sha256:a9cc6ccd9a2ab97bda77ae52a20fe9515dfe06b0621442e41675f4796d1a21ff"}, -] -aws-xray-sdk = [ - {file = "aws-xray-sdk-0.95.tar.gz", hash = "sha256:9e7ba8dd08fd2939376c21423376206bff01d0deaea7d7721c6b35921fed1943"}, - {file = "aws_xray_sdk-0.95-py2.py3-none-any.whl", hash = "sha256:72791618feb22eaff2e628462b0d58f398ce8c1bacfa989b7679817ab1fad60c"}, + {file = "authutils-6.1.0-py3-none-any.whl", hash = "sha256:682dba636694c36fb35af1d9ff576bb8436337c3899f0ef434cda5918d661db9"}, + {file = "authutils-6.1.0.tar.gz", hash = "sha256:7263af0b2ce3a0db19236fd123b34f795d07e07111b7bd18a51808568ddfdc2e"}, ] azure-core = [ {file = "azure-core-1.21.1.zip", hash = "sha256:88d2db5cf9a135a7287dc45fdde6b96f9ca62c9567512a3bb3e20e322ce7deb2"}, @@ -1846,10 +1755,6 @@ dnspython = [ {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, ] -docker = [ - {file = "docker-5.0.3-py2.py3-none-any.whl", hash = "sha256:7a79bb439e3df59d0a72621775d600bc8bc8b422d285824cb37103eab91d1ce0"}, - {file = "docker-5.0.3.tar.gz", hash = "sha256:d916a26b62970e7c2f554110ed6af04c7ccff8e9f81ad17d0d40c75637e227fb"}, -] docopt = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] @@ -2047,87 +1952,63 @@ jmespath = [ {file = "jmespath-0.9.2-py2.py3-none-any.whl", hash = "sha256:3f03b90ac8e0f3ba472e8ebff083e460c89501d8d41979771535efe9a343177e"}, {file = "jmespath-0.9.2.tar.gz", hash = "sha256:54c441e2e08b23f12d7fa7d8e6761768c47c969e6aed10eead57505ba760aee9"}, ] -jsondiff = [ - {file = "jsondiff-1.1.1.tar.gz", hash = "sha256:2d0437782de9418efa34e694aa59f43d7adb1899bd9a793f063867ddba8f7893"}, -] -jsonpickle = [ - {file = "jsonpickle-2.0.0-py2.py3-none-any.whl", hash = "sha256:c1010994c1fbda87a48f8a56698605b598cb0fc6bb7e7927559fc1100e69aeac"}, - {file = "jsonpickle-2.0.0.tar.gz", hash = "sha256:0be49cba80ea6f87a168aa8168d717d00c6ca07ba83df3cec32d3b30bfe6fb9a"}, -] markdown = [ {file = "Markdown-3.3.6-py3-none-any.whl", hash = "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"}, {file = "Markdown-3.3.6.tar.gz", hash = "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006"}, ] markupsafe = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, - {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mock = [ {file = "mock-2.0.0-py2.py3-none-any.whl", hash = "sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1"}, @@ -2138,8 +2019,8 @@ more-itertools = [ {file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"}, ] moto = [ - {file = "moto-1.3.7-py2.py3-none-any.whl", hash = "sha256:4df37936ff8d6a4b8229aab347a7b412cd2ca4823ff47bd1362ddfbc6c5e4ecf"}, - {file = "moto-1.3.7.tar.gz", hash = "sha256:129de2e04cb250d9f8b2c722ec152ed1b5426ef179b4ebb03e9ec36e6eb3fcc5"}, + {file = "moto-1.3.15-py2.py3-none-any.whl", hash = "sha256:3be7e1f406ef7e9c222dbcbfd8cefa2cb1062200e26deae49b5df446e17be3df"}, + {file = "moto-1.3.15.tar.gz", hash = "sha256:fd98f7b219084ba8aadad263849c4dbe8be73979e035d8dc5c86e11a86f11b7f"}, ] msrest = [ {file = "msrest-0.6.21-py2.py3-none-any.whl", hash = "sha256:c840511c845330e96886011a236440fafc2c9aff7b2df9c0a92041ee2dee3782"}, @@ -2157,8 +2038,8 @@ packaging = [ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] paramiko = [ - {file = "paramiko-2.8.1-py2.py3-none-any.whl", hash = "sha256:7b5910f5815a00405af55da7abcc8a9e0d9657f57fcdd9a89894fdbba1c6b8a8"}, - {file = "paramiko-2.8.1.tar.gz", hash = "sha256:85b1245054e5d7592b9088cc6d08da22445417912d3a3e48138675c7a8616438"}, + {file = "paramiko-2.9.1-py2.py3-none-any.whl", hash = "sha256:db5d3f19607941b1c90233588d60213c874392c4961c6297037da989c24f8070"}, + {file = "paramiko-2.9.1.tar.gz", hash = "sha256:a1fdded3b55f61d23389e4fe52d9ae428960ac958d2edf50373faa5d8926edd0"}, ] pbr = [ {file = "pbr-2.0.0-py2.py3-none-any.whl", hash = "sha256:d9b69a26a5cb4e3898eb3c5cea54d2ab3332382167f04e30db5e1f54e1945e45"}, @@ -2173,8 +2054,8 @@ prometheus-client = [ {file = "prometheus_client-0.9.0.tar.gz", hash = "sha256:9da7b32f02439d8c04f7777021c304ed51d9ec180604700c1ba72a4d44dceb03"}, ] prometheus-flask-exporter = [ - {file = "prometheus_flask_exporter-0.18.6-py3-none-any.whl", hash = "sha256:02717c9d15c0956fe54e76bdde7c4116e1a1bddd12be7bc8538bc5b6af431ef1"}, - {file = "prometheus_flask_exporter-0.18.6.tar.gz", hash = "sha256:9a2af3d4ba014e3da6387b3b7cebaed291ccd6f474e240be13f50f8a5af671ca"}, + {file = "prometheus_flask_exporter-0.18.7-py3-none-any.whl", hash = "sha256:38bc68db295d0f895ad0fb319b1bfd200ae273b33397ce497c9b96dceb708ce9"}, + {file = "prometheus_flask_exporter-0.18.7.tar.gz", hash = "sha256:f1f6f23535479d41587a100a24a60cb9199c34986e95f6691496807ee5017e59"}, ] protobuf = [ {file = "protobuf-3.19.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d80f80eb175bf5f1169139c2e0c5ada98b1c098e2b3c3736667f28cbbea39fc8"}, @@ -2203,26 +2084,22 @@ protobuf = [ {file = "protobuf-3.19.1.tar.gz", hash = "sha256:62a8e4baa9cb9e064eb62d1002eca820857ab2138440cb4b3ea4243830f94ca7"}, ] psycopg2 = [ - {file = "psycopg2-2.9.2-cp310-cp310-win32.whl", hash = "sha256:6796ac614412ce374587147150e56d03b7845c9e031b88aacdcadc880e81bb38"}, - {file = "psycopg2-2.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:dfc32db6ce9ecc35a131320888b547199f79822b028934bb5b332f4169393e15"}, - {file = "psycopg2-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:77d09a79f9739b97099d2952bbbf18eaa4eaf825362387acbb9552ec1b3fa228"}, - {file = "psycopg2-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f65cba7924363e0d2f416041b48ff69d559548f2cb168ff972c54e09e1e64db8"}, - {file = "psycopg2-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:b8816c6410fa08d2a022e4e38d128bae97c1855e176a00493d6ec62ccd606d57"}, - {file = "psycopg2-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:26322c3f114de1f60c1b0febf8fdd595c221b4f624524178f515d07350a71bd1"}, - {file = "psycopg2-2.9.2-cp38-cp38-win32.whl", hash = "sha256:77b9105ef37bc005b8ffbcb1ed6d8685bb0e8ce84773738aa56421a007ec5a7a"}, - {file = "psycopg2-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:91c7fd0fe9e6c118e8ff5b665bc3445781d3615fa78e131d0b4f8c85e8ca9ec8"}, - {file = "psycopg2-2.9.2-cp39-cp39-win32.whl", hash = "sha256:a761b60da0ecaf6a9866985bcde26327883ac3cdb90535ab68b8d784f02b05ef"}, - {file = "psycopg2-2.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:fd7ddab7d6afee4e21c03c648c8b667b197104713e57ec404d5b74097af21e31"}, - {file = "psycopg2-2.9.2.tar.gz", hash = "sha256:a84da9fa891848e0270e8e04dcca073bc9046441eeb47069f5c0e36783debbea"}, + {file = "psycopg2-2.9.3-cp310-cp310-win32.whl", hash = "sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362"}, + {file = "psycopg2-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:d3ca6421b942f60c008f81a3541e8faf6865a28d5a9b48544b0ee4f40cac7fca"}, + {file = "psycopg2-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:9572e08b50aed176ef6d66f15a21d823bb6f6d23152d35e8451d7d2d18fdac56"}, + {file = "psycopg2-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:a81e3866f99382dfe8c15a151f1ca5fde5815fde879348fe5a9884a7c092a305"}, + {file = "psycopg2-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:cb10d44e6694d763fa1078a26f7f6137d69f555a78ec85dc2ef716c37447e4b2"}, + {file = "psycopg2-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4295093a6ae3434d33ec6baab4ca5512a5082cc43c0505293087b8a46d108461"}, + {file = "psycopg2-2.9.3-cp38-cp38-win32.whl", hash = "sha256:34b33e0162cfcaad151f249c2649fd1030010c16f4bbc40a604c1cb77173dcf7"}, + {file = "psycopg2-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:0762c27d018edbcb2d34d51596e4346c983bd27c330218c56c4dc25ef7e819bf"}, + {file = "psycopg2-2.9.3-cp39-cp39-win32.whl", hash = "sha256:8cf3878353cc04b053822896bc4922b194792df9df2f1ad8da01fb3043602126"}, + {file = "psycopg2-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:06f32425949bd5fe8f625c49f17ebb9784e1e4fe928b7cce72edc36fb68e4c0c"}, + {file = "psycopg2-2.9.3.tar.gz", hash = "sha256:8e841d1bf3434da985cc5ef13e6f75c8981ced601fd70cc6bf33351b91562981"}, ] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] -pyaml = [ - {file = "pyaml-21.10.1-py2.py3-none-any.whl", hash = "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0"}, - {file = "pyaml-21.10.1.tar.gz", hash = "sha256:c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383"}, -] pyasn1 = [ {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, @@ -2341,20 +2218,6 @@ pytz = [ {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, ] -pywin32 = [ - {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"}, - {file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"}, - {file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"}, - {file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"}, - {file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"}, - {file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"}, - {file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"}, - {file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"}, - {file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"}, - {file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"}, - {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"}, - {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"}, -] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, @@ -2387,8 +2250,8 @@ pyyaml = [ {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] requests = [ - {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, - {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, + {file = "requests-2.27.0-py2.py3-none-any.whl", hash = "sha256:f71a09d7feba4a6b64ffd8e9d9bc60f9bf7d7e19fd0e04362acb1cfc2e3d98df"}, + {file = "requests-2.27.0.tar.gz", hash = "sha256:8e5643905bf20a308e25e4c1dd379117c09000bf8a82ebccc462cfb1b34a16b5"}, ] requests-oauthlib = [ {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, @@ -2475,67 +2338,10 @@ urllib3 = [ userdatamodel = [ {file = "userdatamodel-2.4.0.tar.gz", hash = "sha256:11c3faf2c4a855e51305a02341123442bb6722bc518548e1da023b7a65136457"}, ] -websocket-client = [ - {file = "websocket-client-1.2.3.tar.gz", hash = "sha256:1315816c0acc508997eb3ae03b9d3ff619c9d12d544c9a9b553704b1cc4f6af5"}, - {file = "websocket_client-1.2.3-py3-none-any.whl", hash = "sha256:2eed4cc58e4d65613ed6114af2f380f7910ff416fc8c46947f6e76b6815f56c0"}, -] werkzeug = [ {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, ] -wrapt = [ - {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, - {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, - {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, - {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, - {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, - {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, - {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, - {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, - {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, - {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, - {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, - {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, - {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, - {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, - {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, - {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, - {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, - {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, - {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, - {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, - {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, - {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, - {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, - {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, - {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, - {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, - {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, - {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, - {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, - {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, - {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, - {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, - {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, - {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, - {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, - {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, - {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, - {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, - {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, - {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, - {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, - {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, - {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, - {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, - {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, - {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, - {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, - {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, - {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, - {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, - {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, -] wtforms = [ {file = "WTForms-3.0.0-py3-none-any.whl", hash = "sha256:232dbb0094847dca2f45c72136b5ca1d5dca2a3e24ccd2229823b8b74b3c6698"}, {file = "WTForms-3.0.0.tar.gz", hash = "sha256:4abfbaa1d529a1d0ac927d44af8dbb9833afd910e56448a103f1893b0b176886"}, diff --git a/pyproject.toml b/pyproject.toml index 564b68d52..86692f9db 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ werkzeug = "^1.0.0" cachelib = "^0.2.0" azure-storage-blob = "^12.6.0" Flask-WTF = "^0.14.3" -authutils = "^6.1.0" +authutils = "6.1.0" [tool.poetry.dev-dependencies] From da2198333789fcd320df4a205fa19d110bb74054 Mon Sep 17 00:00:00 2001 From: Alexander VanTol Date: Wed, 5 Jan 2022 12:48:33 -0600 Subject: [PATCH 142/211] Update auth.py --- fence/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fence/auth.py b/fence/auth.py index f05794442..638b63a6e 100644 --- a/fence/auth.py +++ b/fence/auth.py @@ -114,7 +114,7 @@ def set_flask_session_values(user): if id_from_idp: user.id_from_idp = id_from_idp - # TODO Update iss_sub mapping table? + # TODO: update iss_sub mapping table? # setup idp connection for new user (or existing user w/o it setup) idp = ( From 1c675348e232cdb80ddf242ab1dc2df99ff29b5a Mon Sep 17 00:00:00 2001 From: Alexander VanTol Date: Thu, 6 Jan 2022 16:12:47 -0600 Subject: [PATCH 143/211] Update fence/resources/ga4gh/passports.py Co-authored-by: John McCann --- fence/resources/ga4gh/passports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 5efea07b8..2eb464173 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -369,7 +369,7 @@ def sync_validated_visa_authorization( policy_prefix="GA4GH.DRS", ) - # after syncing authorization, perist the visas that were parsed successfully. + # after syncing authorization, persist the visas that were parsed successfully. for visa in ga4gh_visas: if visa not in synced_visas: db_session.remove(visa) From 836bae866a2c475dfcd28e421b2cbe106eb1f839 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Mon, 10 Jan 2022 12:00:39 -0600 Subject: [PATCH 144/211] fix(ga4gh): ensure that usernames from passports are passed to arborist and logged correctly --- fence/blueprints/data/indexd.py | 88 ++++++++++++++----- fence/blueprints/login/ras.py | 2 +- fence/resources/ga4gh/passports.py | 45 ++++++---- fence/resources/openid/ras_oauth2.py | 2 +- tests/conftest.py | 2 +- ...zure_blob_storage_indexed_file_location.py | 2 +- 6 files changed, 96 insertions(+), 45 deletions(-) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index 294a7e6b4..1b91ba736 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -95,16 +95,43 @@ def get_signed_url_for_file( users_from_passports = sync_gen3_users_authz_from_ga4gh_passports( ga4gh_passports, db_session=db_session ) - user_ids_from_passports = [user.id for user in users_from_passports] + # the keys are User.username's + user_ids_from_passports = users_from_passports.keys() logger.debug(f"user_ids_from_passports: {user_ids_from_passports}") # add the user details to `flask.g.audit_data` first, so they are # included in the audit log if `IndexedFile(file_id)` raises a 404 - user_info = _get_user_info(sub_type=int) - flask.g.audit_data = { - "username": user_info["username"], - "sub": user_info["user_id"], - } + if user_ids_from_passports: + if len(user_ids_from_passports) > 1: + logger.warning( + "audit service doesn't support multiple user_ids for a " + "single request yet, so just log userinfo here" + ) + for user_id in user_ids_from_passports: + user_info = _get_user_info_for_id_or_from_request( + sub_type=int, user_id=user_id + ) + audit_data = { + "username": user_info["username"], + "sub": user_info["user_id"], + } + logger.info( + f"passport with multiple user ids is attempting data access. audit log: {audit_data}" + ) + else: + user_info = _get_user_info_for_id_or_from_request( + sub_type=int, user_id=user_ids_from_passports[0] + ) + flask.g.audit_data = { + "username": user_info["username"], + "sub": user_info["user_id"], + } + else: + user_info = _get_user_info_for_id_or_from_request(sub_type=int) + flask.g.audit_data = { + "username": user_info["username"], + "sub": user_info["user_id"], + } indexed_file = IndexedFile(file_id) default_expires_in = config.get("MAX_PRESIGNED_URL_TTL", 3600) @@ -114,7 +141,7 @@ def get_signed_url_for_file( ) prepare_presigned_url_audit_log(requested_protocol, indexed_file) - signed_url = indexed_file.get_signed_url( + signed_url, passport_user_id_used = indexed_file.get_signed_url( requested_protocol, action, expires_in, @@ -124,6 +151,17 @@ def get_signed_url_for_file( user_ids_from_passports=user_ids_from_passports, ) + # a single user from the list was authorized so update the audit log to reflect that + # users info + if passport_user_id_used: + user_info = _get_user_info_for_id_or_from_request( + sub_type=int, user_id=passport_user_id_used + ) + flask.g.audit_data = { + "username": user_info["username"], + "sub": user_info["user_id"], + } + # increment counter for gen3-metrics counter = flask.current_app.prometheus_counters.get("pre_signed_url_req") if counter: @@ -456,13 +494,16 @@ def get_signed_url( ) if action is not None and action not in SUPPORTED_ACTIONS: raise NotSupported("action {} is not supported".format(action)) - return self._get_signed_url( - protocol, - action, - expires_in, - force_signed_url, - r_pays_project, - file_name, + return ( + self._get_signed_url( + protocol, + action, + expires_in, + force_signed_url, + r_pays_project, + file_name, + authorized_user_id, + ), authorized_user_id, ) @@ -553,6 +594,7 @@ def get_authorized_and_user_id(self, action, user_ids_from_passports=None): # handle multiple GA4GH passports as a means of authn/z if user_ids_from_passports: + authorized = False for user_id in user_ids_from_passports: authorized = flask.current_app.arborist.auth_request( jwt=None, @@ -565,13 +607,13 @@ def get_authorized_and_user_id(self, action, user_ids_from_passports=None): if authorized: # for google proxy groups we need to know which user_id gave access return authorized, user_id - return authorized, None + return authorized, None else: try: token = get_jwt() except Unauthorized: # get_jwt raises an Unauthorized error when user is anonymous (no - # availble token), so to allow anonymous users possible access to + # available token), so to allow anonymous users possible access to # public data, we still make the request to Arborist token = None @@ -986,7 +1028,7 @@ def get_signed_url( self.parsed_url.netloc, credential ) - user_info = _get_user_info() + user_info = _get_user_info_for_id_or_from_request() url = generate_aws_presigned_url( http_url, @@ -1113,7 +1155,7 @@ def get_signed_url( ): resource_path = self.get_resource_path() - user_info = _get_user_info(user_id=user_id) + user_info = _get_user_info_for_id_or_from_request(user_id=user_id) if public_data and not force_signed_url: url = "https://storage.cloud.google.com/" + resource_path @@ -1428,7 +1470,7 @@ def get_signed_url( container_name, blob_name = self._get_container_and_blob() - user_info = _get_user_info() + user_info = _get_user_info_for_id_or_from_request() if user_info and user_info.get("user_id") == ANONYMOUS_USER_ID: logger.info(f"Attempting to get a signed url an anonymous user") @@ -1489,10 +1531,12 @@ def delete(self, container, blob): # pylint: disable=R0201 return ("Failed to delete data file.", status_code) -def _get_user_info(sub_type=str, user_id=None): +def _get_user_info_for_id_or_from_request(sub_type=str, user_id=None): """ Attempt to parse the request to get information about user. fallback to - populated information about an anonymous user. + populated information about an anonymous user. If a GA4GH passport was provided, + this will handled elsewhere (or id passed into user_id here). + By default, cast `sub` to str. Use `sub_type` to override this behavior. WARNING: This does NOT actually check authorization information and always falls @@ -1532,7 +1576,7 @@ def _is_anonymous_user(user_info): """ Check if there's a current user authenticated or if request is anonymous """ - user_info = user_info or _get_user_info() + user_info = user_info or _get_user_info_for_id_or_from_request() return str(user_info.get("user_id")) == ANONYMOUS_USER_ID diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index cf820291e..baa9a9171 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -78,7 +78,7 @@ def post_login(self, user=None, token_result=None, id_from_idp=None): users_from_passports = fence.resources.ga4gh.passports.sync_gen3_users_authz_from_ga4gh_passports( [passport], pkey_cache=pkey_cache, db_session=current_session ) - user_ids_from_passports = [user.id for user in users_from_passports] + user_ids_from_passports = users_from_passports.keys() logger.debug(f"user_ids_from_passports: {user_ids_from_passports}") # TODO? diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 5efea07b8..7b12d9790 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -11,7 +11,6 @@ from authutils.errors import JWTError from authutils.token.core import get_iss, get_kid from cdislogging import get_logger -from gen3authz.client.arborist.client import ArboristClient from flask_sqlalchemy_session import current_session from fence.jwt.validate import validate_jwt @@ -19,8 +18,8 @@ from fence.models import ( create_user, query_for_user, + query_for_user_by_id, GA4GHVisaV1, - User, IdentityProvider, IssSubPairToUser, ) @@ -49,15 +48,19 @@ def sync_gen3_users_authz_from_ga4gh_passports( """ db_session = db_session or current_session logger.info("Getting gen3 users from passports") - users_from_all_passports = [] + + # {"username": user, "username2": user2} + users_from_all_passports = {} for passport in passports: try: - # TODO check cache - cached_usernames = get_gen3_usernames_for_passport_from_cache(passport) - if cached_usernames: - users_from_all_passports.extend(cached_usernames) + cached_users = get_gen3_usernames_for_passport_from_cache(passport) + if cached_users: + # TODO get user from id - perhaps we can avoid this? + for user_id in cached_users: + user = query_for_user_by_id(session=db_session, user_id=user_id) + users_from_all_passports[user.username] = user # existence in the cache means that this passport was validated - # previously + # previously (expiration was also checked) continue # below function also validates passport (or raises exception) @@ -128,18 +131,26 @@ def sync_gen3_users_authz_from_ga4gh_passports( users_from_current_passport.append(gen3_user) put_gen3_usernames_for_passport_into_cache( - passport, users_from_current_passport + passport, + [user.id for user in users_from_current_passport], + expires_at=min_visa_expiration, ) - users_from_all_passports.extend(users_from_current_passport) + for user in users_from_current_passport: + users_from_all_passports[user.username] = user db_session.commit() - return list(set(users_from_all_passports)) + + return users_from_all_passports + + +def get_gen3_usernames_for_passport_from_cache(passport, db_session=None): + return -def get_gen3_usernames_for_passport_from_cache(passport): - cached_user_ids = [] - # TODO - return cached_user_ids +def put_gen3_usernames_for_passport_into_cache( + passport, user_ids_from_passports, expires_at, db_session=None +): + return def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): @@ -377,10 +388,6 @@ def sync_validated_visa_authorization( db_session.add(visa) -def put_gen3_usernames_for_passport_into_cache(passport, usernames_from_passports): - pass - - # TODO to be called after login def map_gen3_iss_sub_pair_to_user(gen3_issuer, gen3_subject_id, gen3_user): pass diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index d506f4fe8..e39a92f9f 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -310,7 +310,7 @@ def update_user_authorization(self, user, pkey_cache, db_session=current_session [passport], pkey_cache=pkey_cache, db_session=db_session ) ) - user_ids_from_passports = [user.id for user in users_from_passports] + user_ids_from_passports = users_from_passports.keys() self.logger.debug(f"user_ids_from_passports:{user_ids_from_passports}") # TODO? diff --git a/tests/conftest.py b/tests/conftest.py index b3422a6a7..9b737ea90 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -321,7 +321,7 @@ def validate_jwt_no_key_refresh(*args, **kwargs): mock_validate_jwt.side_effect = validate_jwt_no_key_refresh # ensure there is no application context or cached keys - if flask.current_app: + if flask.current_app and flask.current_app.jwt_public_keys: temp_stored_public_keys = flask.current_app.jwt_public_keys temp_app_context = flask.has_app_context flask.current_app.jwt_public_keys = {} diff --git a/tests/data/test_azure_blob_storage_indexed_file_location.py b/tests/data/test_azure_blob_storage_indexed_file_location.py index 9a9a0cc81..0c04143db 100755 --- a/tests/data/test_azure_blob_storage_indexed_file_location.py +++ b/tests/data/test_azure_blob_storage_indexed_file_location.py @@ -95,7 +95,7 @@ def test_get_signed_url( return_value=storage_account_matches, ): with patch( - "fence.blueprints.data.indexd._get_user_info", + "fence.blueprints.data.indexd._get_user_info_for_id_or_from_request", return_value={"user_id": user_id}, ): azure_blob_storage_indexed_file_location = ( From cf059a20f5114e7a92f2daa0e5deb2f0362d417a Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Mon, 10 Jan 2022 12:45:38 -0600 Subject: [PATCH 145/211] fix(signed_urls): correctly search for user based on provided username, fix tests --- fence/blueprints/data/indexd.py | 27 +++++++++++++++------------ fence/blueprints/login/ras.py | 2 +- fence/resources/openid/ras_oauth2.py | 2 +- tests/test_drs.py | 10 +++++----- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index 1b91ba736..330b47d86 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -96,7 +96,7 @@ def get_signed_url_for_file( ga4gh_passports, db_session=db_session ) # the keys are User.username's - user_ids_from_passports = users_from_passports.keys() + user_ids_from_passports = list(users_from_passports.keys()) logger.debug(f"user_ids_from_passports: {user_ids_from_passports}") # add the user details to `flask.g.audit_data` first, so they are @@ -107,9 +107,9 @@ def get_signed_url_for_file( "audit service doesn't support multiple user_ids for a " "single request yet, so just log userinfo here" ) - for user_id in user_ids_from_passports: + for username in user_ids_from_passports: user_info = _get_user_info_for_id_or_from_request( - sub_type=int, user_id=user_id + sub_type=int, user_id=username, db_session=db_session ) audit_data = { "username": user_info["username"], @@ -120,14 +120,16 @@ def get_signed_url_for_file( ) else: user_info = _get_user_info_for_id_or_from_request( - sub_type=int, user_id=user_ids_from_passports[0] + sub_type=int, user_id=user_ids_from_passports[0], db_session=db_session ) flask.g.audit_data = { "username": user_info["username"], "sub": user_info["user_id"], } else: - user_info = _get_user_info_for_id_or_from_request(sub_type=int) + user_info = _get_user_info_for_id_or_from_request( + sub_type=int, db_session=db_session + ) flask.g.audit_data = { "username": user_info["username"], "sub": user_info["user_id"], @@ -155,7 +157,7 @@ def get_signed_url_for_file( # users info if passport_user_id_used: user_info = _get_user_info_for_id_or_from_request( - sub_type=int, user_id=passport_user_id_used + sub_type=int, user_id=passport_user_id_used, db_session=db_session ) flask.g.audit_data = { "username": user_info["username"], @@ -598,6 +600,7 @@ def get_authorized_and_user_id(self, action, user_ids_from_passports=None): for user_id in user_ids_from_passports: authorized = flask.current_app.arborist.auth_request( jwt=None, + # NOTE: This is actually the fence user.username, not user.id user_id=user_id, service="fence", methods=action, @@ -1531,7 +1534,7 @@ def delete(self, container, blob): # pylint: disable=R0201 return ("Failed to delete data file.", status_code) -def _get_user_info_for_id_or_from_request(sub_type=str, user_id=None): +def _get_user_info_for_id_or_from_request(sub_type=str, user_id=None, db_session=None): """ Attempt to parse the request to get information about user. fallback to populated information about an anonymous user. If a GA4GH passport was provided, @@ -1544,13 +1547,13 @@ def _get_user_info_for_id_or_from_request(sub_type=str, user_id=None): IT WILL ALWAYS GIVE YOU BACK ANONYMOUS USER INFO. Only use this after you've authorized the access to the data via other means. """ + db_session = db_session or current_session + try: if user_id: - if hasattr(flask.current_app, "db"): - with flask.current_app.db.session as session: - result = query_for_user_by_id(session, user_id) - final_username = result.username - final_user_id = result.id + result = query_for_user(db_session, user_id) + final_username = result.username + final_user_id = result.id else: set_current_token( validate_request(scope={"user"}, audience=config.get("BASE_URL")) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index baa9a9171..1865ef83d 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -78,7 +78,7 @@ def post_login(self, user=None, token_result=None, id_from_idp=None): users_from_passports = fence.resources.ga4gh.passports.sync_gen3_users_authz_from_ga4gh_passports( [passport], pkey_cache=pkey_cache, db_session=current_session ) - user_ids_from_passports = users_from_passports.keys() + user_ids_from_passports = list(users_from_passports.keys()) logger.debug(f"user_ids_from_passports: {user_ids_from_passports}") # TODO? diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index e39a92f9f..c0aacd64a 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -310,7 +310,7 @@ def update_user_authorization(self, user, pkey_cache, db_session=current_session [passport], pkey_cache=pkey_cache, db_session=db_session ) ) - user_ids_from_passports = users_from_passports.keys() + user_ids_from_passports = list(users_from_passports.keys()) self.logger.debug(f"user_ids_from_passports:{user_ids_from_passports}") # TODO? diff --git a/tests/test_drs.py b/tests/test_drs.py index d7e4819ce..16a24bfdb 100644 --- a/tests/test_drs.py +++ b/tests/test_drs.py @@ -244,7 +244,7 @@ def test_get_presigned_url_with_query_params( @pytest.mark.parametrize("indexd_client", ["s3", "gs"], indirect=True) @patch("httpx.get") @patch("fence.resources.google.utils._create_proxy_group") -@patch("fence.resources.ga4gh.passports.ArboristClient") +@patch("fence.scripting.fence_create.ArboristClient") def test_passport_use_disabled( mock_arborist, mock_google_proxy_group, @@ -403,7 +403,7 @@ def test_passport_use_disabled( @pytest.mark.parametrize("indexd_client", ["s3", "gs"], indirect=True) @patch("httpx.get") @patch("fence.resources.google.utils._create_proxy_group") -@patch("fence.resources.ga4gh.passports.ArboristClient") +@patch("fence.scripting.fence_create.ArboristClient") def test_get_presigned_url_for_non_public_data_with_passport( mock_arborist, mock_google_proxy_group, @@ -562,7 +562,7 @@ def test_get_presigned_url_for_non_public_data_with_passport( @pytest.mark.parametrize("indexd_client", ["s3", "gs"], indirect=True) @patch("httpx.get") @patch("fence.resources.google.utils._create_proxy_group") -@patch("fence.resources.ga4gh.passports.ArboristClient") +@patch("fence.scripting.fence_create.ArboristClient") def test_get_presigned_url_with_passport_with_incorrect_authz( mock_arborist, mock_google_proxy_group, @@ -720,7 +720,7 @@ def test_get_presigned_url_with_passport_with_incorrect_authz( @pytest.mark.parametrize("indexd_client", ["s3", "gs"], indirect=True) @patch("httpx.get") @patch("fence.resources.google.utils._create_proxy_group") -@patch("fence.resources.ga4gh.passports.ArboristClient") +@patch("fence.scripting.fence_create.ArboristClient") def test_get_presigned_url_for_public_data_with_no_passport( mock_arborist, mock_google_proxy_group, @@ -780,7 +780,7 @@ def test_get_presigned_url_for_public_data_with_no_passport( @pytest.mark.parametrize("indexd_client", ["s3", "gs"], indirect=True) @patch("httpx.get") @patch("fence.resources.google.utils._create_proxy_group") -@patch("fence.resources.ga4gh.passports.ArboristClient") +@patch("fence.scripting.fence_create.ArboristClient") def test_get_presigned_url_for_non_public_data_with_no_passport( mock_arborist, mock_google_proxy_group, From c826806ba2c0fcb7508b67369c599d929ded6c21 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Mon, 10 Jan 2022 13:53:58 -0600 Subject: [PATCH 146/211] chore(ga4gh): refactor a function name, add clarifying comment --- fence/resources/ga4gh/passports.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 96b38cb8f..c6d4c9fe9 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -125,7 +125,9 @@ def sync_gen3_users_authz_from_ga4gh_passports( for raw_visa, validated_decoded_visa in visas ] # NOTE: does not validate, assumes validation occurs above. - sync_validated_visa_authorization( + # This adds the visas to the database session but doesn't commit until + # the end of this function + _sync_validated_visa_authorization( gen3_user, ga4gh_visas, min_visa_expiration, db_session=db_session ) users_from_current_passport.append(gen3_user) @@ -346,7 +348,7 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id, db_session=None): return iss_sub_pair_to_user.user -def sync_validated_visa_authorization( +def _sync_validated_visa_authorization( gen3_user, ga4gh_visas, expiration, db_session=None ): """ From 9d8606d3238990da7f62d9dd650331d96fe6f213 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 11 Jan 2022 14:48:18 -0600 Subject: [PATCH 147/211] chore(passports): refactor passport handling when determining access to make more consistent and clear and reduce db calls --- fence/blueprints/data/indexd.py | 177 ++++++++++++++------------------ poetry.lock | 124 +++++++++++----------- tests/data/test_blank_index.py | 2 +- tests/data/test_data.py | 4 +- tests/data/test_indexed_file.py | 6 +- 5 files changed, 143 insertions(+), 170 deletions(-) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index 330b47d86..e58026862 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -90,41 +90,34 @@ def get_signed_url_for_file( "is not supported by this instance of Gen3." ) - user_ids_from_passports = [] + users_from_passports = {} if ga4gh_passports: + # users_from_passports = {"username": Fence.User} users_from_passports = sync_gen3_users_authz_from_ga4gh_passports( ga4gh_passports, db_session=db_session ) - # the keys are User.username's - user_ids_from_passports = list(users_from_passports.keys()) - logger.debug(f"user_ids_from_passports: {user_ids_from_passports}") # add the user details to `flask.g.audit_data` first, so they are # included in the audit log if `IndexedFile(file_id)` raises a 404 - if user_ids_from_passports: - if len(user_ids_from_passports) > 1: + if users_from_passports: + if len(list(users_from_passports.keys())) > 1: logger.warning( - "audit service doesn't support multiple user_ids for a " + "audit service doesn't support multiple users for a " "single request yet, so just log userinfo here" ) - for username in user_ids_from_passports: - user_info = _get_user_info_for_id_or_from_request( - sub_type=int, user_id=username, db_session=db_session - ) + for username, user in users_from_passports: audit_data = { - "username": user_info["username"], - "sub": user_info["user_id"], + "username": username, + "sub": user.id, } logger.info( f"passport with multiple user ids is attempting data access. audit log: {audit_data}" ) else: - user_info = _get_user_info_for_id_or_from_request( - sub_type=int, user_id=user_ids_from_passports[0], db_session=db_session - ) + username, user = next(iter(users_from_passports.items())) flask.g.audit_data = { - "username": user_info["username"], - "sub": user_info["user_id"], + "username": username, + "sub": user.id, } else: user_info = _get_user_info_for_id_or_from_request( @@ -143,25 +136,22 @@ def get_signed_url_for_file( ) prepare_presigned_url_audit_log(requested_protocol, indexed_file) - signed_url, passport_user_id_used = indexed_file.get_signed_url( + signed_url, authorized_user_from_passport = indexed_file.get_signed_url( requested_protocol, action, expires_in, force_signed_url=force_signed_url, r_pays_project=r_pays_project, file_name=file_name, - user_ids_from_passports=user_ids_from_passports, + users_from_passports=users_from_passports, ) # a single user from the list was authorized so update the audit log to reflect that # users info - if passport_user_id_used: - user_info = _get_user_info_for_id_or_from_request( - sub_type=int, user_id=passport_user_id_used, db_session=db_session - ) + if authorized_user_from_passport: flask.g.audit_data = { - "username": user_info["username"], - "sub": user_info["user_id"], + "username": authorized_user_from_passport.username, + "sub": authorized_user_from_passport.id, } # increment counter for gen3-metrics @@ -378,7 +368,7 @@ def generate_aws_presigned_url_for_part(key, uploadId, partNumber, expires_in): "fence not configured with data upload bucket; can't create signed URL" ) s3_url = "s3://{}/{}".format(bucket, key) - return S3IndexedFileLocation(s3_url).generate_presigne_url_for_part_upload( + return S3IndexedFileLocation(s3_url).generate_presigned_url_for_part_upload( uploadId, partNumber, expires_in ) @@ -459,17 +449,19 @@ def get_signed_url( force_signed_url=True, r_pays_project=None, file_name=None, - user_ids_from_passports=None, + users_from_passports=None, ): - authorized_user_id = None + users_from_passports = users_from_passports or {} + authorized_user = None if self.index_document.get("authz"): action_to_permission = { "upload": "write-storage", "download": "read-storage", } - is_authorized, authorized_user_id = self.get_authorized_and_user_id( + is_authorized, authorized_username = self.get_authorized_with_username( action_to_permission[action], - user_ids_from_passports=user_ids_from_passports, + # keys are usernames + usernames_from_passports=list(users_from_passports.keys()), ) if not is_authorized: msg = ( @@ -478,9 +470,10 @@ def get_signed_url( f"on authorization resource: {self.index_document['authz']}." ) logger.debug( - f"denied. authorized_user_id: {authorized_user_id}\nmsg:\n{msg}" + f"denied. authorized_username: {authorized_username}\nmsg:\n{msg}" ) raise Unauthorized(msg) + authorized_user = users_from_passports.get(authorized_username) else: if self.public_acl and action == "upload": raise Unauthorized( @@ -488,12 +481,11 @@ def get_signed_url( ) # don't check the authorization if the file is public # (downloading public files with no auth is fine) - if not self.public_acl and not self.check_authorization( - action, user_ids_from_passports=user_ids_from_passports - ): + if not self.public_acl and not self.check_legacy_authorization(action): raise Unauthorized( f"You don't have access permission on this file: {self.file_id}" ) + if action is not None and action not in SUPPORTED_ACTIONS: raise NotSupported("action {} is not supported".format(action)) return ( @@ -504,9 +496,9 @@ def get_signed_url( force_signed_url, r_pays_project, file_name, - authorized_user_id, + authorized_user, ), - authorized_user_id, + authorized_user, ) def _get_signed_url( @@ -517,7 +509,7 @@ def _get_signed_url( force_signed_url, r_pays_project, file_name, - user_id=None, + authorized_user=None, ): if action == "upload": # NOTE: self.index_document ensures the GUID exists in indexd and raises @@ -537,6 +529,7 @@ def _get_signed_url( public_data=self.public, force_signed_url=force_signed_url, r_pays_project=r_pays_project, + authorized_user=authorized_user, ) except IndexError: raise NotFound("Can't find any file locations.") @@ -552,7 +545,7 @@ def _get_signed_url( public_data=self.public, force_signed_url=force_signed_url, r_pays_project=r_pays_project, - user_id=user_id, + authorized_user=authorized_user, ) raise NotFound( @@ -569,47 +562,46 @@ def set_acls(self): else: raise Unauthorized("This file is not accessible") - def get_authorized_and_user_id(self, action, user_ids_from_passports=None): + def get_authorized_with_username(self, action, usernames_from_passports=None): """ Return a tuple of (boolean, str) which represents whether they're authorized - and their user_id. user_id is only returned if `user_ids_from_passports` - is provided and one of the ids from the passports is authorized. + and their username. username is only returned if `usernames_from_passports` + is provided and one of the usernames from the passports is authorized. Args: action (str): Authorization action being performed - user_ids_from_passports (list[str], optional): List of user ids parsed + usernames_from_passports (list[str], optional): List of user usernames parsed from validated passports Returns: tuple of (boolean, str): which represents whether they're authorized - and their user_id. user_id is only returned if `user_ids_from_passports` - is provided and one of the ids from the passports is authorized. + and their username. username is only returned if `usernames_from_passports` + is provided and one of the usernames from the passports is authorized. """ if not self.index_document.get("authz"): raise ValueError("index record missing `authz`") logger.debug( f"authz check can user {action} on {self.index_document['authz']} for fence? " - f"if passport provided, IDs parsed: {user_ids_from_passports}" + f"if passport provided, IDs parsed: {usernames_from_passports}" ) # handle multiple GA4GH passports as a means of authn/z - - if user_ids_from_passports: + if usernames_from_passports: authorized = False - for user_id in user_ids_from_passports: + for username in usernames_from_passports: authorized = flask.current_app.arborist.auth_request( jwt=None, - # NOTE: This is actually the fence user.username, not user.id - user_id=user_id, + user_id=username, service="fence", methods=action, resources=self.index_document["authz"], ) # if any passport provides access, user is authorized if authorized: - # for google proxy groups we need to know which user_id gave access - return authorized, user_id + # for google proxy groups and future use: we need to know which + # user_id actually gave access + return authorized, username return authorized, None else: try: @@ -650,7 +642,7 @@ def public_authz(self): return "/open" in self.index_document.get("authz", []) @login_required({"data"}) - def check_authorization(self, action, user_ids_from_passports=None): + def check_legacy_authorization(self, action): # if we have a data file upload without corresponding metadata, the record can # have just the `uploader` field and no ACLs. in this just check that the # current user's username matches the uploader field @@ -665,29 +657,8 @@ def check_authorization(self, action, user_ids_from_passports=None): ) return self.index_document.get("uploader") == username - # handle multiple GA4GH passports as a means of authn/z - project_accesses = [] - if user_ids_from_passports: - for user_id in user_ids_from_passports: - new_project_access = _get_project_access_for_user_id(user_id) - if new_project_access: - project_accesses.append(new_project_access) - - if not project_accesses: - # if we didn't get anything from passports, assume old JWT, get from flask context - project_accesses.append(flask.g.user.project_access) - - has_access = False - for project_access in project_accesses: - given_acls = set(filter_auth_ids(action, project_access)) - has_access = len(self.set_acls & given_acls) > 0 - - # if any of the project_access information results in a success, - # this user has access - if has_access: - break - - return has_access + given_acls = set(filter_auth_ids(action, flask.g.user.project_access)) + return len(self.set_acls & given_acls) > 0 @login_required({"data"}) def delete_files(self, urls=None, delete_all=True): @@ -790,7 +761,7 @@ def get_signed_url( expires_in, public_data=False, force_signed_url=True, - user_ids_from_passports=None, + users_from_passports=None, **kwargs, ): return self.url @@ -986,7 +957,13 @@ def get_bucket_region(self): return bucket_cred["region"] def get_signed_url( - self, action, expires_in, public_data=False, force_signed_url=True, **kwargs + self, + action, + expires_in, + public_data=False, + force_signed_url=True, + authorized_user=None, + **kwargs, ): aws_creds = get_value( @@ -1031,7 +1008,7 @@ def get_signed_url( self.parsed_url.netloc, credential ) - user_info = _get_user_info_for_id_or_from_request() + user_info = _get_user_info_for_id_or_from_request(user=authorized_user) url = generate_aws_presigned_url( http_url, @@ -1066,7 +1043,7 @@ def init_multipart_upload(self, expires_in): self.parsed_url.netloc, self.parsed_url.path.strip("/"), credentials ) - def generate_presigne_url_for_part_upload(self, uploadId, partNumber, expires_in): + def generate_presigned_url_for_part_upload(self, uploadId, partNumber, expires_in): """ Generate presigned url for uploading object part given uploadId and part number @@ -1154,11 +1131,11 @@ def get_signed_url( public_data=False, force_signed_url=True, r_pays_project=None, - user_id=None, + authorized_user=None, ): resource_path = self.get_resource_path() - user_info = _get_user_info_for_id_or_from_request(user_id=user_id) + user_info = _get_user_info_for_id_or_from_request(user=authorized_user) if public_data and not force_signed_url: url = "https://storage.cloud.google.com/" + resource_path @@ -1437,7 +1414,13 @@ def _get_converted_url(self): return urlunparse(new_parsed_url) def get_signed_url( - self, action, expires_in, public_data=False, force_signed_url=True, **kwargs + self, + action, + expires_in, + public_data=False, + force_signed_url=True, + authorized_user=None, + **kwargs, ): """ Get a signed url for a given action @@ -1473,7 +1456,7 @@ def get_signed_url( container_name, blob_name = self._get_container_and_blob() - user_info = _get_user_info_for_id_or_from_request() + user_info = _get_user_info_for_id_or_from_request(user=authorized_user) if user_info and user_info.get("user_id") == ANONYMOUS_USER_ID: logger.info(f"Attempting to get a signed url an anonymous user") @@ -1534,11 +1517,12 @@ def delete(self, container, blob): # pylint: disable=R0201 return ("Failed to delete data file.", status_code) -def _get_user_info_for_id_or_from_request(sub_type=str, user_id=None, db_session=None): +def _get_user_info_for_id_or_from_request( + sub_type=str, user=None, username=None, db_session=None +): """ Attempt to parse the request to get information about user. fallback to - populated information about an anonymous user. If a GA4GH passport was provided, - this will handled elsewhere (or id passed into user_id here). + populated information about an anonymous user. By default, cast `sub` to str. Use `sub_type` to override this behavior. @@ -1550,17 +1534,19 @@ def _get_user_info_for_id_or_from_request(sub_type=str, user_id=None, db_session db_session = db_session or current_session try: - if user_id: - result = query_for_user(db_session, user_id) + if user: + final_username = user.username + final_user_id = sub_type(user.id) + elif username: + result = query_for_user(db_session, username) final_username = result.username - final_user_id = result.id + final_user_id = sub_type(result.id) else: set_current_token( validate_request(scope={"user"}, audience=config.get("BASE_URL")) ) final_user_id = current_token["sub"] - if sub_type: - final_user_id = sub_type(final_user_id) + final_user_id = sub_type(final_user_id) final_username = current_token["context"]["user"]["name"] except Exception as exc: logger.info( @@ -1594,8 +1580,3 @@ def filter_auth_ids(action, list_auth_ids): if checked_permission in values: authorized_dbgaps.append(key) return authorized_dbgaps - - -def _get_project_access_for_user_id(user_id): - # TODO - return {} diff --git a/poetry.lock b/poetry.lock index 8bbe1adee..a18f7e980 100644 --- a/poetry.lock +++ b/poetry.lock @@ -263,7 +263,7 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "2.0.9" +version = "2.0.10" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -362,7 +362,7 @@ test = ["pytest (>=3.6.0,!=3.9.0,!=3.9.1,!=3.9.2)", "pretend", "iso8601", "pytz" [[package]] name = "decorator" -version = "5.1.0" +version = "5.1.1" description = "Decorators for Humans" category = "main" optional = false @@ -658,7 +658,7 @@ grpc = ["grpcio (>=1.8.2,<2.0dev)"] [[package]] name = "google-cloud-storage" -version = "1.43.0" +version = "1.44.0" description = "Google Cloud Storage API client library" category = "main" optional = false @@ -990,7 +990,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "paramiko" -version = "2.9.1" +version = "2.9.2" description = "SSH2 protocol library" category = "main" optional = false @@ -1055,7 +1055,7 @@ prometheus-client = "*" [[package]] name = "protobuf" -version = "3.19.1" +version = "3.19.3" description = "Protocol Buffers" category = "main" optional = false @@ -1130,15 +1130,14 @@ test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner [[package]] name = "pynacl" -version = "1.4.0" +version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] cffi = ">=1.4.1" -six = "*" [package.extras] docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] @@ -1253,7 +1252,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "requests" -version = "2.27.0" +version = "2.27.1" description = "Python HTTP for Humans." category = "main" optional = false @@ -1286,7 +1285,7 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] [[package]] name = "responses" -version = "0.16.0" +version = "0.17.0" description = "A utility library for mocking out the `requests` Python library." category = "dev" optional = false @@ -1504,8 +1503,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "43f3be30215be01158c35d88488b77683c2353f3f230ab0fb3ec2ee6c9c0652a" - +content-hash = "81f0de7206a9b331ba732a2efd530b5fb425e67f2eca3274e93cb49c40a4c586" [metadata.files] addict = [ @@ -1649,8 +1647,8 @@ cffi = [ {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, - {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, + {file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, + {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, ] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, @@ -1749,8 +1747,8 @@ cryptography = [ {file = "cryptography-2.8.tar.gz", hash = "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651"}, ] decorator = [ - {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"}, - {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"}, + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] dnspython = [ {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, @@ -1828,8 +1826,8 @@ google-cloud-core = [ {file = "google_cloud_core-2.2.1-py2.py3-none-any.whl", hash = "sha256:ab6cee07791afe4e210807ceeab749da6a076ab16d496ac734bf7e6ffea27486"}, ] google-cloud-storage = [ - {file = "google-cloud-storage-1.43.0.tar.gz", hash = "sha256:f3b4f4be5c8a1b5727a8f7136c94d3bacdd4b7bf11f9553f51ae4c1d876529d3"}, - {file = "google_cloud_storage-1.43.0-py2.py3-none-any.whl", hash = "sha256:bb3e4088054d50616bd57e4b81bb158db804c91faed39279d666e2fd07d2c118"}, + {file = "google-cloud-storage-1.44.0.tar.gz", hash = "sha256:29edbfeedd157d853049302bf5d104055c6f0cb7ef283537da3ce3f730073001"}, + {file = "google_cloud_storage-1.44.0-py2.py3-none-any.whl", hash = "sha256:cd4a223e9c18d771721a85c98a9c01b97d257edddff833ba63b7b1f0b9b4d6e9"}, ] google-crc32c = [ {file = "google-crc32c-1.3.0.tar.gz", hash = "sha256:276de6273eb074a35bc598f8efbc00c7869c5cf2e29c90748fccc8c898c244df"}, @@ -2039,8 +2037,8 @@ packaging = [ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] paramiko = [ - {file = "paramiko-2.9.1-py2.py3-none-any.whl", hash = "sha256:db5d3f19607941b1c90233588d60213c874392c4961c6297037da989c24f8070"}, - {file = "paramiko-2.9.1.tar.gz", hash = "sha256:a1fdded3b55f61d23389e4fe52d9ae428960ac958d2edf50373faa5d8926edd0"}, + {file = "paramiko-2.9.2-py2.py3-none-any.whl", hash = "sha256:04097dbd96871691cdb34c13db1883066b8a13a0df2afd4cb0a92221f51c2603"}, + {file = "paramiko-2.9.2.tar.gz", hash = "sha256:944a9e5dbdd413ab6c7951ea46b0ab40713235a9c4c5ca81cfe45c6f14fa677b"}, ] pbr = [ {file = "pbr-2.0.0-py2.py3-none-any.whl", hash = "sha256:d9b69a26a5cb4e3898eb3c5cea54d2ab3332382167f04e30db5e1f54e1945e45"}, @@ -2059,30 +2057,32 @@ prometheus-flask-exporter = [ {file = "prometheus_flask_exporter-0.18.7.tar.gz", hash = "sha256:f1f6f23535479d41587a100a24a60cb9199c34986e95f6691496807ee5017e59"}, ] protobuf = [ - {file = "protobuf-3.19.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d80f80eb175bf5f1169139c2e0c5ada98b1c098e2b3c3736667f28cbbea39fc8"}, - {file = "protobuf-3.19.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a529e7df52204565bcd33738a7a5f288f3d2d37d86caa5d78c458fa5fabbd54d"}, - {file = "protobuf-3.19.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28ccea56d4dc38d35cd70c43c2da2f40ac0be0a355ef882242e8586c6d66666f"}, - {file = "protobuf-3.19.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b30a7de128c46b5ecb343917d9fa737612a6e8280f440874e5cc2ba0d79b8f6"}, - {file = "protobuf-3.19.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5935c8ce02e3d89c7900140a8a42b35bc037ec07a6aeb61cc108be8d3c9438a6"}, - {file = "protobuf-3.19.1-cp36-cp36m-win32.whl", hash = "sha256:74f33edeb4f3b7ed13d567881da8e5a92a72b36495d57d696c2ea1ae0cfee80c"}, - {file = "protobuf-3.19.1-cp36-cp36m-win_amd64.whl", hash = "sha256:038daf4fa38a7e818dd61f51f22588d61755160a98db087a046f80d66b855942"}, - {file = "protobuf-3.19.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e51561d72efd5bd5c91490af1f13e32bcba8dab4643761eb7de3ce18e64a853"}, - {file = "protobuf-3.19.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:6e8ea9173403219239cdfd8d946ed101f2ab6ecc025b0fda0c6c713c35c9981d"}, - {file = "protobuf-3.19.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db3532d9f7a6ebbe2392041350437953b6d7a792de10e629c1e4f5a6b1fe1ac6"}, - {file = "protobuf-3.19.1-cp37-cp37m-win32.whl", hash = "sha256:615b426a177780ce381ecd212edc1e0f70db8557ed72560b82096bd36b01bc04"}, - {file = "protobuf-3.19.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d8919368410110633717c406ab5c97e8df5ce93020cfcf3012834f28b1fab1ea"}, - {file = "protobuf-3.19.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:71b0250b0cfb738442d60cab68abc166de43411f2a4f791d31378590bfb71bd7"}, - {file = "protobuf-3.19.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3cd0458870ea7d1c58e948ac8078f6ba8a7ecc44a57e03032ed066c5bb318089"}, - {file = "protobuf-3.19.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:655264ed0d0efe47a523e2255fc1106a22f6faab7cc46cfe99b5bae085c2a13e"}, - {file = "protobuf-3.19.1-cp38-cp38-win32.whl", hash = "sha256:b691d996c6d0984947c4cf8b7ae2fe372d99b32821d0584f0b90277aa36982d3"}, - {file = "protobuf-3.19.1-cp38-cp38-win_amd64.whl", hash = "sha256:e7e8d2c20921f8da0dea277dfefc6abac05903ceac8e72839b2da519db69206b"}, - {file = "protobuf-3.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fd390367fc211cc0ffcf3a9e149dfeca78fecc62adb911371db0cec5c8b7472d"}, - {file = "protobuf-3.19.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d83e1ef8cb74009bebee3e61cc84b1c9cd04935b72bca0cbc83217d140424995"}, - {file = "protobuf-3.19.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36d90676d6f426718463fe382ec6274909337ca6319d375eebd2044e6c6ac560"}, - {file = "protobuf-3.19.1-cp39-cp39-win32.whl", hash = "sha256:e7b24c11df36ee8e0c085e5b0dc560289e4b58804746fb487287dda51410f1e2"}, - {file = "protobuf-3.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:77d2fadcf369b3f22859ab25bd12bb8e98fb11e05d9ff9b7cd45b711c719c002"}, - {file = "protobuf-3.19.1-py2.py3-none-any.whl", hash = "sha256:e813b1c9006b6399308e917ac5d298f345d95bb31f46f02b60cd92970a9afa17"}, - {file = "protobuf-3.19.1.tar.gz", hash = "sha256:62a8e4baa9cb9e064eb62d1002eca820857ab2138440cb4b3ea4243830f94ca7"}, + {file = "protobuf-3.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1cb2ed66aac593adbf6dca4f07cd7ee7e2958b17bbc85b2cc8bc564ebeb258ec"}, + {file = "protobuf-3.19.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:898bda9cd37ec0c781b598891e86435de80c3bfa53eb483a9dac5a11ec93e942"}, + {file = "protobuf-3.19.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ad761ef3be34c8bdc7285bec4b40372a8dad9e70cfbdc1793cd3cf4c1a4ce74"}, + {file = "protobuf-3.19.3-cp310-cp310-win32.whl", hash = "sha256:2cddcbcc222f3144765ccccdb35d3621dc1544da57a9aca7e1944c1a4fe3db11"}, + {file = "protobuf-3.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:6202df8ee8457cb00810c6e76ced480f22a1e4e02c899a14e7b6e6e1de09f938"}, + {file = "protobuf-3.19.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:397d82f1c58b76445469c8c06b8dee1ff67b3053639d054f52599a458fac9bc6"}, + {file = "protobuf-3.19.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e54b8650e849ee8e95e481024bff92cf98f5ec61c7650cb838d928a140adcb63"}, + {file = "protobuf-3.19.3-cp36-cp36m-win32.whl", hash = "sha256:3bf3a07d17ba3511fe5fa916afb7351f482ab5dbab5afe71a7a384274a2cd550"}, + {file = "protobuf-3.19.3-cp36-cp36m-win_amd64.whl", hash = "sha256:afa8122de8064fd577f49ae9eef433561c8ace97a0a7b969d56e8b1d39b5d177"}, + {file = "protobuf-3.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18c40a1b8721026a85187640f1786d52407dc9c1ba8ec38accb57a46e84015f6"}, + {file = "protobuf-3.19.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:af7238849fa79285d448a24db686517570099739527a03c9c2971cce99cc5ae2"}, + {file = "protobuf-3.19.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e765e6dfbbb02c55e4d6d1145743401a84fc0b508f5a81b2c5a738cf86353139"}, + {file = "protobuf-3.19.3-cp37-cp37m-win32.whl", hash = "sha256:c781402ed5396ab56358d7b866d78c03a77cbc26ba0598d8bb0ac32084b1a257"}, + {file = "protobuf-3.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:544fe9705189b249380fae07952d220c97f5c6c9372a6f936cc83a79601dcb70"}, + {file = "protobuf-3.19.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84bf3aa3efb00dbe1c7ed55da0f20800b0662541e582d7e62b3e1464d61ed365"}, + {file = "protobuf-3.19.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3f80a3491eaca767cdd86cb8660dc778f634b44abdb0dffc9b2a8e8d0cd617d0"}, + {file = "protobuf-3.19.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9401d96552befcc7311f5ef8f0fa7dba0ef5fd805466b158b141606cd0ab6a8"}, + {file = "protobuf-3.19.3-cp38-cp38-win32.whl", hash = "sha256:ef02d112c025e83db5d1188a847e358beab3e4bbfbbaf10eaf69e67359af51b2"}, + {file = "protobuf-3.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:1291a0a7db7d792745c99d4657b4c5c4942695c8b1ac1bfb993a34035ec123f7"}, + {file = "protobuf-3.19.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49677e5e9c7ea1245a90c2e8a00d304598f22ea3aa0628f0e0a530a9e70665fa"}, + {file = "protobuf-3.19.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:df2ba379ee42427e8fcc6a0a76843bff6efb34ef5266b17f95043939b5e25b69"}, + {file = "protobuf-3.19.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2acd7ca329be544d1a603d5f13a4e34a3791c90d651ebaf130ba2e43ae5397c6"}, + {file = "protobuf-3.19.3-cp39-cp39-win32.whl", hash = "sha256:b53519b2ebec70cfe24b4ddda21e9843f0918d7c3627a785393fb35d402ab8ad"}, + {file = "protobuf-3.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:8ceaf5fdb72c8e1fcb7be9f2b3b07482ce058a3548180c0bdd5c7e4ac5e14165"}, + {file = "protobuf-3.19.3-py2.py3-none-any.whl", hash = "sha256:f6d4b5b7595a57e69eb7314c67bef4a3c745b4caf91accaf72913d8e0635111b"}, + {file = "protobuf-3.19.3.tar.gz", hash = "sha256:d975a6314fbf5c524d4981e24294739216b5fb81ef3c14b86fb4b045d6690907"}, ] psycopg2 = [ {file = "psycopg2-2.9.3-cp310-cp310-win32.whl", hash = "sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362"}, @@ -2172,24 +2172,16 @@ pyjwt = [ {file = "PyJWT-1.7.1.tar.gz", hash = "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"}, ] pynacl = [ - {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, - {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, - {file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, - {file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, - {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, - {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, - {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, - {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, - {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, - {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, - {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, - {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, - {file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"}, - {file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"}, - {file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"}, - {file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"}, - {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, - {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, ] pyparsing = [ {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, @@ -2251,8 +2243,8 @@ pyyaml = [ {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] requests = [ - {file = "requests-2.27.0-py2.py3-none-any.whl", hash = "sha256:f71a09d7feba4a6b64ffd8e9d9bc60f9bf7d7e19fd0e04362acb1cfc2e3d98df"}, - {file = "requests-2.27.0.tar.gz", hash = "sha256:8e5643905bf20a308e25e4c1dd379117c09000bf8a82ebccc462cfb1b34a16b5"}, + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, ] requests-oauthlib = [ {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, @@ -2260,8 +2252,8 @@ requests-oauthlib = [ {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, ] responses = [ - {file = "responses-0.16.0-py2.py3-none-any.whl", hash = "sha256:f358ef75e8bf431b0aa203cc62625c3a1c80a600dbe9de91b944bf4e9c600b92"}, - {file = "responses-0.16.0.tar.gz", hash = "sha256:a2e3aca2a8277e61257cd3b1c154b1dd0d782b1ae3d38b7fa37cbe3feb531791"}, + {file = "responses-0.17.0-py2.py3-none-any.whl", hash = "sha256:e4fc472fb7374fb8f84fcefa51c515ca4351f198852b4eb7fc88223780b472ea"}, + {file = "responses-0.17.0.tar.gz", hash = "sha256:ec675e080d06bf8d1fb5e5a68a1e5cd0df46b09c78230315f650af5e4036bec7"}, ] retry = [ {file = "retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606"}, diff --git a/tests/data/test_blank_index.py b/tests/data/test_blank_index.py index 8fb1c84cb..2315767f1 100755 --- a/tests/data/test_blank_index.py +++ b/tests/data/test_blank_index.py @@ -415,7 +415,7 @@ def test_generate_aws_presigned_url_for_part(app, indexd_client): blank_index = BlankIndex(uploader=uploader) assert blank_index with patch( - "fence.blueprints.data.indexd.S3IndexedFileLocation.generate_presigne_url_for_part_upload" + "fence.blueprints.data.indexd.S3IndexedFileLocation.generate_presigned_url_for_part_upload" ): blank_index.generate_aws_presigned_url_for_part( key="some key", uploadId="some id", partNumber=1, expires_in=10 diff --git a/tests/data/test_data.py b/tests/data/test_data.py index 7b1264609..f54f74b6d 100755 --- a/tests/data/test_data.py +++ b/tests/data/test_data.py @@ -1414,7 +1414,7 @@ def test_delete_file_locations( ) mock_check_auth = mock.patch.object( fence.blueprints.data.indexd.IndexedFile, - "check_authorization", + "check_legacy_authorization", return_value=True, ) @@ -1481,7 +1481,7 @@ def test_delete_file_locations_by_uploader( ) mock_check_auth = mock.patch.object( fence.blueprints.data.indexd.IndexedFile, - "check_authorization", + "check_legacy_authorization", return_value=True, ) diff --git a/tests/data/test_indexed_file.py b/tests/data/test_indexed_file.py index 52dea317b..1d8e84688 100755 --- a/tests/data/test_indexed_file.py +++ b/tests/data/test_indexed_file.py @@ -402,11 +402,11 @@ def test_set_acl_missing_unauthorized( indexed_file.set_acls -def test_get_authorized_and_user_id_missing_value_error( +def test_get_authorized_with_username_missing_value_error( app, supported_action, supported_protocol, indexd_client_accepting_record ): """ - Test fence.blueprints.data.indexd.IndexedFile get_authorized_and_user_id without authz in indexd record + Test fence.blueprints.data.indexd.IndexedFile get_authorized_with_username without authz in indexd record """ indexd_record_with_no_authz_and_public_acl_populated = { "urls": [f"{supported_protocol}://some/location"], @@ -418,7 +418,7 @@ def test_get_authorized_and_user_id_missing_value_error( with patch("fence.blueprints.data.indexd.flask.current_app", return_value=app): indexed_file = IndexedFile(file_id="some id") with pytest.raises(ValueError): - indexed_file.get_authorized_and_user_id(supported_action) + indexed_file.get_authorized_with_username(supported_action) @pytest.mark.parametrize( From 418765e9387e8d05f329fdc8bf9d21086c09d123 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 11 Jan 2022 14:53:45 -0600 Subject: [PATCH 148/211] chore(misc): remove unneeded lines and casts --- bin/fence_create.py | 1 - fence/blueprints/login/ras.py | 1 - fence/models.py | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/bin/fence_create.py b/bin/fence_create.py index 4ef76461f..7ef09d9ff 100755 --- a/bin/fence_create.py +++ b/bin/fence_create.py @@ -407,7 +407,6 @@ def main(): STORAGE_CREDENTIALS = os.environ.get("STORAGE_CREDENTIALS") or config.get( "STORAGE_CREDENTIALS" ) - usersync = config.get("USERSYNC", {}) arborist = None if args.arborist: diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index 1865ef83d..057fc844e 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -45,7 +45,6 @@ def post_login(self, user=None, token_result=None, id_from_idp=None): userinfo = flask.g.userinfo global_parse_visas_on_login = config["GLOBAL_PARSE_VISAS_ON_LOGIN"] - usersync = config.get("USERSYNC", {}) parse_visas = global_parse_visas_on_login or ( global_parse_visas_on_login == None and ( diff --git a/fence/models.py b/fence/models.py index d09600b42..56448b63b 100644 --- a/fence/models.py +++ b/fence/models.py @@ -61,7 +61,7 @@ def query_for_user(session, username): return ( session.query(User) - .filter(func.lower(User.username) == str(username).lower()) + .filter(func.lower(User.username) == username.lower()) .first() ) From 83dc5897f38ea91e59ab1ada8e7eae4e2c683274 Mon Sep 17 00:00:00 2001 From: John McCann Date: Wed, 12 Jan 2022 08:34:38 -0800 Subject: [PATCH 149/211] refactor(fence/__init__.py): load only w/ config --- fence/__init__.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/fence/__init__.py b/fence/__init__.py index 413bc3b1e..03a9d92f4 100755 --- a/fence/__init__.py +++ b/fence/__init__.py @@ -21,17 +21,13 @@ logger = get_logger(__name__, log_level="debug") # Load the configuration *before* importing modules that rely on it -from fence.config import config, DEFAULT_CFG_PATH +from fence.config import config from fence.settings import CONFIG_SEARCH_FOLDERS -try: - if os.environ.get("FENCE_CONFIG_PATH"): - config.load(config_path=os.environ["FENCE_CONFIG_PATH"]) - else: - config.load(search_folders=CONFIG_SEARCH_FOLDERS) -except: - logger.warning("Unable to load config, using default config...", exc_info=True) - config.load(config_path=DEFAULT_CFG_PATH) +config.load( + config_path=os.environ.get("FENCE_CONFIG_PATH"), + search_folders=CONFIG_SEARCH_FOLDERS, +) from fence.auth import logout, build_redirect_url from fence.blueprints.data.indexd import S3IndexedFileLocation From 798e5c19818c36725d2c85d8d8d2a8e319aa0463 Mon Sep 17 00:00:00 2001 From: John McCann Date: Wed, 12 Jan 2022 08:53:03 -0800 Subject: [PATCH 150/211] Update fence/models.py Co-authored-by: Alexander VanTol --- fence/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fence/models.py b/fence/models.py index 0e12216e5..c07996343 100644 --- a/fence/models.py +++ b/fence/models.py @@ -670,6 +670,7 @@ def _get_issuer_to_idp(): ISSUER_TO_IDP = _get_issuer_to_idp() + # no longer need function since results stored in var del _get_issuer_to_idp From 7fd728f5e5bd3d66b83e6d2a66c8b63eb5c21b48 Mon Sep 17 00:00:00 2001 From: John McCann Date: Wed, 12 Jan 2022 14:35:49 -0800 Subject: [PATCH 151/211] chore(migration): add authz column to project --- fence/models.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fence/models.py b/fence/models.py index 3a09830b5..a22805164 100644 --- a/fence/models.py +++ b/fence/models.py @@ -940,6 +940,13 @@ def migrate(driver): metadata=md, ) + add_column_if_not_exist( + table_name=Project.__tablename__, + column=Column("authz", String), + driver=driver, + metadata=md, + ) + def add_foreign_key_column_if_not_exist( table_name, From 8dd330331a18d7f4707c8347779c4a523d3bc06f Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Thu, 13 Jan 2022 14:42:40 -0600 Subject: [PATCH 152/211] feat(arborist): ensure use of policy prefix for different sources of authorization (DRS, login, token polling) --- fence/blueprints/data/indexd.py | 2 +- fence/blueprints/login/ras.py | 5 ++++- fence/job/visa_update_cronjob.py | 7 ++++++- fence/resources/ga4gh/passports.py | 15 +++++++++++---- fence/resources/openid/ras_oauth2.py | 10 +++++++--- tests/ras/test_ras.py | 19 ++++++++++++++----- 6 files changed, 43 insertions(+), 15 deletions(-) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index e58026862..3572fa1d4 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -94,7 +94,7 @@ def get_signed_url_for_file( if ga4gh_passports: # users_from_passports = {"username": Fence.User} users_from_passports = sync_gen3_users_authz_from_ga4gh_passports( - ga4gh_passports, db_session=db_session + ga4gh_passports, authz_policy_prefix="GA4GH.DRS", db_session=db_session ) # add the user details to `flask.g.audit_data` first, so they are diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index 057fc844e..cc547653a 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -75,7 +75,10 @@ def post_login(self, user=None, token_result=None, id_from_idp=None): # now sync authz updates users_from_passports = fence.resources.ga4gh.passports.sync_gen3_users_authz_from_ga4gh_passports( - [passport], pkey_cache=pkey_cache, db_session=current_session + [passport], + authz_policy_prefix="POST.LOGIN", + pkey_cache=pkey_cache, + db_session=current_session, ) user_ids_from_passports = list(users_from_passports.keys()) logger.debug(f"user_ids_from_passports: {user_ids_from_passports}") diff --git a/fence/job/visa_update_cronjob.py b/fence/job/visa_update_cronjob.py index 7f7945316..5e60d2180 100644 --- a/fence/job/visa_update_cronjob.py +++ b/fence/job/visa_update_cronjob.py @@ -155,7 +155,12 @@ async def updater(self, name, updater_queue, db_session): ) # when getting access token, this persists new refresh token, # it also persists validated visa(s) in the database - client.update_user_authorization(user, self.pkey_cache, db_session) + client.update_user_authorization( + user, + pkey_cache=self.pkey_cache, + authz_policy_prefix="TOKEN.POLLING", + db_session=db_session, + ) else: self.logger.debug( f"Updater {name} NOT updating authorization for " diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index c6d4c9fe9..98094d67a 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -28,7 +28,10 @@ def sync_gen3_users_authz_from_ga4gh_passports( - passports, pkey_cache=None, db_session=None + passports, + authz_policy_prefix, + pkey_cache=None, + db_session=None, ): """ Validate passports and embedded visas, using each valid visa's identity @@ -128,7 +131,11 @@ def sync_gen3_users_authz_from_ga4gh_passports( # This adds the visas to the database session but doesn't commit until # the end of this function _sync_validated_visa_authorization( - gen3_user, ga4gh_visas, min_visa_expiration, db_session=db_session + gen3_user=gen3_user, + ga4gh_visas=ga4gh_visas, + expiration=min_visa_expiration, + policy_prefix=authz_policy_prefix, + db_session=db_session, ) users_from_current_passport.append(gen3_user) @@ -349,7 +356,7 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id, db_session=None): def _sync_validated_visa_authorization( - gen3_user, ga4gh_visas, expiration, db_session=None + gen3_user, ga4gh_visas, expiration, policy_prefix, db_session=None ): """ Wrapper around UserSyncer.sync_single_user_visas method, which parses @@ -379,7 +386,7 @@ def _sync_validated_visa_authorization( ga4gh_visas, db_session, expires=expiration, - policy_prefix="GA4GH.DRS", + policy_prefix=policy_prefix, ) # after syncing authorization, persist the visas that were parsed successfully. diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index c0aacd64a..c683cf7e0 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -286,7 +286,9 @@ def map_iss_sub_pair_to_user( return iss_sub_pair_to_user.user.username @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) - def update_user_authorization(self, user, pkey_cache, db_session=current_session): + def update_user_authorization( + self, user, pkey_cache, authz_policy_prefix, db_session=current_session + ): """ Updates user's RAS refresh token and uses the new access token to retrieve new visas from RAS's /userinfo endpoint and update access @@ -307,11 +309,13 @@ def update_user_authorization(self, user, pkey_cache, db_session=current_session # database) users_from_passports = ( fence.resources.ga4gh.passports.sync_gen3_users_authz_from_ga4gh_passports( - [passport], pkey_cache=pkey_cache, db_session=db_session + [passport], + authz_policy_prefix=authz_policy_prefix, + pkey_cache=pkey_cache, + db_session=db_session, ) ) user_ids_from_passports = list(users_from_passports.keys()) - self.logger.debug(f"user_ids_from_passports:{user_ids_from_passports}") # TODO? # put_gen3_usernames_for_passport_into_cache( diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index a109f0311..d0978bd4d 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -165,7 +165,10 @@ def return_false(): } } ras_client.update_user_authorization( - test_user, pkey_cache=pkey_cache, db_session=db_session + test_user, + authz_policy_prefix="TEST", + pkey_cache=pkey_cache, + db_session=db_session, ) # restore public keys and context @@ -251,7 +254,10 @@ def test_update_visa_empty_passport_returned( } } ras_client.update_user_authorization( - test_user, pkey_cache=pkey_cache, db_session=db_session + test_user, + authz_policy_prefix="TEST", + pkey_cache=pkey_cache, + db_session=db_session, ) # at this point we expect the existing visa to stay around (since it hasn't expired) @@ -342,7 +348,7 @@ def test_update_visa_empty_visa_returned( ) ras_client.update_user_authorization( - test_user, pkey_cache={}, db_session=db_session + test_user, authz_policy_prefix="TEST", pkey_cache={}, db_session=db_session ) # at this point we expect the existing visa to stay around (since it hasn't expired) @@ -467,7 +473,10 @@ def test_update_visa_token_with_invalid_visa( } ras_client.update_user_authorization( - test_user, pkey_cache=pkey_cache, db_session=db_session + test_user, + authz_policy_prefix="TEST", + pkey_cache=pkey_cache, + db_session=db_session, ) # at this point we expect the existing visa to stay around (since it hasn't expired) # and 2 new good visas @@ -581,7 +590,7 @@ def return_false(): # Pass in an empty pkey cache so that the client will have to hit the jwks endpoint. ras_client.update_user_authorization( - test_user, pkey_cache={}, db_session=db_session + test_user, authz_policy_prefix="TEST", pkey_cache={}, db_session=db_session ) # restore public keys and context From 6a45bb2527699f82540e7394f1cf188203134a62 Mon Sep 17 00:00:00 2001 From: Alexander VanTol Date: Fri, 14 Jan 2022 10:13:46 -0600 Subject: [PATCH 153/211] Update visa_update_cronjob.py --- fence/job/visa_update_cronjob.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fence/job/visa_update_cronjob.py b/fence/job/visa_update_cronjob.py index 5e60d2180..fc5a0df9e 100644 --- a/fence/job/visa_update_cronjob.py +++ b/fence/job/visa_update_cronjob.py @@ -63,8 +63,8 @@ async def update_tokens(self, db_session): Producer: Collects users from db and feeds it to the workers Worker: Takes in the users from the Producer and passes it to the Updater to update the tokens and passes those updated tokens for JWT validation - Updater: Updates refresh_tokens and visas by calling the update_user_authorization from - the correct client + Updater: Updates refresh_tokens and visas by calling the update_user_authorization + from the correct client """ start_time = time.time() From b265016dd280eebf69c5be6d65097421c2643fc7 Mon Sep 17 00:00:00 2001 From: Alexander VanTol Date: Fri, 14 Jan 2022 11:53:19 -0600 Subject: [PATCH 154/211] Update fence/blueprints/data/indexd.py Co-authored-by: John McCann --- fence/blueprints/data/indexd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index 3572fa1d4..d6f89b18d 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -100,7 +100,7 @@ def get_signed_url_for_file( # add the user details to `flask.g.audit_data` first, so they are # included in the audit log if `IndexedFile(file_id)` raises a 404 if users_from_passports: - if len(list(users_from_passports.keys())) > 1: + if len(users_from_passports) > 1: logger.warning( "audit service doesn't support multiple users for a " "single request yet, so just log userinfo here" From 2f5fa2f050847ff1eed31f3a8cfef0ff3f9aeb40 Mon Sep 17 00:00:00 2001 From: Alexander VanTol Date: Fri, 14 Jan 2022 11:53:28 -0600 Subject: [PATCH 155/211] Update fence/blueprints/data/indexd.py Co-authored-by: John McCann --- fence/blueprints/data/indexd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index d6f89b18d..a5e8fc168 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -105,7 +105,7 @@ def get_signed_url_for_file( "audit service doesn't support multiple users for a " "single request yet, so just log userinfo here" ) - for username, user in users_from_passports: + for username, user in users_from_passports.items(): audit_data = { "username": username, "sub": user.id, From f8f76b18befc6b83c88f76bf8128e81f67579a87 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Fri, 14 Jan 2022 13:56:17 -0600 Subject: [PATCH 156/211] fix(arborist): ensure client gets created with authz_provider --- fence/resources/ga4gh/passports.py | 4 +++- fence/scripting/fence_create.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 969727725..4d3b067ab 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -378,7 +378,9 @@ def _sync_validated_visa_authorization( None """ db_session = db_session or current_session - default_args = fence.scripting.fence_create.get_default_init_syncer_inputs() + default_args = fence.scripting.fence_create.get_default_init_syncer_inputs( + authz_provider=policy_prefix + ) syncer = fence.scripting.fence_create.init_syncer(**default_args) synced_visas = syncer.sync_single_user_visas( diff --git a/fence/scripting/fence_create.py b/fence/scripting/fence_create.py index 8dbf61d5b..23545646d 100644 --- a/fence/scripting/fence_create.py +++ b/fence/scripting/fence_create.py @@ -203,7 +203,7 @@ def _remove_client_service_accounts(db_session, client): ) -def get_default_init_syncer_inputs(): +def get_default_init_syncer_inputs(authz_provider): DB = os.environ.get("FENCE_DB") or config.get("DB") if DB is None: try: @@ -214,7 +214,7 @@ def get_default_init_syncer_inputs(): arborist = ArboristClient( arborist_base_url=config["ARBORIST"], logger=get_logger("user_syncer.arborist_client"), - authz_provider="ras", + authz_provider=authz_provider, ) dbGaP = os.environ.get("dbGaP") or config.get("dbGaP") if not isinstance(dbGaP, list): From 12ec7261fe0c5fae0a07f38108097db39c6df8a3 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 18 Jan 2022 10:53:32 -0600 Subject: [PATCH 157/211] fix(ga4gh): ensure we log an error but continue when the expiration cannot be correctly parsed for a RAS Visa --- fence/sync/passport_sync/ras_sync.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/fence/sync/passport_sync/ras_sync.py b/fence/sync/passport_sync/ras_sync.py index b429cce35..01e80a630 100644 --- a/fence/sync/passport_sync/ras_sync.py +++ b/fence/sync/passport_sync/ras_sync.py @@ -39,7 +39,16 @@ def _parse_single_visa( if parse_consent_code and consent_group: full_phsid += "." + consent_group privileges = {"read-storage", "read"} - permission_expiration = permission.get("expiration") + + permission_expiration = None + try: + permission_expiration = int(permission.get("expiration", 0)) + except Exception as exc: + self.logger.error( + f"cannot determine visa expiration for {full_phsid} " + f"from: {permission.get('expiration')}. Ignoring this permission." + ) + if permission_expiration and expires <= permission_expiration: project[full_phsid] = privileges info["tags"] = {"dbgap_role": permission.get("role", "")} From 19b937068aaa25df34001d9c8df28adecda8a223 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 18 Jan 2022 11:56:21 -0600 Subject: [PATCH 158/211] fix(ga4gh): ensure no errors with concatenation by casting to strings --- fence/sync/passport_sync/ras_sync.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fence/sync/passport_sync/ras_sync.py b/fence/sync/passport_sync/ras_sync.py index 01e80a630..cab7e05dc 100644 --- a/fence/sync/passport_sync/ras_sync.py +++ b/fence/sync/passport_sync/ras_sync.py @@ -33,8 +33,8 @@ def _parse_single_visa( if time.time() < expires: for permission in ras_dbgap_permissions: - phsid = permission.get("phs_id", "") - consent_group = permission.get("consent_group", "") + phsid = str(permission.get("phs_id", "")) + consent_group = str(permission.get("consent_group", "")) full_phsid = phsid if parse_consent_code and consent_group: full_phsid += "." + consent_group From c40d5c4de31a58db27fd1ec7ffa2d533d3c6c753 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 18 Jan 2022 13:24:29 -0600 Subject: [PATCH 159/211] fix(passports): use correct .delete rather than .remove for sqlalchemy db objects --- fence/resources/ga4gh/passports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 4d3b067ab..1ba0e64ec 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -394,7 +394,7 @@ def _sync_validated_visa_authorization( # after syncing authorization, persist the visas that were parsed successfully. for visa in ga4gh_visas: if visa not in synced_visas: - db_session.remove(visa) + db_session.delete(visa) else: db_session.add(visa) From 0b67abd1b83101a1a72ac9641aa678b2cd159026 Mon Sep 17 00:00:00 2001 From: Alexander VanTol Date: Tue, 18 Jan 2022 13:41:22 -0600 Subject: [PATCH 160/211] Update auth.py --- fence/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fence/auth.py b/fence/auth.py index 638b63a6e..40666ccf5 100644 --- a/fence/auth.py +++ b/fence/auth.py @@ -114,7 +114,7 @@ def set_flask_session_values(user): if id_from_idp: user.id_from_idp = id_from_idp - # TODO: update iss_sub mapping table? + # TODO: do we need to update iss_sub mapping table? # setup idp connection for new user (or existing user w/o it setup) idp = ( From fe823f2dd8db5b3548dd0f8fbb2daa60879ddc1c Mon Sep 17 00:00:00 2001 From: Alexander VanTol Date: Tue, 18 Jan 2022 15:40:23 -0600 Subject: [PATCH 161/211] Update auth.py --- fence/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fence/auth.py b/fence/auth.py index 40666ccf5..638b63a6e 100644 --- a/fence/auth.py +++ b/fence/auth.py @@ -114,7 +114,7 @@ def set_flask_session_values(user): if id_from_idp: user.id_from_idp = id_from_idp - # TODO: do we need to update iss_sub mapping table? + # TODO: update iss_sub mapping table? # setup idp connection for new user (or existing user w/o it setup) idp = ( From 05628d33c3f042eff2e124707722999e5196a6d1 Mon Sep 17 00:00:00 2001 From: Alexander VanTol Date: Tue, 18 Jan 2022 16:51:41 -0600 Subject: [PATCH 162/211] Update ras_sync.py --- fence/sync/passport_sync/ras_sync.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/fence/sync/passport_sync/ras_sync.py b/fence/sync/passport_sync/ras_sync.py index cab7e05dc..999dc558f 100644 --- a/fence/sync/passport_sync/ras_sync.py +++ b/fence/sync/passport_sync/ras_sync.py @@ -33,12 +33,19 @@ def _parse_single_visa( if time.time() < expires: for permission in ras_dbgap_permissions: - phsid = str(permission.get("phs_id", "")) - consent_group = str(permission.get("consent_group", "")) - full_phsid = phsid - if parse_consent_code and consent_group: - full_phsid += "." + consent_group - privileges = {"read-storage", "read"} + phsid = permission.get("phs_id", "") + consent_group = permission.get("consent_group", "") + + if not phsid or not consent_group: + self.logger.error( + f"cannot determine visa permission for phsid {phsid} " + f"and consent_group {consent_group}. Ignoring this permission." + ) + else: + full_phsid = str(phsid) + if parse_consent_code and consent_group: + full_phsid += "." + str(consent_group) + privileges = {"read-storage", "read"} permission_expiration = None try: From 1bf2a3b3db30e79634cc133c9fc54bf7542acc7e Mon Sep 17 00:00:00 2001 From: Alexander VanTol Date: Tue, 18 Jan 2022 16:55:34 -0600 Subject: [PATCH 163/211] Update ras_sync.py --- fence/sync/passport_sync/ras_sync.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/fence/sync/passport_sync/ras_sync.py b/fence/sync/passport_sync/ras_sync.py index 999dc558f..6d6a374f3 100644 --- a/fence/sync/passport_sync/ras_sync.py +++ b/fence/sync/passport_sync/ras_sync.py @@ -35,7 +35,7 @@ def _parse_single_visa( for permission in ras_dbgap_permissions: phsid = permission.get("phs_id", "") consent_group = permission.get("consent_group", "") - + if not phsid or not consent_group: self.logger.error( f"cannot determine visa permission for phsid {phsid} " @@ -47,18 +47,18 @@ def _parse_single_visa( full_phsid += "." + str(consent_group) privileges = {"read-storage", "read"} - permission_expiration = None - try: - permission_expiration = int(permission.get("expiration", 0)) - except Exception as exc: - self.logger.error( - f"cannot determine visa expiration for {full_phsid} " - f"from: {permission.get('expiration')}. Ignoring this permission." - ) + permission_expiration = None + try: + permission_expiration = int(permission.get("expiration", 0)) + except Exception as exc: + self.logger.error( + f"cannot determine visa expiration for {full_phsid} " + f"from: {permission.get('expiration')}. Ignoring this permission." + ) - if permission_expiration and expires <= permission_expiration: - project[full_phsid] = privileges - info["tags"] = {"dbgap_role": permission.get("role", "")} + if permission_expiration and expires <= permission_expiration: + project[full_phsid] = privileges + info["tags"] = {"dbgap_role": permission.get("role", "")} else: # Remove visas if its invalid or expired user.ga4gh_visas_v1 = [] From befab8ae01e28c6fad229ba8afe1a065eb75009e Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Fri, 21 Jan 2022 12:43:23 -0600 Subject: [PATCH 164/211] fix(cli): add parser for the ga4gh cleanup job --- bin/fence_create.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/fence_create.py b/bin/fence_create.py index 7ef09d9ff..6e86371b7 100755 --- a/bin/fence_create.py +++ b/bin/fence_create.py @@ -149,6 +149,7 @@ def parse_arguments(): subparsers.add_parser("expired-service-account-delete") subparsers.add_parser("bucket-access-group-verify") subparsers.add_parser("delete-expired-google-access") + subparsers.add_parser("cleanup-expired-ga4gh-information") hmac_create = subparsers.add_parser("hmac-create") hmac_create.add_argument("yaml-input") From 570044a51be45eaa7f6be674cf19114284ea27af Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Fri, 21 Jan 2022 12:48:24 -0600 Subject: [PATCH 165/211] fix(cli): add import for ga4gh cleanup job --- bin/fence_create.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/fence_create.py b/bin/fence_create.py index 6e86371b7..c906f1731 100755 --- a/bin/fence_create.py +++ b/bin/fence_create.py @@ -18,6 +18,7 @@ delete_client_action, delete_users, delete_expired_google_access, + cleanup_expired_ga4gh_information, google_init, list_client_action, link_external_bucket, From 5dc93d945ba5a66c87b928873dcfb1931bd9059b Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Fri, 21 Jan 2022 13:45:38 -0600 Subject: [PATCH 166/211] chore(logs): add some debug logs for adding/removing visas from the db session --- fence/resources/ga4gh/passports.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 1ba0e64ec..c285a1c83 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -394,8 +394,10 @@ def _sync_validated_visa_authorization( # after syncing authorization, persist the visas that were parsed successfully. for visa in ga4gh_visas: if visa not in synced_visas: + logger.debug(f"deleting visa with id={visa.id} from db session") db_session.delete(visa) else: + logger.debug(f"adding visa with id={visa.id} to db session") db_session.add(visa) From 8d6e69b773c8e6303b69e11f1c3b3a389870d2f3 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Fri, 21 Jan 2022 16:36:46 -0600 Subject: [PATCH 167/211] feat(data): remove legacy concept of "public" data in favor of letting the policy engine make decisions in all cases, fix tests --- fence/blueprints/data/indexd.py | 37 ++----- ...zure_blob_storage_indexed_file_location.py | 96 ++++++++----------- 2 files changed, 46 insertions(+), 87 deletions(-) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index a5e8fc168..dee8810dc 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -77,8 +77,7 @@ def get_signed_url_for_file( r_pays_project = flask.request.args.get("userProject", None) db_session = db_session or current_session - # default to signing the url even if it's a public object - # this will work so long as we're provided a user token + # default to signing the url force_signed_url = True no_force_sign_param = flask.request.args.get("no_force_sign") if no_force_sign_param and no_force_sign_param.lower() == "true": @@ -526,7 +525,6 @@ def _get_signed_url( return self.indexed_file_locations[0].get_signed_url( action, expires_in, - public_data=self.public, force_signed_url=force_signed_url, r_pays_project=r_pays_project, authorized_user=authorized_user, @@ -542,7 +540,6 @@ def _get_signed_url( return file_location.get_signed_url( action, expires_in, - public_data=self.public, force_signed_url=force_signed_url, r_pays_project=r_pays_project, authorized_user=authorized_user, @@ -626,21 +623,10 @@ def get_authorized_with_username(self, action, usernames_from_passports=None): def metadata(self): return self.index_document.get("metadata", {}) - @cached_property - def public(self): - if self.index_document.get("authz", []): - return self.public_authz - else: - return self.public_acl - @cached_property def public_acl(self): return "*" in self.set_acls - @cached_property - def public_authz(self): - return "/open" in self.index_document.get("authz", []) - @login_required({"data"}) def check_legacy_authorization(self, action): # if we have a data file upload without corresponding metadata, the record can @@ -759,7 +745,6 @@ def get_signed_url( self, action, expires_in, - public_data=False, force_signed_url=True, users_from_passports=None, **kwargs, @@ -960,7 +945,6 @@ def get_signed_url( self, action, expires_in, - public_data=False, force_signed_url=True, authorized_user=None, **kwargs, @@ -989,7 +973,7 @@ def get_signed_url( bucket_name, aws_creds, expires_in ) - # if it's public and we don't need to force the signed url, just return the raw + # if we don't need to force the signed url, just return the raw # s3 url aws_access_key_id = get_value( credential, @@ -999,7 +983,7 @@ def get_signed_url( # `aws_access_key_id == "*"` is a special case to support public buckets # where we do *not* want to try signing at all. the other case is that the # data is public and user requested to not sign the url - if aws_access_key_id == "*" or (public_data and not force_signed_url): + if aws_access_key_id == "*" or (not force_signed_url): return http_url region = self.get_bucket_region() @@ -1128,7 +1112,6 @@ def get_signed_url( self, action, expires_in, - public_data=False, force_signed_url=True, r_pays_project=None, authorized_user=None, @@ -1137,9 +1120,9 @@ def get_signed_url( user_info = _get_user_info_for_id_or_from_request(user=authorized_user) - if public_data and not force_signed_url: + if not force_signed_url: url = "https://storage.cloud.google.com/" + resource_path - elif public_data and _is_anonymous_user(user_info): + elif _is_anonymous_user(user_info): url = self._generate_anonymous_google_storage_signed_url( ACTION_DICT["gs"][action], resource_path, int(expires_in) ) @@ -1417,7 +1400,6 @@ def get_signed_url( self, action, expires_in, - public_data=False, force_signed_url=True, authorized_user=None, **kwargs, @@ -1439,11 +1421,6 @@ def get_signed_url( Get a signed url for an action like "upload" or "download". :param int expires_in: The SAS token will expire in a given number of seconds from datetime.utcnow() - :param bool public_data: - Indicate if the Azure Blob Storage Account has public access. - If it's public and we don't need to force the signed url, just return the raw - url. - The default for public_data is False. :param bool force_signed_url: Enforce signing the URL for the Azure Blob Storage Account using a SAS token. The default is True. @@ -1457,7 +1434,7 @@ def get_signed_url( container_name, blob_name = self._get_container_and_blob() user_info = _get_user_info_for_id_or_from_request(user=authorized_user) - if user_info and user_info.get("user_id") == ANONYMOUS_USER_ID: + if _is_anonymous_user(user_info): logger.info(f"Attempting to get a signed url an anonymous user") # if it's public and we don't need to force the signed url, just return the raw @@ -1465,7 +1442,7 @@ def get_signed_url( # `azure_creds == "*"` is a special case to support public buckets # where we do *not* want to try signing at all. the other case is that the # data is public and user requested to not sign the url - if azure_creds == "*" or (public_data and not force_signed_url): + if azure_creds == "*" or (not force_signed_url): return self._get_converted_url() url = self._generate_azure_blob_storage_sas( diff --git a/tests/data/test_azure_blob_storage_indexed_file_location.py b/tests/data/test_azure_blob_storage_indexed_file_location.py index 0c04143db..ee3599b8a 100755 --- a/tests/data/test_azure_blob_storage_indexed_file_location.py +++ b/tests/data/test_azure_blob_storage_indexed_file_location.py @@ -16,56 +16,32 @@ indirect=True, ) @pytest.mark.parametrize( - "action,expires_in,public_data,force_signed_url,azure_creds,user_id,storage_account_matches,expect_signed", + "action,expires_in,force_signed_url,azure_creds,user_id,storage_account_matches,expect_signed", [ - ("download", 5, True, None, "fake conn str", "some user", True, False), - ("download", 5, True, None, "fake conn str", "some user", False, False), - ("download", 5, True, True, "fake conn str", "some user", True, True), - ("download", 5, True, True, "fake conn str", "some user", False, False), - ("download", 5, True, False, "fake conn str", "some user", True, False), - ("download", 5, True, False, "fake conn str", "some user", False, False), - ("download", 5, False, None, "fake conn str", "some user", True, True), - ("download", 5, False, None, "fake conn str", "some user", False, False), - ("download", 5, False, True, "fake conn str", "some user", True, True), - ("download", 5, False, True, "fake conn str", "some user", False, False), - ("download", 5, False, None, "fake conn str", "some user", True, True), - ("download", 5, False, None, "fake conn str", "some user", False, False), - ("download", 5, False, None, "*", "some user", True, True), - ("download", 5, False, None, "*", "some user", False, False), - ("download", 5, False, None, "*", ANONYMOUS_USER_ID, True, True), - ("download", 5, False, None, "*", ANONYMOUS_USER_ID, False, False), - ("download", 5, None, None, "fake conn str", "some user", True, True), - ("download", 5, None, None, "fake conn str", "some user", False, False), - ("download", 5, None, True, "fake conn str", "some user", True, True), - ("download", 5, None, True, "fake conn str", "some user", False, False), - ("download", 5, None, False, "fake conn str", "some user", True, True), - ("download", 5, None, False, "fake conn str", "some user", False, False), - ("download", 5, True, None, "fake conn str", ANONYMOUS_USER_ID, True, False), - ("download", 5, True, None, "fake conn str", ANONYMOUS_USER_ID, False, False), - ("upload", 5, True, None, "fake conn str", "some user", True, False), - ("upload", 5, True, None, "fake conn str", "some user", False, False), - ("upload", 5, True, True, "fake conn str", "some user", True, True), - ("upload", 5, True, True, "fake conn str", "some user", False, False), - ("upload", 5, True, False, "fake conn str", "some user", True, False), - ("upload", 5, True, False, "fake conn str", "some user", False, False), - ("upload", 5, False, None, "fake conn str", "some user", True, True), - ("upload", 5, False, None, "fake conn str", "some user", False, False), - ("upload", 5, False, True, "fake conn str", "some user", True, True), - ("upload", 5, False, True, "fake conn str", "some user", False, False), - ("upload", 5, False, None, "fake conn str", "some user", True, True), - ("upload", 5, False, None, "fake conn str", "some user", False, False), - ("upload", 5, False, None, "*", "some user", True, True), - ("upload", 5, False, None, "*", "some user", False, False), - ("upload", 5, False, None, "*", ANONYMOUS_USER_ID, True, True), - ("upload", 5, False, None, "*", ANONYMOUS_USER_ID, False, False), - ("upload", 5, None, None, "fake conn str", "some user", True, True), - ("upload", 5, None, None, "fake conn str", "some user", False, False), - ("upload", 5, None, True, "fake conn str", "some user", True, True), - ("upload", 5, None, True, "fake conn str", "some user", False, False), - ("upload", 5, None, False, "fake conn str", "some user", True, True), - ("upload", 5, None, False, "fake conn str", "some user", False, False), - ("upload", 5, True, None, "fake conn str", ANONYMOUS_USER_ID, True, False), - ("upload", 5, True, None, "fake conn str", ANONYMOUS_USER_ID, False, False), + ("download", 5, None, "fake conn str", "some user", False, False), + ("download", 5, True, "fake conn str", "some user", True, True), + ("download", 5, True, "fake conn str", "some user", False, False), + ("download", 5, False, "fake conn str", "some user", True, False), + ("download", 5, False, "fake conn str", "some user", False, False), + ("download", 5, None, "fake conn str", "some user", True, True), + ("download", 5, None, "*", "some user", True, True), + ("download", 5, None, "*", "some user", False, False), + ("download", 5, None, "*", ANONYMOUS_USER_ID, True, True), + ("download", 5, None, "*", ANONYMOUS_USER_ID, False, False), + ("download", 5, None, "fake conn str", ANONYMOUS_USER_ID, True, True), + ("download", 5, None, "fake conn str", ANONYMOUS_USER_ID, False, False), + ("upload", 5, None, "fake conn str", "some user", False, False), + ("upload", 5, True, "fake conn str", "some user", True, True), + ("upload", 5, True, "fake conn str", "some user", False, False), + ("upload", 5, False, "fake conn str", "some user", True, False), + ("upload", 5, False, "fake conn str", "some user", False, False), + ("upload", 5, None, "fake conn str", "some user", True, True), + ("upload", 5, None, "*", "some user", True, True), + ("upload", 5, None, "*", "some user", False, False), + ("upload", 5, None, "*", ANONYMOUS_USER_ID, True, True), + ("upload", 5, None, "*", ANONYMOUS_USER_ID, False, False), + ("upload", 5, None, "fake conn str", ANONYMOUS_USER_ID, True, True), + ("upload", 5, None, "fake conn str", ANONYMOUS_USER_ID, False, False), ], ) def test_get_signed_url( @@ -73,7 +49,6 @@ def test_get_signed_url( indexd_client, action, expires_in, - public_data, force_signed_url, azure_creds, user_id, @@ -101,14 +76,21 @@ def test_get_signed_url( azure_blob_storage_indexed_file_location = ( AzureBlobStorageIndexedFileLocation(indexed_file_location_url) ) - return_url = ( - azure_blob_storage_indexed_file_location.get_signed_url( - action=action, - expires_in=expires_in, - public_data=public_data, - force_signed_url=force_signed_url, + if force_signed_url == None: + return_url = ( + azure_blob_storage_indexed_file_location.get_signed_url( + action=action, + expires_in=expires_in, + ) + ) + else: + return_url = ( + azure_blob_storage_indexed_file_location.get_signed_url( + action=action, + expires_in=expires_in, + force_signed_url=force_signed_url, + ) ) - ) if expect_signed: assert "?" in return_url From 0c32f719e45cedb04a044d0b5cbf0d2e8a0f97a1 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Mon, 24 Jan 2022 10:21:02 -0600 Subject: [PATCH 168/211] feat(passport): add db and memory cache and logic to use it under the right circumstances - also add tests --- .secrets.baseline | 14 +- fence/blueprints/login/ras.py | 1 - fence/models.py | 12 +- fence/resources/ga4gh/passports.py | 187 ++++++- tests/conftest.py | 6 + tests/test-fence-config.yaml | 2 +- tests/test_drs.py | 860 +++++++++++++++++++++++++++++ 7 files changed, 1043 insertions(+), 39 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 6c029b80c..b4af1714d 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -195,27 +195,19 @@ } ], "tests/conftest.py": [ - { - "type": "Secret Keyword", - "filename": "tests/conftest.py", - "hashed_secret": "9801ff058ba790388c9efc095cb3e89a819d5ed6", - "is_verified": false, - "line_number": 164, - "is_secret": false - }, { "type": "Private Key", "filename": "tests/conftest.py", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 1472 + "line_number": 1482 }, { "type": "Base64 High Entropy String", "filename": "tests/conftest.py", "hashed_secret": "227dea087477346785aefd575f91dd13ab86c108", "is_verified": false, - "line_number": 1495 + "line_number": 1505 } ], "tests/credentials/google/test_credentials.py": [ @@ -284,5 +276,5 @@ } ] }, - "generated_at": "2021-12-20T23:13:45Z" + "generated_at": "2022-01-24T16:20:56Z" } diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index cc547653a..daec1e2f9 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -81,7 +81,6 @@ def post_login(self, user=None, token_result=None, id_from_idp=None): db_session=current_session, ) user_ids_from_passports = list(users_from_passports.keys()) - logger.debug(f"user_ids_from_passports: {user_ids_from_passports}") # TODO? # put_gen3_usernames_for_passport_into_cache( diff --git a/fence/models.py b/fence/models.py index c2f7ec65b..0a64809dd 100644 --- a/fence/models.py +++ b/fence/models.py @@ -11,7 +11,6 @@ from authlib.flask.oauth2.sqla import OAuth2AuthorizationCodeMixin, OAuth2ClientMixin import bcrypt -import flask from sqlalchemy import ( Integer, BigInteger, @@ -25,7 +24,7 @@ text, event, ) -from sqlalchemy.dialects.postgresql import ARRAY, JSONB +from sqlalchemy.dialects.postgresql import ARRAY, JSONB, UUID from sqlalchemy.orm import relationship, backref from sqlalchemy.sql import func from sqlalchemy import exc as sa_exc @@ -617,6 +616,15 @@ class AssumeRoleCacheGCP(Base): gcp_key_db_entry = Column(String()) +class GA4GHPassportCache(Base): + __tablename__ = "ga4gh_passport_cache" + + passport_hash = Column(UUID(as_uuid=True), primary_key=True) + passport = Column(Text, nullable=False) + expires_at = Column(BigInteger, nullable=False) + user_ids = Column(ARRAY(String(255)), nullable=False) + + class GA4GHVisaV1(Base): __tablename__ = "ga4gh_visa_v1" diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index c285a1c83..159abd081 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -1,8 +1,10 @@ import flask import os import collections +import hashlib import time import datetime +import uuid import jwt # the whole fence_create module is imported to avoid issue with circular imports @@ -20,12 +22,19 @@ query_for_user, query_for_user_by_id, GA4GHVisaV1, + GA4GHPassportCache, IdentityProvider, IssSubPairToUser, ) logger = get_logger(__name__) +# cache will be in following format +# passport: ([user_id_0, user_id_1, ...], expires_at) +# +# NOTE: we'll want to watch the memory usage on this since passports can be pretty large +PASSPORT_CACHE = {} + def sync_gen3_users_authz_from_ga4gh_passports( passports, @@ -50,21 +59,32 @@ def sync_gen3_users_authz_from_ga4gh_passports( embedded within the passports passed in """ db_session = db_session or current_session - logger.info("Getting gen3 users from passports") # {"username": user, "username2": user2} users_from_all_passports = {} for passport in passports: try: - cached_users = get_gen3_usernames_for_passport_from_cache(passport) - if cached_users: - # TODO get user from id - perhaps we can avoid this? - for user_id in cached_users: - user = query_for_user_by_id(session=db_session, user_id=user_id) - users_from_all_passports[user.username] = user - # existence in the cache means that this passport was validated - # previously (expiration was also checked) - continue + cached_usernames = get_gen3_usernames_for_passport_from_cache( + passport=passport, db_session=db_session + ) + if cached_usernames: + # there's a chance a given username exists in the cache but no longer in + # the database. if not all are in db, ignore the cache and actually parse + # and validate the passport + all_users_exist_in_db = True + usernames_to_update = {} + for username in cached_usernames: + user = query_for_user(session=db_session, username=username) + if not user: + all_users_exist_in_db = False + continue + usernames_to_update[user.username] = user + + if all_users_exist_in_db: + users_from_all_passports.update(usernames_to_update) + # existence in the cache and a user in db means that this passport + # was validated previously (expiration was also checked) + continue # below function also validates passport (or raises exception) raw_visas = get_unvalidated_visas_from_valid_passport( @@ -139,29 +159,24 @@ def sync_gen3_users_authz_from_ga4gh_passports( ) users_from_current_passport.append(gen3_user) + for user in users_from_current_passport: + users_from_all_passports[user.username] = user + put_gen3_usernames_for_passport_into_cache( - passport, - [user.id for user in users_from_current_passport], + passport=passport, + user_ids_from_passports=list(users_from_all_passports.keys()), expires_at=min_visa_expiration, + db_session=db_session, ) - for user in users_from_current_passport: - users_from_all_passports[user.username] = user db_session.commit() + logger.info( + f"Got Gen3 usernames from passport(s): {list(users_from_all_passports.keys())}" + ) return users_from_all_passports -def get_gen3_usernames_for_passport_from_cache(passport, db_session=None): - return - - -def put_gen3_usernames_for_passport_into_cache( - passport, user_ids_from_passports, expires_at, db_session=None -): - return - - def get_unvalidated_visas_from_valid_passport(passport, pkey_cache=None): """ Return encoded visas after extracting and validating encoded passport @@ -401,6 +416,130 @@ def _sync_validated_visa_authorization( db_session.add(visa) +def get_gen3_usernames_for_passport_from_cache(passport, db_session=None): + """ + Attempt to retrieve a cached list of users ids for a previously validated and + non-expired passport. + + Args: + passport (str): ga4gh encoded passport JWT + db_session (None, sqlalchemy session): optional database session to use + + Returns: + list[str]: list of usernames for users referred to by the previously validated + and non-expired passport + """ + db_session = db_session or current_session + user_ids_from_passports = None + current_time = int(time.time()) + + # try to retrieve from local in-memory cache + + if passport in PASSPORT_CACHE: + user_ids_from_passports, expires = PASSPORT_CACHE[passport] + if expires > current_time: + logger.debug( + f"Got users {user_ids_from_passports} for provided passport from in-memory cache. " + f"Expires: {expires}, Current Time: {current_time}" + ) + return user_ids_from_passports + else: + # expired, so remove it + del PASSPORT_CACHE[passport] + + # try to retrieve from database cache + + # get an md5 hash of passport (which is 128 bits) and convert to UUID (which is 128 bits) + # for optimal usage of database's underlying UUID column type + passport_hash_as_uuid = uuid.UUID(hashlib.md5(passport.encode("utf-8")).hexdigest()) + cached_passport = ( + db_session.query(GA4GHPassportCache) + .filter(GA4GHPassportCache.passport_hash == passport_hash_as_uuid) + .first() + ) + # we retrieved based on hash, which has a small chance of collision. Mitigate that by + # now verifying that the full passport in the db matches what was provided + if cached_passport and cached_passport.passport == passport: + if cached_passport.expires_at > current_time: + user_ids_from_passports = cached_passport.user_ids + + # update local cache + PASSPORT_CACHE[passport] = ( + user_ids_from_passports, + cached_passport.expires_at, + ) + + logger.debug( + f"Got users {user_ids_from_passports} for provided passport from " + f"database cache and placed in in-memory cache. " + f"Expires: {cached_passport.expires_at}, Current Time: {current_time}" + ) + return user_ids_from_passports + else: + # expired, so remove it + db_session.remove(cached_passport) + db_session.commit() + + return user_ids_from_passports + + +def put_gen3_usernames_for_passport_into_cache( + passport, user_ids_from_passports, expires_at, db_session=None +): + """ + Cache a validated and non-expired passport and map to the user_ids referenced + by the content. + + Args: + passport (str): ga4gh encoded passport JWT + db_session (None, sqlalchemy session): optional database session to use + user_ids_from_passports (list[str]): list of user identifiers referred to by + the previously validated and non-expired passport + expires_at (int): expiration time in unix time + """ + db_session = db_session or current_session + # stores back to cache and db + PASSPORT_CACHE[passport] = user_ids_from_passports, expires_at + + # get an md5 hash of passport (which is 128 bits) and convert to UUID (which is 128 bits) + # for optimal usage of database's underlying UUID column type + passport_hash_as_uuid = uuid.UUID(hashlib.md5(passport.encode("utf-8")).hexdigest()) + + # the improbable collision of hash on 2 different passports will result in an overwrite + # of the previous passport information and the discrepancy will raise an error on + # retrieval (after a comparison of the full stored passport vs provided). e.g. this + # collision will NOT get caught here but instead on the "GET" from cache functionality + db_session.execute( + """\ + INSERT INTO ga4gh_passport_cache ( + passport_hash, + passport, + expires_at, + user_ids + ) VALUES ( + :passport_hash, + :passport, + :expires_at, + :user_ids + ) ON CONFLICT (passport_hash) DO UPDATE SET + passport = EXCLUDED.passport, + expires_at = EXCLUDED.expires_at, + user_ids = EXCLUDED.user_ids;""", + dict( + passport_hash=passport_hash_as_uuid, + passport=passport, + expires_at=expires_at, + user_ids=user_ids_from_passports, + ), + ) + + logger.debug( + f"Cached users {user_ids_from_passports} for provided passport in " + f"database cache and placed in in-memory cache. " + f"Expires: {expires_at}" + ) + + # TODO to be called after login def map_gen3_iss_sub_pair_to_user(gen3_issuer, gen3_subject_id, gen3_user): pass diff --git a/tests/conftest.py b/tests/conftest.py index e9d1565fe..97715ff0d 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -565,6 +565,9 @@ def db_session(db, patch_app_db_session): patch_app_db_session(session) + session.query(models.GA4GHPassportCache).delete() + session.commit() + yield session # clear out user and project tables upon function close in case unit test didn't @@ -572,6 +575,7 @@ def db_session(db, patch_app_db_session): session.query(models.IssSubPairToUser).delete() session.query(models.Project).delete() session.query(models.GA4GHVisaV1).delete() + session.query(models.GA4GHPassportCache).delete() session.commit() session.close() @@ -1247,6 +1251,8 @@ def do_patch(session): "fence.user", "fence.blueprints.login.synapse", "fence.blueprints.login.ras", + "fence.blueprints.data.indexd", + "fence.resources.ga4gh.passports", ] for module in modules_to_patch: monkeypatch.setattr("{}.current_session".format(module), session) diff --git a/tests/test-fence-config.yaml b/tests/test-fence-config.yaml index 4495582d3..f228173b0 100755 --- a/tests/test-fence-config.yaml +++ b/tests/test-fence-config.yaml @@ -635,7 +635,7 @@ GA4GH_VISA_V1_CLAIM_REQUIRED_FIELDS: - "https://stsstg.nih.gov/passport/dbgap/v1.1" source: - "https://ncbi.nlm.nih.gov/gap" -EXPIRED_AUTHZ_REMOVAL_JOB_FREQ_IN_SECONDS: 300 +EXPIRED_AUTHZ_REMOVAL_JOB_FREQ_IN_SECONDS: 1 # Global sync visas during login # None(Default): Allow per client i.e. a fence client can pick whether or not to sync their visas during login with parse_visas param in /authorization endpoint # True: Parse for all clients i.e. a fence client will always sync their visas during login diff --git a/tests/test_drs.py b/tests/test_drs.py index 16a24bfdb..aac4a6b28 100644 --- a/tests/test_drs.py +++ b/tests/test_drs.py @@ -12,6 +12,8 @@ from gen3authz.client.arborist.client import ArboristClient from fence.config import config +from fence.models import GA4GHPassportCache +from tests.utils import add_test_ras_user, TEST_RAS_USERNAME, TEST_RAS_SUB def get_doc(has_version=True, urls=list(), drs_list=0): @@ -834,3 +836,861 @@ def test_get_presigned_url_for_non_public_data_with_no_passport( data=json.dumps(data), ) assert res.status_code == 401 + + +@responses.activate +@patch("httpx.get") +@patch("fence.resources.google.utils._create_proxy_group") +@patch("fence.scripting.fence_create.ArboristClient") +def test_passport_cache_valid_passport( + mock_arborist, + mock_google_proxy_group, + mock_httpx_get, + client, + indexd_client, + kid, + rsa_private_key, + rsa_public_key, + indexd_client_accepting_record, + mock_arborist_requests, + google_proxy_group, + primary_google_service_account, + cloud_manager, + google_signed_url, + db_session, + monkeypatch, +): + """ + Test that when a passport is provided a second time, the in-memory cache gets used + and the database cache is populated. + + NOTE: This is very similar to the test_get_presigned_url_for_non_public_data_with_passport + test with added stuff to check cache functionality + """ + # reset caches + PASSPORT_CACHE = {} + from fence.resources.ga4gh import passports as passports_module + + monkeypatch.setattr(passports_module, "PASSPORT_CACHE", PASSPORT_CACHE) + db_session.query(GA4GHPassportCache).delete() + db_session.commit() + + config["GA4GH_PASSPORTS_TO_DRS_ENABLED"] = True + indexd_record_with_non_public_authz_and_public_acl_populated = { + "did": "1", + "baseid": "", + "rev": "", + "size": 10, + "file_name": "file1", + "urls": ["s3://bucket1/key", "gs://bucket1/key"], + "hashes": {}, + "metadata": {}, + "authz": ["/orgA/programs/phs000991.c1"], + "acl": ["*"], + "form": "", + "created_date": "", + "updated_date": "", + } + indexd_client_accepting_record( + indexd_record_with_non_public_authz_and_public_acl_populated + ) + mock_arborist_requests({"arborist/auth/request": {"POST": ({"auth": True}, 200)}}) + mock_arborist.return_value = MagicMock(ArboristClient) + mock_google_proxy_group.return_value = google_proxy_group + + # Prepare Passport/Visa + current_time = int(time.time()) + headers = {"kid": kid} + decoded_visa = { + "iss": "https://stsstg.nih.gov", + "sub": TEST_RAS_SUB, + "iat": current_time, + "exp": current_time + 1000, + "scope": "openid ga4gh_passport_v1 email profile", + "jti": "jtiajoidasndokmasdl", + "txn": "sapidjspa.asipidja", + "name": "", + "ga4gh_visa_v1": { + "type": "https://ras.nih.gov/visas/v1.1", + "asserted": current_time, + "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", + "source": "https://ncbi.nlm.nih.gov/gap", + }, + "ras_dbgap_permissions": [ + { + "consent_name": "Health/Medical/Biomedical", + "phs_id": "phs000991", + "version": "v1", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 1000, + }, + { + "consent_name": "General Research Use (IRB, PUB)", + "phs_id": "phs000961", + "version": "v1", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 1000, + }, + { + "consent_name": "Disease-Specific (Cardiovascular Disease)", + "phs_id": "phs000279", + "version": "v2", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 1000, + }, + { + "consent_name": "Health/Medical/Biomedical (IRB)", + "phs_id": "phs000286", + "version": "v6", + "participant_set": "p2", + "consent_group": "c3", + "role": "designated user", + "expiration": current_time + 1000, + }, + { + "consent_name": "Disease-Specific (Focused Disease Only, IRB, NPU)", + "phs_id": "phs000289", + "version": "v6", + "participant_set": "p2", + "consent_group": "c2", + "role": "designated user", + "expiration": current_time + 1000, + }, + { + "consent_name": "Disease-Specific (Autism Spectrum Disorder)", + "phs_id": "phs000298", + "version": "v4", + "participant_set": "p3", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 1000, + }, + ], + } + encoded_visa = jwt.encode( + decoded_visa, key=rsa_private_key, headers=headers, algorithm="RS256" + ).decode("utf-8") + + passport_header = { + "type": "JWT", + "alg": "RS256", + "kid": kid, + } + passport = { + "iss": "https://stsstg.nih.gov", + "sub": TEST_RAS_SUB, + "iat": current_time, + "scope": "openid ga4gh_passport_v1 email profile", + "exp": current_time + 1000, + "ga4gh_passport_v1": [encoded_visa], + } + encoded_passport = jwt.encode( + passport, key=rsa_private_key, headers=passport_header, algorithm="RS256" + ).decode("utf-8") + + access_id = indexd_client["indexed_file_location"] + test_guid = "1" + + passports = [encoded_passport] + + data = {"passports": passports} + + keys = [keypair.public_key_to_jwk() for keypair in flask.current_app.keypairs] + mock_httpx_get.return_value = httpx.Response(200, json={"keys": keys}) + + # check database cache + cached_passports = [ + item.passport for item in db_session.query(GA4GHPassportCache).all() + ] + assert encoded_passport not in cached_passports + + # check in-memory cache + assert not PASSPORT_CACHE.get(encoded_passport) + + before_cache_start = time.time() + res = client.post( + "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, + headers={ + "Content-Type": "application/json", + }, + data=json.dumps(data), + ) + before_cache_end = time.time() + before_cache_time = before_cache_end - before_cache_start + assert res.status_code == 200 + + # check that database cache populated + cached_passports = [ + item.passport for item in db_session.query(GA4GHPassportCache).all() + ] + assert encoded_passport in cached_passports + + # check that in-memory cache populated + assert PASSPORT_CACHE.get(encoded_passport) + + after_cache_start = time.time() + res = client.post( + "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, + headers={ + "Content-Type": "application/json", + }, + data=json.dumps(data), + ) + after_cache_end = time.time() + after_cache_time = after_cache_end - after_cache_start + assert res.status_code == 200 + # make sure using the cache is faster + assert after_cache_time < before_cache_time + + +@responses.activate +@patch("httpx.get") +@patch("fence.resources.google.utils._create_proxy_group") +@patch("fence.scripting.fence_create.ArboristClient") +def test_passport_cache_invalid_passport( + mock_arborist, + mock_google_proxy_group, + mock_httpx_get, + client, + indexd_client, + kid, + rsa_private_key, + rsa_public_key, + indexd_client_accepting_record, + mock_arborist_requests, + google_proxy_group, + primary_google_service_account, + cloud_manager, + google_signed_url, + db_session, + monkeypatch, +): + """ + Test that when an invalid passport is provided a second time, the in-memory cache + does NOT get used and the database cache is NOT populated. + + NOTE: This is very similar to the test_get_presigned_url_for_non_public_data_with_passport + test with added stuff to check cache functionality + """ + # reset caches + PASSPORT_CACHE = {} + from fence.resources.ga4gh import passports as passports_module + + monkeypatch.setattr(passports_module, "PASSPORT_CACHE", PASSPORT_CACHE) + db_session.query(GA4GHPassportCache).delete() + db_session.commit() + + config["GA4GH_PASSPORTS_TO_DRS_ENABLED"] = True + indexd_record_with_non_public_authz_and_public_acl_populated = { + "did": "1", + "baseid": "", + "rev": "", + "size": 10, + "file_name": "file1", + "urls": ["s3://bucket1/key", "gs://bucket1/key"], + "hashes": {}, + "metadata": {}, + "authz": ["/orgA/programs/phs000991.c1"], + "acl": [""], + "form": "", + "created_date": "", + "updated_date": "", + } + indexd_client_accepting_record( + indexd_record_with_non_public_authz_and_public_acl_populated + ) + mock_arborist_requests({"arborist/auth/request": {"POST": ({"auth": False}, 200)}}) + mock_arborist.return_value = MagicMock(ArboristClient) + mock_google_proxy_group.return_value = google_proxy_group + + # Prepare Passport/Visa + current_time = int(time.time()) + headers = {"kid": kid} + decoded_visa = { + "iss": "https://stsstg.nih.gov", + "sub": TEST_RAS_SUB, + "iat": current_time, + "exp": current_time + 1000, + "scope": "openid ga4gh_passport_v1 email profile", + "jti": "jtiajoidasndokmasdl", + "txn": "sapidjspa.asipidja", + "name": "", + "ga4gh_visa_v1": { + "type": "https://ras.nih.gov/visas/v1.1", + "asserted": current_time, + "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", + "source": "https://ncbi.nlm.nih.gov/gap", + }, + "ras_dbgap_permissions": [ + { + "consent_name": "Health/Medical/Biomedical", + "phs_id": "phs000991", + "version": "v1", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 1000, + }, + { + "consent_name": "General Research Use (IRB, PUB)", + "phs_id": "phs000961", + "version": "v1", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 1000, + }, + { + "consent_name": "Disease-Specific (Cardiovascular Disease)", + "phs_id": "phs000279", + "version": "v2", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 1000, + }, + { + "consent_name": "Health/Medical/Biomedical (IRB)", + "phs_id": "phs000286", + "version": "v6", + "participant_set": "p2", + "consent_group": "c3", + "role": "designated user", + "expiration": current_time + 1000, + }, + { + "consent_name": "Disease-Specific (Focused Disease Only, IRB, NPU)", + "phs_id": "phs000289", + "version": "v6", + "participant_set": "p2", + "consent_group": "c2", + "role": "designated user", + "expiration": current_time + 1000, + }, + { + "consent_name": "Disease-Specific (Autism Spectrum Disorder)", + "phs_id": "phs000298", + "version": "v4", + "participant_set": "p3", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 1000, + }, + ], + } + encoded_visa = jwt.encode( + decoded_visa, key=rsa_private_key, headers=headers, algorithm="RS256" + ).decode("utf-8") + + passport_header = { + "type": "JWT", + "alg": "RS256", + "kid": kid, + } + passport = { + "iss": "https://stsstg.nih.gov", + "sub": TEST_RAS_SUB, + "iat": current_time, + "scope": "openid ga4gh_passport_v1 email profile", + "exp": current_time + 1000, + "ga4gh_passport_v1": [encoded_visa], + } + invalid_encoded_passport = "invalid" + jwt.encode( + passport, key=rsa_private_key, headers=passport_header, algorithm="RS256" + ).decode("utf-8") + + access_id = indexd_client["indexed_file_location"] + test_guid = "1" + + passports = [invalid_encoded_passport] + + data = {"passports": passports} + + keys = [keypair.public_key_to_jwk() for keypair in flask.current_app.keypairs] + mock_httpx_get.return_value = httpx.Response(200, json={"keys": keys}) + + # check database cache + cached_passports = [ + item.passport for item in db_session.query(GA4GHPassportCache).all() + ] + assert invalid_encoded_passport not in cached_passports + + # check in-memory cache + assert not PASSPORT_CACHE.get(invalid_encoded_passport) + + res = client.post( + "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, + headers={ + "Content-Type": "application/json", + }, + data=json.dumps(data), + ) + assert res.status_code != 200 + + # check that database cache NOT populated + cached_passports = [ + item.passport for item in db_session.query(GA4GHPassportCache).all() + ] + assert invalid_encoded_passport not in cached_passports + + # check that in-memory cache NOT populated + assert not PASSPORT_CACHE.get(invalid_encoded_passport) + + res = client.post( + "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, + headers={ + "Content-Type": "application/json", + }, + data=json.dumps(data), + ) + assert res.status_code != 200 + + +@responses.activate +@patch("httpx.get") +@patch("fence.resources.google.utils._create_proxy_group") +@patch("fence.scripting.fence_create.ArboristClient") +def test_passport_cache_expired_in_memory_valid_in_db( + mock_arborist, + mock_google_proxy_group, + mock_httpx_get, + client, + indexd_client, + kid, + rsa_private_key, + rsa_public_key, + indexd_client_accepting_record, + mock_arborist_requests, + google_proxy_group, + primary_google_service_account, + cloud_manager, + google_signed_url, + db_session, + monkeypatch, +): + """ + Test that when a passport is provided a second time when the the in-memory cache + is expired but the database cache is valid, we still get a successful response. + + Check that cached database is updated and placed in in-memory cache. + + NOTE: This is very similar to the test_get_presigned_url_for_non_public_data_with_passport + test with added stuff to check cache functionality + """ + # reset cache + # PASSPORT_CACHE = {} + from fence.resources.ga4gh import passports as passports_module + + # monkeypatch.setattr(passports_module, "PASSPORT_CACHE", PASSPORT_CACHE) + db_session.query(GA4GHPassportCache).delete() + db_session.commit() + + # # add test user + # test_user = add_test_ras_user(db_session=db_session) + # test_user.username = "abcd-asdj-sajpiasj12iojd-asnoinstsstg.nih.gov" + test_username = "abcd-asdj-sajpiasj12iojd-asnoinstsstg.nih.gov" + # mocked_method = MagicMock(return_value=test_user) + # patch_method = patch( + # "fence.resources.ga4gh.passports.query_for_user", mocked_method + # ) + # patch_method.start() + + config["GA4GH_PASSPORTS_TO_DRS_ENABLED"] = True + indexd_record_with_non_public_authz_and_public_acl_populated = { + "did": "1", + "baseid": "", + "rev": "", + "size": 10, + "file_name": "file1", + "urls": ["s3://bucket1/key", "gs://bucket1/key"], + "hashes": {}, + "metadata": {}, + "authz": ["/orgA/programs/phs000991.c1"], + "acl": [""], + "form": "", + "created_date": "", + "updated_date": "", + } + indexd_client_accepting_record( + indexd_record_with_non_public_authz_and_public_acl_populated + ) + mock_arborist_requests({"arborist/auth/request": {"POST": ({"auth": True}, 200)}}) + mock_arborist.return_value = MagicMock(ArboristClient) + mock_google_proxy_group.return_value = google_proxy_group + + # Prepare Passport/Visa + current_time = int(time.time()) + headers = {"kid": kid} + decoded_visa = { + "iss": "https://stsstg.nih.gov", + "sub": TEST_RAS_SUB, + "iat": current_time, + "exp": current_time + 1000, + "scope": "openid ga4gh_passport_v1 email profile", + "jti": "jtiajoidasndokmasdl", + "txn": "sapidjspa.asipidja", + "name": "", + "ga4gh_visa_v1": { + "type": "https://ras.nih.gov/visas/v1.1", + "asserted": current_time, + "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", + "source": "https://ncbi.nlm.nih.gov/gap", + }, + "ras_dbgap_permissions": [ + { + "consent_name": "Health/Medical/Biomedical", + "phs_id": "phs000991", + "version": "v1", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 1000, + }, + { + "consent_name": "General Research Use (IRB, PUB)", + "phs_id": "phs000961", + "version": "v1", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 1000, + }, + { + "consent_name": "Disease-Specific (Cardiovascular Disease)", + "phs_id": "phs000279", + "version": "v2", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 1000, + }, + { + "consent_name": "Health/Medical/Biomedical (IRB)", + "phs_id": "phs000286", + "version": "v6", + "participant_set": "p2", + "consent_group": "c3", + "role": "designated user", + "expiration": current_time + 1000, + }, + { + "consent_name": "Disease-Specific (Focused Disease Only, IRB, NPU)", + "phs_id": "phs000289", + "version": "v6", + "participant_set": "p2", + "consent_group": "c2", + "role": "designated user", + "expiration": current_time + 1000, + }, + { + "consent_name": "Disease-Specific (Autism Spectrum Disorder)", + "phs_id": "phs000298", + "version": "v4", + "participant_set": "p3", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 1000, + }, + ], + } + encoded_visa = jwt.encode( + decoded_visa, key=rsa_private_key, headers=headers, algorithm="RS256" + ).decode("utf-8") + + passport_header = { + "type": "JWT", + "alg": "RS256", + "kid": kid, + } + passport = { + "iss": "https://stsstg.nih.gov", + "sub": TEST_RAS_SUB, + "iat": current_time, + "scope": "openid ga4gh_passport_v1 email profile", + "exp": current_time + 1000, + "ga4gh_passport_v1": [encoded_visa], + } + encoded_passport = jwt.encode( + passport, key=rsa_private_key, headers=passport_header, algorithm="RS256" + ).decode("utf-8") + + access_id = indexd_client["indexed_file_location"] + test_guid = "1" + + passports = [encoded_passport] + + data = {"passports": passports} + + keys = [keypair.public_key_to_jwk() for keypair in flask.current_app.keypairs] + mock_httpx_get.return_value = httpx.Response(200, json={"keys": keys}) + + # simulate db cache with a valid passport by first calling the endpoint to cache + # res = client.post( + # "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, + # headers={ + # "Content-Type": "application/json", + # }, + # data=json.dumps(data), + # ) + # assert res.status_code == 200 + passports_module.put_gen3_usernames_for_passport_into_cache( + encoded_passport, [test_username], current_time + 1000, db_session=db_session + ) + + # double-check database cache + cached_passport = ( + db_session.query(GA4GHPassportCache) + .filter(GA4GHPassportCache.passport == encoded_passport) + .first() + ) + # greater and NOT == b/c of logic to set internal expiration less than real to allow + # time for expiration job to run + assert cached_passport and cached_passport.expires_at > current_time + + # simulate in-memory cache with an expired passport by overriding the in-memory cache + from fence.resources.ga4gh import passports as passports_module + + PASSPORT_CACHE = {f"{encoded_passport}": ([test_username], current_time - 1)} + assert PASSPORT_CACHE.get(encoded_passport, ("", 0))[1] == current_time - 1 + monkeypatch.setattr(passports_module, "PASSPORT_CACHE", PASSPORT_CACHE) + + res = client.post( + "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, + headers={ + "Content-Type": "application/json", + }, + data=json.dumps(data), + ) + assert res.status_code == 200 + # patch_method.stop() + + # check that database cache still populated + assert ( + len([item.passport for item in db_session.query(GA4GHPassportCache).all()]) == 1 + ) + cached_passport = ( + db_session.query(GA4GHPassportCache) + .filter(GA4GHPassportCache.passport == encoded_passport) + .first() + ) + # greater and NOT == b/c of logic to set internal expiration less than real to allow + # time for expiration job to run + assert cached_passport and cached_passport.expires_at > current_time + + # check that in-memory cache populated with db expiration + # greater and NOT == b/c of logic to set internal expiration less than real to allow + # time for expiration job to run + if PASSPORT_CACHE.get(encoded_passport, ("", 0))[1] == 0: + from fence.resources.ga4gh.passports import PASSPORT_CACHE as import_cache + + assert PASSPORT_CACHE == None + assert PASSPORT_CACHE.get(encoded_passport, ("", 0))[1] > current_time + + +@responses.activate +@patch("httpx.get") +@patch("fence.resources.google.utils._create_proxy_group") +@patch("fence.scripting.fence_create.ArboristClient") +def test_passport_cache_expired( + mock_arborist, + mock_google_proxy_group, + mock_httpx_get, + client, + indexd_client, + kid, + rsa_private_key, + rsa_public_key, + indexd_client_accepting_record, + mock_arborist_requests, + google_proxy_group, + primary_google_service_account, + cloud_manager, + google_signed_url, + db_session, + monkeypatch, +): + """ + Test that when a passport is expired, we don't get a successful response, even + if the passport was previously cached. + + NOTE: This is very similar to the test_get_presigned_url_for_non_public_data_with_passport + test with added stuff to check cache functionality + """ + # reset cache + PASSPORT_CACHE = {} + from fence.resources.ga4gh import passports as passports_module + + monkeypatch.setattr(passports_module, "PASSPORT_CACHE", PASSPORT_CACHE) + db_session.query(GA4GHPassportCache).delete() + db_session.commit() + + config["GA4GH_PASSPORTS_TO_DRS_ENABLED"] = True + indexd_record_with_non_public_authz_and_public_acl_populated = { + "did": "1", + "baseid": "", + "rev": "", + "size": 10, + "file_name": "file1", + "urls": ["s3://bucket1/key", "gs://bucket1/key"], + "hashes": {}, + "metadata": {}, + "authz": ["/orgA/programs/phs000991.c1"], + "acl": [""], + "form": "", + "created_date": "", + "updated_date": "", + } + indexd_client_accepting_record( + indexd_record_with_non_public_authz_and_public_acl_populated + ) + mock_arborist_requests({"arborist/auth/request": {"POST": ({"auth": True}, 200)}}) + mock_arborist.return_value = MagicMock(ArboristClient) + mock_google_proxy_group.return_value = google_proxy_group + + # Prepare Passport/Visa + current_time = int(time.time()) + headers = {"kid": kid} + decoded_visa = { + "iss": "https://stsstg.nih.gov", + "sub": TEST_RAS_SUB, + "iat": current_time, + "exp": current_time + 2, + "scope": "openid ga4gh_passport_v1 email profile", + "jti": "jtiajoidasndokmasdl", + "txn": "sapidjspa.asipidja", + "name": "", + "ga4gh_visa_v1": { + "type": "https://ras.nih.gov/visas/v1.1", + "asserted": current_time, + "value": "https://stsstg.nih.gov/passport/dbgap/v1.1", + "source": "https://ncbi.nlm.nih.gov/gap", + }, + "ras_dbgap_permissions": [ + { + "consent_name": "Health/Medical/Biomedical", + "phs_id": "phs000991", + "version": "v1", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 2, + }, + { + "consent_name": "General Research Use (IRB, PUB)", + "phs_id": "phs000961", + "version": "v1", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 2, + }, + { + "consent_name": "Disease-Specific (Cardiovascular Disease)", + "phs_id": "phs000279", + "version": "v2", + "participant_set": "p1", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 2, + }, + { + "consent_name": "Health/Medical/Biomedical (IRB)", + "phs_id": "phs000286", + "version": "v6", + "participant_set": "p2", + "consent_group": "c3", + "role": "designated user", + "expiration": current_time + 2, + }, + { + "consent_name": "Disease-Specific (Focused Disease Only, IRB, NPU)", + "phs_id": "phs000289", + "version": "v6", + "participant_set": "p2", + "consent_group": "c2", + "role": "designated user", + "expiration": current_time + 2, + }, + { + "consent_name": "Disease-Specific (Autism Spectrum Disorder)", + "phs_id": "phs000298", + "version": "v4", + "participant_set": "p3", + "consent_group": "c1", + "role": "designated user", + "expiration": current_time + 2, + }, + ], + } + encoded_visa = jwt.encode( + decoded_visa, key=rsa_private_key, headers=headers, algorithm="RS256" + ).decode("utf-8") + + passport_header = { + "type": "JWT", + "alg": "RS256", + "kid": kid, + } + passport = { + "iss": "https://stsstg.nih.gov", + "sub": TEST_RAS_SUB, + "iat": current_time, + "scope": "openid ga4gh_passport_v1 email profile", + "exp": current_time + 2, + "ga4gh_passport_v1": [encoded_visa], + } + encoded_passport = jwt.encode( + passport, key=rsa_private_key, headers=passport_header, algorithm="RS256" + ).decode("utf-8") + + access_id = indexd_client["indexed_file_location"] + test_guid = "1" + + passports = [encoded_passport] + + data = {"passports": passports} + + keys = [keypair.public_key_to_jwk() for keypair in flask.current_app.keypairs] + mock_httpx_get.return_value = httpx.Response(200, json={"keys": keys}) + + # check database cache + cached_passports = [ + item.passport for item in db_session.query(GA4GHPassportCache).all() + ] + assert encoded_passport not in cached_passports + + # check in-memory cache + assert not PASSPORT_CACHE.get(encoded_passport) + + res = client.post( + "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, + headers={ + "Content-Type": "application/json", + }, + data=json.dumps(data), + ) + assert res.status_code == 200 + + # ensure passport is expired by sleeping + expire_time = current_time + 2 + current_time = int(time.time()) + if current_time < expire_time: + sleep_time = expire_time - current_time + time.sleep(sleep_time) + + # try again + mock_arborist_requests({"arborist/auth/request": {"POST": ({"auth": False}, 200)}}) + res = client.post( + "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, + headers={ + "Content-Type": "application/json", + }, + data=json.dumps(data), + ) + assert res.status_code != 200 From 7d4edaa651a9adbe2136d93c71b129b7a30fc41c Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 25 Jan 2022 10:05:01 -0600 Subject: [PATCH 169/211] fix(login): initialize in-memory public key cache for RAS login --- fence/blueprints/login/ras.py | 3 ++- openapis/swagger.yaml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index cc547653a..fd2046af7 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -21,6 +21,7 @@ logger = get_logger(__name__) +PKEY_CACHE = {} class RASLogin(DefaultOAuth2Login): @@ -77,7 +78,7 @@ def post_login(self, user=None, token_result=None, id_from_idp=None): users_from_passports = fence.resources.ga4gh.passports.sync_gen3_users_authz_from_ga4gh_passports( [passport], authz_policy_prefix="POST.LOGIN", - pkey_cache=pkey_cache, + pkey_cache=PKEY_CACHE, db_session=current_session, ) user_ids_from_passports = list(users_from_passports.keys()) diff --git a/openapis/swagger.yaml b/openapis/swagger.yaml index a76b0a228..34164c508 100644 --- a/openapis/swagger.yaml +++ b/openapis/swagger.yaml @@ -461,7 +461,7 @@ paths: required: false in: query description: >- - if `no_force_sign=True` and the file requested is actually public, this will + if `no_force_sign=True`, this will request to *not* sign the resulting URL (i.e. just provide the public url without using anonymous signing creds). schema: From e88e74f9a7b2f36164087f989d8952fa90d25c4d Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 25 Jan 2022 12:37:23 -0600 Subject: [PATCH 170/211] fix(passport): use correct db session removal function --- fence/resources/ga4gh/passports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 159abd081..6f86cfe77 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -476,8 +476,8 @@ def get_gen3_usernames_for_passport_from_cache(passport, db_session=None): ) return user_ids_from_passports else: - # expired, so remove it - db_session.remove(cached_passport) + # expired, so delete it + db_session.delete(cached_passport) db_session.commit() return user_ids_from_passports From 79eafe756ad8d476594efe857df1d2c95a259d55 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 25 Jan 2022 15:08:03 -0600 Subject: [PATCH 171/211] fix(passport): don't close session as it needs to be used downstream --- fence/blueprints/login/ras.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index daec1e2f9..a424dcb64 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -56,11 +56,6 @@ def post_login(self, user=None, token_result=None, id_from_idp=None): # do an on-the-fly usersync for this user to give them instant access after logging in through RAS # if GLOBAL_PARSE_VISAS_ON_LOGIN is true then we want to run it regardless of whether or not the client sent parse_visas on request if parse_visas: - # Close previous db sessions. Leaving it open causes a race condition where we're - # viewing user.project_access while trying to update it in usersync - # not closing leads to partially updated records - current_session.close() - # get passport then call sync on it try: passport = ( From 7ad22561c74eedf78e1c80e075966d105c7fbc26 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 25 Jan 2022 15:08:41 -0600 Subject: [PATCH 172/211] fix(passport): don't close session as it needs to be used downstream --- fence/blueprints/login/ras.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index fd2046af7..88f52cc4c 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -57,11 +57,6 @@ def post_login(self, user=None, token_result=None, id_from_idp=None): # do an on-the-fly usersync for this user to give them instant access after logging in through RAS # if GLOBAL_PARSE_VISAS_ON_LOGIN is true then we want to run it regardless of whether or not the client sent parse_visas on request if parse_visas: - # Close previous db sessions. Leaving it open causes a race condition where we're - # viewing user.project_access while trying to update it in usersync - # not closing leads to partially updated records - current_session.close() - # get passport then call sync on it try: passport = ( From bf7372dc5d181b117da686e093888ca8c03692b2 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Wed, 26 Jan 2022 15:16:02 -0600 Subject: [PATCH 173/211] fix(google): handle case where 2 SA's could exist for a user b/c of changing username --- .secrets.baseline | 4 +- fence/blueprints/storage_creds/google.py | 6 +- fence/resources/google/utils.py | 80 ++++++++++++++++++++++-- fence/resources/user/__init__.py | 5 +- 4 files changed, 84 insertions(+), 11 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 6c029b80c..f30c69026 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -181,7 +181,7 @@ "filename": "fence/resources/google/utils.py", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 127 + "line_number": 129 } ], "fence/utils.py": [ @@ -284,5 +284,5 @@ } ] }, - "generated_at": "2021-12-20T23:13:45Z" + "generated_at": "2022-01-26T21:15:54Z" } diff --git a/fence/blueprints/storage_creds/google.py b/fence/blueprints/storage_creds/google.py index 3e21d1e5e..e3d623856 100644 --- a/fence/blueprints/storage_creds/google.py +++ b/fence/blueprints/storage_creds/google.py @@ -183,6 +183,7 @@ def delete(self): :statuscode 405 Method Not Allowed if ?all=true is not included """ user_id = current_token["sub"] + username = current_token.get("context", {}).get("user", {}).get("name") try: all_arg = strtobool(flask.request.args.get("all", "false").lower()) @@ -197,7 +198,7 @@ def delete(self): with GoogleCloudManager() as g_cloud: client_id = current_token.get("azp") or None - service_account = get_service_account(client_id, user_id) + service_account = get_service_account(client_id, user_id, username=username) if service_account: keys_for_account = g_cloud.get_service_account_keys_info( @@ -259,9 +260,10 @@ def delete(self, access_key): :statuscode 404 Access key doesn't exist """ user_id = current_token["sub"] + username = current_token.get("context", {}).get("user", {}).get("name") with GoogleCloudManager() as g_cloud: client_id = current_token.get("azp") or None - service_account = get_service_account(client_id, user_id) + service_account = get_service_account(client_id, user_id, username=username) if service_account: keys_for_account = g_cloud.get_service_account_keys_info( diff --git a/fence/resources/google/utils.py b/fence/resources/google/utils.py index 00e4eb2f1..6cf8007cc 100644 --- a/fence/resources/google/utils.py +++ b/fence/resources/google/utils.py @@ -90,7 +90,9 @@ def _get_primary_service_account_key(user_id, username, proxy_group_id): user_service_account_key = None # Note that client_id is None, which is how we store the user's SA - user_google_service_account = get_service_account(client_id=None, user_id=user_id) + user_google_service_account = get_service_account( + client_id=None, user_id=user_id, username=username + ) if user_google_service_account: user_service_account_key = ( @@ -375,7 +377,7 @@ def add_custom_service_account_key_expiration( current_session.commit() -def get_service_account(client_id, user_id): +def get_service_account(client_id, user_id, username): """ Return the service account (from Fence db) for given client. @@ -388,11 +390,33 @@ def get_service_account(client_id, user_id): Returns: fence.models.GoogleServiceAccount: Client's service account """ - service_account = ( + service_accounts = ( current_session.query(GoogleServiceAccount) .filter_by(client_id=client_id, user_id=user_id) - .first() + .all() ) + if len(service_accounts) == 1: + return service_accounts[0] + + # in rare cases there's a possible that 2 SA's exist for 1 user that haven't + # been cleaned up yet. This happens when a users username is changed. To ensure + # getting the newest SA, we need to check for the SA ID based off the current + # username + service_account = None + + # determine expected SA name based off username + if client_id: + service_account_id = get_valid_service_account_id_for_client( + client_id, user_id, prefix=config["GOOGLE_SERVICE_ACCOUNT_PREFIX"] + ) + else: + service_account_id = get_valid_service_account_id_for_user( + user_id, username, prefix=config["GOOGLE_SERVICE_ACCOUNT_PREFIX"] + ) + + for sa in service_accounts: + if sa.email == service_account_id: + return sa return service_account @@ -426,7 +450,7 @@ def get_or_create_service_account(client_id, user_id, username, proxy_group_id): ) return _update_service_account_db_entry( - client_id, user_id, proxy_group_id, new_service_account + client_id, user_id, username, proxy_group_id, new_service_account ) else: flask.abort( @@ -436,12 +460,56 @@ def get_or_create_service_account(client_id, user_id, username, proxy_group_id): def _update_service_account_db_entry( - client_id, user_id, proxy_group_id, new_service_account + client_id, user_id, username, proxy_group_id, new_service_account ): """ Now that SA exists in Google so lets check our db and update/add as necessary """ + # there may be an old SA for this user before their username got updated, + # let's find it and remove it so we can use a new one + old_service_accounts = [] + old_service_account_db_entries = ( + current_session.query(GoogleServiceAccount) + .filter( + GoogleServiceAccount.user_id == user_id + and GoogleServiceAccount.client_id == client_id + ) + .all() + ) + for sa in old_service_account_db_entries: + # if there's already a match for user_id, client_id, let's check for any that + # DON'T include the new username + if username not in sa.email: + old_service_accounts.append(sa) + + # clear out old SA and keys if there are any + if old_service_accounts: + for old_service_account_db_entry in old_service_accounts: + logger.info( + "Found Google Service Account using old username: " + "{}. Removing from db. Keys should still have access in Google until " + "cronjob removes them (e.g. fence-create google-manage-keys). NOTE: " + "the SA will still exist in Google but fence will use new SA {} for " + "new keys.".format(old_sa_email, new_service_account["email"]) + ) + + old_service_account_keys_db_entries = ( + current_session.query(GoogleServiceAccountKey) + .filter( + GoogleServiceAccountKey.service_account_id + == old_service_account_db_entry.id + ) + .all() + ) + + # remove the keys then the sa itself from db + for old_key in old_service_account_keys_db_entries: + current_session.delete(old_key) + + current_session.commit() + current_session.delete(old_service_account_db_entry) + # if we're now using a prefix for SAs, cleanup the db if config["GOOGLE_SERVICE_ACCOUNT_PREFIX"]: # - if using the old naming convention without a prefix, diff --git a/fence/resources/user/__init__.py b/fence/resources/user/__init__.py index 50b6f3d65..0cf883548 100644 --- a/fence/resources/user/__init__.py +++ b/fence/resources/user/__init__.py @@ -105,7 +105,10 @@ def get_user_info(current_session, username): info["shib_idp"] = flask.session["shib_idp"] # User SAs are stored in db with client_id = None - primary_service_account = get_service_account(client_id=None, user_id=user.id) or {} + primary_service_account = ( + get_service_account(client_id=None, user_id=user.id, username=user.username) + or {} + ) primary_service_account_email = getattr(primary_service_account, "email", None) info["primary_google_service_account"] = primary_service_account_email From 534040f29ef1367119247c77206f7efe05d88a5a Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Thu, 27 Jan 2022 10:52:54 -0600 Subject: [PATCH 174/211] fix(log): pass correct var into log --- fence/resources/google/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fence/resources/google/utils.py b/fence/resources/google/utils.py index 6cf8007cc..f2be33678 100644 --- a/fence/resources/google/utils.py +++ b/fence/resources/google/utils.py @@ -491,7 +491,7 @@ def _update_service_account_db_entry( "{}. Removing from db. Keys should still have access in Google until " "cronjob removes them (e.g. fence-create google-manage-keys). NOTE: " "the SA will still exist in Google but fence will use new SA {} for " - "new keys.".format(old_sa_email, new_service_account["email"]) + "new keys.".format(username, new_service_account["email"]) ) old_service_account_keys_db_entries = ( From 57a6f5a29c2cfd6e5f7706de9dd5b6d321858303 Mon Sep 17 00:00:00 2001 From: John McCann Date: Mon, 31 Jan 2022 07:37:25 -0800 Subject: [PATCH 175/211] chore(gen3authz): use local package --- Dockerfile | 2 ++ gen3authz-1.5.0.tar.gz | Bin 0 -> 13942 bytes poetry.lock | 38 +++++++------------------------------- pyproject.toml | 3 ++- 4 files changed, 11 insertions(+), 32 deletions(-) create mode 100644 gen3authz-1.5.0.tar.gz diff --git a/Dockerfile b/Dockerfile index ad1febe6f..4d61900ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,6 +30,8 @@ RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2 WORKDIR /$appname +# TODO TAKE OUT: ONLY FOR DEVELOPMENT AND TESTING PURPOSES +COPY gen3authz-1.5.0.tar.gz /$appname/ # copy ONLY poetry artifact, install the dependencies but not fence # this will make sure than the dependencies is cached COPY poetry.lock pyproject.toml /$appname/ diff --git a/gen3authz-1.5.0.tar.gz b/gen3authz-1.5.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..58505c1f4d905f2549a1cd7b678515a3f59f5ebd GIT binary patch literal 13942 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.4,<3.0", markers = "python_version < \"3.7\""} httpx = ">=0.20.0,<1.0.0" six = ">=1.16.0,<2.0.0" +[package.source] +type = "file" +url = "gen3authz-1.5.0.tar.gz" + [[package]] name = "gen3cirrus" version = "2.0.0" @@ -1503,7 +1507,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "81f0de7206a9b331ba732a2efd530b5fb425e67f2eca3274e93cb49c40a4c586" +content-hash = "564d24bb38187ec8d9985435e44cbec039433ea1645cc94def200e18e031a34f" [metadata.files] addict = [ @@ -1793,8 +1797,7 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] gen3authz = [ - {file = "gen3authz-1.4.1-py3-none-any.whl", hash = "sha256:a1eac6a314a12216b47518155100d766905be2fa96210565f0f35239391f4f75"}, - {file = "gen3authz-1.4.1.tar.gz", hash = "sha256:354fa107fd7f15fe318d79f410095f51c6225e1ced04b506e0f3a906ad683ff6"}, + {file = "gen3authz-1.5.0.tar.gz", hash = "sha256:d832038ff2969f7146b20e9f544c6ee0b1ccae9444fbc15f5ea0cc1665122a01"}, ] gen3cirrus = [ {file = "gen3cirrus-2.0.0.tar.gz", hash = "sha256:0bd590c407c42dad5f0b896da0fa30bd01ea6bef5ff7dd11324ec59f14a71793"}, @@ -1974,39 +1977,20 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mock = [ @@ -2218,26 +2202,18 @@ pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, diff --git a/pyproject.toml b/pyproject.toml index 5ce461329..762a58299 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,8 @@ flask-cors = "^3.0.3" flask-restful = "^0.3.6" flask_sqlalchemy_session = "^1.1" email_validator = "^1.1.1" -gen3authz = "^1.4.0" +# TODO USE ^1.5.0 WHEN RELEASED. LOCAL COPY ONLY FOR DEVELOPMENT AND TESTING PURPOSES +gen3authz = {path = "./gen3authz-1.5.0.tar.gz"} gen3cirrus = "^2.0.0" gen3config = "^0.1.7" gen3users = "^0.6.0" From cdd04556a0a1afaf0c2f5b5716c1ac9397f62863 Mon Sep 17 00:00:00 2001 From: John McCann Date: Mon, 31 Jan 2022 07:40:34 -0800 Subject: [PATCH 176/211] feat(sync_users.py): use multi-role policy --- fence/sync/sync_users.py | 173 +++++++++++++++++++++++++++++++++------ 1 file changed, 149 insertions(+), 24 deletions(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index bae3f27d4..dd9d6dea4 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -6,6 +6,11 @@ import yaml import copy import datetime +import uuid +import collections + +# TODO take out +import time from contextlib import contextmanager from collections import defaultdict @@ -1813,6 +1818,7 @@ def _update_authz_in_arborist( if not single_user_sync: # TODO make this smarter - it should do a diff, not revoke all and add self.arborist_client.revoke_all_policies_for_user(username) + for project, permissions in user_project_info.items(): # check if this is a dbgap project, if it is, we need to get the right @@ -1940,6 +1946,127 @@ def _update_authz_in_arborist( return True + def _grant_multi_role_arborist_policies( + self, + session, + user_studies, + expires=None, + policy_prefix=None, + ): + for username, studies in user_studies.items(): + # TODO wrapper with logs + user = query_for_user(session=session, username=username) + if user: + username = user.username + self.arborist_client.create_user_if_not_exist(username) + + roles_to_resources = collections.defaultdict(list) + for study, roles in studies.items(): + ordered_roles = tuple(sorted(roles)) + # TODO also check project_to_authz_mapping + study_authz_paths = self._dbgap_study_to_resources.get(study, [study]) + roles_to_resources[ordered_roles].extend(study_authz_paths) + + for roles in roles_to_resources.keys(): + for role in roles: + self._create_arborist_role(role) + + all_resources = [] + for resources in roles_to_resources.values(): + all_resources.extend(resources) + for resources_request_body in utils.combine_provided_and_dbgap_resources( + {}, all_resources + ): + self._create_arborist_resources(resources_request_body) + + for roles, resources in roles_to_resources.items(): + policy_id = "-".join([policy_prefix, str(uuid.uuid4()), str(expires)]) + self._create_arborist_policy(policy_id, roles, resources) + self._grant_arborist_policy(username, policy_id, expires=expires) + + def _create_arborist_role(self, role): + if role in self._created_roles: + return + try: + self.arborist_client.create_role( + # TODO rename + arborist_role_for_permission(role) + ) + except ArboristError as e: + # TODO str(e) necessary as opposed to just e? + self.logger.error( + "could not create `{}` role in Arborist: {}".format(role, str(e)) + ) + else: + self._created_roles.add(role) + self.logger.info( + "`{}` Arborist role already exists or was created".format(role) + ) + + def _create_arborist_resources(self, request_body): + try: + start = time.time() + response_json = self.arborist_client.update_resource( + "/", request_body, merge=True + ) + end = time.time() + self.logger.info("update_resource took {} seconds".format(end - start)) + except ArboristError as e: + # TODO str(e) necessary as opposed to just e? + self.logger.error( + "could not update resource in Arborist: {}".format(str(e)) + ) + else: + self.logger.debug( + "created Arborist resource. response json: {}".format(response_json) + ) + + def _create_arborist_policy(self, policy_id, roles, resources): + try: + start = time.time() + response_json = self.arborist_client.create_policy( + { + "id": policy_id, + "role_ids": roles, + "resource_paths": resources, + } + ) + end = time.time() + self.logger.info("create_policy took {} seconds".format(end - start)) + except ArboristError as e: + self.logger.error( + "could not create policy `{}` in Arborist: {}".format(policy_id, e) + ) + else: + self.logger.debug( + "created Arborist policy `{}`. response json: {}".format( + policy_id, response_json + ) + ) + + def _grant_arborist_policy(self, username, policy_id, expires=None): + try: + start = time.time() + response_json = self.arborist_client.grant_user_policy( + username, + policy_id, + expires_at=expires, + ) + end = time.time() + self.logger.info("grant_user_policy took {} seconds".format(end - start)) + except ArboristError as e: + self.logger.error( + "could not grant policy `{}` to user `{}`: {}".format( + policy_id, username, e + ) + ) + else: + self.logger.debug( + "granted policy `{}` to user `{}`. response json: {}".format( + policy_id, username, response_json + ) + ) + def _add_dbgap_study_to_arborist(self, dbgap_study, dbgap_config): """ Return the arborist resource path after adding the specified dbgap study @@ -1952,9 +2079,9 @@ def _add_dbgap_study_to_arborist(self, dbgap_study, dbgap_config): Returns: str: arborist resource path for study """ - healthy = self._is_arborist_healthy() - if not healthy: - return False + # healthy = self._is_arborist_healthy() + # if not healthy: + # return False default_namespaces = dbgap_config.get("study_to_resource_namespaces", {}).get( "_default", ["/"] @@ -1975,24 +2102,24 @@ def _add_dbgap_study_to_arborist(self, dbgap_study, dbgap_config): # existing resources. Therefore, only create if get_resource returns # the resource doesn't exist. full_resource_path = resource_namespace + dbgap_study - if not self.arborist_client.get_resource(full_resource_path): - response = self.arborist_client.update_resource( - resource_namespace, - {"name": dbgap_study, "description": "synced from dbGaP"}, - create_parents=True, - ) - self.logger.info( - "added arborist resource under parent path: {} for dbgap project {}.".format( - resource_namespace, dbgap_study - ) - ) - self.logger.debug("Arborist response: {}".format(response)) - else: - self.logger.debug( - "Arborist resource already exists: {}".format( - full_resource_path - ) - ) + # if not self.arborist_client.get_resource(full_resource_path): + # response = self.arborist_client.update_resource( + # resource_namespace, + # {"name": dbgap_study, "description": "synced from dbGaP"}, + # create_parents=True, + # ) + # self.logger.info( + # "added arborist resource under parent path: {} for dbgap project {}.".format( + # resource_namespace, dbgap_study + # ) + # ) + # self.logger.debug("Arborist response: {}".format(response)) + # else: + # self.logger.debug( + # "Arborist resource already exists: {}".format( + # full_resource_path + # ) + # ) if dbgap_study not in self._dbgap_study_to_resources: self._dbgap_study_to_resources[dbgap_study] = [] @@ -2192,11 +2319,9 @@ def sync_single_user_visas( # update arborist db (user access) if self.arborist_client: self.logger.info("Synchronizing arborist with authorization info...") - success = self._update_authz_in_arborist( + success = self._grant_multi_role_arborist_policies( sess, user_projects, - user_yaml=user_yaml, - single_user_sync=True, expires=expires, policy_prefix=policy_prefix, ) From 814c02a972e7341edbeaabcc72cb0d291d956f98 Mon Sep 17 00:00:00 2001 From: John McCann Date: Tue, 1 Feb 2022 13:42:39 -0800 Subject: [PATCH 177/211] chore(sync_users.py): move multi-role policy code --- fence/sync/sync_users.py | 260 ++++++++++++--------------------------- 1 file changed, 78 insertions(+), 182 deletions(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index dd9d6dea4..62b78fe2b 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -1819,100 +1819,59 @@ def _update_authz_in_arborist( # TODO make this smarter - it should do a diff, not revoke all and add self.arborist_client.revoke_all_policies_for_user(username) - for project, permissions in user_project_info.items(): + # TODO comment + roles_to_resources = collections.defaultdict(list) + for study, roles in user_project_info.items(): + ordered_roles = tuple(sorted(roles)) + study_authz_paths = self._dbgap_study_to_resources.get(study, [study]) + if study in project_to_authz_mapping: + study_authz_paths = [project_to_authz_mapping[study]] + roles_to_resources[ordered_roles].extend(study_authz_paths) - # check if this is a dbgap project, if it is, we need to get the right - # resource path, otherwise just use given project as path - paths = self._dbgap_study_to_resources.get(project, [project]) + for roles in roles_to_resources.keys(): + for role in roles: + self._create_arborist_role(role) - try: - # check if project is in mapping and convert accordingly - paths = [project_to_authz_mapping[project]] - except KeyError: - pass + self._create_arborist_resources( + [r for resources in roles_to_resources.values() for r in resources] + ) - self.logger.info( - "resource paths for project {}: {}".format(project, paths) - ) - self.logger.debug("permissions: {}".format(permissions)) - for permission in permissions: - # "permission" in the dbgap sense, not the arborist sense - if permission not in self._created_roles: - try: - self.arborist_client.create_role( - arborist_role_for_permission(permission) + if single_user_sync: + for roles, resources in roles_to_resources.items(): + # handle unlikely uuid collisions + policy_id = str(uuid.uuid4()) + self._create_arborist_policy(policy_id, roles, resources) + self._grant_arborist_policy(username, policy_id, expires=expires) + else: + for roles, resources in roles_to_resources.items(): + for role in roles: + for resource in resources: + policy_id = _format_policy_id( + resource, role, prefix=policy_prefix ) - except ArboristError as e: - self.logger.info( - "not creating role for permission `{}`; {}".format( - permission, str(e) - ) + if policy_id not in self._created_policies: + try: + self.arborist_client.update_policy( + policy_id, + { + "description": "policy created by fence sync", + "role_ids": [role], + "resource_paths": [resource], + }, + create_if_not_exist=True, + ) + except ArboristError as e: + self.logger.info( + "not creating policy in arborist; {}".format( + str(e) + ) + ) + self._created_policies.add(policy_id) + + self._grant_arborist_policy( + username, policy_id, expires=expires ) - self._created_roles.add(permission) - - for path in paths: - # If everything was created fine, grant a policy to - # this user which contains exactly just this resource, - # with this permission as a role. - - # format project '/x/y/z' -> 'x.y.z' - # so the policy id will be something like 'x.y.z-create' - policy_id = _format_policy_id( - path, permission, prefix=policy_prefix - ) - - if policy_id not in self._created_policies: - try: - self.arborist_client.update_policy( - policy_id, - { - "description": "policy created by fence sync", - "role_ids": [permission], - "resource_paths": [path], - }, - create_if_not_exist=True, - ) - except ArboristError as e: - self.logger.info( - "not creating policy in arborist; {}".format(str(e)) - ) - self._created_policies.add(policy_id) - - self.arborist_client.grant_user_policy( - username, - policy_id, - expires_at=expires, - ) - # TODO As of 10-11-2021, there's no endpoint yet in Arborist to - # support the creation of policies in bulk. When syncing RAS - # passport authz information at the time of data access, the - # passport policies may need to be created before they can be - # updated. This code has been left commented out for later use - # when bulk Arborist policy creation is suppported. - - # if single_user_sync: - # policy_id_list.append(policy_id) - # policy_json = { - # "id": policy_id, - # "description": "policy created by fence sync", - # "role_ids": [permission], - # "resource_paths": [path], - # } - # policies.append(policy_json) - # if single_user_sync: - # try: - # self.arborist_client.update_bulk_policy(policies) - # self.arborist_client.grant_bulk_user_policy( - # username, policy_id_list - # ) - # except Exception as e: - # self.logger.info( - # "Couldn't update bulk policy for user {}: {}".format( - # username, e - # ) - # ) - # if user_yaml: for policy in user_yaml.policies.get(username, []): self.arborist_client.grant_user_policy( @@ -1946,44 +1905,6 @@ def _update_authz_in_arborist( return True - def _grant_multi_role_arborist_policies( - self, - session, - user_studies, - expires=None, - policy_prefix=None, - ): - for username, studies in user_studies.items(): - # TODO wrapper with logs - user = query_for_user(session=session, username=username) - if user: - username = user.username - self.arborist_client.create_user_if_not_exist(username) - - roles_to_resources = collections.defaultdict(list) - for study, roles in studies.items(): - ordered_roles = tuple(sorted(roles)) - # TODO also check project_to_authz_mapping - study_authz_paths = self._dbgap_study_to_resources.get(study, [study]) - roles_to_resources[ordered_roles].extend(study_authz_paths) - - for roles in roles_to_resources.keys(): - for role in roles: - self._create_arborist_role(role) - - all_resources = [] - for resources in roles_to_resources.values(): - all_resources.extend(resources) - for resources_request_body in utils.combine_provided_and_dbgap_resources( - {}, all_resources - ): - self._create_arborist_resources(resources_request_body) - - for roles, resources in roles_to_resources.items(): - policy_id = "-".join([policy_prefix, str(uuid.uuid4()), str(expires)]) - self._create_arborist_policy(policy_id, roles, resources) - self._grant_arborist_policy(username, policy_id, expires=expires) - def _create_arborist_role(self, role): if role in self._created_roles: return @@ -2003,26 +1924,29 @@ def _create_arborist_role(self, role): "`{}` Arborist role already exists or was created".format(role) ) - def _create_arborist_resources(self, request_body): - try: - start = time.time() - response_json = self.arborist_client.update_resource( - "/", request_body, merge=True - ) - end = time.time() - self.logger.info("update_resource took {} seconds".format(end - start)) - except ArboristError as e: - # TODO str(e) necessary as opposed to just e? - self.logger.error( - "could not update resource in Arborist: {}".format(str(e)) - ) - else: - self.logger.debug( - "created Arborist resource. response json: {}".format(response_json) - ) + def _create_arborist_resources(self, resources): + for request_body in utils.combine_provided_and_dbgap_resources({}, resources): + try: + # TODO take out timing + start = time.time() + response_json = self.arborist_client.update_resource( + "/", request_body, merge=True + ) + end = time.time() + self.logger.info("update_resource took {} seconds".format(end - start)) + except ArboristError as e: + # TODO str(e) necessary as opposed to just e? + self.logger.error( + "could not update resource in Arborist: {}".format(str(e)) + ) + else: + self.logger.debug( + "created Arborist resource. response json: {}".format(response_json) + ) def _create_arborist_policy(self, policy_id, roles, resources): try: + # TODO take out timing start = time.time() response_json = self.arborist_client.create_policy( { @@ -2046,6 +1970,7 @@ def _create_arborist_policy(self, policy_id, roles, resources): def _grant_arborist_policy(self, username, policy_id, expires=None): try: + # TODO take out timing start = time.time() response_json = self.arborist_client.grant_user_policy( username, @@ -2067,6 +1992,7 @@ def _grant_arborist_policy(self, username, policy_id, expires=None): ) ) + # TODO rename and update docstring def _add_dbgap_study_to_arborist(self, dbgap_study, dbgap_config): """ Return the arborist resource path after adding the specified dbgap study @@ -2079,10 +2005,6 @@ def _add_dbgap_study_to_arborist(self, dbgap_study, dbgap_config): Returns: str: arborist resource path for study """ - # healthy = self._is_arborist_healthy() - # if not healthy: - # return False - default_namespaces = dbgap_config.get("study_to_resource_namespaces", {}).get( "_default", ["/"] ) @@ -2096,40 +2018,12 @@ def _add_dbgap_study_to_arborist(self, dbgap_study, dbgap_config): namespace.rstrip("/") + "/programs/" for namespace in namespaces ] - try: - for resource_namespace in arborist_resource_namespaces: - # The update_resource function creates a put request which will overwrite - # existing resources. Therefore, only create if get_resource returns - # the resource doesn't exist. - full_resource_path = resource_namespace + dbgap_study - # if not self.arborist_client.get_resource(full_resource_path): - # response = self.arborist_client.update_resource( - # resource_namespace, - # {"name": dbgap_study, "description": "synced from dbGaP"}, - # create_parents=True, - # ) - # self.logger.info( - # "added arborist resource under parent path: {} for dbgap project {}.".format( - # resource_namespace, dbgap_study - # ) - # ) - # self.logger.debug("Arborist response: {}".format(response)) - # else: - # self.logger.debug( - # "Arborist resource already exists: {}".format( - # full_resource_path - # ) - # ) - - if dbgap_study not in self._dbgap_study_to_resources: - self._dbgap_study_to_resources[dbgap_study] = [] - - self._dbgap_study_to_resources[dbgap_study].append(full_resource_path) - - return arborist_resource_namespaces - except ArboristError as e: - self.logger.error(e) - # keep going; maybe just some conflicts from things existing already + for resource_namespace in arborist_resource_namespaces: + full_resource_path = resource_namespace + dbgap_study + if dbgap_study not in self._dbgap_study_to_resources: + self._dbgap_study_to_resources[dbgap_study] = [] + self._dbgap_study_to_resources[dbgap_study].append(full_resource_path) + return arborist_resource_namespaces def _is_arborist_healthy(self): if not self.arborist_client: @@ -2319,9 +2213,11 @@ def sync_single_user_visas( # update arborist db (user access) if self.arborist_client: self.logger.info("Synchronizing arborist with authorization info...") - success = self._grant_multi_role_arborist_policies( + success = self._update_authz_in_arborist( sess, user_projects, + user_yaml=user_yaml, + single_user_sync=True, expires=expires, policy_prefix=policy_prefix, ) From 8a268cde318ab0bef1b67f4bacd8654b155c6931 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Wed, 2 Feb 2022 12:20:39 -0600 Subject: [PATCH 178/211] fix(google): cleanup extra SA when found --- fence/resources/google/utils.py | 71 +++++++++++---------------------- 1 file changed, 24 insertions(+), 47 deletions(-) diff --git a/fence/resources/google/utils.py b/fence/resources/google/utils.py index f2be33678..761460908 100644 --- a/fence/resources/google/utils.py +++ b/fence/resources/google/utils.py @@ -416,7 +416,28 @@ def get_service_account(client_id, user_id, username): for sa in service_accounts: if sa.email == service_account_id: - return sa + service_account = sa + else: + logger.info( + "Found Google Service Account using invalid/old name: " + "{}. Removing from db. Keys should still have access in Google until " + "cronjob removes them (e.g. fence-create google-manage-keys). NOTE: " + "the SA will still exist in Google but fence will use new SA {} for " + "new keys.".format(sa.email, service_account_id) + ) + + old_service_account_keys_db_entries = ( + current_session.query(GoogleServiceAccountKey) + .filter(GoogleServiceAccountKey.service_account_id == sa.id) + .all() + ) + + # remove the keys then the sa itself from db + for old_key in old_service_account_keys_db_entries: + current_session.delete(old_key) + + current_session.commit() + current_session.delete(sa) return service_account @@ -450,7 +471,7 @@ def get_or_create_service_account(client_id, user_id, username, proxy_group_id): ) return _update_service_account_db_entry( - client_id, user_id, username, proxy_group_id, new_service_account + client_id, user_id, proxy_group_id, new_service_account ) else: flask.abort( @@ -460,56 +481,12 @@ def get_or_create_service_account(client_id, user_id, username, proxy_group_id): def _update_service_account_db_entry( - client_id, user_id, username, proxy_group_id, new_service_account + client_id, user_id, proxy_group_id, new_service_account ): """ Now that SA exists in Google so lets check our db and update/add as necessary """ - # there may be an old SA for this user before their username got updated, - # let's find it and remove it so we can use a new one - old_service_accounts = [] - old_service_account_db_entries = ( - current_session.query(GoogleServiceAccount) - .filter( - GoogleServiceAccount.user_id == user_id - and GoogleServiceAccount.client_id == client_id - ) - .all() - ) - for sa in old_service_account_db_entries: - # if there's already a match for user_id, client_id, let's check for any that - # DON'T include the new username - if username not in sa.email: - old_service_accounts.append(sa) - - # clear out old SA and keys if there are any - if old_service_accounts: - for old_service_account_db_entry in old_service_accounts: - logger.info( - "Found Google Service Account using old username: " - "{}. Removing from db. Keys should still have access in Google until " - "cronjob removes them (e.g. fence-create google-manage-keys). NOTE: " - "the SA will still exist in Google but fence will use new SA {} for " - "new keys.".format(username, new_service_account["email"]) - ) - - old_service_account_keys_db_entries = ( - current_session.query(GoogleServiceAccountKey) - .filter( - GoogleServiceAccountKey.service_account_id - == old_service_account_db_entry.id - ) - .all() - ) - - # remove the keys then the sa itself from db - for old_key in old_service_account_keys_db_entries: - current_session.delete(old_key) - - current_session.commit() - current_session.delete(old_service_account_db_entry) - # if we're now using a prefix for SAs, cleanup the db if config["GOOGLE_SERVICE_ACCOUNT_PREFIX"]: # - if using the old naming convention without a prefix, From efc2e6c3837f82e4be13b6119e1dee9bdee074ba Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Thu, 3 Feb 2022 13:42:29 -0600 Subject: [PATCH 179/211] fix(google): check for id in email, not exact match --- fence/resources/google/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fence/resources/google/utils.py b/fence/resources/google/utils.py index 761460908..2dfefd583 100644 --- a/fence/resources/google/utils.py +++ b/fence/resources/google/utils.py @@ -415,7 +415,7 @@ def get_service_account(client_id, user_id, username): ) for sa in service_accounts: - if sa.email == service_account_id: + if service_account_id in sa.email: service_account = sa else: logger.info( From 5d581f83cfefcda6c9e622c89a3a3334147fb3ce Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Fri, 4 Feb 2022 15:48:21 -0600 Subject: [PATCH 180/211] fix(google): ensure commit for deletion of SA --- fence/resources/google/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fence/resources/google/utils.py b/fence/resources/google/utils.py index 2dfefd583..adeb0983a 100644 --- a/fence/resources/google/utils.py +++ b/fence/resources/google/utils.py @@ -436,8 +436,10 @@ def get_service_account(client_id, user_id, username): for old_key in old_service_account_keys_db_entries: current_session.delete(old_key) + # commit the deletion of keys first, then do SA deletion current_session.commit() current_session.delete(sa) + current_session.commit() return service_account From 0742928f5bce05da356843fb8471486db44cba47 Mon Sep 17 00:00:00 2001 From: John McCann Date: Sun, 6 Feb 2022 22:48:14 -0800 Subject: [PATCH 181/211] feat(sync_users.py): id policy by hash --- fence/sync/sync_users.py | 110 ++++++++++++++++++++++++++------------- 1 file changed, 75 insertions(+), 35 deletions(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 62b78fe2b..f124437c1 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -8,6 +8,7 @@ import datetime import uuid import collections +import hashlib # TODO take out import time @@ -1832,16 +1833,30 @@ def _update_authz_in_arborist( for role in roles: self._create_arborist_role(role) - self._create_arborist_resources( + if not self._create_arborist_resources( [r for resources in roles_to_resources.values() for r in resources] - ) + ): + return False if single_user_sync: - for roles, resources in roles_to_resources.items(): - # handle unlikely uuid collisions - policy_id = str(uuid.uuid4()) - self._create_arborist_policy(policy_id, roles, resources) - self._grant_arborist_policy(username, policy_id, expires=expires) + for ordered_roles, resources in roles_to_resources.items(): + ordered_resources = tuple(sorted(resources)) + # TODO handle unlikely uuid collisions + # policy_id = str(uuid.uuid4()) + policy_hash = self._hash_policy_contents( + ordered_roles, ordered_resources + ) + if not self._create_arborist_policy( + policy_hash, + ordered_roles, + ordered_resources, + skip_if_exists=True, + ): + return False + if not self._grant_arborist_policy( + username, policy_hash, expires=expires + ): + return False else: for roles, resources in roles_to_resources.items(): for role in roles: @@ -1905,46 +1920,56 @@ def _update_authz_in_arborist( return True + # TODO docstring def _create_arborist_role(self, role): if role in self._created_roles: - return + return True try: - self.arborist_client.create_role( - # TODO rename + response_json = self.arborist_client.create_role( arborist_role_for_permission(role) ) except ArboristError as e: - # TODO str(e) necessary as opposed to just e? self.logger.error( - "could not create `{}` role in Arborist: {}".format(role, str(e)) + "could not create `{}` role in Arborist: {}".format(role, e) ) + return False + self._created_roles.add(role) + + if response_json is None: + self.logger.info("role `{}` already exists in Arborist".format(role)) else: - self._created_roles.add(role) - self.logger.info( - "`{}` Arborist role already exists or was created".format(role) - ) + self.logger.info("created role `{}` in Arborist".format(role)) + return True + # TODO docstring def _create_arborist_resources(self, resources): + root = "/" for request_body in utils.combine_provided_and_dbgap_resources({}, resources): try: # TODO take out timing start = time.time() response_json = self.arborist_client.update_resource( - "/", request_body, merge=True + root, request_body, merge=True ) end = time.time() self.logger.info("update_resource took {} seconds".format(end - start)) except ArboristError as e: - # TODO str(e) necessary as opposed to just e? self.logger.error( - "could not update resource in Arborist: {}".format(str(e)) - ) - else: - self.logger.debug( - "created Arborist resource. response json: {}".format(response_json) + "could not update resource `{}` with request body `{}` in Arborist. error: {}".format( + root, request_body, e + ) ) + return False - def _create_arborist_policy(self, policy_id, roles, resources): + self.logger.debug( + "created {} resource(s) in Arborist: `{}`".format(len(resources), resources) + ) + return True + + # TODO docstring + def _create_arborist_policy( + self, policy_id, roles, resources, skip_if_exists=False + ): try: # TODO take out timing start = time.time() @@ -1953,7 +1978,8 @@ def _create_arborist_policy(self, policy_id, roles, resources): "id": policy_id, "role_ids": roles, "resource_paths": resources, - } + }, + skip_if_exists=skip_if_exists, ) end = time.time() self.logger.info("create_policy took {} seconds".format(end - start)) @@ -1961,12 +1987,26 @@ def _create_arborist_policy(self, policy_id, roles, resources): self.logger.error( "could not create policy `{}` in Arborist: {}".format(policy_id, e) ) - else: + return False + + if response_json is None: self.logger.debug( - "created Arborist policy `{}`. response json: {}".format( - policy_id, response_json - ) + "policy `{}` already exists in Arborist".format(policy_id) ) + else: + self.logger.debug("created policy `{}` in Arborist".format(policy_id)) + return True + + # TODO docstring + def _hash_policy_contents(self, roles, resources): + def escape(s): + return s.replace(",", "\,") + + canonical_roles = ",".join(tuple(escape(r) for r in roles)) + canonical_resources = ",".join(tuple(escape(r) for r in resources)) + canonical_policy = f"{canonical_roles},,f{canonical_resources}" + policy_hash = hashlib.sha256(canonical_policy.encode("utf-8")).hexdigest() + return policy_hash def _grant_arborist_policy(self, username, policy_id, expires=None): try: @@ -1985,12 +2025,12 @@ def _grant_arborist_policy(self, username, policy_id, expires=None): policy_id, username, e ) ) - else: - self.logger.debug( - "granted policy `{}` to user `{}`. response json: {}".format( - policy_id, username, response_json - ) - ) + return False + + self.logger.debug( + "granted policy `{}` to user `{}`".format(policy_id, username) + ) + return True # TODO rename and update docstring def _add_dbgap_study_to_arborist(self, dbgap_study, dbgap_config): From 48954d4dc589ed363ee00b01d1605ec9c659adcd Mon Sep 17 00:00:00 2001 From: John McCann Date: Wed, 9 Feb 2022 00:20:48 -0800 Subject: [PATCH 182/211] test(single policy sync): fix tests --- fence/sync/sync_users.py | 16 +++++++++------- tests/dbgap_sync/conftest.py | 2 ++ tests/dbgap_sync/test_user_sync.py | 26 ++++++++++++++++++++------ 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index f124437c1..3ce827ded 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -1809,6 +1809,15 @@ def _update_authz_in_arborist( f"(instead of user.yaml - as it may not be available): {project_to_authz_mapping}" ) + all_resources = [ + r + for resources in self._dbgap_study_to_resources.values() + for r in resources + ] + all_resources.extend(r for r in project_to_authz_mapping.values()) + if not self._create_arborist_resources(all_resources): + return False + for username, user_project_info in user_projects.items(): self.logger.info("processing user `{}`".format(username)) user = query_for_user(session=session, username=username) @@ -1833,16 +1842,9 @@ def _update_authz_in_arborist( for role in roles: self._create_arborist_role(role) - if not self._create_arborist_resources( - [r for resources in roles_to_resources.values() for r in resources] - ): - return False - if single_user_sync: for ordered_roles, resources in roles_to_resources.items(): ordered_resources = tuple(sorted(resources)) - # TODO handle unlikely uuid collisions - # policy_id = str(uuid.uuid4()) policy_hash = self._hash_policy_contents( ordered_roles, ordered_resources ) diff --git a/tests/dbgap_sync/conftest.py b/tests/dbgap_sync/conftest.py index ae72fc5dc..d3f36166d 100644 --- a/tests/dbgap_sync/conftest.py +++ b/tests/dbgap_sync/conftest.py @@ -217,6 +217,8 @@ def mocked_get(path, **kwargs): syncer_obj.arborist_client._user_url = "/user" + syncer_obj._create_arborist_resources = MagicMock() + for element in provider: udm.create_provider(db_session, element["name"], backend=element["backend"]) diff --git a/tests/dbgap_sync/test_user_sync.py b/tests/dbgap_sync/test_user_sync.py index bcb99727c..dbc1c41cb 100644 --- a/tests/dbgap_sync/test_user_sync.py +++ b/tests/dbgap_sync/test_user_sync.py @@ -1,6 +1,7 @@ import os import pytest import yaml +import collections import asyncio import flask @@ -282,12 +283,15 @@ def test_dbgap_consent_codes( user.project_access, {"phs000179": ["read", "read-storage"]} ) - resource_to_parent_paths = {} - for call in syncer.arborist_client.update_resource.call_args_list: + resource_to_parent_paths = collections.defaultdict(list) + for call in syncer._create_arborist_resources.call_args_list: args, kwargs = call - parent_path = args[0] - resource = args[1].get("name") - resource_to_parent_paths.setdefault(resource, []).append(parent_path) + full_paths = args[0] + for full_path in full_paths: + resource_begin = full_path.rfind("/") + 1 + parent_path = full_path[:resource_begin] + resource = full_path[resource_begin:] + resource_to_parent_paths[resource].append(parent_path) if parse_consent_code_config: if enable_common_exchange_area: @@ -549,12 +553,22 @@ def test_update_arborist(syncer, db_session): "data_file", # comes from user.yaml file ] - resource_to_parent_paths = {} + resource_to_parent_paths = collections.defaultdict(list) for call in syncer.arborist_client.update_resource.call_args_list: args, kwargs = call parent_path = args[0] resource = args[1].get("name") resource_to_parent_paths.setdefault(resource, []).append(parent_path) + # usersync updates dbgap projects at once using _create_arborist_resources + # as opposed to individually with gen3authz's update_resource + for call in syncer._create_arborist_resources.call_args_list: + args, kwargs = call + full_paths = args[0] + for full_path in full_paths: + resource_begin = full_path.rfind("/") + 1 + parent_path = full_path[:resource_begin] + resource = full_path[resource_begin:] + resource_to_parent_paths[resource].append(parent_path) for resource in expect_resources: assert resource in list(resource_to_parent_paths.keys()) From 02a157ccc0011c3aca3560d9eee5ce09aeaec702 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Wed, 9 Feb 2022 12:36:28 -0600 Subject: [PATCH 183/211] feat(hashing): use sha256 and don't store plain text passport --- fence/config-default.yaml | 10 +++--- fence/models.py | 5 ++- fence/resources/ga4gh/passports.py | 45 ++++++++---------------- tests/test_drs.py | 55 +++++++++++++++++------------- 4 files changed, 53 insertions(+), 62 deletions(-) diff --git a/fence/config-default.yaml b/fence/config-default.yaml index 1ce59d080..850ddb875 100755 --- a/fence/config-default.yaml +++ b/fence/config-default.yaml @@ -897,12 +897,12 @@ GA4GH_VISA_ISSUER_ALLOWLIST: - 'https://stsstg.nih.gov' GA4GH_VISA_V1_CLAIM_REQUIRED_FIELDS: type: - - "https://ras.nih.gov/visas/v1.1" + - 'https://ras.nih.gov/visas/v1.1' value: - - "https://sts.nih.gov/passport/dbgap/v1.1" - - "https://stsstg.nih.gov/passport/dbgap/v1.1" + - 'https://sts.nih.gov/passport/dbgap/v1.1' + - 'https://stsstg.nih.gov/passport/dbgap/v1.1' source: - - "https://ncbi.nlm.nih.gov/gap" + - 'https://ncbi.nlm.nih.gov/gap' EXPIRED_AUTHZ_REMOVAL_JOB_FREQ_IN_SECONDS: 300 # Global sync visas during login # None(Default): Allow per client i.e. a fence client can pick whether or not to sync their visas during login with parse_visas param in /authorization endpoint @@ -912,5 +912,5 @@ GLOBAL_PARSE_VISAS_ON_LOGIN: # Settings for usersync with visas USERSYNC: visa_types: - ras: ["https://ras.nih.gov/visas/v1", "https://ras.nih.gov/visas/v1.1"] + ras: ['https://ras.nih.gov/visas/v1', 'https://ras.nih.gov/visas/v1.1'] RAS_USERINFO_ENDPOINT: '/openid/connect/v1.1/userinfo' diff --git a/fence/models.py b/fence/models.py index 0a64809dd..d911e7cb4 100644 --- a/fence/models.py +++ b/fence/models.py @@ -24,7 +24,7 @@ text, event, ) -from sqlalchemy.dialects.postgresql import ARRAY, JSONB, UUID +from sqlalchemy.dialects.postgresql import ARRAY, JSONB from sqlalchemy.orm import relationship, backref from sqlalchemy.sql import func from sqlalchemy import exc as sa_exc @@ -619,8 +619,7 @@ class AssumeRoleCacheGCP(Base): class GA4GHPassportCache(Base): __tablename__ = "ga4gh_passport_cache" - passport_hash = Column(UUID(as_uuid=True), primary_key=True) - passport = Column(Text, nullable=False) + passport_hash = Column(String(64), primary_key=True) expires_at = Column(BigInteger, nullable=False) user_ids = Column(ARRAY(String(255)), nullable=False) diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index 6f86cfe77..0f3d4814b 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -4,7 +4,6 @@ import hashlib import time import datetime -import uuid import jwt # the whole fence_create module is imported to avoid issue with circular imports @@ -30,9 +29,7 @@ logger = get_logger(__name__) # cache will be in following format -# passport: ([user_id_0, user_id_1, ...], expires_at) -# -# NOTE: we'll want to watch the memory usage on this since passports can be pretty large +# passport_hash: ([user_id_0, user_id_1, ...], expires_at) PASSPORT_CACHE = {} @@ -433,10 +430,11 @@ def get_gen3_usernames_for_passport_from_cache(passport, db_session=None): user_ids_from_passports = None current_time = int(time.time()) - # try to retrieve from local in-memory cache + passport_hash = hashlib.sha256(passport.encode("utf-8")).hexdigest() - if passport in PASSPORT_CACHE: - user_ids_from_passports, expires = PASSPORT_CACHE[passport] + # try to retrieve from local in-memory cache + if passport_hash in PASSPORT_CACHE: + user_ids_from_passports, expires = PASSPORT_CACHE[passport_hash] if expires > current_time: logger.debug( f"Got users {user_ids_from_passports} for provided passport from in-memory cache. " @@ -445,26 +443,20 @@ def get_gen3_usernames_for_passport_from_cache(passport, db_session=None): return user_ids_from_passports else: # expired, so remove it - del PASSPORT_CACHE[passport] + del PASSPORT_CACHE[passport_hash] # try to retrieve from database cache - - # get an md5 hash of passport (which is 128 bits) and convert to UUID (which is 128 bits) - # for optimal usage of database's underlying UUID column type - passport_hash_as_uuid = uuid.UUID(hashlib.md5(passport.encode("utf-8")).hexdigest()) cached_passport = ( db_session.query(GA4GHPassportCache) - .filter(GA4GHPassportCache.passport_hash == passport_hash_as_uuid) + .filter(GA4GHPassportCache.passport_hash == passport_hash) .first() ) - # we retrieved based on hash, which has a small chance of collision. Mitigate that by - # now verifying that the full passport in the db matches what was provided - if cached_passport and cached_passport.passport == passport: + if cached_passport: if cached_passport.expires_at > current_time: user_ids_from_passports = cached_passport.user_ids # update local cache - PASSPORT_CACHE[passport] = ( + PASSPORT_CACHE[passport_hash] = ( user_ids_from_passports, cached_passport.expires_at, ) @@ -498,36 +490,27 @@ def put_gen3_usernames_for_passport_into_cache( expires_at (int): expiration time in unix time """ db_session = db_session or current_session - # stores back to cache and db - PASSPORT_CACHE[passport] = user_ids_from_passports, expires_at - # get an md5 hash of passport (which is 128 bits) and convert to UUID (which is 128 bits) - # for optimal usage of database's underlying UUID column type - passport_hash_as_uuid = uuid.UUID(hashlib.md5(passport.encode("utf-8")).hexdigest()) + passport_hash = hashlib.sha256(passport.encode("utf-8")).hexdigest() + + # stores back to cache and db + PASSPORT_CACHE[passport_hash] = user_ids_from_passports, expires_at - # the improbable collision of hash on 2 different passports will result in an overwrite - # of the previous passport information and the discrepancy will raise an error on - # retrieval (after a comparison of the full stored passport vs provided). e.g. this - # collision will NOT get caught here but instead on the "GET" from cache functionality db_session.execute( """\ INSERT INTO ga4gh_passport_cache ( passport_hash, - passport, expires_at, user_ids ) VALUES ( :passport_hash, - :passport, :expires_at, :user_ids ) ON CONFLICT (passport_hash) DO UPDATE SET - passport = EXCLUDED.passport, expires_at = EXCLUDED.expires_at, user_ids = EXCLUDED.user_ids;""", dict( - passport_hash=passport_hash_as_uuid, - passport=passport, + passport_hash=passport_hash, expires_at=expires_at, user_ids=user_ids_from_passports, ), diff --git a/tests/test_drs.py b/tests/test_drs.py index aac4a6b28..753ef0b6b 100644 --- a/tests/test_drs.py +++ b/tests/test_drs.py @@ -1,5 +1,6 @@ import flask import httpx +import hashlib import json import jwt import pytest @@ -1004,14 +1005,16 @@ def test_passport_cache_valid_passport( keys = [keypair.public_key_to_jwk() for keypair in flask.current_app.keypairs] mock_httpx_get.return_value = httpx.Response(200, json={"keys": keys}) + passport_hash = hashlib.sha256(encoded_passport.encode("utf-8")).hexdigest() + # check database cache cached_passports = [ - item.passport for item in db_session.query(GA4GHPassportCache).all() + item.passport_hash for item in db_session.query(GA4GHPassportCache).all() ] - assert encoded_passport not in cached_passports + assert passport_hash not in cached_passports # check in-memory cache - assert not PASSPORT_CACHE.get(encoded_passport) + assert not PASSPORT_CACHE.get(passport_hash) before_cache_start = time.time() res = client.post( @@ -1027,12 +1030,12 @@ def test_passport_cache_valid_passport( # check that database cache populated cached_passports = [ - item.passport for item in db_session.query(GA4GHPassportCache).all() + item.passport_hash for item in db_session.query(GA4GHPassportCache).all() ] - assert encoded_passport in cached_passports + assert passport_hash in cached_passports # check that in-memory cache populated - assert PASSPORT_CACHE.get(encoded_passport) + assert PASSPORT_CACHE.get(passport_hash) after_cache_start = time.time() res = client.post( @@ -1215,14 +1218,16 @@ def test_passport_cache_invalid_passport( keys = [keypair.public_key_to_jwk() for keypair in flask.current_app.keypairs] mock_httpx_get.return_value = httpx.Response(200, json={"keys": keys}) + passport_hash = hashlib.sha256(invalid_encoded_passport.encode("utf-8")).hexdigest() + # check database cache cached_passports = [ - item.passport for item in db_session.query(GA4GHPassportCache).all() + item.passport_hash for item in db_session.query(GA4GHPassportCache).all() ] - assert invalid_encoded_passport not in cached_passports + assert passport_hash not in cached_passports # check in-memory cache - assert not PASSPORT_CACHE.get(invalid_encoded_passport) + assert not PASSPORT_CACHE.get(passport_hash) res = client.post( "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, @@ -1235,12 +1240,12 @@ def test_passport_cache_invalid_passport( # check that database cache NOT populated cached_passports = [ - item.passport for item in db_session.query(GA4GHPassportCache).all() + item.passport_hash for item in db_session.query(GA4GHPassportCache).all() ] - assert invalid_encoded_passport not in cached_passports + assert passport_hash not in cached_passports # check that in-memory cache NOT populated - assert not PASSPORT_CACHE.get(invalid_encoded_passport) + assert not PASSPORT_CACHE.get(passport_hash) res = client.post( "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, @@ -1430,6 +1435,8 @@ def test_passport_cache_expired_in_memory_valid_in_db( keys = [keypair.public_key_to_jwk() for keypair in flask.current_app.keypairs] mock_httpx_get.return_value = httpx.Response(200, json={"keys": keys}) + passport_hash = hashlib.sha256(encoded_passport.encode("utf-8")).hexdigest() + # simulate db cache with a valid passport by first calling the endpoint to cache # res = client.post( # "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, @@ -1446,7 +1453,7 @@ def test_passport_cache_expired_in_memory_valid_in_db( # double-check database cache cached_passport = ( db_session.query(GA4GHPassportCache) - .filter(GA4GHPassportCache.passport == encoded_passport) + .filter(GA4GHPassportCache.passport_hash == passport_hash) .first() ) # greater and NOT == b/c of logic to set internal expiration less than real to allow @@ -1456,8 +1463,8 @@ def test_passport_cache_expired_in_memory_valid_in_db( # simulate in-memory cache with an expired passport by overriding the in-memory cache from fence.resources.ga4gh import passports as passports_module - PASSPORT_CACHE = {f"{encoded_passport}": ([test_username], current_time - 1)} - assert PASSPORT_CACHE.get(encoded_passport, ("", 0))[1] == current_time - 1 + PASSPORT_CACHE = {f"{passport_hash}": ([test_username], current_time - 1)} + assert PASSPORT_CACHE.get(passport_hash, ("", 0))[1] == current_time - 1 monkeypatch.setattr(passports_module, "PASSPORT_CACHE", PASSPORT_CACHE) res = client.post( @@ -1468,15 +1475,15 @@ def test_passport_cache_expired_in_memory_valid_in_db( data=json.dumps(data), ) assert res.status_code == 200 - # patch_method.stop() # check that database cache still populated assert ( - len([item.passport for item in db_session.query(GA4GHPassportCache).all()]) == 1 + len([item.passport_hash for item in db_session.query(GA4GHPassportCache).all()]) + == 1 ) cached_passport = ( db_session.query(GA4GHPassportCache) - .filter(GA4GHPassportCache.passport == encoded_passport) + .filter(GA4GHPassportCache.passport_hash == passport_hash) .first() ) # greater and NOT == b/c of logic to set internal expiration less than real to allow @@ -1486,11 +1493,11 @@ def test_passport_cache_expired_in_memory_valid_in_db( # check that in-memory cache populated with db expiration # greater and NOT == b/c of logic to set internal expiration less than real to allow # time for expiration job to run - if PASSPORT_CACHE.get(encoded_passport, ("", 0))[1] == 0: + if PASSPORT_CACHE.get(passport_hash, ("", 0))[1] == 0: from fence.resources.ga4gh.passports import PASSPORT_CACHE as import_cache assert PASSPORT_CACHE == None - assert PASSPORT_CACHE.get(encoded_passport, ("", 0))[1] > current_time + assert PASSPORT_CACHE.get(passport_hash, ("", 0))[1] > current_time @responses.activate @@ -1659,14 +1666,16 @@ def test_passport_cache_expired( keys = [keypair.public_key_to_jwk() for keypair in flask.current_app.keypairs] mock_httpx_get.return_value = httpx.Response(200, json={"keys": keys}) + passport_hash = hashlib.sha256(encoded_passport.encode("utf-8")).hexdigest() + # check database cache cached_passports = [ - item.passport for item in db_session.query(GA4GHPassportCache).all() + item.passport_hash for item in db_session.query(GA4GHPassportCache).all() ] - assert encoded_passport not in cached_passports + assert passport_hash not in cached_passports # check in-memory cache - assert not PASSPORT_CACHE.get(encoded_passport) + assert not PASSPORT_CACHE.get(passport_hash) res = client.post( "/ga4gh/drs/v1/objects/" + test_guid + "/access/" + access_id, From 0d3510d50fde1c05de00595d790b5023fc2e1c50 Mon Sep 17 00:00:00 2001 From: John McCann Date: Thu, 10 Feb 2022 22:23:31 -0800 Subject: [PATCH 184/211] chore(fence_sync): rename function to add study --- fence/sync/sync_users.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 3ce827ded..5433a4923 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -617,7 +617,7 @@ def _add_dbgap_project_for_user( # need to add dbgap project to arborist if self.arborist_client: - self._add_dbgap_study_to_arborist(dbgap_project, dbgap_config) + self._determine_arborist_resource(dbgap_project, dbgap_config) if project.name is None: project.name = dbgap_project @@ -1293,7 +1293,7 @@ def _process_dbgap_project( # need to add dbgap project to arborist if self.arborist_client: - self._add_dbgap_study_to_arborist( + self._determine_arborist_resource( element_dict["auth_id"], dbgap_config ) @@ -2034,18 +2034,15 @@ def _grant_arborist_policy(self, username, policy_id, expires=None): ) return True - # TODO rename and update docstring - def _add_dbgap_study_to_arborist(self, dbgap_study, dbgap_config): + def _determine_arborist_resource(self, dbgap_study, dbgap_config): """ - Return the arborist resource path after adding the specified dbgap study - to arborist. + Determine the arborist resource path and add it to + _self._dbgap_study_to_resources Args: dbgap_study (str): study phs identifier dbgap_config (dict): dictionary of config for dbgap server - Returns: - str: arborist resource path for study """ default_namespaces = dbgap_config.get("study_to_resource_namespaces", {}).get( "_default", ["/"] From 7d0289998a93ffb4ae2095f28dbda36483dabd59 Mon Sep 17 00:00:00 2001 From: John McCann Date: Thu, 10 Feb 2022 23:14:49 -0800 Subject: [PATCH 185/211] chore(sync_single_user_visas): remove plcy prefix --- fence/blueprints/data/indexd.py | 2 +- fence/blueprints/login/ras.py | 1 - fence/job/visa_update_cronjob.py | 1 - fence/resources/ga4gh/passports.py | 7 ++----- fence/resources/openid/ras_oauth2.py | 5 +---- fence/sync/sync_users.py | 20 +++++--------------- tests/ras/test_ras.py | 7 ++----- 7 files changed, 11 insertions(+), 32 deletions(-) diff --git a/fence/blueprints/data/indexd.py b/fence/blueprints/data/indexd.py index dee8810dc..aaa6fdd59 100755 --- a/fence/blueprints/data/indexd.py +++ b/fence/blueprints/data/indexd.py @@ -93,7 +93,7 @@ def get_signed_url_for_file( if ga4gh_passports: # users_from_passports = {"username": Fence.User} users_from_passports = sync_gen3_users_authz_from_ga4gh_passports( - ga4gh_passports, authz_policy_prefix="GA4GH.DRS", db_session=db_session + ga4gh_passports, db_session=db_session ) # add the user details to `flask.g.audit_data` first, so they are diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index 88f52cc4c..5f214ae44 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -72,7 +72,6 @@ def post_login(self, user=None, token_result=None, id_from_idp=None): # now sync authz updates users_from_passports = fence.resources.ga4gh.passports.sync_gen3_users_authz_from_ga4gh_passports( [passport], - authz_policy_prefix="POST.LOGIN", pkey_cache=PKEY_CACHE, db_session=current_session, ) diff --git a/fence/job/visa_update_cronjob.py b/fence/job/visa_update_cronjob.py index fc5a0df9e..cac8d9182 100644 --- a/fence/job/visa_update_cronjob.py +++ b/fence/job/visa_update_cronjob.py @@ -158,7 +158,6 @@ async def updater(self, name, updater_queue, db_session): client.update_user_authorization( user, pkey_cache=self.pkey_cache, - authz_policy_prefix="TOKEN.POLLING", db_session=db_session, ) else: diff --git a/fence/resources/ga4gh/passports.py b/fence/resources/ga4gh/passports.py index c285a1c83..face7a0fc 100644 --- a/fence/resources/ga4gh/passports.py +++ b/fence/resources/ga4gh/passports.py @@ -29,7 +29,6 @@ def sync_gen3_users_authz_from_ga4gh_passports( passports, - authz_policy_prefix, pkey_cache=None, db_session=None, ): @@ -134,7 +133,6 @@ def sync_gen3_users_authz_from_ga4gh_passports( gen3_user=gen3_user, ga4gh_visas=ga4gh_visas, expiration=min_visa_expiration, - policy_prefix=authz_policy_prefix, db_session=db_session, ) users_from_current_passport.append(gen3_user) @@ -356,7 +354,7 @@ def get_or_create_gen3_user_from_iss_sub(issuer, subject_id, db_session=None): def _sync_validated_visa_authorization( - gen3_user, ga4gh_visas, expiration, policy_prefix, db_session=None + gen3_user, ga4gh_visas, expiration, db_session=None ): """ Wrapper around UserSyncer.sync_single_user_visas method, which parses @@ -379,7 +377,7 @@ def _sync_validated_visa_authorization( """ db_session = db_session or current_session default_args = fence.scripting.fence_create.get_default_init_syncer_inputs( - authz_provider=policy_prefix + authz_provider="GA4GH" ) syncer = fence.scripting.fence_create.init_syncer(**default_args) @@ -388,7 +386,6 @@ def _sync_validated_visa_authorization( ga4gh_visas, db_session, expires=expiration, - policy_prefix=policy_prefix, ) # after syncing authorization, persist the visas that were parsed successfully. diff --git a/fence/resources/openid/ras_oauth2.py b/fence/resources/openid/ras_oauth2.py index c683cf7e0..fd739576d 100644 --- a/fence/resources/openid/ras_oauth2.py +++ b/fence/resources/openid/ras_oauth2.py @@ -286,9 +286,7 @@ def map_iss_sub_pair_to_user( return iss_sub_pair_to_user.user.username @backoff.on_exception(backoff.expo, Exception, **DEFAULT_BACKOFF_SETTINGS) - def update_user_authorization( - self, user, pkey_cache, authz_policy_prefix, db_session=current_session - ): + def update_user_authorization(self, user, pkey_cache, db_session=current_session): """ Updates user's RAS refresh token and uses the new access token to retrieve new visas from RAS's /userinfo endpoint and update access @@ -310,7 +308,6 @@ def update_user_authorization( users_from_passports = ( fence.resources.ga4gh.passports.sync_gen3_users_authz_from_ga4gh_passports( [passport], - authz_policy_prefix=authz_policy_prefix, pkey_cache=pkey_cache, db_session=db_session, ) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 5433a4923..fa7616cc4 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -47,12 +47,9 @@ from fence.sync.passport_sync.ras_sync import RASVisa -def _format_policy_id(path, privilege, prefix=None): +def _format_policy_id(path, privilege): resource = ".".join(name for name in path.split("/") if name) - parts = [resource, privilege] - if prefix: - parts.insert(0, prefix) - return "-".join(parts) + return "{}-{}".format(resource, privilege) def download_dir(sftp, remote_dir, local_dir): @@ -1728,7 +1725,6 @@ def _update_authz_in_arborist( user_yaml=None, single_user_sync=False, expires=None, - policy_prefix=None, ): """ Assign users policies in arborist from the information in @@ -1743,7 +1739,6 @@ def _update_authz_in_arborist( user_yaml (UserYAML) optional, if there are policies for users in a user.yaml single_user_sync (bool) whether authz update is for a single user expires (int) time at which authz info in Arborist should expire - policy_prefix (str) prefix to prepend policy names with Return: bool: success @@ -1863,9 +1858,8 @@ def _update_authz_in_arborist( for roles, resources in roles_to_resources.items(): for role in roles: for resource in resources: - policy_id = _format_policy_id( - resource, role, prefix=policy_prefix - ) + # TODO comment about how policy id is formatted + policy_id = _format_policy_id(resource, role) if policy_id not in self._created_policies: try: self.arborist_client.update_policy( @@ -2151,9 +2145,7 @@ def parse_user_visas(self, db_session): return (user_projects, user_info) - def sync_single_user_visas( - self, user, ga4gh_visas, sess=None, expires=None, policy_prefix=None - ): + def sync_single_user_visas(self, user, ga4gh_visas, sess=None, expires=None): """ Sync a single user's visas during login or DRS/data access @@ -2168,7 +2160,6 @@ def sync_single_user_visas( sess (sqlalchemy.orm.session.Session): database session expires (int): time at which synced Arborist policies and inclusion in any GBAG are set to expire - policy_prefix (str): prefix to prepend policy names with Return: list of successfully parsed visas @@ -2258,7 +2249,6 @@ def sync_single_user_visas( user_yaml=user_yaml, single_user_sync=True, expires=expires, - policy_prefix=policy_prefix, ) if success: self.logger.info( diff --git a/tests/ras/test_ras.py b/tests/ras/test_ras.py index d0978bd4d..ad3c73d5a 100644 --- a/tests/ras/test_ras.py +++ b/tests/ras/test_ras.py @@ -166,7 +166,6 @@ def return_false(): } ras_client.update_user_authorization( test_user, - authz_policy_prefix="TEST", pkey_cache=pkey_cache, db_session=db_session, ) @@ -255,7 +254,6 @@ def test_update_visa_empty_passport_returned( } ras_client.update_user_authorization( test_user, - authz_policy_prefix="TEST", pkey_cache=pkey_cache, db_session=db_session, ) @@ -348,7 +346,7 @@ def test_update_visa_empty_visa_returned( ) ras_client.update_user_authorization( - test_user, authz_policy_prefix="TEST", pkey_cache={}, db_session=db_session + test_user, pkey_cache={}, db_session=db_session ) # at this point we expect the existing visa to stay around (since it hasn't expired) @@ -474,7 +472,6 @@ def test_update_visa_token_with_invalid_visa( ras_client.update_user_authorization( test_user, - authz_policy_prefix="TEST", pkey_cache=pkey_cache, db_session=db_session, ) @@ -590,7 +587,7 @@ def return_false(): # Pass in an empty pkey cache so that the client will have to hit the jwks endpoint. ras_client.update_user_authorization( - test_user, authz_policy_prefix="TEST", pkey_cache={}, db_session=db_session + test_user, pkey_cache={}, db_session=db_session ) # restore public keys and context From 37dcb04d29a7d7c6712425bb94e581d3bdb4d4ee Mon Sep 17 00:00:00 2001 From: John McCann Date: Fri, 11 Feb 2022 06:39:52 -0800 Subject: [PATCH 186/211] chore(sync_users.py): _determine_unique_policies --- fence/sync/sync_users.py | 78 ++++++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index fa7616cc4..5a5b91e7c 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -10,9 +10,6 @@ import collections import hashlib -# TODO take out -import time - from contextlib import contextmanager from collections import defaultdict from csv import DictReader @@ -1824,22 +1821,21 @@ def _update_authz_in_arborist( # TODO make this smarter - it should do a diff, not revoke all and add self.arborist_client.revoke_all_policies_for_user(username) - # TODO comment - roles_to_resources = collections.defaultdict(list) - for study, roles in user_project_info.items(): - ordered_roles = tuple(sorted(roles)) - study_authz_paths = self._dbgap_study_to_resources.get(study, [study]) - if study in project_to_authz_mapping: - study_authz_paths = [project_to_authz_mapping[study]] - roles_to_resources[ordered_roles].extend(study_authz_paths) + # as of 2/11/2022, for single_user_sync, as RAS visa parsing has + # previously mapped each project to the same set of privileges + # (i.e.{'read', 'read-storage'}), unique_policies will just be a + # single policy with ('read', 'read-storage') being the single + # key + unique_policies = self._determine_unique_policies( + user_project_info, project_to_authz_mapping + ) - for roles in roles_to_resources.keys(): + for roles in unique_policies.keys(): for role in roles: self._create_arborist_role(role) if single_user_sync: - for ordered_roles, resources in roles_to_resources.items(): - ordered_resources = tuple(sorted(resources)) + for ordered_roles, ordered_resources in unique_policies.items(): policy_hash = self._hash_policy_contents( ordered_roles, ordered_resources ) @@ -1855,7 +1851,7 @@ def _update_authz_in_arborist( ): return False else: - for roles, resources in roles_to_resources.items(): + for roles, resources in unique_policies.items(): for role in roles: for resource in resources: # TODO comment about how policy id is formatted @@ -1916,6 +1912,46 @@ def _update_authz_in_arborist( return True + def _determine_unique_policies(self, user_project_info, project_to_authz_mapping): + """ + Determine and return a dictionary of unique policies. + + Args (examples): + user_project_info (dict): + { + 'phs000002.c1': { 'read-storage', 'read' }, + 'phs000001.c1': { 'read', 'read-storage' }, + 'phs000004.c1': { 'write', 'read' }, + 'phs000003.c1': { 'read', 'write' }, + 'phs000006.c1': { 'write-storage', 'write', 'read-storage', 'read' } + 'phs000005.c1': { 'read', 'read-storage', 'write', 'write-storage' }, + } + project_to_authz_mapping (dict): + { + 'phs000001.c1': '/programs/DEV/projects/phs000001.c1' + } + + Return (for examples): + dict: + { + ('read', 'read-storage'): ('phs000001.c1', 'phs000002.c1'), + ('read', 'write'): ('phs000003.c1', 'phs000004.c1'), + ('read', 'read-storage', 'write', 'write-storage'): ('phs000005.c1', 'phs000006.c1'), + } + """ + roles_to_resources = collections.defaultdict(list) + for study, roles in user_project_info.items(): + ordered_roles = tuple(sorted(roles)) + study_authz_paths = self._dbgap_study_to_resources.get(study, [study]) + if study in project_to_authz_mapping: + study_authz_paths = [project_to_authz_mapping[study]] + roles_to_resources[ordered_roles].extend(study_authz_paths) + + policies = {} + for ordered_roles, unordered_resources in roles_to_resources.items(): + policies[ordered_roles] = tuple(sorted(unordered_resources)) + return policies + # TODO docstring def _create_arborist_role(self, role): if role in self._created_roles: @@ -1942,13 +1978,9 @@ def _create_arborist_resources(self, resources): root = "/" for request_body in utils.combine_provided_and_dbgap_resources({}, resources): try: - # TODO take out timing - start = time.time() response_json = self.arborist_client.update_resource( root, request_body, merge=True ) - end = time.time() - self.logger.info("update_resource took {} seconds".format(end - start)) except ArboristError as e: self.logger.error( "could not update resource `{}` with request body `{}` in Arborist. error: {}".format( @@ -1967,8 +1999,6 @@ def _create_arborist_policy( self, policy_id, roles, resources, skip_if_exists=False ): try: - # TODO take out timing - start = time.time() response_json = self.arborist_client.create_policy( { "id": policy_id, @@ -1977,8 +2007,6 @@ def _create_arborist_policy( }, skip_if_exists=skip_if_exists, ) - end = time.time() - self.logger.info("create_policy took {} seconds".format(end - start)) except ArboristError as e: self.logger.error( "could not create policy `{}` in Arborist: {}".format(policy_id, e) @@ -2006,15 +2034,11 @@ def escape(s): def _grant_arborist_policy(self, username, policy_id, expires=None): try: - # TODO take out timing - start = time.time() response_json = self.arborist_client.grant_user_policy( username, policy_id, expires_at=expires, ) - end = time.time() - self.logger.info("grant_user_policy took {} seconds".format(end - start)) except ArboristError as e: self.logger.error( "could not grant policy `{}` to user `{}`: {}".format( From 04e7e42e9ad9767fa5d79e06175c4f83c425690b Mon Sep 17 00:00:00 2001 From: John McCann Date: Fri, 11 Feb 2022 09:13:42 -0800 Subject: [PATCH 187/211] docs(sync_users.py): comment gen3authz wrappers --- fence/sync/sync_users.py | 116 ++++++++++++++++++++++++++++++++++----- 1 file changed, 103 insertions(+), 13 deletions(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 5a5b91e7c..cae8cdfbb 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -1854,7 +1854,11 @@ def _update_authz_in_arborist( for roles, resources in unique_policies.items(): for role in roles: for resource in resources: - # TODO comment about how policy id is formatted + # grant a policy to this user which is a single + # role on a single resource + + # format project '/x/y/z' -> 'x.y.z' + # so the policy id will be something like 'x.y.z-create' policy_id = _format_policy_id(resource, role) if policy_id not in self._created_policies: try: @@ -1952,8 +1956,17 @@ def _determine_unique_policies(self, user_project_info, project_to_authz_mapping policies[ordered_roles] = tuple(sorted(unordered_resources)) return policies - # TODO docstring def _create_arborist_role(self, role): + """ + Wrapper around gen3authz's create_role with additional logging + + Args: + role (str): what the Arborist identity should be of the created role + + Return: + bool: True if the role was created successfully or it already + exists. False otherwise + """ if role in self._created_roles: return True try: @@ -1973,18 +1986,63 @@ def _create_arborist_role(self, role): self.logger.info("created role `{}` in Arborist".format(role)) return True - # TODO docstring def _create_arborist_resources(self, resources): - root = "/" + """ + Create resources in Arborist + + Args: + resources (list): a list of full Arborist resource paths to create + [ + "/programs/DEV/projects/phs000001.c1", + "/programs/DEV/projects/phs000002.c1", + "/programs/DEV/projects/phs000003.c1" + ] + + Return: + bool: True if the resources were successfully created, False otherwise + + + As of 2/11/2022, for resources above, + utils.combine_provided_and_dbgap_resources({}, resources) returns: + [ + { 'name': 'programs', 'subresources': [ + { 'name': 'DEV', 'subresources': [ + { 'name': 'projects', 'subresources': [ + { 'name': 'phs000001.c1', 'subresources': []}, + { 'name': 'phs000002.c1', 'subresources': []}, + { 'name': 'phs000003.c1', 'subresources': []} + ]} + ]} + ]} + ] + Because this list has a single object, only a single network request gets + sent to Arborist. + + However, for resources = ["/phs000001.c1", "/phs000002.c1", "/phs000003.c1"], + utils.combine_provided_and_dbgap_resources({}, resources) returns: + [ + {'name': 'phs000001.c1', 'subresources': []}, + {'name': 'phs000002.c1', 'subresources': []}, + {'name': 'phs000003.c1', 'subresources': []} + ] + Because this list has 3 objects, 3 network requests get sent to Arborist. + + As a practical matter, for sync_single_user_visas, studies + should be nested under the `/programs` resource as in the former + example (i.e. only one network request gets made). + + TODO for the sake of simplicity, it would be nice if only one network + request was made no matter the input. + """ for request_body in utils.combine_provided_and_dbgap_resources({}, resources): try: response_json = self.arborist_client.update_resource( - root, request_body, merge=True + "/", request_body, merge=True ) except ArboristError as e: self.logger.error( - "could not update resource `{}` with request body `{}` in Arborist. error: {}".format( - root, request_body, e + "could not create Arborist resources using request body `{}`. error: {}".format( + request_body, e ) ) return False @@ -1994,10 +2052,22 @@ def _create_arborist_resources(self, resources): ) return True - # TODO docstring def _create_arborist_policy( self, policy_id, roles, resources, skip_if_exists=False ): + """ + Wrapper around gen3authz's create_policy with additional logging + + Args: + policy_id (str): what the Arborist identity should be of the created policy + roles (iterable): what roles the create policy should have + resources (iterable): what resources the created policy should have + skip_if_exists (bool): if True, this function will not treat an already + existent policy as an error + + Return: + bool: True if policy creation was successful. False otherwise + """ try: response_json = self.arborist_client.create_policy( { @@ -2014,15 +2084,23 @@ def _create_arborist_policy( return False if response_json is None: - self.logger.debug( - "policy `{}` already exists in Arborist".format(policy_id) - ) + self.logger.info("policy `{}` already exists in Arborist".format(policy_id)) else: - self.logger.debug("created policy `{}` in Arborist".format(policy_id)) + self.logger.info("created policy `{}` in Arborist".format(policy_id)) return True - # TODO docstring def _hash_policy_contents(self, roles, resources): + """ + Generate a sha256 hexdigest representing roles and resources. + + Args: + roles (iterable): policy roles + resources (iterable): policy resources + + Return: + str: SHA256 hex digest + """ + def escape(s): return s.replace(",", "\,") @@ -2033,6 +2111,18 @@ def escape(s): return policy_hash def _grant_arborist_policy(self, username, policy_id, expires=None): + """ + Wrapper around gen3authz's grant_user_policy with additional logging + + Args: + username (str): username of user in Arborist who policy should be + granted to + policy_id (str): Arborist policy id + expires (int): POSIX timestamp for when policy should expire + + Return: + bool: True if granting of policy was successful, False otherwise + """ try: response_json = self.arborist_client.grant_user_policy( username, From 9aebb6b0d2b97bb4bfe8f8abc74ceb781e01400e Mon Sep 17 00:00:00 2001 From: John McCann Date: Fri, 11 Feb 2022 10:17:58 -0800 Subject: [PATCH 188/211] chore(dependencies): update local gen3authz --- gen3authz-1.5.0.tar.gz | Bin 13942 -> 13879 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/gen3authz-1.5.0.tar.gz b/gen3authz-1.5.0.tar.gz index 58505c1f4d905f2549a1cd7b678515a3f59f5ebd..f36308c2d44b09a92c347f1ed06c8378ac95cc64 100644 GIT binary patch delta 13596 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 delta 13710 zcmV;9HF3(fZ1!xBV}CoZ-JSKez3uN_`L{m_e<+eKKFO!UpSr)D&hDQ1j?cT@z5Sgp zoZBxR;4@1zKY`{yb|-%$pPk3fBnu|uxO=dBxc}tw!Oo8Nq|-Uv-Q8(?vHASRf5sx( z^=H}Dzqh*HzSr6Q`Sj`Y7iZ5u`usoG+pC%XI|tp}{lfg;*?&LmeBpF9^Z%27oMoSr zsXw?9&dM5$;jDEViRq(K}xJ6@;l{I4I){ABLzbUJ&rm9DaEdbGWLb93YQ)QlG= zH4aen|Ms2F5(2Pbfx1plV?B%G=m z*_EF;(C9evp<*h_#)YD?#0d(WiEOf^AdgCO~ciD9r#cq2Wodf+<281tXy5R3rlg-Q4f|zZNwY z19_P63eYSA;RX1@7{D+kB2^FpWM4!;gh2p9nSX0S!xu)y|-`0Zc)dUXvoHO%pK^NdndB z?T9G;4x5<7!vMPG)2vbh97Ka~Mx=mA=tObmgux`h)?pmec$D2Bd!^JM49XCwujU>B zl7C>hKwHiKQ81n*^d5#K6qfjS+5ac7LK$^Gn)5Rlj#)_aW0b@bcsaQ8Bfwfs5a5a^ z#ge{aD0&jgpCiY2m^=ioo$sK8QRpU2n`wX(DkkJ)H$lY<=mR{?b=a0((9>&{-YLT2 zxjPZVz<09w)b92#aq_Mt6mH-lVN7xeIe*Pefgn=d&=Y`3C%ZKPzr*pb{UG%Fp_>1e zTxz2A>a6bCP&sed@c zHjw*)dstRsQ#4#S7jCYC!IdQpV6ZaK0WdQYaUIZ5A{PT`royN_ou7@>t2KQEvLTog(=p^^lw8*b#WCH4-uL13f>* z)K7?0kS^4XiAY2^hj|daBf|B8TYr#GB7Y)UYS04^M9Ih>kdkOyI#Lt0gcMRo#3M86 zPf?Uh9bY-(#W|p-tks?-pqy|@mufr^SZ+*+hlkPTaR7uniTI%G5madtU%CM}I(ydgEm^ zZST0cPp*W;`d*7=s3by|fJqF(xQ(OG_e0{Nn*{4cWPHpbNqq-rflYK_2!(XYQZreI z{%L!mD$=scZUg=|IDteALaZEuNeR#_m8DIgbe?8nlG-91bnZ-`3K@`&lqGohQS-62 zqHQgk=xs}kSJG)Pn8mXc zChx>g-l6DAjCG_;E|CV~h-5#oB90LewvrQ3Sh+8N-o9hcRnIL?;evgwyRBy6Qj^Cf zAj-!{p;6}wkm(EHComZWNpe8iZpTc*RQziO{1alcgBa+|>KYARYk&H&AlmVqAJMGB zW}oWrE6d6`o3V10yjtnZSkugw$^x{cV-d(f5dt`q6-Raf7>CeHu<)iL1Ij4&1JM@_ zZvwP6qBz>3kx!w=_;U-ao@9)UQ#|*>Y`!&01pErXu)B^2D9B6NT>5|5pz^k$2F$T3 z@^MMh8F4GlJQhrhXF8>>MQ^eUwKd0;T)xiQi0H;0DK$kQx!hA6O*Xs;6qj3aUfw z;Yw`_=(CZRM@}|zgF~Too#&j6aQw9|l!Bx}p4Nks1!G3RAN%;4gq(ETn?_-2+d7HU zbc+av-5A6(w11!YH;laRg#JxB3o`6#D8{S=fMN=HV-pml;vxY@I)M>SrK@e=49wun zRkzf5Ovq9NFl>tDIZ>HR${Ce2ML8N~j!BiLtSZ(HI7M;v6tDQHGT4UjP%)pTEdE5`Q!sp|KeVIpQOi`iCDiY*>%*=epQQmIs;A{Bff|p`x`K<0dx_CmTmfK z?+`zwj{2k@ik`&Rq()VneXC6l+!#|D{V4R`IjEB$3xWF(M93%XJ*HOkDB?6rBTRJN z$3zUlaDVbG53H6Gk--FOE4$fp;mJDyjEbGJ8rBUOhG^pqGup8&xx0;GaVXGkYFowx zv1n$do9r8hAS&=FiqyR2#?n*4AmoNI8ML5fu$xFuo^eTL6qmB#iyh4qZP-GgrN>`7 zemKd*2AbSfJNE3dL_k2rOes;8#>)bwTu&dy&>xbIX_!t_ zk`;xLk8xfLj1+1l8Mq_Vh4w0RwZK$UxRS2KYycw^ev0mt9i^95%xsg82^A+6<~b&_ z=XE{+cBOTY z4}1vYyrPAYM@P;rl34RxY+kDke>lYN3Hm!WM_N!6>XJgM%!D>mKZSv?`$YZ}hBt;o z5e;V(Wwz!xMhO=7|I~06Wf~D!1u=jOm6M60Rp2wR9gxh5{Ktf;Teqkrk?|_XY^7j5 z8vupCmo*d!MD~BfMj1nL0a~p&E4RXeH=(_XB_jr7trf+i3T)bD+Kk9qoY%M_c63fp zC4z@dThTm&Kxwrqr-ilEjj?1hrIC+vVHxeI6g+|4fb5gABUb|0S2b2(>N#&B(4r{~ znz)6A1_8Ri1kqX`(eaIW(TcQ!SymXUHgshyeQdE9$YFm|uWv`6*Ky6HnMG(?48tJU zbRDYIN3Q=O&ajeRy&?V5kJ%~1X*(wO4%G;umd-#Oq+%#|Aqb~|H6*ejwk|k83RKiC z(HML>V*I(BA>@&XTQRUiJPBS+kVK6Agx8gdzK2{3H~>+o43HGXqGd-9W0IU1o7~ol z5E5Q4sIhJ^%T68>s$b>-5DBuTNk6 z`20`LU!1p{KRti_^e4dbjcE!t5q4LJ>mVh`p2mYfyU8rnuv@Ab+H^I$k|LSbFBv2L@%-kc9y? zEF%ImwEn~&=VSO-TWtw2n*-9Rz_oR2^%*7<7`D8WfCdc5ba6pW0#ykEP$~W9#CKv6;norp5M#223U?>& zhcXI9cFQ)39khITsRjCj^F{hsL7H3c0SDu!_y#>8b|p2zh@KW;(?8nL5{1@kh_<=q zvIqrnA#KxGP)(EaPEC z=ug~FCM1cJajmInrt>UG%xam89D_^-uN<9K4x+ZpapV468XKlVbEJTwsW#?sEN-;S zMvV(I@beeXP_I^PgQNE+uUQ41l;gUMK#*SlOF{l9HtH^ z2$<|F^!vCH)QZOI+^;w>@=QN+i(IdTIz?!29_Ll487EazoXRra0GFY~ z%5E2#nSWEIjMdU=UzjZ`v_h&dNwJ~}$Wh=LWw497BERw}H@_g7z<7aPkC&8F>#jOH zV}cxhGPuGOYi0(snD}-Mp9|+L;SG2eR#`8oxMY%{jbX|KoiY=YMv~=YP7J^FI%A{^wAi|JmC;=pGy#?s+?py9ZCYo7ww& zJb(W)or5`q>%d+XPr`>d|I_JqV6x}u|K8rg{^tD8Lww$5aU6QnSY*lkqCpnlvEyp4 za2slyDm*0)aT~T33r~N9vb3@D-(c?Iy1v_xdtl-1TleYtvoq(-(-Qw?Wk2~HD6l#Cd<+2pVz0IzD0`cClI{uFxZ_jNz zrp<45y+hae&`1M<^^dOCJ*dL?daH!BNHeOk?}4Co8;F1i2nBH`}E)gk9W}{Ikeu~Gk0^rg`O!~7R9B%PG(2It&V0la!Kcql+2guJQ zkyhx)02GP;1pQd}>rVrBqYXCp-{$=H@8$e=ckgg_e>3HOhxT8Z47T;N?OqQLR`hz_ zbpGk=zfKn>dw%{O>>liF?7xTj@PC$TJTB5`;PT5n^`zm`!*J`{&V_S)?Cc&jcwZ!L zx9!o9K(G^<@Vnh=h%gn^@9{hvKZ9bMxwg^&8~y(q(f_mh5{1nFHB!P@eF2j>4K{(rxb|C{mu zwdDVky}c(J`M;6>1^G`4P(6OSx(-+-|Mzwe3-W(wuXDJO{}1tz@jqG?Z8YdKpNC;P znTx^s9=@N*;eSfw-e@#sL@>Dfh;dcL-bN6oN)5OAUX z?{;_h4-X3ZzjLs^(f<$fab358nwyU*$m3utSz=&kWKc|?kLJ?h8GmFG&v}KZC+J*S zg1j)Ck);R=Le|xj!wQC7&(Kp0usHyO5!MgPZ4oR0jC>3mQSvJEuA#KbafvnqfG`uOcE3B#c8P5s0e zO8=V0na~YM<3;_n_J5W&auydiHVo1*9#a-2`F1R_9zGI@GRizfe@||6lO+{Vp5xQD z{`n@7whTR2Fl@{21cy%AVDh!6-q{Pe`sFMOLf!1dpZ7%%sSoxbT3kJm4*aR;MdBt5 zB9S(+!_wZ8wiZ6|gM!7xWfu%uW?7YEOJTY@B1UW@R>acT8CqF=H|xde+n;?oqspM@VPk5D#wg zyEKzsK-`^Rj=8ab?=bb6c#_&haPS+(<$uEm=7;a_q?$k@0`d4EzCcapbYzbg^4@HU%Ccz{d%M-WXf+lUll`gcj^|UF?WQVX<9%yU21buG5`VkKqxSTu zUri-%8c&->we(=97LF0vB!1|&>qe&32$hGbmQ`-|Brp8gPns@|xeX4LIz24`RHJGD zdr-KEtl!Fw3>5^Shi?jcLBb$2rCA|$B-|9>QrT896v8~E(M4?O6m*pJ&HP-4vF zI?q7)&X_)zJ5rdF{1zI2n!hD{buG<9uJBF}I`DoS#0ejE z&mEnhGnztYNb~acOD>T1BF;`#VKJ;=+8nzYy~I}KF*1;OsPuCW4a6Rpx5utLD68IV zNvnU`wQ3>eJ(*0V2vWz~I{d6Se;5zwi2MTdFh+0F&6C+J3J!&nl@~&PWy{N}Q`>bg zk~mz{GQ2Hy7#zl=z6!oA){t|cwvIZ2l&>{T;@Pym61{dSn8k26GFt9Ip|oUoC?>xZ z-irvuBqz=%Q_zz(Wr35?a;@kf}fMu`#-yn`5Xhe^-a)3Cmu zBi;O2h$((@Epkz3a3w&e&8CR)J|UW7dKS(_PjL`XI=8`OHnFpRpmsZ$9XpPOP`EEf zcz@40poIAViSt2#Za`1l;5du)T__e2^Bynf;l&ZkKZ6mjve(LPU9I=2KIi~DO-Yj$ zk{8EN`(h@W2DlLH!jaom_8&#bU|-e*52$GYK~TCdqVh1HrJqWO@5KBjPKKZm#*}1~ zV_BD%RSQX%mn9H?l8vje#Uu!r*+FP-$1HmjLCKJ)6*WyP$o9;#NUkk(vQS;?4R{C? zdMF#{MgWw|vI>I;7&qSrJu5*kNjSSo{-xWll!~oJ(dY;BQjTfUm88;j^{Sd>2IjFoydd}o}PS1j>sTzzH%`kY2>BUk?EY+04x*x}3>!>z7 zqL|0B=k3pzKpdEyAVrnHOyy-nf|5H@s_)tKNJ`#)l_-oeICeX=KuJnck{Ap{`?x*$rPkLOsWP{Xv#-s=EBAGOO0qW< z;eh3$9*@s4|G`;DL)}^uACzNM9RY0C@X&qcJlvOD9q|GSh?9*RjXA^`Pty=zKtP_)tVcMP{^=cy;D%x2-kE= zoXmk=YegvUj-bVYu3ReHT*h?*qNKWtlz z3dQDW$*-wvx5`UW#}&QEnLj$Jtd?PRipwy6RZB3smX58r^N`BnF?*$G8F`j(p0X|5 zQ+YR|&JkO=SPV{?x#W@VFqBC{!TYfso!5kJo&spH9BJqaAy*<;v1U*=JdH|qU$fJbq1!Wy&d$?V%BW zX|Iy;T2rf0zg{KTC*PHo_;xMCaWzVE-PgFd$IXhNTJ2lBu2(fk7J5sTY|Cr4<*|34pm&L512+yB#j1MnisYCfN-9K+Ko%LP??b5axJ!Yt0FeJrU z!&K!s5rCz0`V}VkwI2pUD!`ls9P~~^g-OgZwg!TFO=+WKV(Nr0cf@!ZCA(FBknsjN z&lzEwa874@J8V@1w7>_%YcIE}+dGvhO@+ChlG#1iILK#8vK~9b81tXM6BM3dI$mD3 z?fl107TrPgu=Z>N?Q3LUJzu(4$u|$ynZC*RWMXHz$;WzC9u?cm5)~|vlPg}anK^<7cbAB zAL-_0oIvm;ugK5D$mTAZ6|4&-2RT=6X@8GB$BJPUJwNSDQ>C2*iaCydowxeOMGZ_n z`4>P`(*@wkuNNh$o2%d|TL47x+Y&|JyLwgUNMqXu4NcRFwy4O-j$2felybRgc*I{m zl%qy``-Wt;ATpGwEGgF=pq%VfuGEEDUHp*SQ6)K{ps|1oZ%Oe`+>dn-lyWua7;Hrd zP=IrmKp?(?`cTqB;O^>wOAg8X)EDR`bX8R395aBcDTL8CG0d5KWyRK7stgxtKcZZf z#Zp2(jU~I%q^*uwf0}n@izRl?02F5?^g^=< z2wH|h`*C2Yjp*_>2WUc@-;kNqivv*xN6 zj24*Zw?1z1a8j_RCVpO)q6L1G501Vsdsx!uc5x19vb*J7imsK^6U%#^SKg6{cdKhO zE7eFf7G-P<9JgIxd=;En85EyfH1hBD&ojJRgEpw%$blt)>2P`JT1@D&ktoCJIcB1{ z+&6{+vnO8S12DEN7~46DUtZd5XpL|)*GY9(0(xfgl<$LR)9o;H&vD-}HrS7oQpNTG z#blbzz1E_&V++kX+l}s4q4#8T`r7PlDYK1%7|PJ~W!0uN_$y!Y0&jqRDTQpLwU@+r zDUG-mp`2LbXKOl7jj||(s@aj7dTF{ zX2A@)gHdmF<*1s)#2*ZzNTF1&anC{Bl)&+xaD*@y*}YwHQys;2Vf451V*E%gz2mlB zCKHIvrm0n#b&kvOt0J*GbA*t zj;l6o{f`VNThlXV4dywsIHd_H{Q=AJ4^wv~`;=8K8K1dHwjf3g*42BryiVAw)FG?mEKDRf~oZGRu zwYY9mv!`*D8A0LWMH8aXk!5|rSdc~{EpGgOw&mw*c}VIiud`@(8SO{YYQ!x5uB0hQ zjN+}8;FUz?D!@viYa>*@Oi~tJt@|zxSd%BrnZwJx*Vf04<(fE6o>gmSMWcKSs$|k1 zkpR%WK!DzGHkqb&_y~idQFgpz^{9&LWb|Z(70!~E1^Q%t*2_6k6?|7^UTV9NzmK$k zGcCF86EdwW0BW?Wtyb^4QcGHJ@93jxkn)7MOQ7ZxuUha448vFX69&%8{yQ#jBx7MM zkk&}>6$Vgm!=zB?#p?Ews-{jKxl=bc-h|@PN>nOe(UdCf&IJ_Zmh(Tj`0sza-aq3Y zYMLsorDoe~1s!!~yM;&iKh198UOk+D-NO51mdM|aBB~>(s zX`aPx=}_B4hml|KhA1|}9sNAKlu&4xDB_CY>^RhZ37&x}_$2iq zYP=Qqa|%!Au6V4ER0G;eqSPFJ+6z~WQq!WGixpN3O;?n%P`^uPno%%7){ILpC~cNd z7jAmC)>oH6q|$ojwLUCPq3;EV;kD2i$li$7gqL9VPRy~fYfLGz0JjhK4bZO`5D)kk z+*}{Hu)TeO;?#Ob7YD`l%vAyALO|JejQl>H08&i;xkhUNwbmMry|rL}txj?(D^m-# zj~5pF^szS-KkJz;x}R@R99(?XAvo-vZN}zNzo!AqK`RvV#eh}y-t0Wrv{v;a3u-eM zR_o84rL4{vD=S&nY^~=uRWkJbT&6p+8+H2g^_Z&I45)qQaHw*PN<-f|TFup2!Uk)G zY>b3bTR=!swY8wagbh`H`I#1MfB6>M>8$8u6)eEDw(5LDPFNwD$aZtlds-e)tO?}# z%Bzz3<$%_w&%8Uup{;}o+IQCYeS{d^JwgC^qU8es5^S@M$E&)95L&Hu7Eh{%jU#jE zyI|T2M(h=*#hhnlLEpYdd054^Bx#{UaDzI_#oSTup%)^)9z6X-GTBc@GQ6Ep#;K z<82#}9{qC1-Go+ZeQsH1%kK|r{6X1kYD-5&QK(Wc{)Jg%ve>3vFpgr}7hec_wCWH1 zeQ5}`>)m>{6>PAADY3#C!n~VNsf1L1MO#Kyr{}bl)P8w?2ili(glrH`z$ys(K^SDx zn9_aSDfM;v3bq~`%EzxksmsuDKXnv4uil(HDv8y5{(JiDgCnBhly|Vzr1J!x4(X5! z?%f7(794lUF}+F+;#eG5y9K&DMC%w0^+)sb$MS2;OPW3v)B9-g25k40rD1hcm}_zn z8IQdcR8!7>++B}e_c8aRq`JRxF~zhx_pmHJPhZ;VRoR-jk1s71jFmL5Y*{bb8daew z?=02UMBU!m^=VPh@9Fx8xEJyV&~3>ppUTuVYIig9sW>atC|~53+E?92dP)52v8t$Mb*Lqtv;7#D|wOEicuH-OF0DftJnHf|~AJ z)iGO>lUmu@L`j8Gs*eXN3?x9Jh1*))B{XRoXynZp-7ki{W-y30zjg1j*JriKUy;S4 zjcbcN^-HGNz(-=@2jQaA!*4j0>6EAJ*1H}2z7m_}CC1<0hs^dDWeujs$ zm<9w1t*H_K?uk{KA5>Lc=#CZh_Z%5xD=&%aSBA`7I?~50R9X!>g$>qL{9o$7zYw$9WQd#IEH=6j1_%wXdg>Mbzf$ zsFeMbM#>pXIR@dVaRZkoplpnk6o@&mON(UNcj$W?3G%l-CePiMS;654k zQLML$O&|ls?vd%>heD87b)#0VTV;;*oll`*D9H~jB=ytkPE{|jZ8n5U$SFO7F$3s)KH6(a@8$il>;q=xmmg? z;a-ltSVDYp7O-V9BoWu~JF%J&sflY;&kx&-E=-a3g0=2tyTu=xshB=y-!k8#W;500 zVy=2%R$_duRlowq>#@=*8`W8V3sG=uB`!)=)kU@*aaPhGh2YNGQnfsS>&e*#f-^s! zi>8l#OU4>H#yUZ?U?2HWg|=O)pw=L#fx1AyRH-9%E{Q9*erpN`MZV{l@Te|k9VvFzM z?RNSuosWLzoAG(j1K-@_M>*}Su&+Dn&F@z`=gnnTT#2H>a$+;HQnqaw`B!{Yv=L;S zFRi{!j$ai|)M}ortYaPFdRtCLsFFX);3YL6Q}F>&%vmpUy6r=wV*iHPrz;PiQQsZw z$ViIY53Gv8$bNfN$lk(#8UE}OCl_>up8mc1dXt_Ob;Xfs}LgAEI4-E)r88@J!;&)`)pDB%?Pc3I?3}<ua`H#yUV=k~z z+DQt^#Lw`q2VDezgqM!8xUI??0!=`tI;Op5IQ%yei@@}SW}QS(1nesBE4n=$|f~D(lRQg(7k7Maf??`hY~TMuj~2-v&n&Q^#XZ zhV@-p%wqL^?&^_o2A7M9+ca6Wx>91a%8=F)A-p)g`Ocv$J(EclU&lK*2m)!c`x{STw>w+`Lc=Znbi-CcI%_0Zd`*? zzLb~8URx(kxrREf$f?{p!Z3zro!nNii#qK~vn zsRRG_)?-X>$reL;#oAslW;Ptsh!FH?z3cnB(lNPZ+?=fE6%O>Tq?5S-7z`< zXGxku4Kv^t-A6%0@!)Xo6c=sVYSj=I;b^b87+U8D#i#_y)!Du8B z%=p%p{ZMNpb zC^;H`cVg!CnY#YqX=~vr+8UO>3)@#+ek8L^Vr)WFi_ZtxYdV!tZHuRRtOA-vw#&^6 zW);epW+fJ=*b<}n!vw`+DJ!`_WJfS0c_y#5&?Wj53|RJwYFH55i+eF3H_`rWF=BmA zD6=sEkDo8JSXK+3R~sd(CWOknhijP^i>~W`ZLJV~zMHvb?nv8feDSM)ylrp$yI210 zPasJ|@@d-W@Tcx?r?b0fzT@+5cW-~^3+MJB2mt!iPoVja-O1m`XXmj)yD*Qt2fK&+ zPaYra?08Q)I|olb3+~)}KJ{lTqFuaI@ZVcqZ{O=|r^#SjKig(`wypG?{OwKWYfp`T zgT1}F`QP2yKgiGjoi6ad@>LO^o3zIm1~vzOKWY*(9{-R=hLx$V=hKbIy%ipg3P(JDxW5m6ltXE&7QCfwwFA)FAPsZTAe)ZM68~j#*`fMta zX3JC6iWM}m@-%Fg$tq#1ojE5Cx(i?v#nDoZLFRBoG!7!VURduz(*PTm&o&5u(Tg`@ zx6w!&`)?!vAHx1Se6qK(|2Fnt(f*?VL;BI}zr(Wqx4*If9^}LE)nSnG&?-~v1GO1+Nj5ejyD4J1!@+FuJ@>~8j2VbaW{)Jvw=S7#J9JDsZ4TnNK#qS?V z-G38Z2|vuP=1hko-E1Nm+B`Qj!2lhRBn`$D+e=Hu8I8|83;|C$RsX>_6Ur+03qu{m1qn z23giM`c}99cDpdy^Yj0((>>VOe-H7I_MYD#s9;?h+#1^kxj=@2;JJrJT5lpUpI$ei zmUr^~(?7w3Rxu{j$9uU3Duf7T&5)yveR1Vq2VgbeB_NRu2+5VS0JF|d)HNZ6n9z6F znlHif^=I1fktR&V_1dR@(J&a`W~G}efmh~PQ7Fr#@~{0M#4SJGO3h*YUOxKv&zr2R zAfn}k<;`WdDo5DyA#{b*+f4>d?Z@05^@OU$0H~t^{e|HP} zuj>DmzjqJ!I{O>{ZzKOVE~2LFbYPaAZ|1o==7QG{LP8<@xG(-@YAeB;uBnEtlyP3 zh?5xC5zG6~YVYxrh5DGeBepGc$9fLr~UW(~|TlrC`KWAAyJw zK_!Z5;L)8QjHZ03Mx^mM>i^xc{_k$||AXlNL#_Y!b`N$sJG=Yd!Orf%{^3Ub|1R}E zNQ>#cHU3)qAGCeZ{@>j>-1vVF^7+!)`f3aJLj}?J$eCrMt;hI;ch?g$o?XR%QBPfx`;Fu7dWSCIiP=G+(zTzY z-yZLHd+l#_5fb1|r0lWHgS(zcSwF(8AV^D#YWzY1$9FVS)pILpB)g5(yRk<*Z z#=U%D%H_##u|}LAfT!or&YU+-uY!R;jvwV-^W9=t2ub{{!EWrojr~`)|Moijo9$n}OZ)HDAAj6BeeuJ~hqV88 z4~p}D=U{(tWB)zG2lsBFa@_he+ZRX9j@NB}yujN&9nBjJ{gRlZaW Date: Mon, 14 Feb 2022 13:44:36 -0800 Subject: [PATCH 189/211] chore(_hash_policy_contents): rename inputs --- fence/sync/sync_users.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index cae8cdfbb..21105acd5 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -2089,13 +2089,13 @@ def _create_arborist_policy( self.logger.info("created policy `{}` in Arborist".format(policy_id)) return True - def _hash_policy_contents(self, roles, resources): + def _hash_policy_contents(self, ordered_roles, ordered_resources): """ - Generate a sha256 hexdigest representing roles and resources. + Generate a sha256 hexdigest representing ordered_roles and ordered_resources. Args: - roles (iterable): policy roles - resources (iterable): policy resources + ordered_roles (iterable): policy roles in sorted order + ordered_resources (iterable): policy resources in sorted order Return: str: SHA256 hex digest @@ -2104,10 +2104,11 @@ def _hash_policy_contents(self, roles, resources): def escape(s): return s.replace(",", "\,") - canonical_roles = ",".join(tuple(escape(r) for r in roles)) - canonical_resources = ",".join(tuple(escape(r) for r in resources)) + canonical_roles = ",".join(escape(r) for r in ordered_roles) + canonical_resources = ",".join(escape(r) for r in ordered_resources) canonical_policy = f"{canonical_roles},,f{canonical_resources}" policy_hash = hashlib.sha256(canonical_policy.encode("utf-8")).hexdigest() + return policy_hash def _grant_arborist_policy(self, username, policy_id, expires=None): From acc1c824419e49c8021c41dbff105d1a241671cf Mon Sep 17 00:00:00 2001 From: John McCann Date: Tue, 15 Feb 2022 07:43:34 -0800 Subject: [PATCH 190/211] chore(single user sync): always attempt plcy grnt --- fence/sync/sync_users.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 21105acd5..343079d5d 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -1807,8 +1807,7 @@ def _update_authz_in_arborist( for r in resources ] all_resources.extend(r for r in project_to_authz_mapping.values()) - if not self._create_arborist_resources(all_resources): - return False + self._create_arborist_resources(all_resources) for username, user_project_info in user_projects.items(): self.logger.info("processing user `{}`".format(username)) @@ -1839,17 +1838,18 @@ def _update_authz_in_arborist( policy_hash = self._hash_policy_contents( ordered_roles, ordered_resources ) - if not self._create_arborist_policy( + self._create_arborist_policy( policy_hash, ordered_roles, ordered_resources, skip_if_exists=True, - ): - return False - if not self._grant_arborist_policy( + ) + # return here as it is not expected single_user_sync + # will need any of the remaining user_yaml operations + # left in _update_authz_in_arborist + return self._grant_arborist_policy( username, policy_hash, expires=expires - ): - return False + ) else: for roles, resources in unique_policies.items(): for role in roles: From da70723e5218686f8bd49805a1627a4522d5533d Mon Sep 17 00:00:00 2001 From: John McCann Date: Tue, 15 Feb 2022 10:01:25 -0800 Subject: [PATCH 191/211] chore(dependencies): use nonlocal gen3authz ^1.5.0 --- Dockerfile | 2 -- poetry.lock | 9 +++------ pyproject.toml | 3 +-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4d61900ee..ad1febe6f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,8 +30,6 @@ RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2 WORKDIR /$appname -# TODO TAKE OUT: ONLY FOR DEVELOPMENT AND TESTING PURPOSES -COPY gen3authz-1.5.0.tar.gz /$appname/ # copy ONLY poetry artifact, install the dependencies but not fence # this will make sure than the dependencies is cached COPY poetry.lock pyproject.toml /$appname/ diff --git a/poetry.lock b/poetry.lock index e0867701e..d9fd5064c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -526,10 +526,6 @@ contextvars = {version = ">=2.4,<3.0", markers = "python_version < \"3.7\""} httpx = ">=0.20.0,<1.0.0" six = ">=1.16.0,<2.0.0" -[package.source] -type = "file" -url = "gen3authz-1.5.0.tar.gz" - [[package]] name = "gen3cirrus" version = "2.0.0" @@ -1507,7 +1503,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "564d24bb38187ec8d9985435e44cbec039433ea1645cc94def200e18e031a34f" +content-hash = "c281c2792a1151a272c16b34d7fc996e1f436709644762c2cb90346dfd43c547" [metadata.files] addict = [ @@ -1797,7 +1793,8 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] gen3authz = [ - {file = "gen3authz-1.5.0.tar.gz", hash = "sha256:d832038ff2969f7146b20e9f544c6ee0b1ccae9444fbc15f5ea0cc1665122a01"}, + {file = "gen3authz-1.5.0-py3-none-any.whl", hash = "sha256:205e960b2fd026b1e7d80c7606e7ef6d3afab6b882d6977845de937f9f564e7b"}, + {file = "gen3authz-1.5.0.tar.gz", hash = "sha256:0cb1223d272c7ea1a134d421b9133734df70bb9a2723fb612d388aa9976bb1c2"}, ] gen3cirrus = [ {file = "gen3cirrus-2.0.0.tar.gz", hash = "sha256:0bd590c407c42dad5f0b896da0fa30bd01ea6bef5ff7dd11324ec59f14a71793"}, diff --git a/pyproject.toml b/pyproject.toml index 762a58299..fcbc5b0fd 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,8 +26,7 @@ flask-cors = "^3.0.3" flask-restful = "^0.3.6" flask_sqlalchemy_session = "^1.1" email_validator = "^1.1.1" -# TODO USE ^1.5.0 WHEN RELEASED. LOCAL COPY ONLY FOR DEVELOPMENT AND TESTING PURPOSES -gen3authz = {path = "./gen3authz-1.5.0.tar.gz"} +gen3authz = "^1.5.0" gen3cirrus = "^2.0.0" gen3config = "^0.1.7" gen3users = "^0.6.0" From 28c6e827f3b21e3f1897d89afbefde9489752c4d Mon Sep 17 00:00:00 2001 From: John McCann Date: Wed, 16 Feb 2022 21:38:54 -0800 Subject: [PATCH 192/211] chore(dependencies): remove unused gen3authz tar --- gen3authz-1.5.0.tar.gz | Bin 13879 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 gen3authz-1.5.0.tar.gz diff --git a/gen3authz-1.5.0.tar.gz b/gen3authz-1.5.0.tar.gz deleted file mode 100644 index f36308c2d44b09a92c347f1ed06c8378ac95cc64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13879 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 F0RX!YCc*#! From c1b53a4268175f510599ce76765c5b1c2a161cc0 Mon Sep 17 00:00:00 2001 From: John McCann Date: Wed, 16 Feb 2022 23:43:58 -0800 Subject: [PATCH 193/211] fix(access token polling): honor sync client --- Dockerfile | 2 ++ gen3authz-1.5.1.tar.gz | Bin 0 -> 14086 bytes poetry.lock | 11 +++++++---- pyproject.toml | 3 ++- 4 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 gen3authz-1.5.1.tar.gz diff --git a/Dockerfile b/Dockerfile index ad1febe6f..de749be36 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,6 +30,8 @@ RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2 WORKDIR /$appname +# TODO TAKE OUT: ONLY FOR DEVELOPMENT AND TESTING PURPOSES +COPY gen3authz-1.5.1.tar.gz /$appname/ # copy ONLY poetry artifact, install the dependencies but not fence # this will make sure than the dependencies is cached COPY poetry.lock pyproject.toml /$appname/ diff --git a/gen3authz-1.5.1.tar.gz b/gen3authz-1.5.1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..5429b54181c3029e4fd1e3da4a90e9bb0e782e38 GIT binary patch literal 14086 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 E01fW5KmY&$ literal 0 HcmV?d00001 diff --git a/poetry.lock b/poetry.lock index d9fd5064c..b437e9e5d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -513,7 +513,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "gen3authz" -version = "1.5.0" +version = "1.5.1" description = "Gen3 authz client" category = "main" optional = false @@ -526,6 +526,10 @@ contextvars = {version = ">=2.4,<3.0", markers = "python_version < \"3.7\""} httpx = ">=0.20.0,<1.0.0" six = ">=1.16.0,<2.0.0" +[package.source] +type = "file" +url = "gen3authz-1.5.1.tar.gz" + [[package]] name = "gen3cirrus" version = "2.0.0" @@ -1503,7 +1507,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "c281c2792a1151a272c16b34d7fc996e1f436709644762c2cb90346dfd43c547" +content-hash = "75a454e62e6833249828c3a34f349067ecf99fbaf9b7dadd765aa457c400f7a0" [metadata.files] addict = [ @@ -1793,8 +1797,7 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] gen3authz = [ - {file = "gen3authz-1.5.0-py3-none-any.whl", hash = "sha256:205e960b2fd026b1e7d80c7606e7ef6d3afab6b882d6977845de937f9f564e7b"}, - {file = "gen3authz-1.5.0.tar.gz", hash = "sha256:0cb1223d272c7ea1a134d421b9133734df70bb9a2723fb612d388aa9976bb1c2"}, + {file = "gen3authz-1.5.1.tar.gz", hash = "sha256:b6514803a720029228e546863dbb63a73ce7fe3541a145140b03a8e75f122d3a"}, ] gen3cirrus = [ {file = "gen3cirrus-2.0.0.tar.gz", hash = "sha256:0bd590c407c42dad5f0b896da0fa30bd01ea6bef5ff7dd11324ec59f14a71793"}, diff --git a/pyproject.toml b/pyproject.toml index fcbc5b0fd..3ff591523 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,8 @@ flask-cors = "^3.0.3" flask-restful = "^0.3.6" flask_sqlalchemy_session = "^1.1" email_validator = "^1.1.1" -gen3authz = "^1.5.0" +# TODO USE ^1.5.1 WHEN RELEASED. LOCAL COPY ONLY FOR DEVELOPMENT AND TESTING PURPOSES +gen3authz = {path = "./gen3authz-1.5.1.tar.gz"} gen3cirrus = "^2.0.0" gen3config = "^0.1.7" gen3users = "^0.6.0" From 7291260a3d5a07b013852248c381c20dedb65bbc Mon Sep 17 00:00:00 2001 From: John McCann Date: Thu, 17 Feb 2022 13:25:20 -0800 Subject: [PATCH 194/211] chore(dependencies): use nonlocal gen3authz ^1.5.1 --- Dockerfile | 2 -- gen3authz-1.5.1.tar.gz | Bin 14086 -> 0 bytes poetry.lock | 9 +++------ pyproject.toml | 3 +-- 4 files changed, 4 insertions(+), 10 deletions(-) delete mode 100644 gen3authz-1.5.1.tar.gz diff --git a/Dockerfile b/Dockerfile index de749be36..ad1febe6f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,8 +30,6 @@ RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2 WORKDIR /$appname -# TODO TAKE OUT: ONLY FOR DEVELOPMENT AND TESTING PURPOSES -COPY gen3authz-1.5.1.tar.gz /$appname/ # copy ONLY poetry artifact, install the dependencies but not fence # this will make sure than the dependencies is cached COPY poetry.lock pyproject.toml /$appname/ diff --git a/gen3authz-1.5.1.tar.gz b/gen3authz-1.5.1.tar.gz deleted file mode 100644 index 5429b54181c3029e4fd1e3da4a90e9bb0e782e38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14086 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 E01fW5KmY&$ diff --git a/poetry.lock b/poetry.lock index b437e9e5d..e6d085192 100644 --- a/poetry.lock +++ b/poetry.lock @@ -526,10 +526,6 @@ contextvars = {version = ">=2.4,<3.0", markers = "python_version < \"3.7\""} httpx = ">=0.20.0,<1.0.0" six = ">=1.16.0,<2.0.0" -[package.source] -type = "file" -url = "gen3authz-1.5.1.tar.gz" - [[package]] name = "gen3cirrus" version = "2.0.0" @@ -1507,7 +1503,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "75a454e62e6833249828c3a34f349067ecf99fbaf9b7dadd765aa457c400f7a0" +content-hash = "5c0ae6cc529d940e7f68498a5849ab702ab629c4a67a75b7043f40fed8121976" [metadata.files] addict = [ @@ -1797,7 +1793,8 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] gen3authz = [ - {file = "gen3authz-1.5.1.tar.gz", hash = "sha256:b6514803a720029228e546863dbb63a73ce7fe3541a145140b03a8e75f122d3a"}, + {file = "gen3authz-1.5.1-py3-none-any.whl", hash = "sha256:249ab21471e1ae3c283a30f3e32819db218aaa63d8d7151ac7b6b83496ac3518"}, + {file = "gen3authz-1.5.1.tar.gz", hash = "sha256:897f804a2f9d29181a6074fa14335fc9f18e757dd2e5dcec2a68bb9a008bfcb8"}, ] gen3cirrus = [ {file = "gen3cirrus-2.0.0.tar.gz", hash = "sha256:0bd590c407c42dad5f0b896da0fa30bd01ea6bef5ff7dd11324ec59f14a71793"}, diff --git a/pyproject.toml b/pyproject.toml index 3ff591523..9bc5a86b0 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,8 +26,7 @@ flask-cors = "^3.0.3" flask-restful = "^0.3.6" flask_sqlalchemy_session = "^1.1" email_validator = "^1.1.1" -# TODO USE ^1.5.1 WHEN RELEASED. LOCAL COPY ONLY FOR DEVELOPMENT AND TESTING PURPOSES -gen3authz = {path = "./gen3authz-1.5.1.tar.gz"} +gen3authz = "^1.5.1" gen3cirrus = "^2.0.0" gen3config = "^0.1.7" gen3users = "^0.6.0" From cb7a1947d46abdf1a131630d08e800072b6f8a60 Mon Sep 17 00:00:00 2001 From: John McCann Date: Thu, 17 Feb 2022 14:30:37 -0800 Subject: [PATCH 195/211] chore(ci): trigger unit tests From c374134286f0c7a9fae1d63f14ebc9e4f2805727 Mon Sep 17 00:00:00 2001 From: John McCann Date: Mon, 21 Feb 2022 15:22:16 -0800 Subject: [PATCH 196/211] chore(uwsgi.ini): decrease timeouts to 45s --- deployment/uwsgi/uwsgi.ini | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/deployment/uwsgi/uwsgi.ini b/deployment/uwsgi/uwsgi.ini index 63334161a..a2f7741c4 100644 --- a/deployment/uwsgi/uwsgi.ini +++ b/deployment/uwsgi/uwsgi.ini @@ -11,10 +11,8 @@ harakiri-verbose = true # No global HARAKIRI, using only user HARAKIRI, because export overwrites it # Cannot overwrite global HARAKIRI with user's: https://git.io/fjYuD # harakiri = 45 -# TODO reduce http and socket timeouts after performance improvements -# to DRS endpoint -http-timeout = 60 -socket-timeout = 60 +http-timeout = 45 +socket-timeout = 45 worker-reload-mercy = 45 reload-mercy = 45 mule-reload-mercy = 45 From d554da17eea645eaf4ffd2be512642f3d6c154b4 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 22 Feb 2022 12:47:26 -0600 Subject: [PATCH 197/211] chore(poetry): update deps --- poetry.lock | 223 +++++++++++++++++++++++++++++----------------------- 1 file changed, 125 insertions(+), 98 deletions(-) diff --git a/poetry.lock b/poetry.lock index e6d085192..ecb7dd882 100644 --- a/poetry.lock +++ b/poetry.lock @@ -81,11 +81,11 @@ fastapi = ["fastapi (>=0.54.1,<0.55.0)"] [[package]] name = "azure-core" -version = "1.21.1" +version = "1.22.1" description = "Microsoft Azure Core Library for Python" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [package.dependencies] requests = ">=2.18.4" @@ -263,7 +263,7 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "2.0.10" +version = "2.0.12" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -370,18 +370,19 @@ python-versions = ">=3.5" [[package]] name = "dnspython" -version = "2.1.0" +version = "2.2.0" description = "DNS toolkit" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6,<4.0" [package.extras] -dnssec = ["cryptography (>=2.6)"] -doh = ["requests", "requests-toolbelt"] -idna = ["idna (>=2.1)"] -curio = ["curio (>=1.2)", "sniffio (>=1.1)"] -trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] +dnssec = ["cryptography (>=2.6,<37.0)"] +curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] +doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"] +idna = ["idna (>=2.1,<4.0)"] +trio = ["trio (>=0.14,<0.20)"] +wmi = ["wmi (>=1.5.1,<2.0.0)"] [[package]] name = "docopt" @@ -643,7 +644,7 @@ six = "*" [[package]] name = "google-cloud-core" -version = "2.2.1" +version = "2.2.2" description = "Google Cloud API client core library" category = "main" optional = false @@ -686,7 +687,7 @@ testing = ["pytest"] [[package]] name = "google-resumable-media" -version = "2.1.0" +version = "2.2.1" description = "Utilities for Google Media Downloads and Resumable Uploads" category = "main" optional = false @@ -738,7 +739,7 @@ http2 = ["h2 (>=3,<5)"] [[package]] name = "httplib2" -version = "0.20.2" +version = "0.20.4" description = "A comprehensive HTTP client library." category = "main" optional = false @@ -966,16 +967,16 @@ six = ">=1.6.1" [[package]] name = "oauthlib" -version = "3.1.1" +version = "3.2.0" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" category = "main" optional = false python-versions = ">=3.6" [package.extras] -rsa = ["cryptography (>=3.0.0,<4)"] +rsa = ["cryptography (>=3.0.0)"] signals = ["blinker (>=1.4.0)"] -signedtoken = ["cryptography (>=3.0.0,<4)", "pyjwt (>=2.0.0,<3)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "packaging" @@ -1055,7 +1056,7 @@ prometheus-client = "*" [[package]] name = "protobuf" -version = "3.19.3" +version = "3.19.4" description = "Protocol Buffers" category = "main" optional = false @@ -1106,7 +1107,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pycryptodome" -version = "3.12.0" +version = "3.14.1" description = "Cryptographic library for Python" category = "main" optional = false @@ -1145,7 +1146,7 @@ tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] [[package]] name = "pyparsing" -version = "3.0.6" +version = "3.0.7" description = "Python parsing module" category = "main" optional = false @@ -1270,7 +1271,7 @@ use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "requests-oauthlib" -version = "1.3.0" +version = "1.3.1" description = "OAuthlib authentication support for Requests." category = "main" optional = false @@ -1415,7 +1416,7 @@ resolved_reference = "4d39265d6e478acd5e1afe6e5dc722418f887d78" [[package]] name = "typing-extensions" -version = "4.0.1" +version = "4.1.1" description = "Backported and Experimental Type Hints for Python 3.6+" category = "main" optional = false @@ -1535,8 +1536,8 @@ authutils = [ {file = "authutils-6.1.0.tar.gz", hash = "sha256:7263af0b2ce3a0db19236fd123b34f795d07e07111b7bd18a51808568ddfdc2e"}, ] azure-core = [ - {file = "azure-core-1.21.1.zip", hash = "sha256:88d2db5cf9a135a7287dc45fdde6b96f9ca62c9567512a3bb3e20e322ce7deb2"}, - {file = "azure_core-1.21.1-py2.py3-none-any.whl", hash = "sha256:3d70e9ec64de92dfae330c15bc69085caceb2d83813ef6c01cc45326f2a4be83"}, + {file = "azure-core-1.22.1.zip", hash = "sha256:4b6e405268a33b873107796495cec3f2f1b1ffe935624ce0fbddff36d38d3a4d"}, + {file = "azure_core-1.22.1-py3-none-any.whl", hash = "sha256:407381c74e2ccc16adb1f29c4a1b381ebd39e8661bbf60422926d8252d5b757d"}, ] azure-storage-blob = [ {file = "azure-storage-blob-12.9.0.zip", hash = "sha256:cff66a115c73c90e496c8c8b3026898a3ce64100840276e9245434e28a864225"}, @@ -1647,8 +1648,8 @@ cffi = [ {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, - {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, @@ -1751,8 +1752,8 @@ decorator = [ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] dnspython = [ - {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, - {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, + {file = "dnspython-2.2.0-py3-none-any.whl", hash = "sha256:081649da27ced5e75709a1ee542136eaba9842a0fe4c03da4fb0a3d3ed1f3c44"}, + {file = "dnspython-2.2.0.tar.gz", hash = "sha256:e79351e032d0b606b98d38a4b0e6e2275b31a5b85c873e587cc11b73aca026d6"}, ] docopt = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, @@ -1822,8 +1823,8 @@ google-auth-httplib2 = [ {file = "google_auth_httplib2-0.1.0-py2.py3-none-any.whl", hash = "sha256:31e49c36c6b5643b57e82617cb3e021e3e1d2df9da63af67252c02fa9c1f4a10"}, ] google-cloud-core = [ - {file = "google-cloud-core-2.2.1.tar.gz", hash = "sha256:476d1f71ab78089e0638e0aaf34bfdc99bab4fce8f4170ba6321a5243d13c5c7"}, - {file = "google_cloud_core-2.2.1-py2.py3-none-any.whl", hash = "sha256:ab6cee07791afe4e210807ceeab749da6a076ab16d496ac734bf7e6ffea27486"}, + {file = "google-cloud-core-2.2.2.tar.gz", hash = "sha256:7d19bf8868b410d0bdf5a03468a3f3f2db233c0ee86a023f4ecc2b7a4b15f736"}, + {file = "google_cloud_core-2.2.2-py2.py3-none-any.whl", hash = "sha256:d9cffaf86df6a876438d4e8471183bbe404c9a15de9afe60433bc7dce8cb4252"}, ] google-cloud-storage = [ {file = "google-cloud-storage-1.44.0.tar.gz", hash = "sha256:29edbfeedd157d853049302bf5d104055c6f0cb7ef283537da3ce3f730073001"}, @@ -1875,8 +1876,8 @@ google-crc32c = [ {file = "google_crc32c-1.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:7f6fe42536d9dcd3e2ffb9d3053f5d05221ae3bbcefbe472bdf2c71c793e3183"}, ] google-resumable-media = [ - {file = "google-resumable-media-2.1.0.tar.gz", hash = "sha256:725b989e0dd387ef2703d1cc8e86217474217f4549593c477fd94f4024a0f911"}, - {file = "google_resumable_media-2.1.0-py2.py3-none-any.whl", hash = "sha256:cdc75ea0361e39704dc7df7da59fbd419e73c8bc92eac94d8a020d36baa9944b"}, + {file = "google-resumable-media-2.2.1.tar.gz", hash = "sha256:b1edfb98867c9fa25aa7af12d6468665b83c532b7349effab805a027ea8bbee5"}, + {file = "google_resumable_media-2.2.1-py2.py3-none-any.whl", hash = "sha256:fd616af31b83d48da040c8c09b6994606e1734efb8af9acc97cf5d6070e9ba72"}, ] googleapis-common-protos = [ {file = "googleapis-common-protos-1.54.0.tar.gz", hash = "sha256:a4031d6ec6c2b1b6dc3e0be7e10a1bd72fb0b18b07ef9be7b51f2c1004ce2437"}, @@ -1891,8 +1892,8 @@ httpcore = [ {file = "httpcore-0.13.3.tar.gz", hash = "sha256:5d674b57a11275904d4fd0819ca02f960c538e4472533620f322fc7db1ea0edc"}, ] httplib2 = [ - {file = "httplib2-0.20.2-py3-none-any.whl", hash = "sha256:6b937120e7d786482881b44b8eec230c1ee1c5c1d06bce8cc865f25abbbf713b"}, - {file = "httplib2-0.20.2.tar.gz", hash = "sha256:e404681d2fbcec7506bcb52c503f2b021e95bee0ef7d01e5c221468a2406d8dc"}, + {file = "httplib2-0.20.4-py3-none-any.whl", hash = "sha256:8b6a905cb1c79eefd03f8669fd993c36dc341f7c558f056cb5a33b5c2f458543"}, + {file = "httplib2-0.20.4.tar.gz", hash = "sha256:58a98e45b4b1a48273073f905d2961666ecf0fbac4250ea5b47aef259eb5c585"}, ] httpx = [ {file = "httpx-0.20.0-py3-none-any.whl", hash = "sha256:33af5aad9bdc82ef1fc89219c1e36f5693bf9cd0ebe330884df563445682c0f8"}, @@ -1974,20 +1975,39 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mock = [ @@ -2010,8 +2030,8 @@ oauth2client = [ {file = "oauth2client-3.0.0.tar.gz", hash = "sha256:5b5b056ec6f2304e7920b632885bd157fa71d1a7f3ddd00a43b1541a8d1a2460"}, ] oauthlib = [ - {file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"}, - {file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"}, + {file = "oauthlib-3.2.0-py3-none-any.whl", hash = "sha256:6db33440354787f9b7f3a6dbd4febf5d0f93758354060e802f6c06cb493022fe"}, + {file = "oauthlib-3.2.0.tar.gz", hash = "sha256:23a8208d75b902797ea29fd31fa80a15ed9dc2c6c16fe73f5d346f83f6fa27a2"}, ] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, @@ -2038,32 +2058,32 @@ prometheus-flask-exporter = [ {file = "prometheus_flask_exporter-0.18.7.tar.gz", hash = "sha256:f1f6f23535479d41587a100a24a60cb9199c34986e95f6691496807ee5017e59"}, ] protobuf = [ - {file = "protobuf-3.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1cb2ed66aac593adbf6dca4f07cd7ee7e2958b17bbc85b2cc8bc564ebeb258ec"}, - {file = "protobuf-3.19.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:898bda9cd37ec0c781b598891e86435de80c3bfa53eb483a9dac5a11ec93e942"}, - {file = "protobuf-3.19.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ad761ef3be34c8bdc7285bec4b40372a8dad9e70cfbdc1793cd3cf4c1a4ce74"}, - {file = "protobuf-3.19.3-cp310-cp310-win32.whl", hash = "sha256:2cddcbcc222f3144765ccccdb35d3621dc1544da57a9aca7e1944c1a4fe3db11"}, - {file = "protobuf-3.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:6202df8ee8457cb00810c6e76ced480f22a1e4e02c899a14e7b6e6e1de09f938"}, - {file = "protobuf-3.19.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:397d82f1c58b76445469c8c06b8dee1ff67b3053639d054f52599a458fac9bc6"}, - {file = "protobuf-3.19.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e54b8650e849ee8e95e481024bff92cf98f5ec61c7650cb838d928a140adcb63"}, - {file = "protobuf-3.19.3-cp36-cp36m-win32.whl", hash = "sha256:3bf3a07d17ba3511fe5fa916afb7351f482ab5dbab5afe71a7a384274a2cd550"}, - {file = "protobuf-3.19.3-cp36-cp36m-win_amd64.whl", hash = "sha256:afa8122de8064fd577f49ae9eef433561c8ace97a0a7b969d56e8b1d39b5d177"}, - {file = "protobuf-3.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18c40a1b8721026a85187640f1786d52407dc9c1ba8ec38accb57a46e84015f6"}, - {file = "protobuf-3.19.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:af7238849fa79285d448a24db686517570099739527a03c9c2971cce99cc5ae2"}, - {file = "protobuf-3.19.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e765e6dfbbb02c55e4d6d1145743401a84fc0b508f5a81b2c5a738cf86353139"}, - {file = "protobuf-3.19.3-cp37-cp37m-win32.whl", hash = "sha256:c781402ed5396ab56358d7b866d78c03a77cbc26ba0598d8bb0ac32084b1a257"}, - {file = "protobuf-3.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:544fe9705189b249380fae07952d220c97f5c6c9372a6f936cc83a79601dcb70"}, - {file = "protobuf-3.19.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84bf3aa3efb00dbe1c7ed55da0f20800b0662541e582d7e62b3e1464d61ed365"}, - {file = "protobuf-3.19.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3f80a3491eaca767cdd86cb8660dc778f634b44abdb0dffc9b2a8e8d0cd617d0"}, - {file = "protobuf-3.19.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9401d96552befcc7311f5ef8f0fa7dba0ef5fd805466b158b141606cd0ab6a8"}, - {file = "protobuf-3.19.3-cp38-cp38-win32.whl", hash = "sha256:ef02d112c025e83db5d1188a847e358beab3e4bbfbbaf10eaf69e67359af51b2"}, - {file = "protobuf-3.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:1291a0a7db7d792745c99d4657b4c5c4942695c8b1ac1bfb993a34035ec123f7"}, - {file = "protobuf-3.19.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49677e5e9c7ea1245a90c2e8a00d304598f22ea3aa0628f0e0a530a9e70665fa"}, - {file = "protobuf-3.19.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:df2ba379ee42427e8fcc6a0a76843bff6efb34ef5266b17f95043939b5e25b69"}, - {file = "protobuf-3.19.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2acd7ca329be544d1a603d5f13a4e34a3791c90d651ebaf130ba2e43ae5397c6"}, - {file = "protobuf-3.19.3-cp39-cp39-win32.whl", hash = "sha256:b53519b2ebec70cfe24b4ddda21e9843f0918d7c3627a785393fb35d402ab8ad"}, - {file = "protobuf-3.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:8ceaf5fdb72c8e1fcb7be9f2b3b07482ce058a3548180c0bdd5c7e4ac5e14165"}, - {file = "protobuf-3.19.3-py2.py3-none-any.whl", hash = "sha256:f6d4b5b7595a57e69eb7314c67bef4a3c745b4caf91accaf72913d8e0635111b"}, - {file = "protobuf-3.19.3.tar.gz", hash = "sha256:d975a6314fbf5c524d4981e24294739216b5fb81ef3c14b86fb4b045d6690907"}, + {file = "protobuf-3.19.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f51d5a9f137f7a2cec2d326a74b6e3fc79d635d69ffe1b036d39fc7d75430d37"}, + {file = "protobuf-3.19.4-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:09297b7972da685ce269ec52af761743714996b4381c085205914c41fcab59fb"}, + {file = "protobuf-3.19.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072fbc78d705d3edc7ccac58a62c4c8e0cec856987da7df8aca86e647be4e35c"}, + {file = "protobuf-3.19.4-cp310-cp310-win32.whl", hash = "sha256:7bb03bc2873a2842e5ebb4801f5c7ff1bfbdf426f85d0172f7644fcda0671ae0"}, + {file = "protobuf-3.19.4-cp310-cp310-win_amd64.whl", hash = "sha256:f358aa33e03b7a84e0d91270a4d4d8f5df6921abe99a377828839e8ed0c04e07"}, + {file = "protobuf-3.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1c91ef4110fdd2c590effb5dca8fdbdcb3bf563eece99287019c4204f53d81a4"}, + {file = "protobuf-3.19.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c438268eebb8cf039552897d78f402d734a404f1360592fef55297285f7f953f"}, + {file = "protobuf-3.19.4-cp36-cp36m-win32.whl", hash = "sha256:835a9c949dc193953c319603b2961c5c8f4327957fe23d914ca80d982665e8ee"}, + {file = "protobuf-3.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4276cdec4447bd5015453e41bdc0c0c1234eda08420b7c9a18b8d647add51e4b"}, + {file = "protobuf-3.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6cbc312be5e71869d9d5ea25147cdf652a6781cf4d906497ca7690b7b9b5df13"}, + {file = "protobuf-3.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54a1473077f3b616779ce31f477351a45b4fef8c9fd7892d6d87e287a38df368"}, + {file = "protobuf-3.19.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:435bb78b37fc386f9275a7035fe4fb1364484e38980d0dd91bc834a02c5ec909"}, + {file = "protobuf-3.19.4-cp37-cp37m-win32.whl", hash = "sha256:16f519de1313f1b7139ad70772e7db515b1420d208cb16c6d7858ea989fc64a9"}, + {file = "protobuf-3.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:cdc076c03381f5c1d9bb1abdcc5503d9ca8b53cf0a9d31a9f6754ec9e6c8af0f"}, + {file = "protobuf-3.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:69da7d39e39942bd52848438462674c463e23963a1fdaa84d88df7fbd7e749b2"}, + {file = "protobuf-3.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48ed3877fa43e22bcacc852ca76d4775741f9709dd9575881a373bd3e85e54b2"}, + {file = "protobuf-3.19.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd95d1dfb9c4f4563e6093a9aa19d9c186bf98fa54da5252531cc0d3a07977e7"}, + {file = "protobuf-3.19.4-cp38-cp38-win32.whl", hash = "sha256:b38057450a0c566cbd04890a40edf916db890f2818e8682221611d78dc32ae26"}, + {file = "protobuf-3.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:7ca7da9c339ca8890d66958f5462beabd611eca6c958691a8fe6eccbd1eb0c6e"}, + {file = "protobuf-3.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:36cecbabbda242915529b8ff364f2263cd4de7c46bbe361418b5ed859677ba58"}, + {file = "protobuf-3.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c1068287025f8ea025103e37d62ffd63fec8e9e636246b89c341aeda8a67c934"}, + {file = "protobuf-3.19.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96bd766831596d6014ca88d86dc8fe0fb2e428c0b02432fd9db3943202bf8c5e"}, + {file = "protobuf-3.19.4-cp39-cp39-win32.whl", hash = "sha256:84123274d982b9e248a143dadd1b9815049f4477dc783bf84efe6250eb4b836a"}, + {file = "protobuf-3.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:3112b58aac3bac9c8be2b60a9daf6b558ca3f7681c130dcdd788ade7c9ffbdca"}, + {file = "protobuf-3.19.4-py2.py3-none-any.whl", hash = "sha256:8961c3a78ebfcd000920c9060a262f082f29838682b1f7201889300c1fbe0616"}, + {file = "protobuf-3.19.4.tar.gz", hash = "sha256:9df0c10adf3e83015ced42a9a7bd64e13d06c4cf45c340d2c63020ea04499d0a"}, ] psycopg2 = [ {file = "psycopg2-2.9.3-cp310-cp310-win32.whl", hash = "sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362"}, @@ -2117,36 +2137,36 @@ pycparser = [ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] pycryptodome = [ - {file = "pycryptodome-3.12.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:90ad3381ccdc6a24cc2841e295706a168f32abefe64c679695712acac71fd5da"}, - {file = "pycryptodome-3.12.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e80f7469b0b3ea0f694230477d8501dc5a30a717e94fddd4821e6721f3053eae"}, - {file = "pycryptodome-3.12.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:b91404611767a7485837a6f1fd20cf9a5ae0ad362040a022cd65827ecb1b0d00"}, - {file = "pycryptodome-3.12.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:db66ccda65d5d20c17b00768e462a86f6f540f9aea8419a7f76cc7d9effd82cd"}, - {file = "pycryptodome-3.12.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:dc88355c4b261ed259268e65705b28b44d99570337694d593f06e3b1698eaaf3"}, - {file = "pycryptodome-3.12.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:6f8f5b7b53516da7511951910ab458e799173722c91fea54e2ba2f56d102e4aa"}, - {file = "pycryptodome-3.12.0-cp27-cp27m-win32.whl", hash = "sha256:93acad54a72d81253242eb0a15064be559ec9d989e5173286dc21cad19f01765"}, - {file = "pycryptodome-3.12.0-cp27-cp27m-win_amd64.whl", hash = "sha256:5a8c24d39d4a237dbfe181ea6593792bf9b5582c7fcfa7b8e0e12fda5eec07af"}, - {file = "pycryptodome-3.12.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:32d15da81959faea6cbed95df2bb44f7f796211c110cf90b5ad3b2aeeb97fc8e"}, - {file = "pycryptodome-3.12.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:aed7eb4b64c600fbc5e6d4238991ad1b4179a558401f203d1fcbd24883748982"}, - {file = "pycryptodome-3.12.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:341c6bbf932c406b4f3ee2372e8589b67ac0cf4e99e7dc081440f43a3cde9f0f"}, - {file = "pycryptodome-3.12.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:de0b711d673904dd6c65307ead36cb76622365a393569bf880895cba21195b7a"}, - {file = "pycryptodome-3.12.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:3558616f45d8584aee3eba27559bc6fd0ba9be6c076610ed3cc62bd5229ffdc3"}, - {file = "pycryptodome-3.12.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:a78e4324e566b5fbc2b51e9240950d82fa9e1c7eb77acdf27f58712f65622c1d"}, - {file = "pycryptodome-3.12.0-cp35-abi3-manylinux1_i686.whl", hash = "sha256:3f2f3dd596c6128d91314e60a6bcf4344610ef0e97f4ae4dd1770f86dd0748d8"}, - {file = "pycryptodome-3.12.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:e05f994f30f1cda3cbe57441f41220d16731cf99d868bb02a8f6484c454c206b"}, - {file = "pycryptodome-3.12.0-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:4cded12e13785bbdf4ba1ff5fb9d261cd98162145f869e4fbc4a4b9083392f0b"}, - {file = "pycryptodome-3.12.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:1181c90d1a6aee68a84826825548d0db1b58d8541101f908d779d601d1690586"}, - {file = "pycryptodome-3.12.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:6bb0d340c93bcb674ea8899e2f6408ec64c6c21731a59481332b4b2a8143cc60"}, - {file = "pycryptodome-3.12.0-cp35-abi3-win32.whl", hash = "sha256:39da5807aa1ff820799c928f745f89432908bf6624b9e981d2d7f9e55d91b860"}, - {file = "pycryptodome-3.12.0-cp35-abi3-win_amd64.whl", hash = "sha256:212c7f7fe11cad9275fbcff50ca977f1c6643f13560d081e7b0f70596df447b8"}, - {file = "pycryptodome-3.12.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:b07a4238465eb8c65dd5df2ab8ba6df127e412293c0ed7656c003336f557a100"}, - {file = "pycryptodome-3.12.0-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:a6e1bcd9d5855f1a3c0f8d585f44c81b08f39a02754007f374fb8db9605ba29c"}, - {file = "pycryptodome-3.12.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:aceb1d217c3a025fb963849071446cf3aca1353282fe1c3cb7bd7339a4d47947"}, - {file = "pycryptodome-3.12.0-pp27-pypy_73-win32.whl", hash = "sha256:f699360ae285fcae9c8f53ca6acf33796025a82bb0ccd7c1c551b04c1726def3"}, - {file = "pycryptodome-3.12.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d845c587ceb82ac7cbac7d0bf8c62a1a0fe7190b028b322da5ca65f6e5a18b9e"}, - {file = "pycryptodome-3.12.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:d8083de50f6dec56c3c6f270fb193590999583a1b27c9c75bc0b5cac22d438cc"}, - {file = "pycryptodome-3.12.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:9ea2f6674c803602a7c0437fccdc2ea036707e60456974fe26ca263bd501ec45"}, - {file = "pycryptodome-3.12.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:5d4264039a2087977f50072aaff2346d1c1c101cb359f9444cf92e3d1f42b4cd"}, - {file = "pycryptodome-3.12.0.zip", hash = "sha256:12c7343aec5a3b3df5c47265281b12b611f26ec9367b6129199d67da54b768c1"}, + {file = "pycryptodome-3.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:75a3a364fee153e77ed889c957f6f94ec6d234b82e7195b117180dcc9fc16f96"}, + {file = "pycryptodome-3.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:aae395f79fa549fb1f6e3dc85cf277f0351e15a22e6547250056c7f0c990d6a5"}, + {file = "pycryptodome-3.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f403a3e297a59d94121cb3ee4b1cf41f844332940a62d71f9e4a009cc3533493"}, + {file = "pycryptodome-3.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ce7a875694cd6ccd8682017a7c06c6483600f151d8916f2b25cf7a439e600263"}, + {file = "pycryptodome-3.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a36ab51674b014ba03da7f98b675fcb8eabd709a2d8e18219f784aba2db73b72"}, + {file = "pycryptodome-3.14.1-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:50a5346af703330944bea503106cd50c9c2212174cfcb9939db4deb5305a8367"}, + {file = "pycryptodome-3.14.1-cp27-cp27m-win32.whl", hash = "sha256:36e3242c4792e54ed906c53f5d840712793dc68b726ec6baefd8d978c5282d30"}, + {file = "pycryptodome-3.14.1-cp27-cp27m-win_amd64.whl", hash = "sha256:c880a98376939165b7dc504559f60abe234b99e294523a273847f9e7756f4132"}, + {file = "pycryptodome-3.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:dcd65355acba9a1d0fc9b923875da35ed50506e339b35436277703d7ace3e222"}, + {file = "pycryptodome-3.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:766a8e9832128c70012e0c2b263049506cbf334fb21ff7224e2704102b6ef59e"}, + {file = "pycryptodome-3.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:2562de213960693b6d657098505fd4493c45f3429304da67efcbeb61f0edfe89"}, + {file = "pycryptodome-3.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d1b7739b68a032ad14c5e51f7e4e1a5f92f3628bba024a2bda1f30c481fc85d8"}, + {file = "pycryptodome-3.14.1-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:27e92c1293afcb8d2639baf7eb43f4baada86e4de0f1fb22312bfc989b95dae2"}, + {file = "pycryptodome-3.14.1-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:f2772af1c3ef8025c85335f8b828d0193fa1e43256621f613280e2c81bfad423"}, + {file = "pycryptodome-3.14.1-cp35-abi3-manylinux1_i686.whl", hash = "sha256:9ec761a35dbac4a99dcbc5cd557e6e57432ddf3e17af8c3c86b44af9da0189c0"}, + {file = "pycryptodome-3.14.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:e64738207a02a83590df35f59d708bf1e7ea0d6adce712a777be2967e5f7043c"}, + {file = "pycryptodome-3.14.1-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:e24d4ec4b029611359566c52f31af45c5aecde7ef90bf8f31620fd44c438efe7"}, + {file = "pycryptodome-3.14.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:8b5c28058102e2974b9868d72ae5144128485d466ba8739abd674b77971454cc"}, + {file = "pycryptodome-3.14.1-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:924b6aad5386fb54f2645f22658cb0398b1f25bc1e714a6d1522c75d527deaa5"}, + {file = "pycryptodome-3.14.1-cp35-abi3-win32.whl", hash = "sha256:53dedbd2a6a0b02924718b520a723e88bcf22e37076191eb9b91b79934fb2192"}, + {file = "pycryptodome-3.14.1-cp35-abi3-win_amd64.whl", hash = "sha256:ea56a35fd0d13121417d39a83f291017551fa2c62d6daa6b04af6ece7ed30d84"}, + {file = "pycryptodome-3.14.1-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:028dcbf62d128b4335b61c9fbb7dd8c376594db607ef36d5721ee659719935d5"}, + {file = "pycryptodome-3.14.1-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:69f05aaa90c99ac2f2af72d8d7f185f729721ad7c4be89e9e3d0ab101b0ee875"}, + {file = "pycryptodome-3.14.1-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:12ef157eb1e01a157ca43eda275fa68f8db0dd2792bc4fe00479ab8f0e6ae075"}, + {file = "pycryptodome-3.14.1-pp27-pypy_73-win32.whl", hash = "sha256:f572a3ff7b6029dd9b904d6be4e0ce9e309dcb847b03e3ac8698d9d23bb36525"}, + {file = "pycryptodome-3.14.1-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9924248d6920b59c260adcae3ee231cd5af404ac706ad30aa4cd87051bf09c50"}, + {file = "pycryptodome-3.14.1-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:e0c04c41e9ade19fbc0eff6aacea40b831bfcb2c91c266137bcdfd0d7b2f33ba"}, + {file = "pycryptodome-3.14.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:893f32210de74b9f8ac869ed66c97d04e7d351182d6d39ebd3b36d3db8bda65d"}, + {file = "pycryptodome-3.14.1-pp36-pypy36_pp73-win32.whl", hash = "sha256:7fb90a5000cc9c9ff34b4d99f7f039e9c3477700e309ff234eafca7b7471afc0"}, + {file = "pycryptodome-3.14.1.tar.gz", hash = "sha256:e04e40a7f8c1669195536a37979dd87da2c32dbdc73d6fe35f0077b0c17c803b"}, ] pyjwt = [ {file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"}, @@ -2165,8 +2185,8 @@ pynacl = [ {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, ] pyparsing = [ - {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, - {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pytest = [ {file = "pytest-3.10.1-py2.py3-none-any.whl", hash = "sha256:3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec"}, @@ -2199,18 +2219,26 @@ pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, @@ -2220,9 +2248,8 @@ requests = [ {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, ] requests-oauthlib = [ - {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, - {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"}, - {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, + {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, + {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, ] responses = [ {file = "responses-0.17.0-py2.py3-none-any.whl", hash = "sha256:e4fc472fb7374fb8f84fcefa51c515ca4351f198852b4eb7fc88223780b472ea"}, @@ -2290,8 +2317,8 @@ sqlalchemy = [ ] storageclient = [] typing-extensions = [ - {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, - {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, + {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, + {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, ] uritemplate = [ {file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"}, From fe3ff02f4f93511ca28834c142c4519e04091294 Mon Sep 17 00:00:00 2001 From: Alexander VanTol Date: Wed, 23 Feb 2022 09:35:32 -0600 Subject: [PATCH 198/211] Update ras.py --- fence/blueprints/login/ras.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fence/blueprints/login/ras.py b/fence/blueprints/login/ras.py index ced1d8e39..467d97ec7 100644 --- a/fence/blueprints/login/ras.py +++ b/fence/blueprints/login/ras.py @@ -2,7 +2,7 @@ import jwt import os -# the whole fence_create module is imported to avoid issue with circular imports +# the whole fence_create module is imported to avoid issues with circular imports import fence.scripting.fence_create from distutils.util import strtobool from urllib.parse import urlparse, parse_qs From 9fe8250ea4f13a24ca0539c793bcbe909dd5059d Mon Sep 17 00:00:00 2001 From: Alexander VanTol Date: Thu, 24 Feb 2022 09:32:15 -0600 Subject: [PATCH 199/211] Update uwsgi.ini --- deployment/uwsgi/uwsgi.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment/uwsgi/uwsgi.ini b/deployment/uwsgi/uwsgi.ini index 63334161a..bd889a133 100644 --- a/deployment/uwsgi/uwsgi.ini +++ b/deployment/uwsgi/uwsgi.ini @@ -11,8 +11,8 @@ harakiri-verbose = true # No global HARAKIRI, using only user HARAKIRI, because export overwrites it # Cannot overwrite global HARAKIRI with user's: https://git.io/fjYuD # harakiri = 45 -# TODO reduce http and socket timeouts after performance improvements -# to DRS endpoint +# TODO reduce http and socket timeouts after performance +# improvements to DRS endpoint http-timeout = 60 socket-timeout = 60 worker-reload-mercy = 45 From bd68d0640763fbf2fa0517f2cd8db42ef50bec42 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Mon, 28 Feb 2022 14:44:35 -0600 Subject: [PATCH 200/211] fix(sync): make sure to merge new Arborist resources with existing ones during sync to avoid overwriting passport resources --- fence/sync/sync_users.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 343079d5d..9160258d4 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -1620,7 +1620,7 @@ def _update_arborist(self, session, user_yaml): self.logger.debug( "attempting to update arborist resource: {}".format(resource) ) - self.arborist_client.update_resource("/", resource) + self.arborist_client.update_resource("/", resource, merge=True) except ArboristError as e: self.logger.error(e) # keep going; maybe just some conflicts from things existing already @@ -1801,6 +1801,9 @@ def _update_authz_in_arborist( f"(instead of user.yaml - as it may not be available): {project_to_authz_mapping}" ) + self.logger.debug( + f"_dbgap_study_to_resources: {self._dbgap_study_to_resources}" + ) all_resources = [ r for resources in self._dbgap_study_to_resources.values() From 5f682ba3dce4010cfbf2e1c5800cadc74f32aa67 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Tue, 1 Mar 2022 09:22:22 -0600 Subject: [PATCH 201/211] feat(sync): remove unneeded manual merge of resources since resource update supports merge --- fence/sync/sync_users.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/fence/sync/sync_users.py b/fence/sync/sync_users.py index 9160258d4..7f8f26f75 100644 --- a/fence/sync/sync_users.py +++ b/fence/sync/sync_users.py @@ -1598,21 +1598,8 @@ def _update_arborist(self, session, user_yaml): self.logger.debug("user_yaml resources: {}".format(resources)) self.logger.debug("dbgap resource paths: {}".format(dbgap_resource_paths)) - # existing resources are also added to prevent resources created from - # DRS endpoint from being overwritten - try: - existing_resources = self.arborist_client.list_resources() - except ArboristError as e: - self.logger.error("could not list Arborist resources: {}".format(e)) - # intentionally fail; avoid overwriting of resources - raise - - existing_paths = [r["path"] for r in existing_resources.get("resources", [])] - combined_resources = utils.combine_provided_and_dbgap_resources( - resources, existing_paths - ) combined_resources = utils.combine_provided_and_dbgap_resources( - combined_resources, dbgap_resource_paths + resources, dbgap_resource_paths ) for resource in combined_resources: From 1aa1ec2a2065efd2d81bc54e70ea5690a00efbc6 Mon Sep 17 00:00:00 2001 From: Hara Prasad Date: Tue, 8 Mar 2022 15:14:37 -0800 Subject: [PATCH 202/211] Update Jenkinsfile --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 2449f8191..d1bb68447 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,6 @@ #!groovy -@Library('cdis-jenkins-lib@master') _ +@Library('cdis-jenkins-lib@chore/skipQuayBuild') _ testPipeline { } From f6380fa1ad35e2691d99ae507a23afc61de98736 Mon Sep 17 00:00:00 2001 From: Hara Prasad Date: Wed, 9 Mar 2022 08:06:57 -0800 Subject: [PATCH 203/211] Update Jenkinsfile --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index d1bb68447..2449f8191 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,6 @@ #!groovy -@Library('cdis-jenkins-lib@chore/skipQuayBuild') _ +@Library('cdis-jenkins-lib@master') _ testPipeline { } From 33f3d0de6adb302d7d1c39b09e4fcf78420fe5b2 Mon Sep 17 00:00:00 2001 From: Hara Prasad Date: Wed, 9 Mar 2022 09:30:27 -0800 Subject: [PATCH 204/211] Update Jenkinsfile --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 2449f8191..78106b2c1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,6 @@ #!groovy -@Library('cdis-jenkins-lib@master') _ +@Library('cdis-jenkins-lib@fix/skipQuayBuildLogic') _ testPipeline { } From d46f4f504cfe364548c3eba8083a738f60acd15c Mon Sep 17 00:00:00 2001 From: Hara Prasad Date: Wed, 9 Mar 2022 09:49:10 -0800 Subject: [PATCH 205/211] Update Jenkinsfile --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 78106b2c1..2449f8191 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,6 @@ #!groovy -@Library('cdis-jenkins-lib@fix/skipQuayBuildLogic') _ +@Library('cdis-jenkins-lib@master') _ testPipeline { } From ebfd2e3bcaad14b5b0ebf73327fa2f7699c5e66a Mon Sep 17 00:00:00 2001 From: Alexander VanTol Date: Wed, 9 Mar 2022 12:27:47 -0600 Subject: [PATCH 206/211] Update uwsgi.ini --- deployment/uwsgi/uwsgi.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/uwsgi/uwsgi.ini b/deployment/uwsgi/uwsgi.ini index bd889a133..5072a4691 100644 --- a/deployment/uwsgi/uwsgi.ini +++ b/deployment/uwsgi/uwsgi.ini @@ -11,7 +11,7 @@ harakiri-verbose = true # No global HARAKIRI, using only user HARAKIRI, because export overwrites it # Cannot overwrite global HARAKIRI with user's: https://git.io/fjYuD # harakiri = 45 -# TODO reduce http and socket timeouts after performance +# TODO reduce HTTP and socket timeouts after performance # improvements to DRS endpoint http-timeout = 60 socket-timeout = 60 From 51fc4a3d86777d4545b2a529e61c55cd8d23aa34 Mon Sep 17 00:00:00 2001 From: Alexander VT Date: Thu, 24 Mar 2022 14:23:53 -0500 Subject: [PATCH 207/211] chore(docs): add passport documentation --- docs/ga4gh_passports.md | 58 ++++++++++++++++++ docs/images/ga4gh/caching_after.png | Bin 0 -> 84313 bytes docs/images/ga4gh/caching_before.png | Bin 0 -> 75008 bytes docs/images/ga4gh/expiration.png | Bin 0 -> 965132 bytes docs/images/ga4gh/gen3_as_client.png | Bin 0 -> 44114 bytes .../ga4gh/gen3_as_client_and_drs_server.png | Bin 0 -> 63375 bytes docs/images/ga4gh/gen3_as_drs.png | Bin 0 -> 54456 bytes docs/images/ga4gh/passport_jwt_handling.png | Bin 0 -> 324342 bytes docs/images/ga4gh/passport_to_drs_flow.png | Bin 0 -> 436403 bytes 9 files changed, 58 insertions(+) create mode 100644 docs/ga4gh_passports.md create mode 100644 docs/images/ga4gh/caching_after.png create mode 100644 docs/images/ga4gh/caching_before.png create mode 100644 docs/images/ga4gh/expiration.png create mode 100644 docs/images/ga4gh/gen3_as_client.png create mode 100644 docs/images/ga4gh/gen3_as_client_and_drs_server.png create mode 100644 docs/images/ga4gh/gen3_as_drs.png create mode 100644 docs/images/ga4gh/passport_jwt_handling.png create mode 100644 docs/images/ga4gh/passport_to_drs_flow.png diff --git a/docs/ga4gh_passports.md b/docs/ga4gh_passports.md new file mode 100644 index 000000000..2ee168367 --- /dev/null +++ b/docs/ga4gh_passports.md @@ -0,0 +1,58 @@ +# Passport Support in Gen3 Framework Services (G3FS) + +G3FS will support a data access flow accepting Global Alliance for Genomics and Health (GA4GH) Passport(s) as means of authentication and authorization to access file objects. + +For National Institutes of Health (NIH) data, we will no longer rely on dbGaP User Access Telemetry files from the hourly usersync for authorization, but instead on NIH's Researcher Auth Service (RAS) Passports. + +The adoption of GA4GH specifications across NIH-funded Platforms is a strategic initiative that is pushed for on numerous fronts. + +> Our overall goal is interoperability through accepted standards (like GA4GH). + +As we are a GA4GH Driver Project, throughout the process of implementing passport support, we have identified numerous gaps and concerns with GA4GH’s specifications along the way. We are at a point now where most have been either addressed, waived, risks accepted, or solutions punted to a future version. There are ongoing discussions about modifications for the future. + +Please refer to official documentation about RAS Milestones for all historic and official decisions and designs related to RAS. This document will serve as an **unofficial technical overview** to maintainers of Gen3 and **may not be updated as regularly or represented as clearly as other public facing documents**. + +## Passport and Visa JSON Web Token (JWT) Handling + +Overview of the standards-based verification and validation flow for JWTs. This shows external DRS Client(s) communicating with Gen3 Framework Services (as a GA4GH DRS Server) and how G3FS interacts with Passport Brokers to validate and verify JWTs. + +![Passport and Visa JWT Handling](images/ga4gh/passport_jwt_handling.png) + +## G3FS: Configurable Roles for Data Access + +Gen3 Framework Services are capable of acting in many different roles. As data repositories (or DRS Servers in GA4GH terminology), as authorization decision makers (GA4GH Claims Clearinghouses), and/or as token issuers (GA4GH Passport Brokers). G3FS is also capable of being a client to other Passport Brokers. G3FS must be a client to an upstream Identity Provider (IdP) as it does not ever store user passwords but relies on authentication from another trusted source. + +In order to describe the role of the passport in these various configurations, the following diagrams may help. + +![Gen3 as DRS Server](images/ga4gh/gen3_as_drs.png) + +![Gen3 as Client](images/ga4gh/gen3_as_client.png) + +![Gen3 as Both](images/ga4gh/gen3_as_client_and_drs_server.png) + +## Performance Improvements + +In some respect, the support for passports required an auth re-architecture to: + +1. accept third-party generated token(s) to be a source of truth for authentication and authorization +2. parse that authorization information at the time of data access request (rather than synced before) + +Passports can be provided to our data access APIs before we've ever seen that user, whereas previously we used to bulk sync all authorization **before** data access (behind the scenes as a cronjob). Becuase of this new, dynamic authorization decision making upon data requests, we knew that we'd need to take extra steps to ensure non-degraded performance. + +We added a number of things to mitigate the performance impact on researchers' workflows. Most notably, we introduced a cache for valid passports such that when we recieve thousands of requests to access data and the _exact same_ passport is sent thousands of times over a few minutes, we are able to validate and parse it once and rely on that for subsequent requests. The cache only lives as long as policy and standards allow (which is usually less than an hour). + +To illustrate the need for such a cache, see the images below for before and after. + +![Before Caching](images/ga4gh/caching_before.png) + +![After Caching](images/ga4gh/caching_after.png) + +## Backend Updates and Expiration + +In order to ensure the removal of access at the right time, the cronjobs we have are updated based on the figure and notes below. We are requiring movement away from the deprecated, legacy, limited Fence authorization support in favor of the new policy engine (which allows expiration of policies out of the box). + +There is an argument here for event-based architecture, but Gen3 does not currently support such an architecture. We are instead extending the support of our cronjobs to ensure expirations occur at the right time. + +![Cronjobs and Expirations](images/ga4gh/expiration.png) + +> _All diagrams are originally from an **internal** CTDS Document. The link to that document is [here](https://lucid.app/lucidchart/5c52b868-5cd2-4c6e-b53b-de2981f7da98) for internal people who need to edit the above diagrams._ diff --git a/docs/images/ga4gh/caching_after.png b/docs/images/ga4gh/caching_after.png new file mode 100644 index 0000000000000000000000000000000000000000..e64dbc99888183ea36a32f8c684a6685a421e79c GIT binary patch literal 84313 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

lq;& zPChc@uQq*?>*+1FWzQXEl2XhJn`+9MOz_l^qJXyxj;mb)oS5l|y{oVXKNVVTi6>@m z+fYxs@_tC;b{HG=NZy4FTjlwlh8`W*6nz)slXHu(Bi*Zf-O4MwacVB6n2-g1a*rkQ=G>NAdNuUFRT{>E9`@-=Uw0LL0%~%vY8qkK?Ky_zZ6x zF6*4VD<}H9Y$bBLerYvgau8!((`@V?Yf~dW&$J+^{`ymCKf+Q;H*y^qH;({0T{-Y- z5p}xFAPn?7MM(@ywyCIu+m$$@)p8Gd$Tiw=iSd|vKU?g5i7_Jf8q?nc{Yc%OJ6p~e zuhwR4gDrGDE3lj(arhl4w_GB6sNyoBU5FN{eP@GUIN6Sh2$~>XBv%f_?Go%QVVCJb zrV^h`Zz$e5vEp}#7NYg$T7fW`#;#IKjy$I@rLQ*!CI{Yu+xlH>|J{qkfH)q6hWAne z$3ZS7EtI_tcB!W5b>(W%a6?$kkpw*uB6WJhRU8Od0+b=x{;0sH6x$?^*tyynu;y`{ z3Y{pPiApyq_~NwYee~Fzf?s0Gck^DMvDe#;g;bGpEnPK*Tv2ie{6`6b(xTO5)zNnO z@m!BjGhNBEf#x!%t7+7bL4e(Sy(Sq)iuaC^>t{1^*9QuIH5f;J^c~i9)II}E%w7E; z0YBVm$|cYK<3x5w9YE3*)k=<;NYAG)=8KKJ9+gBj05WC^9Qkq=tjpB{Pu9p?dL;`4W4GbCL@5)GbH7EWE%DjZ$jk^+MOp&<@RRo+>V zCSd#P@p3a!ZNV5PM|h6P_%GTIdO+B3+IavJexe>a(Yi}`BnqDTZa1UXq&iOJJ#<~v zw~?L#kZw9Ov|HPm6^Q71SRRgiz6tQr$A%XYd}TheWjiz#jL9NE557z%l4?bBvVnxo zgn}M_=PVr);D=+S8<7TSVhXA9q2$CuVBQ}Hghsxl#&_C12&Z!jK;TW-v{psMp}tsp z53c#pG;RxJC)u~`;0|wHMyM6l{WJEuvjPc35>IG}jyfH{?&pV%ijlSP6S1E_R_&aG zK-eCr2Tcf*A+%Ry!SX6)*y*4D`i7-_v!xjXF=d)kv|oI5Hgh4X2e7{rZYV8f{`#qB z+Xzz(f%plg4#ASJ49nlQcR5;Pr*FE=@5T4BJX=vLLg7w zh7WG@q))3hlAe!sxS^_Cg9!rHacNkkpfwRQd!6Io)M(=rmX}KTY7xyr`wlM@ z52Zz}A4k3q}hld$U$FH*X(TX2L&tU8OB_(xXvu zj1F>mZm;lIGtn#{x|+awAUOC@b||4mZyu!yrQtOJZzFnF$0k32)jz{zvna zPvdvBtop*&e>uzm9fm;i*FJ<37jHctpf~@{I;!$lVfyl|o z$w|q`OUWsi%POhLD5=UROUTHm%E){yUBCN(6}ao`>Ulf-|6Sny=CyuWU-d5=>sI}H H^TGcDS;NJS literal 0 HcmV?d00001 diff --git a/docs/images/ga4gh/caching_before.png b/docs/images/ga4gh/caching_before.png new file mode 100644 index 0000000000000000000000000000000000000000..d7371f06e7388a2d4e53373ae2fbde7e630ef763 GIT binary patch literal 75008 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 literal 0 HcmV?d00001 diff --git a/docs/images/ga4gh/expiration.png b/docs/images/ga4gh/expiration.png new file mode 100644 index 0000000000000000000000000000000000000000..87e269e58c8f423fe047709fe9c0cc09fa4439cf GIT binary patch literal 965132 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%0A+3^QLS4(_Mt29lYE(`pq#C&XT!c65 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