Files
notifications-admin/app/notify_client/user_api_client.py

210 lines
7.5 KiB
Python
Raw Normal View History

from flask import current_app
from notifications_python_client.errors import HTTPError
from app.notify_client import NotifyAdminAPIClient, cache
from app.utils.user_permissions import translate_permissions_from_ui_to_db
ALLOWED_ATTRIBUTES = {
'name',
'email_address',
'mobile_number',
'auth_type',
'updated_by',
Update `email_access_validated_at` on link click When someone uses a fresh password reset link they have proved that they have access to their inbox. At the moment, when revalidating a user’s email address we wait until after they’ve put in the 2FA code before updating the timestamp which records when they last validated their email address[1]. We can’t think of a good reason that we need the extra assurance of a valid 2FA code to assert that the user has access to their email – they’ve done that just by clicking the link. When the user clicks the link we already update their failed login count before they 2fa. Think it makes sense to handle `email_access_validated_at` then too. As a bonus, the functional tests never go as far as getting a 2FA code after a password reset[2], so the functional test user never gets its timestamp updated. This causes the functional tests start failing after 90 days. By moving the update to this point we ensure that the functional tests will keep passing indefinitely. 1. This code in the API (https://github.com/alphagov/notifications-api/blob/91542ad33eb995c9b3e617cde30a9714846dd37b/app/dao/users_dao.py#L131) which is called by this code in the admin app (https://github.com/alphagov/notifications-admin/blob/9ba37249a47f08b1396e63a34bc591c781739686/app/utils/login.py#L26) 2. https://github.com/alphagov/notifications-functional-tests/blob/5837eb01dc5c9b5bb255d2d1c54e64f778ff4f4e/tests/functional/preview_and_dev/test_email_auth.py#L43-L46
2021-08-17 16:14:47 +01:00
'current_session_id',
'email_access_validated_at',
}
class UserApiClient(NotifyAdminAPIClient):
def init_app(self, app):
super().init_app(app)
self.admin_url = app.config['ADMIN_BASE_URL']
def register_user(self, name, email_address, mobile_number, password, auth_type):
data = {
"name": name,
"email_address": email_address,
"mobile_number": mobile_number,
"password": password,
"auth_type": auth_type
}
user_data = self.post("/user", data)
return user_data['data']
Cache `GET /user` response in Redis In the same way, and for the same reasons that we’re caching the service object. Here’s a sample of the data returned by the API – so we should make sure that any changes to this data invalidate the cache. If we ever change a user’s phone number (for example) directly in the database, then we will need to invalidate this cache manually. ```python {      'data':{         'organisations':[            '4c707b81-4c6d-4d33-9376-17f0de6e0405'       ],       'logged_in_at':'2018-04-10T11:41:03.781990Z',       'id':'2c45486e-177e-40b8-997d-5f4f81a461ca',       'email_address':'test@example.gov.uk',       'platform_admin':False,       'password_changed_at':'2018-01-01 10:10:10.100000',       'permissions':{            '42a9d4f2-1444-4e22-9133-52d9e406213f':[               'manage_api_keys',             'send_letters',             'manage_users',             'manage_templates',             'view_activity',             'send_texts',             'send_emails',             'manage_settings'          ],          'a928eef8-0f25-41ca-b480-0447f29b2c20':[               'manage_users',             'manage_templates',             'manage_settings',             'send_texts',             'send_emails',             'send_letters',             'manage_api_keys',             'view_activity'          ],       },       'state':'active',       'mobile_number':'07700900123',       'failed_login_count':0,       'name':'Example',       'services':[            '6078a8c0-52f5-4c4f-b724-d7d1ff2d3884',          '6afe3c1c-7fda-4d8d-aa8d-769c4bdf7803',       ],       'current_session_id':'fea2ade1-db0a-4c90-93e7-c64a877ce83e',       'auth_type':'sms_auth'    } } ```
2018-04-10 13:30:52 +01:00
def get_user(self, user_id):
return self._get_user(user_id)['data']
Cache `GET /user` response in Redis In the same way, and for the same reasons that we’re caching the service object. Here’s a sample of the data returned by the API – so we should make sure that any changes to this data invalidate the cache. If we ever change a user’s phone number (for example) directly in the database, then we will need to invalidate this cache manually. ```python {      'data':{         'organisations':[            '4c707b81-4c6d-4d33-9376-17f0de6e0405'       ],       'logged_in_at':'2018-04-10T11:41:03.781990Z',       'id':'2c45486e-177e-40b8-997d-5f4f81a461ca',       'email_address':'test@example.gov.uk',       'platform_admin':False,       'password_changed_at':'2018-01-01 10:10:10.100000',       'permissions':{            '42a9d4f2-1444-4e22-9133-52d9e406213f':[               'manage_api_keys',             'send_letters',             'manage_users',             'manage_templates',             'view_activity',             'send_texts',             'send_emails',             'manage_settings'          ],          'a928eef8-0f25-41ca-b480-0447f29b2c20':[               'manage_users',             'manage_templates',             'manage_settings',             'send_texts',             'send_emails',             'send_letters',             'manage_api_keys',             'view_activity'          ],       },       'state':'active',       'mobile_number':'07700900123',       'failed_login_count':0,       'name':'Example',       'services':[            '6078a8c0-52f5-4c4f-b724-d7d1ff2d3884',          '6afe3c1c-7fda-4d8d-aa8d-769c4bdf7803',       ],       'current_session_id':'fea2ade1-db0a-4c90-93e7-c64a877ce83e',       'auth_type':'sms_auth'    } } ```
2018-04-10 13:30:52 +01:00
@cache.set('user-{user_id}')
Cache `GET /user` response in Redis In the same way, and for the same reasons that we’re caching the service object. Here’s a sample of the data returned by the API – so we should make sure that any changes to this data invalidate the cache. If we ever change a user’s phone number (for example) directly in the database, then we will need to invalidate this cache manually. ```python {      'data':{         'organisations':[            '4c707b81-4c6d-4d33-9376-17f0de6e0405'       ],       'logged_in_at':'2018-04-10T11:41:03.781990Z',       'id':'2c45486e-177e-40b8-997d-5f4f81a461ca',       'email_address':'test@example.gov.uk',       'platform_admin':False,       'password_changed_at':'2018-01-01 10:10:10.100000',       'permissions':{            '42a9d4f2-1444-4e22-9133-52d9e406213f':[               'manage_api_keys',             'send_letters',             'manage_users',             'manage_templates',             'view_activity',             'send_texts',             'send_emails',             'manage_settings'          ],          'a928eef8-0f25-41ca-b480-0447f29b2c20':[               'manage_users',             'manage_templates',             'manage_settings',             'send_texts',             'send_emails',             'send_letters',             'manage_api_keys',             'view_activity'          ],       },       'state':'active',       'mobile_number':'07700900123',       'failed_login_count':0,       'name':'Example',       'services':[            '6078a8c0-52f5-4c4f-b724-d7d1ff2d3884',          '6afe3c1c-7fda-4d8d-aa8d-769c4bdf7803',       ],       'current_session_id':'fea2ade1-db0a-4c90-93e7-c64a877ce83e',       'auth_type':'sms_auth'    } } ```
2018-04-10 13:30:52 +01:00
def _get_user(self, user_id):
return self.get("/user/{}".format(user_id))
def get_user_by_email(self, email_address):
2021-03-05 15:05:48 +00:00
user_data = self.post('/user/email', data={'email': email_address})
return user_data['data']
def get_user_by_email_or_none(self, email_address):
try:
return self.get_user_by_email(email_address)
except HTTPError as e:
if e.status_code == 404:
return None
raise e
2016-01-21 11:33:53 +00:00
@cache.delete('user-{user_id}')
def update_user_attribute(self, user_id, **kwargs):
data = dict(kwargs)
disallowed_attributes = set(data.keys()) - ALLOWED_ATTRIBUTES
if disallowed_attributes:
raise TypeError('Not allowed to update user attributes: {}'.format(
", ".join(disallowed_attributes)
))
2016-11-10 12:10:01 +00:00
url = "/user/{}".format(user_id)
user_data = self.post(url, data=data)
return user_data['data']
@cache.delete('user-{user_id}')
def archive_user(self, user_id):
return self.post('/user/{}/archive'.format(user_id), data=None)
@cache.delete('user-{user_id}')
def reset_failed_login_count(self, user_id):
url = "/user/{}/reset-failed-login-count".format(user_id)
user_data = self.post(url, data={})
return user_data['data']
@cache.delete('user-{user_id}')
def update_password(self, user_id, password):
data = {"_password": password}
url = "/user/{}/update-password".format(user_id)
user_data = self.post(url, data=data)
return user_data['data']
@cache.delete('user-{user_id}')
2016-01-27 17:26:22 +00:00
def verify_password(self, user_id, password):
2016-01-21 11:33:53 +00:00
try:
current_app.logger.warn(f"Checking password for {user_id}")
2016-01-27 17:26:22 +00:00
url = "/user/{}/verify/password".format(user_id)
data = {"password": password}
2016-01-27 17:13:56 +00:00
self.post(url, data=data)
return True
2016-01-21 11:33:53 +00:00
except HTTPError as e:
if e.status_code == 400 or e.status_code == 404:
current_app.logger.error(f"Password for {user_id} was invalid")
2016-01-21 11:33:53 +00:00
return False
raise
2016-01-21 11:33:53 +00:00
def send_verify_code(self, user_id, code_type, to, next_string=None):
data = {'to': to}
if next_string:
data['next'] = next_string
if code_type == 'email':
data['email_auth_link_host'] = self.admin_url
endpoint = '/user/{0}/{1}-code'.format(user_id, code_type)
current_app.logger.warn(f"Sending verify_code {code_type} to {user_id}")
self.post(endpoint, data=data)
def send_verify_email(self, user_id, to):
data = {
'to': to,
'admin_base_url': self.admin_url,
}
endpoint = '/user/{0}/email-verification'.format(user_id)
self.post(endpoint, data=data)
def send_already_registered_email(self, user_id, to):
data = {'email': to}
endpoint = '/user/{0}/email-already-registered'.format(user_id)
self.post(endpoint, data=data)
@cache.delete('user-{user_id}')
def check_verify_code(self, user_id, code, code_type):
data = {'code_type': code_type, 'code': code}
endpoint = '/user/{}/verify/code'.format(user_id)
try:
current_app.logger.warn(f"Checking verify code for {user_id}")
self.post(endpoint, data=data)
return True, ''
except HTTPError as e:
if e.status_code == 400 or e.status_code == 404:
current_app.logger.error(f"Verify code for {user_id} was invalid")
return False, e.message
raise
def get_users_for_service(self, service_id):
endpoint = '/service/{}/users'.format(service_id)
return self.get(endpoint)['data']
2023-07-12 12:09:44 -04:00
def get_users_for_organization(self, org_id):
endpoint = '/organizations/{}/users'.format(org_id)
return self.get(endpoint)['data']
2018-02-19 16:53:29 +00:00
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}')
def add_user_to_service(self, service_id, user_id, permissions, folder_permissions):
# permissions passed in are the combined UI permissions, not DB permissions
endpoint = '/service/{}/users/{}'.format(service_id, user_id)
data = {
'permissions': [{'permission': x} for x in translate_permissions_from_ui_to_db(permissions)],
'folder_permissions': folder_permissions,
}
self.post(endpoint, data=data)
@cache.delete('user-{user_id}')
2023-07-12 12:09:44 -04:00
def add_user_to_organization(self, org_id, user_id):
resp = self.post('/organizations/{}/users/{}'.format(org_id, user_id), data={})
return resp['data']
2018-02-19 16:53:29 +00:00
@cache.delete('service-{service_id}-template-folders')
@cache.delete('user-{user_id}')
def set_user_permissions(self, user_id, service_id, permissions, folder_permissions=None):
# permissions passed in are the combined UI permissions, not DB permissions
data = {
'permissions': [{'permission': x} for x in translate_permissions_from_ui_to_db(permissions)],
}
if folder_permissions is not None:
data['folder_permissions'] = folder_permissions
endpoint = '/user/{}/service/{}/permission'.format(user_id, service_id)
2016-03-07 18:18:52 +00:00
self.post(endpoint, data=data)
def send_reset_password_url(self, email_address, next_string=None):
2016-03-07 18:18:52 +00:00
endpoint = '/user/reset-password'
data = {
'email': email_address,
'admin_base_url': self.admin_url,
}
if next_string:
data['next'] = next_string
2016-03-07 18:18:52 +00:00
self.post(endpoint, data=data)
def find_users_by_full_or_partial_email(self, email_address):
endpoint = '/user/find-users-by-email'
data = {'email': email_address}
users = self.post(endpoint, data=data)
return users
@cache.delete('user-{user_id}')
def activate_user(self, user_id):
Cache `GET /user` response in Redis In the same way, and for the same reasons that we’re caching the service object. Here’s a sample of the data returned by the API – so we should make sure that any changes to this data invalidate the cache. If we ever change a user’s phone number (for example) directly in the database, then we will need to invalidate this cache manually. ```python {      'data':{         'organisations':[            '4c707b81-4c6d-4d33-9376-17f0de6e0405'       ],       'logged_in_at':'2018-04-10T11:41:03.781990Z',       'id':'2c45486e-177e-40b8-997d-5f4f81a461ca',       'email_address':'test@example.gov.uk',       'platform_admin':False,       'password_changed_at':'2018-01-01 10:10:10.100000',       'permissions':{            '42a9d4f2-1444-4e22-9133-52d9e406213f':[               'manage_api_keys',             'send_letters',             'manage_users',             'manage_templates',             'view_activity',             'send_texts',             'send_emails',             'manage_settings'          ],          'a928eef8-0f25-41ca-b480-0447f29b2c20':[               'manage_users',             'manage_templates',             'manage_settings',             'send_texts',             'send_emails',             'send_letters',             'manage_api_keys',             'view_activity'          ],       },       'state':'active',       'mobile_number':'07700900123',       'failed_login_count':0,       'name':'Example',       'services':[            '6078a8c0-52f5-4c4f-b724-d7d1ff2d3884',          '6afe3c1c-7fda-4d8d-aa8d-769c4bdf7803',       ],       'current_session_id':'fea2ade1-db0a-4c90-93e7-c64a877ce83e',       'auth_type':'sms_auth'    } } ```
2018-04-10 13:30:52 +01:00
return self.post("/user/{}/activate".format(user_id), data=None)
def send_change_email_verification(self, user_id, new_email):
endpoint = '/user/{}/change-email-verification'.format(user_id)
data = {'email': new_email}
self.post(endpoint, data)
2023-07-12 12:09:44 -04:00
def get_organizations_and_services_for_user(self, user_id):
endpoint = '/user/{}/organizations-and-services'.format(user_id)
return self.get(endpoint)
user_api_client = UserApiClient()