diff --git a/Pipfile b/Pipfile index 0642d16c1..7d25a6d1f 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 56a7d4569..8ea86fb14 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6caca205deea077b4abccb1f3f5a3d77ffec7785e1005414d9a726200e536d03" + "sha256": "d4cb8b10d5a8c4f87daa0a4d3c72e66d1532facfcf86f8647b7033a6ff0da699" }, "pipfile-spec": 6, "requires": { @@ -370,6 +370,14 @@ ], "version": "==0.33.3" }, + "exceptiongroup": { + "hashes": [ + "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5", + "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f" + ], + "index": "pypi", + "version": "==1.1.2" + }, "fido2": { "hashes": [ "sha256:b45e89a6109cfcb7f1bb513776aa2d6408e95c4822f83a253918b944083466ec" @@ -1440,7 +1448,7 @@ "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5", "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f" ], - "markers": "python_version < '3.11'", + "index": "pypi", "version": "==1.1.2" }, "execnet": { 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',