From 988f50a7f63328a9133d98f607da8a34ab16ac09 Mon Sep 17 00:00:00 2001 From: mprahl Date: Mar 03 2020 19:48:47 +0000 Subject: Move auth.py to web/auth.py --- diff --git a/module_build_service/auth.py b/module_build_service/auth.py deleted file mode 100644 index 2d92acd..0000000 --- a/module_build_service/auth.py +++ /dev/null @@ -1,234 +0,0 @@ -# -*- coding: utf-8 -*- -# SPDX-License-Identifier: MIT -"""Auth system based on the client certificate and FAS account""" -import json -import ssl - -import requests -from flask import g - -from dogpile.cache import make_region - -from module_build_service.errors import Unauthorized, Forbidden -from module_build_service import app, log, conf - -try: - import ldap3 -except ImportError: - log.warning("ldap3 import not found. ldap/krb disabled.") - - -client_secrets = None -region = make_region().configure("dogpile.cache.memory") - - -def _json_loads(content): - if not isinstance(content, str): - content = content.decode("utf-8") - return json.loads(content) - - -def _load_secrets(): - global client_secrets - if client_secrets: - return - - if "OIDC_CLIENT_SECRETS" not in app.config: - raise Forbidden("OIDC_CLIENT_SECRETS must be set in server config.") - - secrets = _json_loads(open(app.config["OIDC_CLIENT_SECRETS"], "r").read()) - client_secrets = list(secrets.values())[0] - - -def _get_token_info(token): - """ - Asks the token_introspection_uri for the validity of a token. - """ - if not client_secrets: - return None - - request = { - "token": token, - "token_type_hint": "Bearer", - "client_id": client_secrets["client_id"], - "client_secret": client_secrets["client_secret"], - } - headers = {"Content-type": "application/x-www-form-urlencoded"} - - resp = requests.post(client_secrets["token_introspection_uri"], data=request, headers=headers) - return resp.json() - - -def _get_user_info(token): - """ - Asks the userinfo_uri for more information on a user. - """ - if not client_secrets: - return None - - headers = {"authorization": "Bearer " + token} - resp = requests.get(client_secrets["userinfo_uri"], headers=headers) - return resp.json() - - -def get_user_oidc(request): - """ - Returns the client's username and groups based on the OIDC token provided. - """ - _load_secrets() - - if "authorization" not in request.headers: - raise Unauthorized("No 'authorization' header found.") - - header = request.headers["authorization"].strip() - prefix = "Bearer " - if not header.startswith(prefix): - raise Unauthorized("Authorization headers must start with %r" % prefix) - - token = header[len(prefix):].strip() - try: - data = _get_token_info(token) - except Exception as e: - error = "Cannot verify OIDC token: %s" % str(e) - log.exception(error) - raise Exception(error) - - if not data or "active" not in data or not data["active"]: - raise Unauthorized("OIDC token invalid or expired.") - - if "OIDC_REQUIRED_SCOPE" not in app.config: - raise Forbidden("OIDC_REQUIRED_SCOPE must be set in server config.") - - presented_scopes = data["scope"].split(" ") - required_scopes = [ - "openid", - "https://id.fedoraproject.org/scope/groups", - app.config["OIDC_REQUIRED_SCOPE"], - ] - for scope in required_scopes: - if scope not in presented_scopes: - raise Unauthorized("Required OIDC scope %r not present: %r" % (scope, presented_scopes)) - - try: - extended_data = _get_user_info(token) - except Exception: - error = "OpenIDC auth error: Cannot determine the user's groups" - log.exception(error) - raise Unauthorized(error) - - username = data["username"] - # If the user is part of the whitelist, then the group membership check is skipped - if username in conf.allowed_users: - groups = set() - else: - try: - groups = set(extended_data["groups"]) - except Exception: - error = "Could not find groups in UserInfo from OIDC" - log.exception("%s (extended_data: %s)", error, extended_data) - raise Unauthorized(error) - - return username, groups - - -def get_user_kerberos(request): - remote_name = request.environ.get("REMOTE_USER") - if not remote_name: - # When Kerberos authentication is enabled, MBS expects the - # authentication is done by a specific Apache module which sets - # REMOTE_USER properly. - raise Unauthorized("No REMOTE_USER is set.") - - try: - username, realm = remote_name.split("@") - except ValueError: - raise Unauthorized("Value of REMOTE_NAME is not in format username@REALM") - - # Currently, MBS does not handle the realm to authorize user. Just keep it - # here for any possible further use. - - # If the user is part of the whitelist, then the group membership check is skipped - if username in conf.allowed_users: - groups = [] - else: - groups = get_ldap_group_membership(username) - return username, set(groups) - - -@region.cache_on_arguments() -def get_ldap_group_membership(uid): - """ Small wrapper on getting the group membership so that we can use caching - :param uid: a string of the uid of the user - :return: a list of groups the user is a member of - """ - ldap_con = Ldap() - return ldap_con.get_user_membership(uid) - - -class Ldap(object): - """ A class that handles LDAP connections and queries - """ - - connection = None - base_dn = None - - def __init__(self): - if not conf.ldap_uri: - raise Forbidden("LDAP_URI must be set in server config.") - if conf.ldap_groups_dn: - self.base_dn = conf.ldap_groups_dn - else: - raise Forbidden("LDAP_GROUPS_DN must be set in server config.") - - if conf.ldap_uri.startswith("ldaps://"): - tls = ldap3.Tls( - ca_certs_file="/etc/pki/tls/certs/ca-bundle.crt", validate=ssl.CERT_REQUIRED) - server = ldap3.Server(conf.ldap_uri, use_ssl=True, tls=tls) - else: - server = ldap3.Server(conf.ldap_uri) - self.connection = ldap3.Connection(server) - try: - self.connection.open() - except ldap3.core.exceptions.LDAPSocketOpenError as error: - log.error( - 'The connection to "{0}" failed. The following error was raised: {1}'.format( - conf.ldap_uri, str(error))) - raise Forbidden( - "The connection to the LDAP server failed. Group membership couldn't be obtained.") - - def get_user_membership(self, uid): - """ Gets the group membership of a user - :param uid: a string of the uid of the user - :return: a list of common names of the posixGroups the user is a member of - """ - ldap_filter = "(memberUid={0})".format(uid) - # Only get the groups in the base container/OU - self.connection.search( - self.base_dn, ldap_filter, search_scope=ldap3.LEVEL, attributes=["cn"]) - groups = self.connection.response - try: - return [group["attributes"]["cn"][0] for group in groups] - except KeyError: - log.exception( - "The LDAP groups could not be determined based on the search results " - 'of "{0}"'.format(str(groups))) - return [] - - -def get_user(request): - """ Authenticates the user and returns the username and group name - :param request: a Flask request - :return: a tuple with a string representing the user name and a set with the user's group - membership such as ('mprahl', {'factory2', 'devel'}) - """ - if conf.no_auth is True: - log.debug("Authorization is disabled.") - return "anonymous", {"packager"} - - if "user" not in g and "groups" not in g: - get_user_func_name = "get_user_{0}".format(conf.auth_method) - get_user_func = globals().get(get_user_func_name) - if not get_user_func: - raise RuntimeError('The function "{0}" is not implemented'.format(get_user_func_name)) - g.user, g.groups = get_user_func(request) - return g.user, g.groups diff --git a/module_build_service/views.py b/module_build_service/views.py index d1ab46d..7eb9167 100644 --- a/module_build_service/views.py +++ b/module_build_service/views.py @@ -5,7 +5,6 @@ This is the implementation of the orchestrator's public RESTful API. """ import json -import module_build_service.auth from flask import request, url_for, Blueprint, Response from flask.views import MethodView from six import string_types @@ -18,6 +17,7 @@ from module_build_service.errors import ValidationError, Forbidden, NotFound, Pr from module_build_service.backports import jsonify from module_build_service.monitor import registry from module_build_service.common.submit import fetch_mmd +import module_build_service.web.auth from module_build_service.web.submit import ( submit_module_build_from_scm, submit_module_build_from_yaml ) @@ -178,7 +178,7 @@ class ModuleBuildAPI(AbstractQueryableBuildAPI): @validate_api_version() def patch(self, api_version, id): - username, groups = module_build_service.auth.get_user(request) + username, groups = module_build_service.web.auth.get_user(request) try: r = json.loads(request.get_data().decode("utf-8")) @@ -289,7 +289,7 @@ class ImportModuleAPI(MethodView): raise Forbidden("Import module API is disabled.") # auth checks - username, groups = module_build_service.auth.get_user(request) + username, groups = module_build_service.web.auth.get_user(request) ModuleBuildAPI.check_groups( username, groups, allowed_groups=conf.allowed_groups_to_import_module) @@ -348,7 +348,7 @@ class BaseHandler(object): } def __init__(self, request, data=None): - self.username, self.groups = module_build_service.auth.get_user(request) + self.username, self.groups = module_build_service.web.auth.get_user(request) self.data = data or _dict_from_request(request) # canonicalize and validate scratch option diff --git a/module_build_service/web/auth.py b/module_build_service/web/auth.py new file mode 100644 index 0000000..2d92acd --- /dev/null +++ b/module_build_service/web/auth.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT +"""Auth system based on the client certificate and FAS account""" +import json +import ssl + +import requests +from flask import g + +from dogpile.cache import make_region + +from module_build_service.errors import Unauthorized, Forbidden +from module_build_service import app, log, conf + +try: + import ldap3 +except ImportError: + log.warning("ldap3 import not found. ldap/krb disabled.") + + +client_secrets = None +region = make_region().configure("dogpile.cache.memory") + + +def _json_loads(content): + if not isinstance(content, str): + content = content.decode("utf-8") + return json.loads(content) + + +def _load_secrets(): + global client_secrets + if client_secrets: + return + + if "OIDC_CLIENT_SECRETS" not in app.config: + raise Forbidden("OIDC_CLIENT_SECRETS must be set in server config.") + + secrets = _json_loads(open(app.config["OIDC_CLIENT_SECRETS"], "r").read()) + client_secrets = list(secrets.values())[0] + + +def _get_token_info(token): + """ + Asks the token_introspection_uri for the validity of a token. + """ + if not client_secrets: + return None + + request = { + "token": token, + "token_type_hint": "Bearer", + "client_id": client_secrets["client_id"], + "client_secret": client_secrets["client_secret"], + } + headers = {"Content-type": "application/x-www-form-urlencoded"} + + resp = requests.post(client_secrets["token_introspection_uri"], data=request, headers=headers) + return resp.json() + + +def _get_user_info(token): + """ + Asks the userinfo_uri for more information on a user. + """ + if not client_secrets: + return None + + headers = {"authorization": "Bearer " + token} + resp = requests.get(client_secrets["userinfo_uri"], headers=headers) + return resp.json() + + +def get_user_oidc(request): + """ + Returns the client's username and groups based on the OIDC token provided. + """ + _load_secrets() + + if "authorization" not in request.headers: + raise Unauthorized("No 'authorization' header found.") + + header = request.headers["authorization"].strip() + prefix = "Bearer " + if not header.startswith(prefix): + raise Unauthorized("Authorization headers must start with %r" % prefix) + + token = header[len(prefix):].strip() + try: + data = _get_token_info(token) + except Exception as e: + error = "Cannot verify OIDC token: %s" % str(e) + log.exception(error) + raise Exception(error) + + if not data or "active" not in data or not data["active"]: + raise Unauthorized("OIDC token invalid or expired.") + + if "OIDC_REQUIRED_SCOPE" not in app.config: + raise Forbidden("OIDC_REQUIRED_SCOPE must be set in server config.") + + presented_scopes = data["scope"].split(" ") + required_scopes = [ + "openid", + "https://id.fedoraproject.org/scope/groups", + app.config["OIDC_REQUIRED_SCOPE"], + ] + for scope in required_scopes: + if scope not in presented_scopes: + raise Unauthorized("Required OIDC scope %r not present: %r" % (scope, presented_scopes)) + + try: + extended_data = _get_user_info(token) + except Exception: + error = "OpenIDC auth error: Cannot determine the user's groups" + log.exception(error) + raise Unauthorized(error) + + username = data["username"] + # If the user is part of the whitelist, then the group membership check is skipped + if username in conf.allowed_users: + groups = set() + else: + try: + groups = set(extended_data["groups"]) + except Exception: + error = "Could not find groups in UserInfo from OIDC" + log.exception("%s (extended_data: %s)", error, extended_data) + raise Unauthorized(error) + + return username, groups + + +def get_user_kerberos(request): + remote_name = request.environ.get("REMOTE_USER") + if not remote_name: + # When Kerberos authentication is enabled, MBS expects the + # authentication is done by a specific Apache module which sets + # REMOTE_USER properly. + raise Unauthorized("No REMOTE_USER is set.") + + try: + username, realm = remote_name.split("@") + except ValueError: + raise Unauthorized("Value of REMOTE_NAME is not in format username@REALM") + + # Currently, MBS does not handle the realm to authorize user. Just keep it + # here for any possible further use. + + # If the user is part of the whitelist, then the group membership check is skipped + if username in conf.allowed_users: + groups = [] + else: + groups = get_ldap_group_membership(username) + return username, set(groups) + + +@region.cache_on_arguments() +def get_ldap_group_membership(uid): + """ Small wrapper on getting the group membership so that we can use caching + :param uid: a string of the uid of the user + :return: a list of groups the user is a member of + """ + ldap_con = Ldap() + return ldap_con.get_user_membership(uid) + + +class Ldap(object): + """ A class that handles LDAP connections and queries + """ + + connection = None + base_dn = None + + def __init__(self): + if not conf.ldap_uri: + raise Forbidden("LDAP_URI must be set in server config.") + if conf.ldap_groups_dn: + self.base_dn = conf.ldap_groups_dn + else: + raise Forbidden("LDAP_GROUPS_DN must be set in server config.") + + if conf.ldap_uri.startswith("ldaps://"): + tls = ldap3.Tls( + ca_certs_file="/etc/pki/tls/certs/ca-bundle.crt", validate=ssl.CERT_REQUIRED) + server = ldap3.Server(conf.ldap_uri, use_ssl=True, tls=tls) + else: + server = ldap3.Server(conf.ldap_uri) + self.connection = ldap3.Connection(server) + try: + self.connection.open() + except ldap3.core.exceptions.LDAPSocketOpenError as error: + log.error( + 'The connection to "{0}" failed. The following error was raised: {1}'.format( + conf.ldap_uri, str(error))) + raise Forbidden( + "The connection to the LDAP server failed. Group membership couldn't be obtained.") + + def get_user_membership(self, uid): + """ Gets the group membership of a user + :param uid: a string of the uid of the user + :return: a list of common names of the posixGroups the user is a member of + """ + ldap_filter = "(memberUid={0})".format(uid) + # Only get the groups in the base container/OU + self.connection.search( + self.base_dn, ldap_filter, search_scope=ldap3.LEVEL, attributes=["cn"]) + groups = self.connection.response + try: + return [group["attributes"]["cn"][0] for group in groups] + except KeyError: + log.exception( + "The LDAP groups could not be determined based on the search results " + 'of "{0}"'.format(str(groups))) + return [] + + +def get_user(request): + """ Authenticates the user and returns the username and group name + :param request: a Flask request + :return: a tuple with a string representing the user name and a set with the user's group + membership such as ('mprahl', {'factory2', 'devel'}) + """ + if conf.no_auth is True: + log.debug("Authorization is disabled.") + return "anonymous", {"packager"} + + if "user" not in g and "groups" not in g: + get_user_func_name = "get_user_{0}".format(conf.auth_method) + get_user_func = globals().get(get_user_func_name) + if not get_user_func: + raise RuntimeError('The function "{0}" is not implemented'.format(get_user_func_name)) + g.user, g.groups = get_user_func(request) + return g.user, g.groups diff --git a/tests/test_auth.py b/tests/test_auth.py deleted file mode 100644 index b72fe5d..0000000 --- a/tests/test_auth.py +++ /dev/null @@ -1,245 +0,0 @@ -# -*- coding: utf-8 -*- -# SPDX-License-Identifier: MIT -from os import path - -import pytest -import requests -import mock -from mock import patch, PropertyMock, Mock - -import module_build_service.auth -import module_build_service.errors -import module_build_service.config as mbs_config -from module_build_service import app - - -class TestAuthModule: - def test_get_user_no_token(self): - base_dir = path.abspath(path.dirname(__file__)) - client_secrets = path.join(base_dir, "client_secrets.json") - with patch.dict( - "module_build_service.app.config", - {"OIDC_CLIENT_SECRETS": client_secrets, "OIDC_REQUIRED_SCOPE": "mbs-scope"}, - ): - request = mock.MagicMock() - request.cookies.return_value = {} - - with pytest.raises(module_build_service.errors.Unauthorized) as cm: - with app.app_context(): - module_build_service.auth.get_user(request) - assert str(cm.value) == "No 'authorization' header found." - - @patch("module_build_service.auth._get_token_info") - @patch("module_build_service.auth._get_user_info") - def test_get_user_failure(self, get_user_info, get_token_info): - base_dir = path.abspath(path.dirname(__file__)) - client_secrets = path.join(base_dir, "client_secrets.json") - with patch.dict( - "module_build_service.app.config", - {"OIDC_CLIENT_SECRETS": client_secrets, "OIDC_REQUIRED_SCOPE": "mbs-scope"}, - ): - # https://www.youtube.com/watch?v=G-LtddOgUCE - name = "Joey Jo Jo Junior Shabadoo" - mocked_get_token_info = { - "active": False, - "username": name, - "scope": "openid https://id.fedoraproject.org/scope/groups mbs-scope" - } - get_token_info.return_value = mocked_get_token_info - - get_user_info.return_value = {"groups": ["group"]} - - headers = {"authorization": "Bearer foobar"} - request = mock.MagicMock() - request.headers.return_value = mock.MagicMock(spec_set=dict) - request.headers.__getitem__.side_effect = headers.__getitem__ - request.headers.__setitem__.side_effect = headers.__setitem__ - request.headers.__contains__.side_effect = headers.__contains__ - - with pytest.raises(module_build_service.errors.Unauthorized) as cm: - with app.app_context(): - module_build_service.auth.get_user(request) - assert str(cm.value) == "OIDC token invalid or expired." - - @patch("module_build_service.auth._get_token_info") - @patch("module_build_service.auth._get_user_info") - def test_get_user_not_in_groups(self, get_user_info, get_token_info): - base_dir = path.abspath(path.dirname(__file__)) - client_secrets = path.join(base_dir, "client_secrets.json") - with patch.dict( - "module_build_service.app.config", - {"OIDC_CLIENT_SECRETS": client_secrets, "OIDC_REQUIRED_SCOPE": "mbs-scope"}, - ): - # https://www.youtube.com/watch?v=G-LtddOgUCE - name = "Joey Jo Jo Junior Shabadoo" - mocked_get_token_info = { - "active": True, - "username": name, - "scope": "openid https://id.fedoraproject.org/scope/groups mbs-scope" - } - get_token_info.return_value = mocked_get_token_info - - get_user_info.side_effect = requests.Timeout("It happens...") - - headers = {"authorization": "Bearer foobar"} - request = mock.MagicMock() - request.headers.return_value = mock.MagicMock(spec_set=dict) - request.headers.__getitem__.side_effect = headers.__getitem__ - request.headers.__setitem__.side_effect = headers.__setitem__ - request.headers.__contains__.side_effect = headers.__contains__ - - with pytest.raises(module_build_service.errors.Unauthorized) as cm: - with app.app_context(): - module_build_service.auth.get_user(request) - assert str(cm.value) == "OpenIDC auth error: Cannot determine the user's groups" - - @pytest.mark.parametrize("allowed_users", (set(), {"Joey Jo Jo Junior Shabadoo"})) - @patch.object(mbs_config.Config, "allowed_users", new_callable=PropertyMock) - @patch("module_build_service.auth._get_token_info") - @patch("module_build_service.auth._get_user_info") - def test_get_user_good(self, get_user_info, get_token_info, m_allowed_users, allowed_users): - m_allowed_users.return_value = allowed_users - base_dir = path.abspath(path.dirname(__file__)) - client_secrets = path.join(base_dir, "client_secrets.json") - with patch.dict( - "module_build_service.app.config", - {"OIDC_CLIENT_SECRETS": client_secrets, "OIDC_REQUIRED_SCOPE": "mbs-scope"}, - ): - # https://www.youtube.com/watch?v=G-LtddOgUCE - name = "Joey Jo Jo Junior Shabadoo" - mocked_get_token_info = { - "active": True, - "username": name, - "scope": ("openid https://id.fedoraproject.org/scope/groups mbs-scope"), - } - get_token_info.return_value = mocked_get_token_info - - get_user_info.return_value = {"groups": ["group"]} - - headers = {"authorization": "Bearer foobar"} - request = mock.MagicMock() - request.headers.return_value = mock.MagicMock(spec_set=dict) - request.headers.__getitem__.side_effect = headers.__getitem__ - request.headers.__setitem__.side_effect = headers.__setitem__ - request.headers.__contains__.side_effect = headers.__contains__ - - with app.app_context(): - username, groups = module_build_service.auth.get_user(request) - username_second_call, groups_second_call = module_build_service.auth.get_user( - request) - assert username == name - if allowed_users: - assert groups == set() - else: - assert groups == set(get_user_info.return_value["groups"]) - - # Test the real auth method has been called just once. - get_user_info.assert_called_once() - assert username_second_call == username - assert groups_second_call == groups - - @patch.object(mbs_config.Config, "no_auth", new_callable=PropertyMock, return_value=True) - def test_disable_authentication(self, conf_no_auth): - request = mock.MagicMock() - username, groups = module_build_service.auth.get_user(request) - assert username == "anonymous" - assert groups == {"packager"} - - @patch("module_build_service.auth.client_secrets", None) - def test_misconfiguring_oidc_client_secrets_should_be_failed(self): - request = mock.MagicMock() - with pytest.raises(module_build_service.errors.Forbidden) as cm: - with app.app_context(): - module_build_service.auth.get_user(request) - assert str(cm.value) == "OIDC_CLIENT_SECRETS must be set in server config." - - @patch("module_build_service.auth._get_token_info") - @patch("module_build_service.auth._get_user_info") - def test_get_required_scope_not_present(self, get_user_info, get_token_info): - base_dir = path.abspath(path.dirname(__file__)) - client_secrets = path.join(base_dir, "client_secrets.json") - with patch.dict( - "module_build_service.app.config", - {"OIDC_CLIENT_SECRETS": client_secrets, "OIDC_REQUIRED_SCOPE": "mbs-scope"}, - ): - # https://www.youtube.com/watch?v=G-LtddOgUCE - name = "Joey Jo Jo Junior Shabadoo" - mocked_get_token_info = { - "active": True, - "username": name, - "scope": "openid https://id.fedoraproject.org/scope/groups", - } - get_token_info.return_value = mocked_get_token_info - - get_user_info.return_value = {"groups": ["group"]} - - headers = {"authorization": "Bearer foobar"} - request = mock.MagicMock() - request.headers.return_value = mock.MagicMock(spec_set=dict) - request.headers.__getitem__.side_effect = headers.__getitem__ - request.headers.__setitem__.side_effect = headers.__setitem__ - request.headers.__contains__.side_effect = headers.__contains__ - - with pytest.raises(module_build_service.errors.Unauthorized) as cm: - with app.app_context(): - module_build_service.auth.get_user(request) - assert str(cm.value) == ( - "Required OIDC scope 'mbs-scope' not present: " - "['openid', 'https://id.fedoraproject.org/scope/groups']" - ) - - @patch("module_build_service.auth._get_token_info") - @patch("module_build_service.auth._get_user_info") - def test_get_required_scope_not_set_in_cfg(self, get_user_info, get_token_info): - base_dir = path.abspath(path.dirname(__file__)) - client_secrets = path.join(base_dir, "client_secrets.json") - with patch.dict("module_build_service.app.config", {"OIDC_CLIENT_SECRETS": client_secrets}): - # https://www.youtube.com/watch?v=G-LtddOgUCE - name = "Joey Jo Jo Junior Shabadoo" - mocked_get_token_info = { - "active": True, - "username": name, - "scope": "openid https://id.fedoraproject.org/scope/groups", - } - get_token_info.return_value = mocked_get_token_info - - get_user_info.return_value = {"groups": ["group"]} - - headers = {"authorization": "Bearer foobar"} - request = mock.MagicMock() - request.headers.return_value = mock.MagicMock(spec_set=dict) - request.headers.__getitem__.side_effect = headers.__getitem__ - request.headers.__setitem__.side_effect = headers.__setitem__ - request.headers.__contains__.side_effect = headers.__contains__ - - with pytest.raises(module_build_service.errors.Forbidden) as cm: - with app.app_context(): - module_build_service.auth.get_user(request) - assert str(cm.value) == "OIDC_REQUIRED_SCOPE must be set in server config." - - @pytest.mark.parametrize("remote_name", ["", None, "someone"]) - def test_get_user_kerberos_unauthorized(self, remote_name): - request = Mock() - request.environ.get.return_value = remote_name - - with pytest.raises(module_build_service.errors.Unauthorized): - module_build_service.auth.get_user_kerberos(request) - - @patch.object(module_build_service.auth.conf, "allowed_users", new=["someone", "somebody"]) - def test_get_user_kerberos_user_is_in_allowed_users_group(self): - request = Mock() - request.environ.get.return_value = "someone@realm" - - username, groups = module_build_service.auth.get_user_kerberos(request) - assert "someone" == username - assert set() == groups - - @patch.object(module_build_service.auth.conf, "allowed_users", new=["someone", "somebody"]) - @patch("module_build_service.auth.get_ldap_group_membership", return_value=["group1", "group2"]) - def test_get_user_kerberos_user_is_not_in_allowed_users_group(self, get_ldap_group_membership): - request = Mock() - request.environ.get.return_value = "x-man@realm" - - username, groups = module_build_service.auth.get_user_kerberos(request) - assert "x-man" == username - assert {"group1", "group2"} == groups diff --git a/tests/test_build/test_build.py b/tests/test_build/test_build.py index 1ce55d7..fb7a6d4 100644 --- a/tests/test_build/test_build.py +++ b/tests/test_build/test_build.py @@ -479,7 +479,7 @@ class TestBuild(BaseTestBuild): pass @pytest.mark.parametrize("mmd_version", [1, 2]) - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_normal( self, mocked_scm, mocked_get_user, conf_system, dbg, hmsc, mmd_version @@ -558,7 +558,7 @@ class TestBuild(BaseTestBuild): assert len(module_build.module_builds_trace) == 5 @patch("module_build_service.builder.KojiModuleBuilder.get_session") - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_buildonly( self, mocked_scm, mocked_get_user, mocked_get_session, conf_system, dbg, hmsc @@ -626,7 +626,7 @@ class TestBuild(BaseTestBuild): ] @pytest.mark.parametrize("gating_result", (True, False)) - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_no_components( self, mocked_scm, mocked_get_user, conf_system, dbg, hmsc, gating_result @@ -673,7 +673,7 @@ class TestBuild(BaseTestBuild): return_value=True, ) @patch("module_build_service.common.submit._is_eol_in_pdc", return_value=True) - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_eol_module( self, mocked_scm, mocked_get_user, is_eol, check, conf_system, dbg, hmsc @@ -699,7 +699,7 @@ class TestBuild(BaseTestBuild): assert data["status"] == 400 assert data["message"] == u"Module python3:master is marked as EOL in PDC." - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_from_yaml_not_allowed( self, mocked_scm, mocked_get_user, conf_system, dbg, hmsc @@ -724,7 +724,7 @@ class TestBuild(BaseTestBuild): assert data["status"] == 403 assert data["message"] == "YAML submission is not enabled" - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_from_yaml_allowed( self, mocked_scm, mocked_get_user, conf_system, dbg, hmsc @@ -754,7 +754,7 @@ class TestBuild(BaseTestBuild): module_build = models.ModuleBuild.get_by_id(db_session, module_build_id) assert module_build.state == models.BUILD_STATES["ready"] - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_cancel( self, mocked_scm, mocked_get_user, conf_system, dbg, hmsc @@ -816,7 +816,7 @@ class TestBuild(BaseTestBuild): if build.task_id: assert build.task_id in cancelled_tasks - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_instant_complete( self, mocked_scm, mocked_get_user, conf_system, dbg, hmsc @@ -852,7 +852,7 @@ class TestBuild(BaseTestBuild): models.BUILD_STATES["ready"], ] - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch( "module_build_service.config.Config.num_concurrent_builds", @@ -911,7 +911,7 @@ class TestBuild(BaseTestBuild): models.BUILD_STATES["ready"], ] - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch( "module_build_service.config.Config.num_concurrent_builds", @@ -976,7 +976,7 @@ class TestBuild(BaseTestBuild): num_builds = [k for k, g in itertools.groupby(TestBuild._global_var)] assert num_builds.count(1) == 2 - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch( "module_build_service.config.Config.num_concurrent_builds", @@ -1045,7 +1045,7 @@ class TestBuild(BaseTestBuild): # there were failed components in batch 2. assert c.module_build.batch == 2 - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch( "module_build_service.config.Config.num_concurrent_builds", @@ -1105,7 +1105,7 @@ class TestBuild(BaseTestBuild): assert c.module_build.batch == 2 @pytest.mark.usefixtures("reuse_component_init_data") - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_reuse_all( self, mocked_scm, mocked_get_user, conf_system, dbg, hmsc @@ -1177,7 +1177,7 @@ class TestBuild(BaseTestBuild): assert build.reused_component_id == reused_component_ids[build.package] @pytest.mark.usefixtures("reuse_component_init_data") - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_reuse_all_without_build_macros( self, mocked_scm, mocked_get_user, conf_system, dbg, hmsc @@ -1255,7 +1255,7 @@ class TestBuild(BaseTestBuild): ] assert build.package != "module-build-macros" - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_resume( self, mocked_scm, mocked_get_user, conf_system, dbg, hmsc @@ -1390,7 +1390,7 @@ class TestBuild(BaseTestBuild): models.BUILD_STATES["ready"], ] - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_resume_recover_orphaned_macros( self, mocked_scm, mocked_get_user, conf_system, dbg, hmsc @@ -1511,7 +1511,7 @@ class TestBuild(BaseTestBuild): models.BUILD_STATES["ready"], ] - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_resume_failed_init( self, mocked_scm, mocked_get_user, conf_system, dbg, hmsc @@ -1584,7 +1584,7 @@ class TestBuild(BaseTestBuild): models.BUILD_STATES["ready"], ] - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_resume_init_fail( self, mocked_scm, mocked_get_user, conf_system, dbg, hmsc @@ -1626,7 +1626,7 @@ class TestBuild(BaseTestBuild): } assert data == expected - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch( "module_build_service.config.Config.modules_allow_scratch", @@ -1669,7 +1669,7 @@ class TestBuild(BaseTestBuild): # make sure scratch build has expected context with unique suffix assert module_build.context == "9c690d0e_1" - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch( "module_build_service.config.Config.modules_allow_scratch", @@ -1713,7 +1713,7 @@ class TestBuild(BaseTestBuild): # make sure normal build has expected context without suffix assert module_build.context == "9c690d0e" - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch( "module_build_service.config.Config.modules_allow_scratch", @@ -1755,7 +1755,7 @@ class TestBuild(BaseTestBuild): # make sure second scratch build has expected context with unique suffix assert module_build.context == "9c690d0e_2" - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_repo_regen_not_started_batch( self, mocked_scm, mocked_get_user, conf_system, dbg, hmsc @@ -1831,7 +1831,7 @@ class TestBuild(BaseTestBuild): module = models.ModuleBuild.get_by_id(db_session, module_build_id) assert module.state == models.BUILD_STATES["build"] - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_br_metadata_only_module( self, mocked_scm, mocked_get_user, conf_system, dbg, hmsc @@ -1893,7 +1893,7 @@ class TestLocalBuild(BaseTestBuild): pass @patch("module_build_service.scheduler.handlers.modules.handle_stream_collision_modules") - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch( "module_build_service.config.Config.mock_resultsdir", diff --git a/tests/test_views/test_views.py b/tests/test_views/test_views.py index d49f25b..777cd84 100644 --- a/tests/test_views/test_views.py +++ b/tests/test_views/test_views.py @@ -930,7 +930,7 @@ class TestViews: assert data["meta"]["total"] == 0 @pytest.mark.parametrize("api_version", [1, 2]) - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build(self, mocked_scm, mocked_get_user, api_version): FakeSCM( @@ -984,7 +984,7 @@ class TestViews: assert module.buildrequires[0].context == "00000000" assert module.buildrequires[0].stream_version == 280000 - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_no_base_module(self, mocked_scm, mocked_get_user): FakeSCM( @@ -1012,7 +1012,7 @@ class TestViews: "error": "Unprocessable Entity", } - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch( "module_build_service.config.Config.rebuild_strategy_allow_override", @@ -1037,7 +1037,7 @@ class TestViews: data = json.loads(rv.data) assert data["rebuild_strategy"] == "only-changed" - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch( "module_build_service.config.Config.rebuild_strategies_allowed", @@ -1075,7 +1075,7 @@ class TestViews: } assert data == expected_error - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_dep_not_present(self, mocked_scm, mocked_get_user): FakeSCM( @@ -1104,7 +1104,7 @@ class TestViews: } assert data == expected_error - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_rebuild_strategy_override_not_allowed(self, mocked_scm, mocked_get_user): FakeSCM( @@ -1133,7 +1133,7 @@ class TestViews: } assert data == expected_error - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_componentless_build(self, mocked_scm, mocked_get_user): FakeSCM( @@ -1182,7 +1182,7 @@ class TestViews: assert data["status"] == 401 assert data["error"] == "Unauthorized" - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) def test_submit_build_scm_url_error(self, mocked_get_user): rv = self.client.post( "/module-build-service/1/module-builds/", @@ -1193,7 +1193,7 @@ class TestViews: assert data["status"] == 403 assert data["error"] == "Forbidden" - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) def test_submit_build_scm_url_without_hash(self, mocked_get_user): rv = self.client.post( "/module-build-service/1/module-builds/", @@ -1210,7 +1210,7 @@ class TestViews: assert data["status"] == 400 assert data["error"] == "Bad Request" - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_bad_modulemd(self, mocked_scm, mocked_get_user): FakeSCM(mocked_scm, "bad", "bad.yaml") @@ -1231,7 +1231,7 @@ class TestViews: assert data["status"] == 422 assert data["error"] == "Unprocessable Entity" - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_includedmodule_custom_repo_not_allowed(self, mocked_scm, mocked_get_user): FakeSCM(mocked_scm, "includedmodules", ["includedmodules.yaml", "testmodule.yaml"]) @@ -1248,7 +1248,7 @@ class TestViews: assert data["status"] == 403 assert data["error"] == "Forbidden" - @patch("module_build_service.auth.get_user", return_value=other_user) + @patch("module_build_service.web.auth.get_user", return_value=other_user) def test_cancel_build(self, mocked_get_user): rv = self.client.patch( "/module-build-service/1/module-builds/7", data=json.dumps({"state": "failed"})) @@ -1258,7 +1258,7 @@ class TestViews: assert data["state_reason"] == "Canceled by some_other_user." @pytest.mark.parametrize("module_state", (BUILD_STATES["failed"], BUILD_STATES["ready"])) - @patch("module_build_service.auth.get_user", return_value=other_user) + @patch("module_build_service.web.auth.get_user", return_value=other_user) def test_cancel_build_in_invalid_state(self, mocked_get_user, module_state): module = ModuleBuild.get_by_id(db_session, 7) module.state = module_state @@ -1277,7 +1277,7 @@ class TestViews: "status": 400, } - @patch("module_build_service.auth.get_user", return_value=("sammy", set())) + @patch("module_build_service.web.auth.get_user", return_value=("sammy", set())) def test_cancel_build_unauthorized_no_groups(self, mocked_get_user): rv = self.client.patch( "/module-build-service/1/module-builds/7", data=json.dumps({"state": "failed"})) @@ -1286,7 +1286,7 @@ class TestViews: assert data["status"] == 403 assert data["error"] == "Forbidden" - @patch("module_build_service.auth.get_user", return_value=("sammy", {"packager"})) + @patch("module_build_service.web.auth.get_user", return_value=("sammy", {"packager"})) def test_cancel_build_unauthorized_not_owner(self, mocked_get_user): rv = self.client.patch( "/module-build-service/1/module-builds/7", data=json.dumps({"state": "failed"})) @@ -1296,7 +1296,7 @@ class TestViews: assert data["error"] == "Forbidden" @patch( - "module_build_service.auth.get_user", return_value=("sammy", {"packager", "mbs-admin"}) + "module_build_service.web.auth.get_user", return_value=("sammy", {"packager", "mbs-admin"}) ) def test_cancel_build_admin(self, mocked_get_user): with patch( @@ -1311,7 +1311,7 @@ class TestViews: assert data["state"] == 4 assert data["state_reason"] == "Canceled by sammy." - @patch("module_build_service.auth.get_user", return_value=("sammy", {"packager"})) + @patch("module_build_service.web.auth.get_user", return_value=("sammy", {"packager"})) def test_cancel_build_no_admin(self, mocked_get_user): with patch( "module_build_service.config.Config.admin_groups", @@ -1325,7 +1325,7 @@ class TestViews: assert data["status"] == 403 assert data["error"] == "Forbidden" - @patch("module_build_service.auth.get_user", return_value=other_user) + @patch("module_build_service.web.auth.get_user", return_value=other_user) def test_cancel_build_wrong_param(self, mocked_get_user): rv = self.client.patch( "/module-build-service/1/module-builds/7", data=json.dumps({"some_param": "value"})) @@ -1335,7 +1335,7 @@ class TestViews: assert data["error"] == "Bad Request" assert data["message"] == "Invalid JSON submitted" - @patch("module_build_service.auth.get_user", return_value=other_user) + @patch("module_build_service.web.auth.get_user", return_value=other_user) def test_cancel_build_wrong_state(self, mocked_get_user): rv = self.client.patch( "/module-build-service/1/module-builds/7", data=json.dumps({"state": "some_state"})) @@ -1347,7 +1347,7 @@ class TestViews: "status": 400, } - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) def test_submit_build_unsupported_scm_scheme(self, mocked_get_user): scmurl = "unsupported://example.com/modules/" "testmodule.git?#0000000000000000000000000000000000000000" @@ -1363,7 +1363,7 @@ class TestViews: assert data["status"] in (400, 403) assert data["error"] in ("Bad Request", "Forbidden") - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_version_set_error(self, mocked_scm, mocked_get_user): FakeSCM( @@ -1389,7 +1389,7 @@ class TestViews: ) assert data["error"] == "Bad Request" - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_wrong_stream(self, mocked_scm, mocked_get_user): FakeSCM( @@ -1415,7 +1415,7 @@ class TestViews: ) assert data["error"] == "Bad Request" - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) def test_submit_build_set_owner(self, mocked_get_user): data = { "branch": "master", @@ -1428,7 +1428,7 @@ class TestViews: assert result["status"] == 400 assert "The request contains 'owner' parameter" in result["message"] - @patch("module_build_service.auth.get_user", return_value=anonymous_user) + @patch("module_build_service.web.auth.get_user", return_value=anonymous_user) @patch("module_build_service.scm.SCM") @patch( "module_build_service.config.Config.no_auth", new_callable=PropertyMock, return_value=True @@ -1449,7 +1449,7 @@ class TestViews: build = ModuleBuild.get_by_id(db_session, result["id"]) assert (build.owner == result["owner"] == "foo") is True - @patch("module_build_service.auth.get_user", return_value=("svc_account", set())) + @patch("module_build_service.web.auth.get_user", return_value=("svc_account", set())) @patch("module_build_service.scm.SCM") @patch("module_build_service.config.Config.allowed_users", new_callable=PropertyMock) def test_submit_build_allowed_users(self, allowed_users, mocked_scm, mocked_get_user): @@ -1465,7 +1465,7 @@ class TestViews: rv = self.client.post("/module-build-service/1/module-builds/", data=json.dumps(data)) assert rv.status_code == 201 - @patch("module_build_service.auth.get_user", return_value=anonymous_user) + @patch("module_build_service.web.auth.get_user", return_value=anonymous_user) @patch("module_build_service.scm.SCM") @patch("module_build_service.config.Config.no_auth", new_callable=PropertyMock) def test_patch_set_different_owner(self, mocked_no_auth, mocked_scm, mocked_get_user): @@ -1494,7 +1494,7 @@ class TestViews: assert r3.status_code == 400 assert "The request contains 'owner' parameter" in json.loads(r3.data)["message"] - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_commit_hash_not_found(self, mocked_scm, mocked_get_user): FakeSCM( @@ -1519,7 +1519,7 @@ class TestViews: assert data["status"] == 422 assert data["error"] == "Unprocessable Entity" - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch("module_build_service.config.Config.allow_custom_scmurls", new_callable=PropertyMock) def test_submit_custom_scmurl(self, allow_custom_scmurls, mocked_scm, mocked_get_user): @@ -1546,7 +1546,7 @@ class TestViews: @pytest.mark.parametrize( "br_override_streams, req_override_streams", ((["f28"], None), (["f28"], ["f28"])) ) - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_dep_override( self, mocked_scm, mocked_get_user, br_override_streams, req_override_streams @@ -1587,7 +1587,7 @@ class TestViews: assert set(dep.get_buildtime_streams("platform")) == expected_br assert set(dep.get_runtime_streams("platform")) == expected_req - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_invalid_basemodule_stream(self, mocked_scm, mocked_get_user): # By default tests do not provide platform:f28.0.0, but just platform:f28. @@ -1615,7 +1615,7 @@ class TestViews: } assert rv.status_code == 422 - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_with_base_module_name(self, mocked_scm, mocked_get_user): FakeSCM( @@ -1635,7 +1635,7 @@ class TestViews: } assert rv.status_code == 400 - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_with_xmd(self, mocked_scm, mocked_get_user): FakeSCM( @@ -1660,7 +1660,7 @@ class TestViews: assert rv.status_code == 400 @pytest.mark.parametrize("dep_type", ("buildrequire", "require")) - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_override_unused(self, mocked_scm, mocked_get_user, dep_type): FakeSCM( @@ -1701,7 +1701,7 @@ class TestViews: {"require_overrides": "platform:f28"}, ), ) - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_invalid_override(self, mocked_scm, mocked_get_user, optional_params): FakeSCM( @@ -1844,7 +1844,7 @@ class TestViews: assert rv.headers["Access-Control-Allow-Origin"] == "*" @pytest.mark.parametrize("api_version", [1, 2]) - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch.object( module_build_service.config.Config, "allowed_groups_to_import_module", @@ -1860,7 +1860,7 @@ class TestViews: assert data["message"] == "Import module API is disabled." @pytest.mark.parametrize("api_version", [1, 2]) - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) def test_import_build_user_not_allowed(self, mocked_get_user, api_version): post_url = "/module-build-service/{0}/import-module/".format(api_version) rv = self.client.post(post_url) @@ -1873,7 +1873,7 @@ class TestViews: ) @pytest.mark.parametrize("api_version", [1, 2]) - @patch("module_build_service.auth.get_user", return_value=import_module_user) + @patch("module_build_service.web.auth.get_user", return_value=import_module_user) def test_import_build_scm_invalid_json(self, mocked_get_user, api_version): post_url = "/module-build-service/{0}/import-module/".format(api_version) rv = self.client.post(post_url, data="") @@ -1883,7 +1883,7 @@ class TestViews: assert data["message"] == "Invalid JSON submitted" @pytest.mark.parametrize("api_version", [1, 2]) - @patch("module_build_service.auth.get_user", return_value=import_module_user) + @patch("module_build_service.web.auth.get_user", return_value=import_module_user) def test_import_build_scm_url_not_allowed(self, mocked_get_user, api_version): post_url = "/module-build-service/{0}/import-module/".format(api_version) rv = self.client.post( @@ -1895,7 +1895,7 @@ class TestViews: assert data["message"].endswith("/tests/scm_data/mariadb is not allowed") @pytest.mark.parametrize("api_version", [1, 2]) - @patch("module_build_service.auth.get_user", return_value=import_module_user) + @patch("module_build_service.web.auth.get_user", return_value=import_module_user) @patch.object( module_build_service.config.Config, "allow_custom_scmurls", @@ -1920,7 +1920,7 @@ class TestViews: ) @pytest.mark.parametrize("api_version", [1, 2]) - @patch("module_build_service.auth.get_user", return_value=import_module_user) + @patch("module_build_service.web.auth.get_user", return_value=import_module_user) @patch.object( module_build_service.config.Config, "scmurls", @@ -1959,7 +1959,7 @@ class TestViews: assert data["module"]["rebuild_strategy"] == "all" @pytest.mark.parametrize("api_version", [1, 2]) - @patch("module_build_service.auth.get_user", return_value=import_module_user) + @patch("module_build_service.web.auth.get_user", return_value=import_module_user) @patch.object( module_build_service.config.Config, "scmurls", @@ -2000,7 +2000,7 @@ class TestViews: assert data["module"]["rebuild_strategy"] == "all" @pytest.mark.parametrize("api_version", [1, 2]) - @patch("module_build_service.auth.get_user", return_value=import_module_user) + @patch("module_build_service.web.auth.get_user", return_value=import_module_user) @patch.object( module_build_service.config.Config, "scmurls", @@ -2023,7 +2023,7 @@ class TestViews: assert data["message"] == expected_msg @pytest.mark.parametrize("api_version", [1, 2]) - @patch("module_build_service.auth.get_user", return_value=import_module_user) + @patch("module_build_service.web.auth.get_user", return_value=import_module_user) @patch.object( module_build_service.config.Config, "scmurls", @@ -2070,7 +2070,7 @@ class TestViews: assert br_moduleb == buildrequires.get("moduleb") @pytest.mark.parametrize("api_version", [1, 2]) - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch( "module_build_service.config.Config.modules_allow_scratch", @@ -2132,7 +2132,7 @@ class TestViews: assert module.buildrequires[0].stream_version == 280000 @pytest.mark.parametrize("api_version", [1, 2]) - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch( "module_build_service.config.Config.modules_allow_scratch", @@ -2163,7 +2163,7 @@ class TestViews: assert rv.status_code == expected_error["status"] @pytest.mark.parametrize("api_version", [1, 2]) - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch( "module_build_service.config.Config.modules_allow_scratch", new_callable=PropertyMock, @@ -2227,7 +2227,7 @@ class TestViews: assert module.buildrequires[0].context == "00000000" assert module.buildrequires[0].stream_version == 280000 - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch( "module_build_service.config.Config.modules_allow_scratch", new_callable=PropertyMock, @@ -2261,7 +2261,7 @@ class TestViews: assert data == expected_error @pytest.mark.parametrize("api_version", [1, 2]) - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch( "module_build_service.config.Config.modules_allow_scratch", new_callable=PropertyMock, @@ -2298,7 +2298,7 @@ class TestViews: "branch, platform_override", (("10", None), ("10-rhel-8.0.0", "el8.0.0"), ("10-LP-product1.2", "product1.2")), ) - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch.object( module_build_service.config.Config, "br_stream_override_regexes", new_callable=PropertyMock @@ -2345,7 +2345,7 @@ class TestViews: # The requires should not change assert dep.get_runtime_streams("platform") == ["f28"] - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch.object( module_build_service.config.Config, "br_stream_override_regexes", new_callable=PropertyMock @@ -2391,7 +2391,7 @@ class TestViews: # The requires should not change assert dep.get_runtime_streams("platform") == ["f28"] - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_br_xyz_version_no_virtual_streams(self, mocked_scm, mocked_get_user): """ @@ -2419,7 +2419,7 @@ class TestViews: rv = self.client.post(post_url, data=json.dumps({"branch": "master", "scmurl": scm_url})) assert rv.status_code == 201 - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch( "module_build_service.config.Config.allowed_privileged_module_names", @@ -2453,7 +2453,7 @@ class TestViews: mmd = load_mmd(data["modulemd"]) assert mmd.get_xmd()["mbs"]["disttag_marking"] == "product12" - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_request_platform_virtual_stream(self, mocked_scm, mocked_get_user): # Create a platform with el8.25.0 but with the virtual stream el8 @@ -2605,7 +2605,7 @@ class TestViews: new_callable=PropertyMock, ) @patch("requests.get") - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_automatic_z_stream_detection( self, mocked_scm, mocked_get_user, mock_get, mock_pp_streams, mock_pp_url, mock_datetime, @@ -2661,7 +2661,7 @@ class TestViews: mock_get.assert_not_called() @pytest.mark.parametrize("reuse_components_from", (7, "testmodule:4.3.43:7:00000000")) - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_reuse_components_from( self, mocked_scm, mocked_get_user, reuse_components_from, @@ -2710,7 +2710,7 @@ class TestViews: ) ) ) - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") def test_submit_build_reuse_components_from_errors( self, mocked_scm, mocked_get_user, reuse_components_from, expected_error, @@ -2735,7 +2735,7 @@ class TestViews: assert rv.status_code == 400 assert data["message"] == expected_error - @patch("module_build_service.auth.get_user", return_value=user) + @patch("module_build_service.web.auth.get_user", return_value=user) @patch("module_build_service.scm.SCM") @patch( "module_build_service.config.Config.rebuild_strategy_allow_override", diff --git a/tests/test_web/test_auth.py b/tests/test_web/test_auth.py new file mode 100644 index 0000000..ff9f615 --- /dev/null +++ b/tests/test_web/test_auth.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT +from os import path + +import pytest +import requests +import mock +from mock import patch, PropertyMock, Mock + +import module_build_service.web.auth +import module_build_service.errors +import module_build_service.config as mbs_config +from module_build_service import app + + +class TestAuthModule: + def test_get_user_no_token(self): + base_dir = path.abspath(path.dirname(__file__)) + client_secrets = path.join(base_dir, "client_secrets.json") + with patch.dict( + "module_build_service.app.config", + {"OIDC_CLIENT_SECRETS": client_secrets, "OIDC_REQUIRED_SCOPE": "mbs-scope"}, + ): + request = mock.MagicMock() + request.cookies.return_value = {} + + with pytest.raises(module_build_service.errors.Unauthorized) as cm: + with app.app_context(): + module_build_service.web.auth.get_user(request) + assert str(cm.value) == "No 'authorization' header found." + + @patch("module_build_service.web.auth._get_token_info") + @patch("module_build_service.web.auth._get_user_info") + def test_get_user_failure(self, get_user_info, get_token_info): + base_dir = path.abspath(path.dirname(__file__)) + client_secrets = path.join(base_dir, "client_secrets.json") + with patch.dict( + "module_build_service.app.config", + {"OIDC_CLIENT_SECRETS": client_secrets, "OIDC_REQUIRED_SCOPE": "mbs-scope"}, + ): + # https://www.youtube.com/watch?v=G-LtddOgUCE + name = "Joey Jo Jo Junior Shabadoo" + mocked_get_token_info = { + "active": False, + "username": name, + "scope": "openid https://id.fedoraproject.org/scope/groups mbs-scope" + } + get_token_info.return_value = mocked_get_token_info + + get_user_info.return_value = {"groups": ["group"]} + + headers = {"authorization": "Bearer foobar"} + request = mock.MagicMock() + request.headers.return_value = mock.MagicMock(spec_set=dict) + request.headers.__getitem__.side_effect = headers.__getitem__ + request.headers.__setitem__.side_effect = headers.__setitem__ + request.headers.__contains__.side_effect = headers.__contains__ + + with pytest.raises(module_build_service.errors.Unauthorized) as cm: + with app.app_context(): + module_build_service.web.auth.get_user(request) + assert str(cm.value) == "OIDC token invalid or expired." + + @patch("module_build_service.web.auth._get_token_info") + @patch("module_build_service.web.auth._get_user_info") + def test_get_user_not_in_groups(self, get_user_info, get_token_info): + base_dir = path.abspath(path.dirname(__file__)) + client_secrets = path.join(base_dir, "client_secrets.json") + with patch.dict( + "module_build_service.app.config", + {"OIDC_CLIENT_SECRETS": client_secrets, "OIDC_REQUIRED_SCOPE": "mbs-scope"}, + ): + # https://www.youtube.com/watch?v=G-LtddOgUCE + name = "Joey Jo Jo Junior Shabadoo" + mocked_get_token_info = { + "active": True, + "username": name, + "scope": "openid https://id.fedoraproject.org/scope/groups mbs-scope" + } + get_token_info.return_value = mocked_get_token_info + + get_user_info.side_effect = requests.Timeout("It happens...") + + headers = {"authorization": "Bearer foobar"} + request = mock.MagicMock() + request.headers.return_value = mock.MagicMock(spec_set=dict) + request.headers.__getitem__.side_effect = headers.__getitem__ + request.headers.__setitem__.side_effect = headers.__setitem__ + request.headers.__contains__.side_effect = headers.__contains__ + + with pytest.raises(module_build_service.errors.Unauthorized) as cm: + with app.app_context(): + module_build_service.web.auth.get_user(request) + assert str(cm.value) == "OpenIDC auth error: Cannot determine the user's groups" + + @pytest.mark.parametrize("allowed_users", (set(), {"Joey Jo Jo Junior Shabadoo"})) + @patch.object(mbs_config.Config, "allowed_users", new_callable=PropertyMock) + @patch("module_build_service.web.auth._get_token_info") + @patch("module_build_service.web.auth._get_user_info") + def test_get_user_good(self, get_user_info, get_token_info, m_allowed_users, allowed_users): + m_allowed_users.return_value = allowed_users + base_dir = path.abspath(path.dirname(__file__)) + client_secrets = path.join(base_dir, "client_secrets.json") + with patch.dict( + "module_build_service.app.config", + {"OIDC_CLIENT_SECRETS": client_secrets, "OIDC_REQUIRED_SCOPE": "mbs-scope"}, + ): + # https://www.youtube.com/watch?v=G-LtddOgUCE + name = "Joey Jo Jo Junior Shabadoo" + mocked_get_token_info = { + "active": True, + "username": name, + "scope": ("openid https://id.fedoraproject.org/scope/groups mbs-scope"), + } + get_token_info.return_value = mocked_get_token_info + + get_user_info.return_value = {"groups": ["group"]} + + headers = {"authorization": "Bearer foobar"} + request = mock.MagicMock() + request.headers.return_value = mock.MagicMock(spec_set=dict) + request.headers.__getitem__.side_effect = headers.__getitem__ + request.headers.__setitem__.side_effect = headers.__setitem__ + request.headers.__contains__.side_effect = headers.__contains__ + + with app.app_context(): + username, groups = module_build_service.web.auth.get_user(request) + username_second_call, groups_second_call = module_build_service.web.auth.get_user( + request) + assert username == name + if allowed_users: + assert groups == set() + else: + assert groups == set(get_user_info.return_value["groups"]) + + # Test the real auth method has been called just once. + get_user_info.assert_called_once() + assert username_second_call == username + assert groups_second_call == groups + + @patch.object(mbs_config.Config, "no_auth", new_callable=PropertyMock, return_value=True) + def test_disable_authentication(self, conf_no_auth): + request = mock.MagicMock() + username, groups = module_build_service.web.auth.get_user(request) + assert username == "anonymous" + assert groups == {"packager"} + + @patch("module_build_service.web.auth.client_secrets", None) + def test_misconfiguring_oidc_client_secrets_should_be_failed(self): + request = mock.MagicMock() + with pytest.raises(module_build_service.errors.Forbidden) as cm: + with app.app_context(): + module_build_service.web.auth.get_user(request) + assert str(cm.value) == "OIDC_CLIENT_SECRETS must be set in server config." + + @patch("module_build_service.web.auth._get_token_info") + @patch("module_build_service.web.auth._get_user_info") + def test_get_required_scope_not_present(self, get_user_info, get_token_info): + base_dir = path.abspath(path.dirname(__file__)) + client_secrets = path.join(base_dir, "client_secrets.json") + with patch.dict( + "module_build_service.app.config", + {"OIDC_CLIENT_SECRETS": client_secrets, "OIDC_REQUIRED_SCOPE": "mbs-scope"}, + ): + # https://www.youtube.com/watch?v=G-LtddOgUCE + name = "Joey Jo Jo Junior Shabadoo" + mocked_get_token_info = { + "active": True, + "username": name, + "scope": "openid https://id.fedoraproject.org/scope/groups", + } + get_token_info.return_value = mocked_get_token_info + + get_user_info.return_value = {"groups": ["group"]} + + headers = {"authorization": "Bearer foobar"} + request = mock.MagicMock() + request.headers.return_value = mock.MagicMock(spec_set=dict) + request.headers.__getitem__.side_effect = headers.__getitem__ + request.headers.__setitem__.side_effect = headers.__setitem__ + request.headers.__contains__.side_effect = headers.__contains__ + + with pytest.raises(module_build_service.errors.Unauthorized) as cm: + with app.app_context(): + module_build_service.web.auth.get_user(request) + assert str(cm.value) == ( + "Required OIDC scope 'mbs-scope' not present: " + "['openid', 'https://id.fedoraproject.org/scope/groups']" + ) + + @patch("module_build_service.web.auth._get_token_info") + @patch("module_build_service.web.auth._get_user_info") + def test_get_required_scope_not_set_in_cfg(self, get_user_info, get_token_info): + base_dir = path.abspath(path.dirname(__file__)) + client_secrets = path.join(base_dir, "client_secrets.json") + with patch.dict("module_build_service.app.config", {"OIDC_CLIENT_SECRETS": client_secrets}): + # https://www.youtube.com/watch?v=G-LtddOgUCE + name = "Joey Jo Jo Junior Shabadoo" + mocked_get_token_info = { + "active": True, + "username": name, + "scope": "openid https://id.fedoraproject.org/scope/groups", + } + get_token_info.return_value = mocked_get_token_info + + get_user_info.return_value = {"groups": ["group"]} + + headers = {"authorization": "Bearer foobar"} + request = mock.MagicMock() + request.headers.return_value = mock.MagicMock(spec_set=dict) + request.headers.__getitem__.side_effect = headers.__getitem__ + request.headers.__setitem__.side_effect = headers.__setitem__ + request.headers.__contains__.side_effect = headers.__contains__ + + with pytest.raises(module_build_service.errors.Forbidden) as cm: + with app.app_context(): + module_build_service.web.auth.get_user(request) + assert str(cm.value) == "OIDC_REQUIRED_SCOPE must be set in server config." + + @pytest.mark.parametrize("remote_name", ["", None, "someone"]) + def test_get_user_kerberos_unauthorized(self, remote_name): + request = Mock() + request.environ.get.return_value = remote_name + + with pytest.raises(module_build_service.errors.Unauthorized): + module_build_service.web.auth.get_user_kerberos(request) + + @patch.object(module_build_service.web.auth.conf, "allowed_users", new=["someone", "somebody"]) + def test_get_user_kerberos_user_is_in_allowed_users_group(self): + request = Mock() + request.environ.get.return_value = "someone@realm" + + username, groups = module_build_service.web.auth.get_user_kerberos(request) + assert "someone" == username + assert set() == groups + + @patch.object(module_build_service.web.auth.conf, "allowed_users", new=["someone", "somebody"]) + @patch( + "module_build_service.web.auth.get_ldap_group_membership", + return_value=["group1", "group2"], + ) + def test_get_user_kerberos_user_is_not_in_allowed_users_group(self, get_ldap_group_membership): + request = Mock() + request.environ.get.return_value = "x-man@realm" + + username, groups = module_build_service.web.auth.get_user_kerberos(request) + assert "x-man" == username + assert {"group1", "group2"} == groups