notify-admin-641 let platform admins export listing of all users (#647)

Co-authored-by: Kenneth Kehl <@kkehl@flexion.us>
Co-authored-by: stvnrlly <steven.reilly@gsa.gov>
This commit is contained in:
Kenneth Kehl
2023-07-26 09:08:55 -07:00
committed by GitHub
parent 71f3ae2316
commit dc342385ce
9 changed files with 171 additions and 44 deletions

View File

@@ -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"

56
Pipfile.lock generated
View File

@@ -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": {

View File

@@ -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

View File

@@ -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',

View File

@@ -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}')

View File

@@ -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 %}
<h1 class="font-body-2xl margin-bottom-3">
Users Report
</h1>
{% call form_wrapper() %}
{{ form.start_date(param_extensions={"hint": {"text": "Use the format YYYY-MM-DD"}}) }}
{{ form.end_date(param_extensions={"hint": {"text": "Use the format YYYY-MM-DD"}}) }}
{{ page_footer('Download report (CSV)') }}
{% endcall %}
<h2 class="font-body-lg">
Data included in the report
</h2>
<div class="bottom-gutter-3-2">
{% call mapping_table(
caption='Descriptions of billing report data',
field_headings=['Name', 'Description'],
field_headings_visible=True,
caption_visible=False
) %}
{% for column_heading, description in [
('sms cost', 'The total cost of text messages sent after a service has used its free allowance.'),
('sms chargeable units', 'The number of fragments sent after a service has used its free allowance. This number takes into account the cost multiplier for <a class="govuk-link govuk-link--no-visited-state" href="https://www.notifications.service.gov.uk/pricing#international-numbers">sending international text messages</a>.' | safe),
('purchase order number, contact names, contact email addresses and billing reference', 'We add this data manually based on the information we get from services. You can help by adding it to the service settings page.'),
] %}
{% call row() %}
{{ text_field(column_heading) }}
{{ text_field(description) }}
{% endcall %}
{% endfor %}
{% endcall %}
</div>
{% endblock %}

View File

@@ -28,4 +28,7 @@
<p>
<a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.get_daily_sms_provider_volumes') }}">Daily SMS provider volumes Report</a>
</p>
<p>
<a class="govuk-link govuk-link--no-visited-state" href="{{ url_for('main.get_users_report') }}">Users Report</a>
</p>
{% endblock %}

View File

@@ -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,

View File

@@ -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',