diff --git a/app.py b/app.py index 881299ef5..e28391615 100644 --- a/app.py +++ b/app.py @@ -99,7 +99,7 @@ app.config.update(environ_config) # Fix remote address handling for Flask. if app.config.get("PROXY_COUNT", 1): - app.wsgi_app = ProxyFix(app.wsgi_app) + app.wsgi_app = ProxyFix(app.wsgi_app) # type: ignore[method-assign] # Allow user to define a custom storage preference for the local instance. _distributed_storage_preference = os.environ.get("QUAY_DISTRIBUTED_STORAGE_PREFERENCE", "").split() @@ -124,7 +124,7 @@ features.import_features(app.config) # Register additional experimental artifact types. # TODO: extract this into a real, dynamic registration system. if features.GENERAL_OCI_SUPPORT: - for media_type, layer_types in app.config.get("ALLOWED_OCI_ARTIFACT_TYPES").items(): + for media_type, layer_types in app.config["ALLOWED_OCI_ARTIFACT_TYPES"].items(): register_artifact_type(media_type, layer_types) if features.HELM_OCI_SUPPORT: diff --git a/auth/test/test_registry_jwt.py b/auth/test/test_registry_jwt.py index 4ec1f8c6b..35bfdbb2e 100644 --- a/auth/test/test_registry_jwt.py +++ b/auth/test/test_registry_jwt.py @@ -6,7 +6,8 @@ import jwt import pytest from cryptography.hazmat.primitives import serialization -from app import app, instance_keys +from app import app as flask_app +from app import instance_keys from auth.auth_context_type import ValidatedAuthContext from auth.registry_jwt_auth import InvalidJWTException, identity_from_bearer_token from data import model # TODO: remove this after service keys are decoupled @@ -15,7 +16,7 @@ from initdb import finished_database_for_testing, setup_database_for_testing from util.morecollections import AttrDict from util.security.registry_jwt import ANONYMOUS_SUB, build_context_and_subject -TEST_AUDIENCE = app.config["SERVER_HOSTNAME"] +TEST_AUDIENCE = flask_app.config["SERVER_HOSTNAME"] TEST_USER = AttrDict({"username": "joeuser", "uuid": "foobar", "enabled": True}) MAX_SIGNED_S = 3660 TOKEN_VALIDITY_LIFETIME_S = 60 * 60 # 1 hour diff --git a/data/userfiles.py b/data/userfiles.py index 6df5894a3..a7c416b71 100644 --- a/data/userfiles.py +++ b/data/userfiles.py @@ -33,7 +33,7 @@ class UserfilesHandlers(View): buffered, mimetype=self._magic.from_buffer(file_header_bytes), as_attachment=True, - attachment_filename=file_id, + download_name=file_id, ) except IOError: logger.exception("Error reading user file") diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index 24c4cc194..5cedcfdbe 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -4,6 +4,7 @@ from calendar import timegm from email.utils import formatdate from functools import partial, wraps +import pytz from flask import Blueprint, request, session from flask_restful import Api, Resource, abort, reqparse from flask_restful.utils import unpack @@ -438,7 +439,7 @@ def require_fresh_login(func): ) if ( - last_login >= valid_span + last_login.replace(tzinfo=pytz.UTC) >= valid_span.replace(tzinfo=pytz.UTC) or not authentication.supports_fresh_login or not authentication.has_password_set(user.username) ): diff --git a/endpoints/api/test/test_appspecifictoken.py b/endpoints/api/test/test_appspecifictoken.py index f0b5236b1..5fdbc0d05 100644 --- a/endpoints/api/test/test_appspecifictoken.py +++ b/endpoints/api/test/test_appspecifictoken.py @@ -7,8 +7,8 @@ from endpoints.api.test.shared import conduct_api_call from endpoints.test.shared import client_with_identity -def test_app_specific_tokens(app, client): - with client_with_identity("devtable", client) as cl: +def test_app_specific_tokens(app): + with client_with_identity("devtable", app) as cl: # Add an app specific token. token_data = {"title": "Testing 123"} resp = conduct_api_call(cl, AppTokens, "POST", None, token_data, 200).json @@ -41,11 +41,11 @@ def test_app_specific_tokens(app, client): conduct_api_call(cl, AppToken, "GET", {"token_uuid": token_uuid}, None, 404) -def test_delete_expired_app_token(app, client): +def test_delete_expired_app_token(app): user = model.user.get_user("devtable") expiration = datetime.now() - timedelta(seconds=10) token = model.appspecifictoken.create_token(user, "some token", expiration) - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: # Delete the token. conduct_api_call(cl, AppToken, "DELETE", {"token_uuid": token.uuid}, None, 204) diff --git a/endpoints/api/test/test_disallow_for_apps.py b/endpoints/api/test/test_disallow_for_apps.py index a539687c4..6ad8430e6 100644 --- a/endpoints/api/test/test_disallow_for_apps.py +++ b/endpoints/api/test/test_disallow_for_apps.py @@ -85,7 +85,7 @@ FIELD_ARGS = {"trigger_uuid": "1234", "field_name": "foobar"} (BuildTriggerSourceNamespaces, "get", TRIGGER_ARGS), ], ) -def test_disallowed_for_apps(resource, method, params, client): +def test_disallowed_for_apps(resource, method, params, app): namespace = "devtable" repository = "someapprepo" @@ -95,5 +95,5 @@ def test_disallowed_for_apps(resource, method, params, client): params = params or {} params["repository"] = "%s/%s" % (namespace, repository) - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: conduct_api_call(cl, resource, method, params, None, 501) diff --git a/endpoints/api/test/test_disallow_for_nonnormal.py b/endpoints/api/test/test_disallow_for_nonnormal.py index 18b954644..bac29c095 100644 --- a/endpoints/api/test/test_disallow_for_nonnormal.py +++ b/endpoints/api/test/test_disallow_for_nonnormal.py @@ -58,7 +58,7 @@ FIELD_ARGS = {"trigger_uuid": "1234", "field_name": "foobar"} (BuildTriggerSources, "post", TRIGGER_ARGS), ], ) -def test_disallowed_for_nonnormal(state, resource, method, params, client): +def test_disallowed_for_nonnormal(state, resource, method, params, app): namespace = "devtable" repository = "somenewstaterepo" @@ -70,5 +70,5 @@ def test_disallowed_for_nonnormal(state, resource, method, params, client): params = params or {} params["repository"] = "%s/%s" % (namespace, repository) - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: conduct_api_call(cl, resource, method, params, {}, 503) diff --git a/endpoints/api/test/test_endtoend_auth.py b/endpoints/api/test/test_endtoend_auth.py index 330dbd1c6..6e43f916e 100644 --- a/endpoints/api/test/test_endtoend_auth.py +++ b/endpoints/api/test/test_endtoend_auth.py @@ -48,10 +48,10 @@ def test_entity_search(auth_engine, requires_email, client): assert entity["kind"] == "external" -def test_link_external_entity(auth_engine, requires_email, client): +def test_link_external_entity(auth_engine, requires_email, app): with auth_engine(requires_email=requires_email) as auth: with patch("endpoints.api.search.authentication", auth): - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: # Try an unknown user. conduct_api_call( cl, diff --git a/endpoints/api/test/test_logs.py b/endpoints/api/test/test_logs.py index 736cacd03..568427d42 100644 --- a/endpoints/api/test/test_logs.py +++ b/endpoints/api/test/test_logs.py @@ -15,16 +15,15 @@ from endpoints.test.shared import client_with_identity os.environ.get("TEST_DATABASE_URI", "").find("mysql") >= 0, reason="Queue code is very sensitive to times on MySQL, making this flaky", ) -def test_export_logs(client): - with client_with_identity("devtable", client) as cl: - assert export_action_logs_queue.get() is None +def test_export_logs(app): + timecode = time.time() - timecode = time.time() + def get_time(): + return timecode - 2 - def get_time(): - return timecode - 2 - - with patch("time.time", get_time): + with patch("time.time", get_time): + with client_with_identity("devtable", app) as cl: + assert export_action_logs_queue.get() is None # Call to export logs. body = { "callback_url": "http://some/url", @@ -39,13 +38,13 @@ def test_export_logs(client): assert export_action_logs_queue.get() is not None -def test_invalid_date_range(client): +def test_invalid_date_range(app): starttime = "02/02/2020" endtime = "01/01/2020" parsed_starttime, parsed_endtime = _validate_logs_arguments(starttime, endtime) assert parsed_starttime >= parsed_endtime - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: conduct_api_call( cl, OrgLogs, diff --git a/endpoints/api/test/test_manifest.py b/endpoints/api/test/test_manifest.py index 7b8c75faa..d9ea4cd28 100644 --- a/endpoints/api/test/test_manifest.py +++ b/endpoints/api/test/test_manifest.py @@ -6,8 +6,8 @@ from endpoints.api.test.shared import conduct_api_call from endpoints.test.shared import client_with_identity -def test_repository_manifest(client): - with client_with_identity("devtable", client) as cl: +def test_repository_manifest(app): + with client_with_identity("devtable", app) as cl: repo_ref = registry_model.lookup_repository("devtable", "simple") tags = registry_model.list_all_active_repository_tags(repo_ref) for tag in tags: diff --git a/endpoints/api/test/test_mirror.py b/endpoints/api/test/test_mirror.py index 695f3e701..2623a8b9a 100644 --- a/endpoints/api/test/test_mirror.py +++ b/endpoints/api/test/test_mirror.py @@ -49,7 +49,7 @@ def _setup_mirror(): ("admin", "admin"), ], ) -def test_create_mirror_sets_permissions(existing_robot_permission, expected_permission, client): +def test_create_mirror_sets_permissions(existing_robot_permission, expected_permission, app): mirror_bot, _ = model.user.create_robot( "newmirrorbot", model.user.get_namespace_user("devtable") ) @@ -59,7 +59,7 @@ def test_create_mirror_sets_permissions(existing_robot_permission, expected_perm mirror_bot.username, "devtable", "simple", existing_robot_permission ) - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: params = {"repository": "devtable/simple"} request_body = { "external_reference": "quay.io/foobar/barbaz", @@ -78,25 +78,25 @@ def test_create_mirror_sets_permissions(existing_robot_permission, expected_perm assert config.root_rule.rule_value == ["latest", "foo", "bar"] -def test_get_mirror_does_not_exist(client): - with client_with_identity("devtable", client) as cl: +def test_get_mirror_does_not_exist(app): + with client_with_identity("devtable", app) as cl: params = {"repository": "devtable/simple"} resp = conduct_api_call(cl, RepoMirrorResource, "GET", params, None, 404) -def test_get_repo_does_not_exist(client): - with client_with_identity("devtable", client) as cl: +def test_get_repo_does_not_exist(app): + with client_with_identity("devtable", app) as cl: params = {"repository": "devtable/unicorn"} resp = conduct_api_call(cl, RepoMirrorResource, "GET", params, None, 404) -def test_get_mirror(client): +def test_get_mirror(app): """ Verify that performing a `GET` request returns expected and accurate data. """ mirror = _setup_mirror() - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: params = {"repository": "devtable/simple"} resp = conduct_api_call(cl, RepoMirrorResource, "GET", params, None, 200).json @@ -171,13 +171,13 @@ def test_get_mirror(client): ("root_rule", {"rule_kind": "incorrect", "rule_value": ["3.1", "3.1*"]}, 400), ], ) -def test_change_config(key, value, expected_status, client): +def test_change_config(key, value, expected_status, app): """ Verify that changing each attribute works as expected. """ mirror = _setup_mirror() - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: params = {"repository": "devtable/simple"} if key in ("http_proxy", "https_proxy", "no_proxy"): request_body = {"external_registry_config": {"proxy": {key: value}}} @@ -187,7 +187,7 @@ def test_change_config(key, value, expected_status, client): request_body = {key: value} conduct_api_call(cl, RepoMirrorResource, "PUT", params, request_body, expected_status) - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: params = {"repository": "devtable/simple"} resp = conduct_api_call(cl, RepoMirrorResource, "GET", params, None, 200) @@ -240,12 +240,12 @@ def test_change_config(key, value, expected_status, client): ({"external_registry_username": "", "external_registry_password": ""}, 201), ], ) -def test_change_credentials(request_body, expected_status, client): +def test_change_credentials(request_body, expected_status, app): """ Verify credentials can only be modified as a pair. """ mirror = _setup_mirror() - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: params = {"repository": "devtable/simple"} conduct_api_call(cl, RepoMirrorResource, "PUT", params, request_body, expected_status) diff --git a/endpoints/api/test/test_organization.py b/endpoints/api/test/test_organization.py index aee2d0f60..4f05317e6 100644 --- a/endpoints/api/test/test_organization.py +++ b/endpoints/api/test/test_organization.py @@ -17,8 +17,8 @@ from endpoints.test.shared import client_with_identity (100000000000000000000, 400), ], ) -def test_change_tag_expiration(expiration, expected_code, client): - with client_with_identity("devtable", client) as cl: +def test_change_tag_expiration(expiration, expected_code, app): + with client_with_identity("devtable", app) as cl: conduct_api_call( cl, Organization, @@ -29,10 +29,10 @@ def test_change_tag_expiration(expiration, expected_code, client): ) -def test_get_organization_collaborators(client): +def test_get_organization_collaborators(app): params = {"orgname": "buynlarge"} - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: resp = conduct_api_call(cl, OrganizationCollaboratorList, "GET", params) collaborator_names = [c["name"] for c in resp.json["collaborators"]] diff --git a/endpoints/api/test/test_permission.py b/endpoints/api/test/test_permission.py index 470144601..d2e9535ca 100644 --- a/endpoints/api/test/test_permission.py +++ b/endpoints/api/test/test_permission.py @@ -18,8 +18,8 @@ from endpoints.test.shared import client_with_identity pytest.param("buynlarge/orgrepo", "buynlarge+coolrobot", 200, id="valid robot under org"), ], ) -def test_robot_permission(repository, username, expected_code, client): - with client_with_identity("devtable", client) as cl: +def test_robot_permission(repository, username, expected_code, app): + with client_with_identity("devtable", app) as cl: conduct_api_call( cl, RepositoryUserPermission, diff --git a/endpoints/api/test/test_repository.py b/endpoints/api/test/test_repository.py index eb70f6832..4e25b48f5 100644 --- a/endpoints/api/test/test_repository.py +++ b/endpoints/api/test/test_repository.py @@ -19,29 +19,29 @@ from features import FeatureNameValue ("invalid_req", False, 400), ], ) -def test_post_changetrust(trust_enabled, repo_found, expected_status, client): +def test_post_changetrust(trust_enabled, repo_found, expected_status, app): with patch("endpoints.api.repository.tuf_metadata_api") as mock_tuf: with patch( "endpoints.api.repository_models_pre_oci.model.repository.get_repository" ) as mock_model: mock_model.return_value = MagicMock() if repo_found else None mock_tuf.get_default_tags_with_expiration.return_value = ["tags", "expiration"] - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: params = {"repository": "devtable/repo"} request_body = {"trust_enabled": trust_enabled} conduct_api_call(cl, RepositoryTrust, "POST", params, request_body, expected_status) -def test_signing_disabled(client): +def test_signing_disabled(app): with patch("features.SIGNING", FeatureNameValue("SIGNING", False)): - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: params = {"repository": "devtable/simple"} response = conduct_api_call(cl, Repository, "GET", params).json assert not response["trust_enabled"] -def test_list_starred_repos(client): - with client_with_identity("devtable", client) as cl: +def test_list_starred_repos(app): + with client_with_identity("devtable", app) as cl: params = { "starred": "true", } @@ -70,8 +70,8 @@ def test_list_starred_repos(client): assert "public/publicrepo" not in repos -def test_list_repos(client, initialized_db): - with client_with_identity("devtable", client) as cl: +def test_list_repos(initialized_db, app): + with client_with_identity("devtable", app) as cl: params = {"starred": "true", "repo_kind": "application"} response = conduct_api_call(cl, RepositoryList, "GET", params).json repo_states = {r["state"] for r in response["repositories"]} @@ -79,8 +79,8 @@ def test_list_repos(client, initialized_db): assert state in ["NORMAL", "MIRROR", "READ_ONLY", "MARKED_FOR_DELETION"] -def test_list_starred_app_repos(client, initialized_db): - with client_with_identity("devtable", client) as cl: +def test_list_starred_app_repos(initialized_db, app): + with client_with_identity("devtable", app) as cl: params = {"starred": "true", "repo_kind": "application"} devtable = model.user.get_user("devtable") @@ -94,8 +94,8 @@ def test_list_starred_app_repos(client, initialized_db): assert "devtable/someappr" in repos -def test_list_repositories_last_modified(client): - with client_with_identity("devtable", client) as cl: +def test_list_repositories_last_modified(app): + with client_with_identity("devtable", app) as cl: params = { "namespace": "devtable", "last_modified": "true", @@ -127,12 +127,12 @@ def test_list_repositories_last_modified(client): pytest.param("devtable/nested1/nested2", True, 201, id="Slashes Allowed Multiple Levels"), ], ) -def test_create_repository(repo_name, extended_repo_names, expected_status, client): +def test_create_repository(repo_name, extended_repo_names, expected_status, app): with patch( "features.EXTENDED_REPOSITORY_NAMES", FeatureNameValue("EXTENDED_REPOSITORY_NAMES", extended_repo_names), ): - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: body = { "namespace": "devtable", "repository": repo_name, @@ -141,7 +141,7 @@ def test_create_repository(repo_name, extended_repo_names, expected_status, clie } result = conduct_api_call( - client, RepositoryList, "post", None, body, expected_code=expected_status + cl, RepositoryList, "post", None, body, expected_code=expected_status ).json if expected_status == 201: assert result["name"] == repo_name @@ -155,8 +155,8 @@ def test_create_repository(repo_name, extended_repo_names, expected_status, clie False, ], ) -def test_get_repo(has_tag_manifest, client, initialized_db): - with client_with_identity("devtable", client) as cl: +def test_get_repo(has_tag_manifest, initialized_db, app): + with client_with_identity("devtable", app) as cl: params = {"repository": "devtable/simple"} response = conduct_api_call(cl, Repository, "GET", params).json assert response["kind"] == "image" @@ -171,8 +171,8 @@ def test_get_repo(has_tag_manifest, client, initialized_db): (database.RepositoryState.MIRROR, False), ], ) -def test_get_repo_state_can_write(state, can_write, client, initialized_db): - with client_with_identity("devtable", client) as cl: +def test_get_repo_state_can_write(state, can_write, initialized_db, app): + with client_with_identity("devtable", app) as cl: params = {"repository": "devtable/simple"} response = conduct_api_call(cl, Repository, "GET", params).json assert response["can_write"] @@ -181,14 +181,14 @@ def test_get_repo_state_can_write(state, can_write, client, initialized_db): repo.state = state repo.save() - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: params = {"repository": "devtable/simple"} response = conduct_api_call(cl, Repository, "GET", params).json assert response["can_write"] == can_write -def test_delete_repo(client, initialized_db): - with client_with_identity("devtable", client) as cl: +def test_delete_repo(initialized_db, app): + with client_with_identity("devtable", app) as cl: resp = conduct_api_call(cl, RepositoryList, "GET", {"namespace": "devtable"}).json repos = {repo["name"] for repo in resp["repositories"]} assert "simple" in repos diff --git a/endpoints/api/test/test_repositorynotification.py b/endpoints/api/test/test_repositorynotification.py index d26f5dcef..868130b64 100644 --- a/endpoints/api/test/test_repositorynotification.py +++ b/endpoints/api/test/test_repositorynotification.py @@ -14,8 +14,8 @@ from endpoints.test.shared import client_with_identity @pytest.fixture() -def authd_client(client): - with client_with_identity("devtable", client) as cl: +def authd_client(app): + with client_with_identity("devtable", app) as cl: yield cl diff --git a/endpoints/api/test/test_robot.py b/endpoints/api/test/test_robot.py index fd7d01142..d36770504 100644 --- a/endpoints/api/test/test_robot.py +++ b/endpoints/api/test/test_robot.py @@ -28,8 +28,8 @@ from util.names import parse_robot_username {"description": "this is a description", "unstructured_metadata": {"foo": "bar"}}, ], ) -def test_create_robot_with_metadata(endpoint, body, client): - with client_with_identity("devtable", client) as cl: +def test_create_robot_with_metadata(endpoint, body, app): + with client_with_identity("devtable", app) as cl: # Create the robot with the specified body. conduct_api_call( cl, @@ -63,8 +63,8 @@ def test_create_robot_with_metadata(endpoint, body, client): (OrgRobot, {"orgname": "buynlarge", "robot_shortname": "coolrobot"}), ], ) -def test_retrieve_robot(endpoint, params, app, client): - with client_with_identity("devtable", client) as cl: +def test_retrieve_robot(endpoint, params, app): + with client_with_identity("devtable", app) as cl: result = conduct_api_call(cl, endpoint, "GET", params, None) assert result.json["token"] is not None @@ -91,13 +91,13 @@ def test_retrieve_robot(endpoint, params, app, client): 5, ], ) -def test_retrieve_robots(endpoint, params, bot_endpoint, include_token, limit, app, client): +def test_retrieve_robots(endpoint, params, bot_endpoint, include_token, limit, app): params["token"] = "true" if include_token else "false" if limit is not None: params["limit"] = limit - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: result = conduct_api_call(cl, endpoint, "GET", params, None) if limit is not None: @@ -126,8 +126,8 @@ def test_retrieve_robots(endpoint, params, bot_endpoint, include_token, limit, a False, ], ) -def test_retrieve_robots_token_permission(username, is_admin, with_permissions, app, client): - with client_with_identity(username, client) as cl: +def test_retrieve_robots_token_permission(username, is_admin, with_permissions, app): + with client_with_identity(username, app) as cl: params = {"orgname": "buynlarge", "token": "true"} if with_permissions: params["permissions"] = "true" diff --git a/endpoints/api/test/test_search.py b/endpoints/api/test/test_search.py index e5e33bbb2..9e76266d3 100644 --- a/endpoints/api/test/test_search.py +++ b/endpoints/api/test/test_search.py @@ -19,12 +19,12 @@ from endpoints.test.shared import client_with_identity ("repository"), ], ) -def test_repository_search(query, client): +def test_repository_search(query, app): # Prime the caches. database.Repository.kind.get_id("image") database.Repository.kind.get_name(1) - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: params = {"query": query} with assert_query_count(4): result = conduct_api_call(cl, ConductRepositorySearch, "GET", params, None, 200).json @@ -41,8 +41,8 @@ def test_repository_search(query, client): ("repository"), ], ) -def test_search_query_count(query, client): - with client_with_identity("devtable", client) as cl: +def test_search_query_count(query, app): + with client_with_identity("devtable", app) as cl: params = {"query": query} with assert_query_count(10): result = conduct_api_call(cl, ConductSearch, "GET", params, None, 200).json @@ -62,7 +62,7 @@ def test_search_query_count(query, client): 6, ], ) -def test_repository_search_pagination(page_count, client): +def test_repository_search_pagination(page_count, app): # Create at least a few pages of results. all_repositories = set() user = model.user.get_user("devtable") @@ -71,7 +71,7 @@ def test_repository_search_pagination(page_count, client): all_repositories.add(repo_name) model.repository.create_repository("devtable", repo_name, user) - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: for page_index in range(0, page_count): params = {"query": "somerepo", "page": page_index + 1} diff --git a/endpoints/api/test/test_security.py b/endpoints/api/test/test_security.py index 374dffbc9..830b6ee62 100644 --- a/endpoints/api/test/test_security.py +++ b/endpoints/api/test/test_security.py @@ -41,7 +41,7 @@ from endpoints.api.namespacequota import * from endpoints.api.repository import Repository -from test.fixtures import * +from test.fixtures import * # type: ignore[assignment] # isort: skip ORG_PARAMS = {"orgname": "buynlarge"} TEAM_PARAMS = {"orgname": "buynlarge", "teamname": "owners"} @@ -6062,8 +6062,8 @@ SECURITY_TESTS: List[ @pytest.mark.parametrize("resource,method,params,body,identity,expected", SECURITY_TESTS) -def test_api_security(resource, method, params, body, identity, expected, client): - with client_with_identity(identity, client) as cl: +def test_api_security(resource, method, params, body, identity, expected, app): + with client_with_identity(identity, app) as cl: conduct_api_call(cl, resource, method, params, body, expected) @@ -6122,13 +6122,13 @@ def test_all_apis_tested(app): ("DELETE", 200), ], ) -def test_team_sync_security(is_superuser, allow_nonsuperuser, method, expected, client): +def test_team_sync_security(is_superuser, allow_nonsuperuser, method, expected, app): def is_superuser_method(_): return is_superuser with patch("auth.permissions.usermanager.is_superuser", is_superuser_method): with toggle_feature("NONSUPERUSER_TEAM_SYNCING_SETUP", allow_nonsuperuser): - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: expect_success = is_superuser or allow_nonsuperuser expected_status = expected if expect_success else 403 conduct_api_call( diff --git a/endpoints/api/test/test_signing.py b/endpoints/api/test/test_signing.py index b055f779b..542eca7ae 100644 --- a/endpoints/api/test/test_signing.py +++ b/endpoints/api/test/test_signing.py @@ -47,10 +47,10 @@ def tags_equal(expected, actual): (None, {"delegations": None}), # API returns None on exceptions ], ) -def test_get_signatures(targets_map, expected, client): +def test_get_signatures(targets_map, expected, app): with patch("endpoints.api.signing.tuf_metadata_api") as mock_tuf: mock_tuf.get_all_tags_with_expiration.return_value = targets_map - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: params = {"repository": "devtable/trusted"} assert tags_equal( expected, conduct_api_call(cl, RepositorySignatures, "GET", params, None, 200).json diff --git a/endpoints/api/test/test_superuser.py b/endpoints/api/test/test_superuser.py index 75f462041..c7b5d0412 100644 --- a/endpoints/api/test/test_superuser.py +++ b/endpoints/api/test/test_superuser.py @@ -19,8 +19,8 @@ from endpoints.test.shared import client_with_identity (False), ], ) -def test_list_all_users(disabled, client): - with client_with_identity("devtable", client) as cl: +def test_list_all_users(disabled, app): + with client_with_identity("devtable", app) as cl: params = {"disabled": disabled} result = conduct_api_call(cl, SuperUserList, "GET", params, None, 200).json assert len(result["users"]) @@ -29,14 +29,14 @@ def test_list_all_users(disabled, client): assert user["enabled"] -def test_list_all_orgs(client): - with client_with_identity("devtable", client) as cl: +def test_list_all_orgs(app): + with client_with_identity("devtable", app) as cl: result = conduct_api_call(cl, SuperUserOrganizationList, "GET", None, None, 200).json assert len(result["organizations"]) == 5 -def test_paginate_orgs(client): - with client_with_identity("devtable", client) as cl: +def test_paginate_orgs(app): + with client_with_identity("devtable", app) as cl: params = {"limit": 3} firstResult = conduct_api_call(cl, SuperUserOrganizationList, "GET", params, None, 200).json assert len(firstResult["organizations"]) == 3 @@ -49,8 +49,8 @@ def test_paginate_orgs(client): assert secondResult.get("next_page", None) is None -def test_paginate_test_list_all_users(client): - with client_with_identity("devtable", client) as cl: +def test_paginate_test_list_all_users(app): + with client_with_identity("devtable", app) as cl: params = {"limit": 6} firstResult = conduct_api_call(cl, SuperUserList, "GET", params, None, 200).json assert len(firstResult["users"]) == 6 @@ -61,8 +61,8 @@ def test_paginate_test_list_all_users(client): assert secondResult.get("next_page", None) is None -def test_change_install_user(client): - with client_with_identity("devtable", client) as cl: +def test_change_install_user(app): + with client_with_identity("devtable", app) as cl: params = {"username": "randomuser"} body = {"email": "new_email123@test.com"} result = conduct_api_call(cl, SuperUserManagement, "PUT", params, body, 200).json diff --git a/endpoints/api/test/test_tag.py b/endpoints/api/test/test_tag.py index 10f58acdd..a5c8e845b 100644 --- a/endpoints/api/test/test_tag.py +++ b/endpoints/api/test/test_tag.py @@ -17,8 +17,8 @@ from endpoints.test.shared import client_with_identity ("aksdjhasd", 400), ], ) -def test_change_tag_expiration_default(expiration_time, expected_status, client, app): - with client_with_identity("devtable", client) as cl: +def test_change_tag_expiration_default(expiration_time, expected_status, app): + with client_with_identity("devtable", app) as cl: params = { "repository": "devtable/simple", "tag": "latest", @@ -31,8 +31,8 @@ def test_change_tag_expiration_default(expiration_time, expected_status, client, conduct_api_call(cl, RepositoryTag, "put", params, request_body, expected_status) -def test_change_tag_expiration(client, app): - with client_with_identity("devtable", client) as cl: +def test_change_tag_expiration(app): + with client_with_identity("devtable", app) as cl: params = { "repository": "devtable/simple", "tag": "latest", @@ -68,8 +68,8 @@ def test_change_tag_expiration(client, app): (True, "newtag", 201), ], ) -def test_move_tag(manifest_exists, test_tag, expected_status, client, app): - with client_with_identity("devtable", client) as cl: +def test_move_tag(manifest_exists, test_tag, expected_status, app): + with client_with_identity("devtable", app) as cl: test_image = "unknown" if manifest_exists: repo_ref = registry_model.lookup_repository("devtable", "simple") @@ -98,12 +98,12 @@ def test_move_tag(manifest_exists, test_tag, expected_status, client, app): ("buynlarge", "anotherorgrepo", 6), # +2 for permissions checks. ], ) -def test_list_repo_tags(repo_namespace, repo_name, client, query_count, app): +def test_list_repo_tags(repo_namespace, repo_name, query_count, app): # Pre-cache media type loads to ensure consistent query count. Manifest.media_type.get_name(1) params = {"repository": repo_namespace + "/" + repo_name} - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: with assert_query_count(query_count): tags = conduct_api_call(cl, ListRepositoryTags, "get", params).json["tags"] @@ -118,22 +118,22 @@ def test_list_repo_tags(repo_namespace, repo_name, client, query_count, app): ("devtable", "gargantuan", 4), ], ) -def test_list_repo_tags_filter(repo_namespace, repo_name, client, query_count, app): +def test_list_repo_tags_filter(repo_namespace, repo_name, query_count, app): Manifest.media_type.get_name(1) params = {"repository": repo_namespace + "/" + repo_name} - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: with assert_query_count(query_count): params["filter_tag_name"] = "like:v" tags = conduct_api_call(cl, ListRepositoryTags, "get", params).json["tags"] assert len(tags) == 5 - with client_with_identity("devtable", client) as cl: - with assert_query_count(query_count): + with client_with_identity("devtable", app) as cl: + with assert_query_count(query_count - 1): params["filter_tag_name"] = "eq:prod" tags = conduct_api_call(cl, ListRepositoryTags, "get", params).json["tags"] assert len(tags) == 1 - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: params["filter_tag_name"] = "random" resp = conduct_api_call(cl, ListRepositoryTags, "get", params, None, expected_code=400) diff --git a/endpoints/api/test/test_team.py b/endpoints/api/test/test_team.py index 731a38f52..445f3fc0e 100644 --- a/endpoints/api/test/test_team.py +++ b/endpoints/api/test/test_team.py @@ -15,10 +15,10 @@ SYNCED_TEAM_PARAMS = {"orgname": "sellnsmall", "teamname": "synced"} UNSYNCED_TEAM_PARAMS = {"orgname": "sellnsmall", "teamname": "owners"} -def test_team_syncing(client): +def test_team_syncing(app): with mock_ldap() as ldap: with patch("endpoints.api.team.authentication", ldap): - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: config = { "group_dn": "cn=AwesomeFolk", } @@ -42,25 +42,32 @@ def test_team_syncing(client): assert sync_info is None -def test_team_member_sync_info(client): +def test_team_member_sync_info_unsynced_superuser(app): with mock_ldap() as ldap: with patch("endpoints.api.team.authentication", ldap): # Check for an unsynced team, with superuser. - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: resp = conduct_api_call(cl, TeamMemberList, "GET", UNSYNCED_TEAM_PARAMS) assert "can_sync" in resp.json assert resp.json["can_sync"]["service"] == "ldap" - assert "synced" not in resp.json + +def test_team_member_sync_info_unsynced_nonsuperuser(app): + with mock_ldap() as ldap: + with patch("endpoints.api.team.authentication", ldap): # Check for an unsynced team, with non-superuser. - with client_with_identity("randomuser", client) as cl: + with client_with_identity("randomuser", app) as cl: resp = conduct_api_call(cl, TeamMemberList, "GET", UNSYNCED_TEAM_PARAMS) assert "can_sync" not in resp.json assert "synced" not in resp.json + +def test_team_member_sync_info_synced_superuser(app): + with mock_ldap() as ldap: + with patch("endpoints.api.team.authentication", ldap): # Check for a synced team, with superuser. - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: resp = conduct_api_call(cl, TeamMemberList, "GET", SYNCED_TEAM_PARAMS) assert "can_sync" in resp.json assert resp.json["can_sync"]["service"] == "ldap" @@ -69,8 +76,12 @@ def test_team_member_sync_info(client): assert "last_updated" in resp.json["synced"] assert "group_dn" in resp.json["synced"]["config"] + +def test_team_member_sync_info_synced_nonsuperuser(app): + with mock_ldap() as ldap: + with patch("endpoints.api.team.authentication", ldap): # Check for a synced team, with non-superuser. - with client_with_identity("randomuser", client) as cl: + with client_with_identity("randomuser", app) as cl: resp = conduct_api_call(cl, TeamMemberList, "GET", SYNCED_TEAM_PARAMS) assert "can_sync" not in resp.json @@ -79,11 +90,11 @@ def test_team_member_sync_info(client): assert "config" not in resp.json["synced"] -def test_organization_teams_sync_bool(client): +def test_organization_teams_sync_bool(app): with mock_ldap() as ldap: with patch("endpoints.api.organization.authentication", ldap): # Ensure synced teams are marked as such in the organization teams list. - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: resp = conduct_api_call(cl, Organization, "GET", {"orgname": "sellnsmall"}) assert not resp.json["teams"]["owners"]["is_synced"] diff --git a/endpoints/api/test/test_trigger.py b/endpoints/api/test/test_trigger.py index 594711de7..43469e5ae 100644 --- a/endpoints/api/test/test_trigger.py +++ b/endpoints/api/test/test_trigger.py @@ -32,7 +32,7 @@ def test_super_user_build_endpoints(context, dockerfile_path, expected): assert is_parent(context, dockerfile_path) == expected -def test_enabled_disabled_trigger(app, client): +def test_enabled_disabled_trigger(app): trigger = model.build.list_build_triggers("devtable", "building")[0] trigger.config = json.dumps({"hook_id": "someid"}) trigger.save() @@ -46,7 +46,7 @@ def test_enabled_disabled_trigger(app, client): "enabled": False, } - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: result = conduct_api_call(cl, BuildTrigger, "PUT", params, body, 200).json assert not result["enabled"] @@ -54,6 +54,6 @@ def test_enabled_disabled_trigger(app, client): "enabled": True, } - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: result = conduct_api_call(cl, BuildTrigger, "PUT", params, body, 200).json assert result["enabled"] diff --git a/endpoints/api/test/test_user.py b/endpoints/api/test/test_user.py index 1f7d1a955..88041fc1d 100644 --- a/endpoints/api/test/test_user.py +++ b/endpoints/api/test/test_user.py @@ -10,9 +10,9 @@ from endpoints.test.shared import client_with_identity, conduct_call from features import FeatureNameValue -def test_user_metadata_update(client): +def test_user_metadata_update(app): with patch("features.USER_METADATA", FeatureNameValue("USER_METADATA", True)): - with client_with_identity("devtable", client) as cl: + with client_with_identity("devtable", app) as cl: metadata = { "given_name": "Quay", "family_name": "User", diff --git a/endpoints/decorators.py b/endpoints/decorators.py index ae2ec816d..01fc43f7b 100644 --- a/endpoints/decorators.py +++ b/endpoints/decorators.py @@ -6,7 +6,8 @@ import logging import os from functools import wraps -from flask import abort, make_response, request +from flask import request +from ua_parser import user_agent_parser import features from app import app, ip_resolver, model_cache, usermanager @@ -235,11 +236,40 @@ def require_xhr_from_browser(func): text attacks. """ + # https://github.com/pallets/werkzeug/issues/2078 + browsers = ( + "aol", + "ask", + "camino", + "chrome", + "firefox", + "galeon", + "google", + "kmeleon", + "konqueror", + "links", + "lynx", + "msie", + "msn", + "netscape", + "opera", + "safari", + "seamonkey", + "webkit", + "yahoo", + ) + @wraps(func) def wrapper(*args, **kwargs): if app.config.get("BROWSER_API_CALLS_XHR_ONLY", False): - if request.method == "GET" and request.user_agent.browser: + if ( + request.method == "GET" + and request.user_agent.string + and user_agent_parser.ParseUserAgent(request.user_agent.string)["family"].lower() + in browsers + ): has_xhr_header = request.headers.get("X-Requested-With") == "XMLHttpRequest" + if not has_xhr_header and not app.config.get("DEBUGGING") == True: logger.warning( "Disallowed possible RTA to URL %s with user agent %s", diff --git a/endpoints/test/shared.py b/endpoints/test/shared.py index ef9d7ad6e..9c519aa1c 100644 --- a/endpoints/test/shared.py +++ b/endpoints/test/shared.py @@ -12,21 +12,21 @@ CSRF_TOKEN_KEY = "_csrf_token" @contextmanager -def client_with_identity(auth_username, client): - with client.session_transaction() as sess: - if auth_username and auth_username is not None: - loaded = model.user.get_user(auth_username) - sess["user_id"] = loaded.uuid - sess["login_time"] = datetime.datetime.now() - else: - sess["user_id"] = "anonymous" +def client_with_identity(auth_username, app): + if auth_username and auth_username is not None: + loaded = model.user.get_user(auth_username) + else: + loaded = None - yield client + with app.test_client(user=loaded) as cl: + yield cl - with client.session_transaction() as sess: - sess["user_id"] = None - sess["login_time"] = None - sess[CSRF_TOKEN_KEY] = None + with cl.session_transaction() as sess: + sess["_user_id"] = None + sess["user_id"] = None + sess["_fresh"] = False + sess["login_time"] = None + sess[CSRF_TOKEN_KEY] = None @contextmanager diff --git a/endpoints/test/test_anon_checked.py b/endpoints/test/test_anon_checked.py index 08bb47cac..5a36c5000 100644 --- a/endpoints/test/test_anon_checked.py +++ b/endpoints/test/test_anon_checked.py @@ -18,7 +18,15 @@ def test_verify_blueprint(blueprint): self.first_registration = True self.app = app - def add_url_rule(self, rule, endpoint, view_function, methods=None): + def add_url_rule( + self, + rule, + endpoint=None, + view_function=None, + methods=None, + provide_automatic_options=None, + **options, + ): result = "__anon_protected" in dir(view_function) or "__anon_allowed" in dir( view_function ) diff --git a/endpoints/test/test_decorators.py b/endpoints/test/test_decorators.py index d9587c466..c95244d0f 100644 --- a/endpoints/test/test_decorators.py +++ b/endpoints/test/test_decorators.py @@ -15,7 +15,11 @@ from endpoints.test.shared import conduct_call ("curl/whatever", False, 200), ("Mozilla/whatever", True, 200), ("Mozilla/5.0", True, 200), - ("Mozilla/5.0 (Windows NT 5.1; Win64; x64)", False, 400), + ( + "Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/534.34 (KHTML, like Gecko) Safari/534.34", + False, + 400, + ), ], ) def test_require_xhr_from_browser(user_agent, include_header, expected_code, app, client): diff --git a/mypy.ini b/mypy.ini index 9d2f47848..35cf31d65 100644 --- a/mypy.ini +++ b/mypy.ini @@ -183,6 +183,9 @@ ignore_missing_imports = True [mypy-tldextract] ignore_missing_imports = True +[mypy-ua_parser] +ignore_missing_imports = True + [mypy-werkzeug.*] ignore_missing_imports = True diff --git a/oauth/provider.py b/oauth/provider.py index e58ff970e..8b077f654 100644 --- a/oauth/provider.py +++ b/oauth/provider.py @@ -6,11 +6,7 @@ import logging from io import StringIO from requests import Response - -try: - from werkzeug.exceptions import Unauthorized -except ImportError: - Unauthorized = Exception +from werkzeug.exceptions import Unauthorized from oauth import utils diff --git a/path_converters.py b/path_converters.py index f1a43f92e..119c1d730 100644 --- a/path_converters.py +++ b/path_converters.py @@ -3,7 +3,15 @@ from werkzeug.routing import BaseConverter import features -class APIRepositoryPathConverter(BaseConverter): +class QuayBaseConverter(BaseConverter): + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + + if "part_isolating" not in cls.__dict__: + cls.part_isolating = "/" not in cls.regex + + +class APIRepositoryPathConverter(QuayBaseConverter): """ Converter for handling repository paths. @@ -17,7 +25,7 @@ class APIRepositoryPathConverter(BaseConverter): # TODO(kleesc): Remove after fully deprecating V1 push/pull -class V1CreateRepositoryPathConverter(BaseConverter): +class V1CreateRepositoryPathConverter(QuayBaseConverter): """ Converter for handling PUT repository path. Handles both library and non-library paths (if configured). @@ -41,7 +49,7 @@ class V1CreateRepositoryPathConverter(BaseConverter): self.regex = r"([^/]+(/[^/]+)+)(? uri - """ - - def create_uri(repository_and_namespace, checksum): - """ - Creates a uri for a download blob from a repository, namespace, and checksum from earlier - context. - """ - with context: - relative_layer_url = url_for( - "v2.download_blob", repository=repository_and_namespace, digest=checksum - ) - return urljoin(url_scheme_and_hostname.get_url(), relative_layer_url) - - return create_uri diff --git a/util/secscan/test/test_secscan_util.py b/util/secscan/test/test_secscan_util.py deleted file mode 100644 index c6bca27ac..000000000 --- a/util/secscan/test/test_secscan_util.py +++ /dev/null @@ -1,28 +0,0 @@ -import pytest - -from app import app -from util.config import URLSchemeAndHostname -from util.secscan.secscan_util import get_blob_download_uri_getter - -from test.fixtures import * - - -@pytest.mark.parametrize( - "url_scheme_and_hostname, repo_namespace, checksum, expected_value,", - [ - ( - URLSchemeAndHostname("http", "localhost:5000"), - "devtable/simple", - "tarsum+sha256:123", - "http://localhost:5000/v2/devtable/simple/blobs/tarsum%2Bsha256:123", - ), - ], -) -def test_blob_download_uri_getter( - app, url_scheme_and_hostname, repo_namespace, checksum, expected_value -): - blob_uri_getter = get_blob_download_uri_getter( - app.test_request_context("/"), url_scheme_and_hostname - ) - - assert blob_uri_getter(repo_namespace, checksum) == expected_value