diff --git a/MANIFEST.in b/MANIFEST.in --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,4 @@ include version.txt include README.md recursive-include swh py.typed +include conftest.py diff --git a/conftest.py b/conftest.py new file mode 100644 --- /dev/null +++ b/conftest.py @@ -0,0 +1,6 @@ +# Copyright (C) 2021 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +pytest_plugins = ["swh.auth.pytest_plugin"] diff --git a/swh/auth/tests/conftest.py b/swh/auth/pytest_plugin.py rename from swh/auth/tests/conftest.py rename to swh/auth/pytest_plugin.py --- a/swh/auth/tests/conftest.py +++ b/swh/auth/pytest_plugin.py @@ -5,31 +5,34 @@ from copy import copy from datetime import datetime, timezone -from typing import Optional +from typing import Dict, List, Optional from unittest.mock import Mock from keycloak.exceptions import KeycloakError import pytest from swh.auth.keycloak import KeycloakOpenIDConnect - -from .sample_data import ( - OIDC_PROFILE, - RAW_REALM_PUBLIC_KEY, - REALM, - SERVER_URL, - USER_INFO, -) +from swh.auth.tests.sample_data import OIDC_PROFILE, RAW_REALM_PUBLIC_KEY, USER_INFO class KeycloackOpenIDConnectMock(KeycloakOpenIDConnect): """Mock KeycloakOpenIDConnect class to allow testing Args: + server_url: Server main auth url (cf. + :py:data:`swh.auth.tests.sample_data.SERVER_URL`) + realm_name: Realm (cf. :py:data:`swh.auth.tests.sample_data.REALM_NAME`) + client_id: Client id (cf. :py:data:`swh.auth.tests.sample_data.CLIENT_ID`) auth_success: boolean flag to simulate authentication success or failure - exp: expiration + exp: expiration delay user_groups: user groups configuration (if any) user_permissions: user permissions configuration (if any) + oidc_profile: Dict response from a call to a token authentication query (cf. + :py:data:`swh.auth.tests.sample_data.OIDC_PROFILE`) + user_info: Dict response from a call to userinfo query (cf. + :py:data:`swh.auth.tests.sample_data.USER_INFO`) + raw_realm_public_key: A raw ascii text representing the realm public key (cf. + :py:data:`swh.auth.tests.sample_data.RAW_REALM_PUBLIC_KEY`) """ @@ -40,8 +43,11 @@ client_id: str, auth_success: bool = True, exp: Optional[int] = None, - user_groups=[], - user_permissions=[], + user_groups: List[str] = [], + user_permissions: List[str] = [], + oidc_profile: Dict = OIDC_PROFILE, + user_info: Dict = USER_INFO, + raw_realm_public_key: str = RAW_REALM_PUBLIC_KEY, ): super().__init__( server_url=server_url, realm_name=realm_name, client_id=client_id @@ -49,7 +55,7 @@ self.exp = exp self.user_groups = user_groups self.user_permissions = user_permissions - self._keycloak.public_key = lambda: RAW_REALM_PUBLIC_KEY + self._keycloak.public_key = lambda: raw_realm_public_key self._keycloak.well_know = lambda: { "issuer": f"{self.server_url}realms/{self.realm_name}", "authorization_endpoint": ( @@ -81,14 +87,15 @@ "protocol/openid-connect/certs" ), } - self.set_auth_success(auth_success) + self.set_auth_success(auth_success, oidc_profile, user_info) def decode_token(self, token): options = {} if self.auth_success: - # skip signature expiration check as we use a static oidc_profile - # for the tests with expired tokens in it + # skip signature expiration and audience checks as we use a static + # oidc_profile for the tests with expired tokens in it options["verify_exp"] = False + options["verify_aud"] = False decoded = super().decode_token(token, options) # tweak auth and exp time for tests expire_in = decoded["exp"] - decoded["auth_time"] @@ -99,13 +106,20 @@ decoded["auth_time"] = int(datetime.now(tz=timezone.utc).timestamp()) decoded["exp"] = decoded["auth_time"] + expire_in decoded["groups"] = self.user_groups + decoded["aud"] = [self.client_id, "account"] + decoded["azp"] = self.client_id if self.user_permissions: decoded["resource_access"][self.client_id] = { "roles": self.user_permissions } return decoded - def set_auth_success(self, auth_success: bool) -> None: + def set_auth_success( + self, + auth_success: bool, + oidc_profile: Optional[Dict] = None, + user_info: Optional[Dict] = None, + ) -> None: # following type ignore because mypy is not too happy about affecting mock to # method "Cannot assign to a method affecting mock". Ignore for now. self.authorization_code = Mock() # type: ignore @@ -114,9 +128,9 @@ self.logout = Mock() # type: ignore self.auth_success = auth_success if auth_success: - self.authorization_code.return_value = copy(OIDC_PROFILE) - self.refresh_token.return_value = copy(OIDC_PROFILE) - self.userinfo.return_value = copy(USER_INFO) + self.authorization_code.return_value = copy(oidc_profile) + self.refresh_token.return_value = copy(oidc_profile) + self.userinfo.return_value = copy(user_info) else: self.authorization_url = Mock() # type: ignore exception = KeycloakError( @@ -130,15 +144,19 @@ def keycloak_mock_factory( - server_url=SERVER_URL, - realm_name=REALM, - client_id="swh-client-id", - auth_success=True, - exp=None, - user_groups=[], - user_permissions=[], + server_url: str, + realm_name: str, + client_id: str, + auth_success: bool = True, + exp: Optional[int] = None, + user_groups: List[str] = [], + user_permissions: List[str] = [], + oidc_profile: Dict = OIDC_PROFILE, + user_info: Dict = USER_INFO, + raw_realm_public_key: str = RAW_REALM_PUBLIC_KEY, ): - """Keycloak mock fixture factory + """Keycloak mock fixture factory. Report to + :py:class:`swh.auth.pytest_plugin.KeycloackOpenIDConnectMock` docstring. """ @@ -152,6 +170,9 @@ exp=exp, user_groups=user_groups, user_permissions=user_permissions, + oidc_profile=oidc_profile, + user_info=user_info, + raw_realm_public_key=raw_realm_public_key, ) return keycloak_open_id_connect diff --git a/swh/auth/tests/sample_data.py b/swh/auth/tests/sample_data.py --- a/swh/auth/tests/sample_data.py +++ b/swh/auth/tests/sample_data.py @@ -4,8 +4,8 @@ # See top-level LICENSE file for more information SERVER_URL = "http://keycloak:8080/keycloak/auth/" -REALM = "SoftwareHeritage" -CLIENT_ID = "swh-web" +REALM_NAME = "SoftwareHeritage" +CLIENT_ID = "client-id" # Decoded token (out of the access token) DECODED_TOKEN = { @@ -132,11 +132,3 @@ "drX/q4E+Nzj8Tr8p7Z5CimInls40QuOTIhs6C2SwFHUgQgXl9hB9umiZJlwYEpDv0/LO2zYie" "Hl5Lv7Iig4FOIXIVCaDGQIDAQAB" ) - -REALM_PUBLIC_KEY = { - "realm": REALM, - "public_key": RAW_REALM_PUBLIC_KEY, - "token-service": f"{SERVER_URL}realms/{REALM}/protocol/openid-connect", - "account-service": f"{SERVER_URL}realms/{REALM}/account", - "tokens-not-before": 0, -} diff --git a/swh/auth/tests/test_keycloak.py b/swh/auth/tests/test_keycloak.py --- a/swh/auth/tests/test_keycloak.py +++ b/swh/auth/tests/test_keycloak.py @@ -9,11 +9,20 @@ from keycloak.exceptions import KeycloakError import pytest -from swh.auth.tests.conftest import keycloak_mock_factory -from swh.auth.tests.sample_data import CLIENT_ID, DECODED_TOKEN, OIDC_PROFILE, USER_INFO - -# dataset we have here is bound to swh-web -keycloak_mock = keycloak_mock_factory(client_id=CLIENT_ID) +from swh.auth.pytest_plugin import keycloak_mock_factory +from swh.auth.tests.sample_data import ( + CLIENT_ID, + DECODED_TOKEN, + OIDC_PROFILE, + REALM_NAME, + SERVER_URL, + USER_INFO, +) + +# Make keycloak fixture to use for tests below. +keycloak_mock = keycloak_mock_factory( + server_url=SERVER_URL, realm_name=REALM_NAME, client_id=CLIENT_ID, +) def test_keycloak_well_known(keycloak_mock):