diff --git a/Makefile b/Makefile index 99e756358ae067353f28bab608382a1d6c312001..7649784e4ab75a7ebdead74c45276fdfbfc0d93a 100644 --- a/Makefile +++ b/Makefile @@ -33,27 +33,27 @@ ci-test: docker-build-test pytest test: stop-test docker-build-test pytest .PHONY: test -docker-build-env: +build-env: docker-compose -f docker-compose.full.yml up --remove-orphans -.PHONY: docker-build-env +.PHONY: build-env -docker-rebuild-env: +rebuild-env: docker-compose -f docker-compose.full.yml build --no-cache --parallel -.PHONY: docker-build-env +.PHONY: rebuild-env -docker-build-env-local: +build-env-local: docker-compose -f docker-compose.local.yml build docker-compose -f docker-compose.local.yml up --remove-orphans -.PHONY: docker-build-env-local +.PHONY: build-env-local -docker-shell-env: +shell-env: docker-compose -f docker-compose.full.yml exec notifications-routing /bin/bash -.PHONY: docker-shell-env +.PHONY: shell-env -docker-stop: +stop: docker-compose -f docker-compose.full.yml down --volumes docker-compose -f docker-compose.full.yml rm -f -.PHONY: docker-stop +.PHONY: stop stop-test: docker-compose -f docker-compose.test.yml down --volumes @@ -66,3 +66,7 @@ job: shell-job: docker-compose -f docker-compose.job.yml run notifications-job-daily-feed /bin/bash .PHONY: shell-job + +logs: + docker-compose -f docker-compose.full.yml logs -f +.PHONY: logs diff --git a/notifications_routing/authorization_service.py b/notifications_routing/authorization_service.py index e54a4828d2bc01be399dfd5e84e88ee748077c39..f6f4bde864f55f4b12331b9c9bc28c58461f78fa 100644 --- a/notifications_routing/authorization_service.py +++ b/notifications_routing/authorization_service.py @@ -1,9 +1,12 @@ """Authorization Service handling.""" import json +import logging +from typing import Dict, Optional import requests from notifications_routing.config import Config +from notifications_routing.data_source.data_source import DataSource from .exceptions import BadResponseCodeError @@ -20,27 +23,63 @@ def get_auth_access_token(): return token +def _get_group_users(group_id: str, headers: Dict[str, str], url: str = None): + if not url: + url = f"{Config.CERN_GROUP_URL}/{group_id}/{Config.CERN_GROUP_QUERY}" + + url = f"{Config.CERN_AUTH_SERVICE_URL}{url}" + r = requests.get(url, headers=headers) + if r.status_code != requests.codes.ok: + raise BadResponseCodeError(url, status_code=r.status_code) + + return json.loads(r.content) + + def get_group_users_api(group_id: str): """Get members of a group.""" token = get_auth_access_token() headers = {"Authorization": "Bearer {}".format(token)} - def _get_group_users(url: str = None): - if not url: - url = f"{Config.CERN_GROUP_URL}/{group_id}/{Config.CERN_GROUP_QUERY}" - - url = f"{Config.CERN_AUTH_SERVICE_URL}{url}" - r = requests.get(url, headers=headers) - if r.status_code != requests.codes.ok: - raise BadResponseCodeError(url, status_code=r.status_code) - - return json.loads(r.content) - - response = _get_group_users() + response = _get_group_users(group_id, headers) data = response["data"] while response.get("pagination").get("next"): - response = _get_group_users(response["pagination"]["next"]) + response = _get_group_users(group_id, headers, response["pagination"]["next"]) data.extend(response["data"]) - return data + group_users = [] + for member in data: + user = _prepare_user(member) + + # Should Audit failures to failed_targets + if user: + group_users.append(user) + + return group_users + + +def _prepare_user(member: Dict[str, str]) -> Optional[Dict[str, str]]: + """Prepare user data (username and email) according to type and available fields.""" + if member["type"] == "Application": + return None + + if member["unconfirmed"]: + return {DataSource.USERNAME: member["unconfirmedEmail"], DataSource.EMAIL: member["unconfirmedEmail"]} + + # always respect "activeUser" + # activeUser field is omitted in Application and Service types + if "activeUser" in member and member["activeUser"] is False: + if member["externalEmail"]: + return {DataSource.USERNAME: member["upn"], DataSource.EMAIL: member["externalEmail"]} + # member is not active and we don't active an externalEmail + return None + + if member["primaryAccountEmail"]: + return {DataSource.USERNAME: member["upn"], DataSource.EMAIL: member["primaryAccountEmail"]} + + # some secondary accounts don't have primaryAccountEmail + if member["type"] == "Secondary": + return {DataSource.USERNAME: member["upn"], DataSource.EMAIL: member["upn"] + "@cern.ch"} + + logging.error("Could not extract user info: %s", member) + return None diff --git a/notifications_routing/config.py b/notifications_routing/config.py index dc52ba5e8c95cca90d86a8f4ad6302176b7b3fb5..086da2de12370a82fcf8f6f8b1ad038358f9a26c 100644 --- a/notifications_routing/config.py +++ b/notifications_routing/config.py @@ -33,7 +33,8 @@ class Config: CERN_GROUP_URL = os.getenv("CERN_GROUP_URL", "/api/v1.0/Group") CERN_GROUP_QUERY = os.getenv( "CERN_GROUP_QUERY", - "memberidentities/precomputed?field=upn&field=primaryAccountEmail" "&field=unconfirmed&field=unconfirmedEmail", + "memberidentities/precomputed?field=upn&field=primaryAccountEmail" + "&field=unconfirmed&field=unconfirmedEmail&field=type&field=externalEmail&field=activeUser", ) # DB diff --git a/notifications_routing/data_source/postgres/postgres_data_source.py b/notifications_routing/data_source/postgres/postgres_data_source.py index 14f9264d720ead1640e11b3df4e11a37d6162237..6a06435b2febe0162f450662c9e24921d5c793d5 100644 --- a/notifications_routing/data_source/postgres/postgres_data_source.py +++ b/notifications_routing/data_source/postgres/postgres_data_source.py @@ -169,18 +169,7 @@ class PostgresDataSource(DataSource): :return: A list of user data in the format of dicts containing username and email :raises: BadResponseCodeError """ - group_users = [] - - members = get_group_users_api(group_id) - for member in members: - if member["unconfirmed"]: - user = {DataSource.USERNAME: member["unconfirmedEmail"], DataSource.EMAIL: member["unconfirmedEmail"]} - else: - user = {DataSource.USERNAME: member["upn"], DataSource.EMAIL: member["primaryAccountEmail"]} - - group_users.append(user) - - return group_users + return get_group_users_api(group_id) def get_user_preferences(self, user_id: str, channel_id: str, **kwargs) -> Dict[str, List["Preference"]]: """Return the list of user preferences. diff --git a/tests/unit/test_authorization_service.py b/tests/unit/test_authorization_service.py index f6118ce556451d1d0454bbf4ee355a0805e33703..ab3e245e14ba239f3e78932f67c0ee3fb8d35606 100644 --- a/tests/unit/test_authorization_service.py +++ b/tests/unit/test_authorization_service.py @@ -1,5 +1,6 @@ """Unit Tests for the Auth service.""" import json +import logging from unittest import mock from unittest.mock import call @@ -31,7 +32,9 @@ def auth_user_1(): "upn": "user1", "unconfirmed": False, "unconfirmedEmail": None, - "id": "dcb3e437-a27e-4193-a2b9-ba4eea55f315", + "externalEmail": None, + "type": "Person", + "activeUser": True, } @@ -43,42 +46,185 @@ def auth_user_2(): "upn": "user2", "unconfirmed": False, "unconfirmedEmail": None, - "id": "dcb3e437-a27e-4193-a2b9-ba4eea55f315", + "externalEmail": None, + "type": "Person", + "activeUser": True, } @pytest.fixture(scope="function") -def auth_user_3(): +def auth_user_data_inconsistent(): """Auth user dict.""" return { - "primaryAccountEmail": "user3@cern.ch", - "upn": "user3", + "primaryAccountEmail": "inconsistent@cern.ch", + "upn": "inconsistent", + "unconfirmed": False, + "unconfirmedEmail": None, + "type": "Person", + "externalEmail": None, + # some times data is not consistent and activeUser is not filled + # "activeUser": True + } + + +@pytest.fixture(scope="function") +def auth_user_data_inconsistent_error(): + """Auth user dict.""" + return { + "primaryAccountEmail": None, + "upn": "missing email", + "unconfirmed": False, + "unconfirmedEmail": None, + "type": "Person", + "externalEmail": None, + "activeUser": True, + } + + +@pytest.fixture(scope="function") +def auth_group_user_unconfirmed(): + """Auth user dict.""" + return { + "externalEmail": None, + "primaryAccountEmail": None, + "type": "Person", + "upn": None, + "unconfirmed": True, + "unconfirmedEmail": "unconfirmed@hotmail.fr", + } + + +@pytest.fixture(scope="function") +def auth_group_user_application(): + """Auth user dict.""" + return { + "primaryAccountEmail": None, + "upn": "application-test", + "unconfirmed": False, + "unconfirmedEmail": None, + "type": "Application", + "externalEmail": None, + } + + +@pytest.fixture(scope="function") +def auth_group_user_service(): + """Auth user dict.""" + return { + "primaryAccountEmail": "service-email@cern.ch", + "upn": "service", + "unconfirmed": False, + "unconfirmedEmail": None, + "type": "Service", + "externalEmail": None, + } + + +@pytest.fixture(scope="function") +def auth_group_user_inactive_1(): + """Auth user dict.""" + return { + "externalEmail": "external@aol.com", + "primaryAccountEmail": "inactive1@cern.ch", + "type": "Person", + "upn": "inactive1", + "unconfirmed": False, + "unconfirmedEmail": None, + "activeUser": False, + } + + +@pytest.fixture(scope="function") +def auth_group_user_inactive_2(): + """Auth user dict.""" + return { + "externalEmail": None, + "primaryAccountEmail": "inactive2@cern.ch", + "type": "Person", + "upn": "inactive2", + "unconfirmed": False, + "unconfirmedEmail": None, + "activeUser": False, + } + + +@pytest.fixture(scope="function") +def auth_group_user_secondary(): + """Auth user dict.""" + return { + "externalEmail": None, + "primaryAccountEmail": "secondary@cern.ch", + "type": "Secondary", + "upn": "secondary", "unconfirmed": False, "unconfirmedEmail": None, - "id": "dcb3e437-a27e-4193-a2b9-ba4eea55f315", } @mock.patch("notifications_routing.authorization_service.get_auth_access_token") @mock.patch.object(requests, "get") -def test_process_users(mock_get, mock_get_token, appctx, auth_user_1, auth_user_2, auth_user_3): +@mock.patch.object(logging, "error") +def test_process_users( + mock_logging_error, + mock_get, + mock_get_token, + appctx, + auth_user_1, + auth_user_2, + auth_user_data_inconsistent, + auth_group_user_inactive_1, + auth_group_user_inactive_2, + auth_group_user_service, + auth_group_user_application, + auth_group_user_secondary, + auth_group_user_unconfirmed, + auth_user_data_inconsistent_error, +): """Test process users for normal.""" mock_get_token.return_value = "jwt" mock_get.side_effect = [ - MockResponse({"pagination": {"next": "/next/url/page"}, "data": [auth_user_1, auth_user_2]}, 200), - MockResponse({"pagination": {"next": None}, "data": [auth_user_3]}, 200), + MockResponse( + { + "pagination": {"next": "/next/url/page"}, + "data": [ + auth_user_1, + auth_user_2, + auth_user_data_inconsistent, + auth_group_user_inactive_1, + auth_group_user_inactive_2, # should be ignored, email is not extractable + auth_group_user_service, + auth_group_user_application, # should be ignore + auth_group_user_secondary, + auth_user_data_inconsistent_error, + ], + }, + 200, + ), + MockResponse({"pagination": {"next": None}, "data": [auth_group_user_unconfirmed]}, 200), ] result = get_group_users_api("group_id") - assert result == [auth_user_1, auth_user_2, auth_user_3] + assert result == [ + {"username": "user1", "email": "user1@cern.ch"}, + {"username": "user2", "email": "user2@cern.ch"}, + {"username": "inconsistent", "email": "inconsistent@cern.ch"}, + {"username": "inactive1", "email": "external@aol.com"}, + {"username": "service", "email": "service-email@cern.ch"}, + {"username": "secondary", "email": "secondary@cern.ch"}, + {"username": "unconfirmed@hotmail.fr", "email": "unconfirmed@hotmail.fr"}, + ] + + mock_logging_error.assert_called_once() mock_get_token.assert_called_once() expected_first_call_url = ( "https://authorization-service-api.web.cern.ch" - "/api/v1.0/Group/group_id/memberidentities/precomputed?field=upn&field" - "=primaryAccountEmail&field=unconfirmed&field=unconfirmedEmail" + "/api/v1.0/Group/group_id/memberidentities/precomputed?field=upn&field=primaryAccountEmail" + "&field=unconfirmed&field=unconfirmedEmail&field=type&field=externalEmail&field=activeUser" ) expected_headers = {"Authorization": "Bearer jwt"} - mock_get.assert_has_calls([ - call(expected_first_call_url, headers=expected_headers), - call("https://authorization-service-api.web.cern.ch/next/url/page", headers=expected_headers) - ]) + mock_get.assert_has_calls( + [ + call(expected_first_call_url, headers=expected_headers), + call("https://authorization-service-api.web.cern.ch/next/url/page", headers=expected_headers), + ] + ) diff --git a/tests/unit/test_postgres_data_source.py b/tests/unit/test_postgres_data_source.py index c7f70fd29b1018d1fa134bd3f27b9c024b9c3c4e..2e3f6ffae76aefa6d2cf3e0494ebc05790821d56 100644 --- a/tests/unit/test_postgres_data_source.py +++ b/tests/unit/test_postgres_data_source.py @@ -136,18 +136,8 @@ def test_get_channel_unsubscribed_users_(mock_get_scalar, db_mock, channel): def test_get_group_users(mock_get_group_users_api, db_mock): """Test get group users.""" mock_get_group_users_api.return_value = [ - { - "upn": "testuser", - "primaryAccountEmail": "testuser@cern.ch", - "unconfirmed": False, - "unconfirmedEmail": None, - }, - { - "upn": None, - "primaryAccountEmail": None, - "unconfirmed": True, - "unconfirmedEmail": "unconfirmedEmail@mail.ch", - }, + {"username": "testuser", "email": "testuser@cern.ch"}, + {"username": "unconfirmedEmail@mail.ch", "email": "unconfirmedEmail@mail.ch"}, ] assert db_mock.get_group_users("186d8dfc-2774-43a8-91b5-a887fcb6ba4a") == [