diff --git a/Pipfile b/Pipfile index ba2fc7d31..7955aa6be 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,7 @@ name = "pypi" [packages] ago = "~=0.0.95" blinker = "~=1.4" +exceptiongroup = "==1.1.2" fido2 = "~=0.9" flask = "~=2.3" flask-basicauth = "~=0.2" diff --git a/Pipfile.lock b/Pipfile.lock index 6477e9356..81a6c4557 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b2e8852b009644badb4ecedfd2e3040a91c6082b6794954332edc825b02ec233" + "sha256": "d27aff981cc071e27820a959f05da0a7884d9f1a1452f8b5e74bb516b8dae7cb" }, "pipfile-spec": 6, "requires": { @@ -24,14 +24,6 @@ "index": "pypi", "version": "==0.0.95" }, - "anyio": { - "hashes": [ - "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", - "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5" - ], - "markers": "python_version >= '3.7'", - "version": "==3.7.1" - }, "async-timeout": { "hashes": [ "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15", @@ -243,7 +235,7 @@ "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac", "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==3.2.0" }, "click": { @@ -351,11 +343,11 @@ }, "dnspython": { "hashes": [ - "sha256:46b4052a55b56beea3a3bdd7b30295c292bd6827dd442348bc116f2d35b17f0a", - "sha256:758e691dbb454d5ccf4e1b154a19e52847f79e21a42fef17b969144af29a4e6c" + "sha256:5b7488477388b8c0b70a8ce93b227c5603bc7b77f1565afe8e729c36c51447d7", + "sha256:c33971c79af5be968bb897e95c2448e11a645ee84d93b265ce0b7aabe5dfdca8" ], "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==2.4.0" + "version": "==2.4.1" }, "docopt": { "hashes": [ @@ -383,7 +375,7 @@ "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5", "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f" ], - "markers": "python_version < '3.11'", + "index": "pypi", "version": "==1.1.2" }, "fido2": { @@ -540,22 +532,6 @@ "ref": "1299ea9e967a61ae2edebe191082fd169b864c64", "version": "==20.1.0" }, - "h11": { - "hashes": [ - "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", - "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" - ], - "markers": "python_version >= '3.7'", - "version": "==0.14.0" - }, - "httpcore": { - "hashes": [ - "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888", - "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87" - ], - "markers": "python_version >= '3.8'", - "version": "==0.17.3" - }, "humanize": { "hashes": [ "sha256:7ca0e43e870981fa684acb5b062deb307218193bca1a01f2b2676479df849b3a", @@ -1142,14 +1118,6 @@ ], "version": "==2.0.1" }, - "sniffio": { - "hashes": [ - "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", - "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.0" - }, "texttable": { "hashes": [ "sha256:290348fb67f7746931bcdfd55ac7584ecd4e5b0846ab164333f0794b121760f2", @@ -1427,7 +1395,7 @@ "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac", "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==3.2.0" }, "cryptography": { @@ -1480,7 +1448,7 @@ "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5", "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f" ], - "markers": "python_version < '3.11'", + "index": "pypi", "version": "==1.1.2" }, "execnet": { @@ -1610,7 +1578,7 @@ "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" ], - "markers": "python_full_version >= '3.8.0'", + "markers": "python_version >= '3.8'", "version": "==3.0.0" }, "markupsafe": { @@ -1813,7 +1781,7 @@ "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==32.0.1" }, "pluggy": { @@ -1986,7 +1954,7 @@ "sha256:8f87bc7ee54675732fa66a05ebfe489e27264caeeff3728c945d25971b6485ec", "sha256:d653d6bccede5844304c605d5aac802c7cf9621efd700b46c7ec2b51ea914898" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==13.4.2" }, "s3transfer": { @@ -2033,7 +2001,7 @@ "sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d", "sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c" ], - "markers": "python_full_version >= '3.8.0'", + "markers": "python_version >= '3.8'", "version": "==5.1.0" }, "toml": { diff --git a/app/main/views/platform_admin.py b/app/main/views/platform_admin.py index a34ea51c4..fb4316604 100644 --- a/app/main/views/platform_admin.py +++ b/app/main/views/platform_admin.py @@ -1,4 +1,5 @@ import itertools +import json from collections import OrderedDict from datetime import datetime @@ -12,6 +13,7 @@ from app import ( notification_api_client, platform_stats_api_client, service_api_client, + user_api_client, ) from app.extensions import redis_client from app.main import main @@ -292,6 +294,32 @@ def get_billing_report(): return render_template('views/platform-admin/get-billing-report.html', form=form) +@main.route("/platform-admin/reports/get-users-report", methods=['GET', 'POST']) +@user_is_platform_admin +def get_users_report(): + headers = [ + "name", "services", "platform admin", "permissions", "password changed at", + "state" + ] + try: + result = user_api_client.get_all_users() + + except HTTPError as e: + raise e + + rows = [] + for r in result: + rows.append(_get_user_row(r)) + if rows: + return Spreadsheet.from_rows([headers] + rows).as_csv_data, 200, { + 'Content-Type': 'text/csv; charset=utf-8', + 'Content-Disposition': f'attachment; filename="User Report {datetime.utcnow()}.csv"' + } + else: + flash('No results') + return render_template('views/platform-admin/get-users-report.html') + + @main.route("/platform-admin/reports/volumes-by-service", methods=['GET', 'POST']) @user_is_platform_admin def get_volumes_by_service(): @@ -540,3 +568,40 @@ def format_stats_by_service(services): 'created_at': service['created_at'], 'active': service['active'] } + + +def _get_user_row(r): + + # [{ + # 'name': 'Kenneth Kehl', + # 'organizations': [], + # 'password_changed_at': '2023-07-21 14:12:54.832850', 'permissions': { + # '672b8a66-e22e-40f6-b1e5-39cc1c6bf857': ['manage_users', 'manage_templates', 'manage_settings', 'send_texts', + # 'send_emails', 'manage_api_keys', 'view_activity']}, + # 'platform_admin': True, 'services': ['672b8a66-e22e-40f6-b1e5-39cc1c6bf857'], 'state': 'active'}] + + row = [] + row.append(r['name']) + + service_id_name_lookup = {} + services = [] + for s in r['services']: + my_service = service_api_client.get_service(s) + service_id_name_lookup[my_service['data']['id']] = my_service['data']['name'] + services.append(my_service['data']['name']) + services = str(services) + services = services.replace("[", "") + services = services.replace("]", "") + row.append(services) + row.append(r['platform_admin']) + permissions = r['permissions'] + for k, v in service_id_name_lookup.items(): + if permissions.get(k): + permissions[v] = permissions[k] + del permissions[k] + + permissions = json.dumps(permissions, indent=4) + row.append(permissions) + row.append(r['password_changed_at']) + row.append(r['state']) + return row diff --git a/app/navigation.py b/app/navigation.py index 09065c58a..625164392 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -95,6 +95,7 @@ class HeaderNavigation(Navigation): 'live_services_csv', 'notifications_sent_by_service', 'get_billing_report', + 'get_users_report', 'get_daily_volumes', 'get_daily_sms_provider_volumes', 'get_volumes_by_service', diff --git a/app/notify_client/user_api_client.py b/app/notify_client/user_api_client.py index 2d4cefaa9..55071175f 100644 --- a/app/notify_client/user_api_client.py +++ b/app/notify_client/user_api_client.py @@ -154,6 +154,10 @@ class UserApiClient(NotifyAdminAPIClient): endpoint = '/organizations/{}/users'.format(org_id) return self.get(endpoint)['data'] + def get_all_users(self): + endpoint = '/user' + return self.get(endpoint)['data'] + @cache.delete('service-{service_id}') @cache.delete('service-{service_id}-template-folders') @cache.delete('user-{user_id}') diff --git a/app/templates/views/platform-admin/get-users-report.html b/app/templates/views/platform-admin/get-users-report.html new file mode 100644 index 000000000..c26de3d04 --- /dev/null +++ b/app/templates/views/platform-admin/get-users-report.html @@ -0,0 +1,44 @@ +{% extends "views/platform-admin/_base_template.html" %} +{% from "components/form.html" import form_wrapper %} +{% from "components/table.html" import mapping_table, row, text_field %} + +{% block per_page_title %} + Billing Report +{% endblock %} + +{% block platform_admin_content %} + +
Daily SMS provider volumes Report
++ Users Report +
{% endblock %} diff --git a/tests/app/main/views/test_platform_admin.py b/tests/app/main/views/test_platform_admin.py index 519e81eaf..907403711 100644 --- a/tests/app/main/views/test_platform_admin.py +++ b/tests/app/main/views/test_platform_admin.py @@ -1008,6 +1008,46 @@ def test_get_daily_volumes_report_calls_api_and_download_data( ) +def test_get_users_report( + client_request, + platform_admin_user, + mocker +): + mocker.patch( + "app.main.views.platform_admin.user_api_client.get_all_users", + return_value=[{ + 'name': 'Johnny Sokko', + 'organizations': [], + 'password_changed_at': '2023-07-21 14:12:54.832850', 'permissions': { + 'test service': [ + 'manage_users', 'manage_templates', 'manage_settings', 'send_texts', + 'send_emails', 'manage_api_keys', 'view_activity']}, + 'platform_admin': True, 'services': ['test service'], 'state': 'active'} + ] + + + ) + + client_request.login(platform_admin_user) + response = client_request.post_response( + 'main.get_users_report', + _data={}, + _expected_status=200, + ) + + assert response.content_type == 'text/csv; charset=utf-8' + assert 'attachment' in response.headers['Content-Disposition'] + assert 'filename' in response.headers['Content-Disposition'] + assert 'User Report' in response.headers['Content-Disposition'] + + my_response = response.get_data(as_text=True) + + assert 'Johnny Sokko' in my_response + assert 'manage_users' in my_response + assert 'test service' in my_response + assert 'active' in my_response + + def test_get_daily_sms_provider_volumes_report_calls_api_and_download_data( client_request, platform_admin_user, diff --git a/tests/app/test_navigation.py b/tests/app/test_navigation.py index fefa3722f..7afce65f3 100644 --- a/tests/app/test_navigation.py +++ b/tests/app/test_navigation.py @@ -103,6 +103,7 @@ EXCLUDED_ENDPOINTS = tuple(map(Navigation.get_endpoint_with_blueprint, { 'find_users_by_email', 'forgot_password', 'get_billing_report', + 'get_users_report', 'get_daily_volumes', 'get_daily_sms_provider_volumes', 'get_volumes_by_service',