2018-01-11 22:36:13 +00:00
|
|
|
from itertools import chain
|
2016-03-01 10:45:13 +00:00
|
|
|
|
2019-01-07 14:49:33 +00:00
|
|
|
from flask import abort, request, session
|
2018-02-20 11:22:17 +00:00
|
|
|
from flask_login import AnonymousUserMixin, UserMixin
|
2019-04-04 11:18:22 +01:00
|
|
|
from werkzeug.utils import cached_property
|
2018-09-20 09:43:28 +01:00
|
|
|
|
2019-04-04 11:18:22 +01:00
|
|
|
from app.models.organisation import Organisation
|
|
|
|
|
from app.notify_client.organisations_api_client import organisations_client
|
2018-12-12 12:29:08 +00:00
|
|
|
from app.utils import is_gov_user
|
|
|
|
|
|
2018-01-11 22:12:30 +00:00
|
|
|
roles = {
|
|
|
|
|
'send_messages': ['send_texts', 'send_emails', 'send_letters'],
|
|
|
|
|
'manage_templates': ['manage_templates'],
|
|
|
|
|
'manage_service': ['manage_users', 'manage_settings'],
|
2018-06-12 11:44:34 +01:00
|
|
|
'manage_api_keys': ['manage_api_keys'],
|
|
|
|
|
'view_activity': ['view_activity'],
|
2018-01-11 22:12:30 +00:00
|
|
|
}
|
|
|
|
|
|
2018-06-12 11:44:34 +01:00
|
|
|
# same dict as above, but flipped round
|
2018-02-28 15:37:49 +00:00
|
|
|
roles_by_permission = {
|
2018-06-12 11:44:34 +01:00
|
|
|
permission: next(
|
|
|
|
|
role for role, permissions in roles.items() if permission in permissions
|
|
|
|
|
) for permission in chain(*list(roles.values()))
|
2018-02-28 15:37:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
all_permissions = set(roles_by_permission.values())
|
2018-01-11 22:36:13 +00:00
|
|
|
|
2018-08-08 08:45:58 +01:00
|
|
|
permissions = (
|
2018-08-08 14:23:24 +01:00
|
|
|
('view_activity', 'See dashboard'),
|
|
|
|
|
('send_messages', 'Send messages'),
|
2018-08-08 08:45:58 +01:00
|
|
|
('manage_templates', 'Add and edit templates'),
|
2018-08-08 14:23:24 +01:00
|
|
|
('manage_service', 'Manage settings, team and usage'),
|
2018-08-08 08:45:58 +01:00
|
|
|
('manage_api_keys', 'Manage API integration'),
|
|
|
|
|
)
|
|
|
|
|
|
2018-01-11 22:12:30 +00:00
|
|
|
|
2018-02-21 10:18:56 +00:00
|
|
|
def _get_service_id_from_view_args():
|
2019-05-03 09:59:02 +01:00
|
|
|
return str(request.view_args.get('service_id', '')) or None
|
2018-02-21 10:18:56 +00:00
|
|
|
|
|
|
|
|
|
2018-03-01 11:34:53 +00:00
|
|
|
def _get_org_id_from_view_args():
|
2019-05-03 09:59:02 +01:00
|
|
|
return str(request.view_args.get('org_id', '')) or None
|
2018-03-01 11:34:53 +00:00
|
|
|
|
|
|
|
|
|
2018-02-28 15:37:49 +00:00
|
|
|
def translate_permissions_from_db_to_admin_roles(permissions):
|
|
|
|
|
"""
|
|
|
|
|
Given a list of database permissions, return a set of roles
|
|
|
|
|
|
|
|
|
|
look them up in roles_by_permission, falling back to just passing through from the api if they aren't in the dict
|
|
|
|
|
"""
|
|
|
|
|
return {roles_by_permission.get(permission, permission) for permission in permissions}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def translate_permissions_from_admin_roles_to_db(permissions):
|
|
|
|
|
"""
|
|
|
|
|
Given a list of admin roles (ie: checkboxes on a permissions edit page for example), return a set of db permissions
|
|
|
|
|
|
|
|
|
|
Looks them up in the roles dict, falling back to just passing through if they're not recognised.
|
|
|
|
|
"""
|
|
|
|
|
return set(chain.from_iterable(roles.get(permission, [permission]) for permission in permissions))
|
|
|
|
|
|
|
|
|
|
|
2016-03-01 10:45:13 +00:00
|
|
|
class User(UserMixin):
|
|
|
|
|
def __init__(self, fields, max_failed_login_count=3):
|
2018-03-01 11:15:20 +00:00
|
|
|
self.id = fields.get('id')
|
|
|
|
|
self.name = fields.get('name')
|
|
|
|
|
self.email_address = fields.get('email_address')
|
|
|
|
|
self.mobile_number = fields.get('mobile_number')
|
|
|
|
|
self.password_changed_at = fields.get('password_changed_at')
|
2018-02-28 15:37:49 +00:00
|
|
|
self._set_permissions(fields.get('permissions', {}))
|
2018-03-01 11:15:20 +00:00
|
|
|
self.auth_type = fields.get('auth_type')
|
|
|
|
|
self.failed_login_count = fields.get('failed_login_count')
|
|
|
|
|
self.state = fields.get('state')
|
2016-03-01 10:45:13 +00:00
|
|
|
self.max_failed_login_count = max_failed_login_count
|
2018-07-10 17:24:20 +01:00
|
|
|
self.logged_in_at = fields.get('logged_in_at')
|
2016-03-17 10:46:47 +00:00
|
|
|
self.platform_admin = fields.get('platform_admin')
|
2017-02-17 14:06:09 +00:00
|
|
|
self.current_session_id = fields.get('current_session_id')
|
2018-03-07 18:10:14 +00:00
|
|
|
self.services = fields.get('services', [])
|
2018-03-01 11:15:20 +00:00
|
|
|
self.organisations = fields.get('organisations', [])
|
2016-03-01 10:45:13 +00:00
|
|
|
|
2018-02-28 15:37:49 +00:00
|
|
|
def _set_permissions(self, permissions_by_service):
|
|
|
|
|
"""
|
|
|
|
|
Permissions is a dict {'service_id': ['permission a', 'permission b', 'permission c']}
|
|
|
|
|
|
|
|
|
|
The api currently returns some granular permissions that we don't set or use separately (but may want
|
|
|
|
|
to in the future):
|
|
|
|
|
* send_texts, send_letters and send_emails become send_messages
|
|
|
|
|
* manage_user and manage_settings become
|
|
|
|
|
users either have all three permissions for a service or none of them, they're not helpful to distinguish
|
|
|
|
|
between on the front end. So lets collapse them into "send_messages" and "manage_service". If we want to split
|
|
|
|
|
them out later, we'll need to rework this function.
|
|
|
|
|
"""
|
|
|
|
|
self._permissions = {
|
|
|
|
|
service: translate_permissions_from_db_to_admin_roles(permissions)
|
|
|
|
|
for service, permissions
|
|
|
|
|
in permissions_by_service.items()
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-01 10:45:13 +00:00
|
|
|
def get_id(self):
|
|
|
|
|
return self.id
|
|
|
|
|
|
2017-02-17 14:06:09 +00:00
|
|
|
def logged_in_elsewhere(self):
|
2017-02-23 16:38:18 +00:00
|
|
|
# if the current user (ie: db object) has no session, they've never logged in before
|
|
|
|
|
return self.current_session_id is not None and session.get('current_session_id') != self.current_session_id
|
2017-02-17 14:06:09 +00:00
|
|
|
|
2016-05-04 13:01:55 +01:00
|
|
|
@property
|
2016-03-01 10:45:13 +00:00
|
|
|
def is_active(self):
|
|
|
|
|
return self.state == 'active'
|
|
|
|
|
|
2018-12-12 12:29:08 +00:00
|
|
|
@property
|
|
|
|
|
def is_gov_user(self):
|
|
|
|
|
return is_gov_user(self.email_address)
|
|
|
|
|
|
2016-05-04 13:01:55 +01:00
|
|
|
@property
|
2016-03-01 10:45:13 +00:00
|
|
|
def is_authenticated(self):
|
2017-02-17 14:06:09 +00:00
|
|
|
return (
|
|
|
|
|
not self.logged_in_elsewhere() and
|
|
|
|
|
super(User, self).is_authenticated
|
|
|
|
|
)
|
2016-03-01 10:45:13 +00:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def permissions(self):
|
|
|
|
|
return self._permissions
|
|
|
|
|
|
|
|
|
|
@permissions.setter
|
|
|
|
|
def permissions(self, permissions):
|
|
|
|
|
raise AttributeError("Read only property")
|
|
|
|
|
|
2018-03-01 10:37:55 +00:00
|
|
|
def has_permissions(self, *permissions, restrict_admin_usage=False):
|
2018-01-11 22:36:13 +00:00
|
|
|
unknown_permissions = set(permissions) - all_permissions
|
|
|
|
|
if unknown_permissions:
|
2018-02-28 15:37:49 +00:00
|
|
|
raise TypeError('{} are not valid permissions'.format(list(unknown_permissions)))
|
2018-01-11 22:36:13 +00:00
|
|
|
|
2016-03-18 16:20:37 +00:00
|
|
|
# Service id is always set on the request for service specific views.
|
2018-02-21 10:18:56 +00:00
|
|
|
service_id = _get_service_id_from_view_args()
|
2018-03-01 11:34:53 +00:00
|
|
|
org_id = _get_org_id_from_view_args()
|
|
|
|
|
|
2018-03-01 12:08:22 +00:00
|
|
|
if not service_id and not org_id:
|
|
|
|
|
# we shouldn't have any pages that require permissions, but don't specify a service or organisation.
|
|
|
|
|
# use @user_is_platform_admin for platform admin only pages
|
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
# platform admins should be able to do most things (except eg send messages, or create api keys)
|
|
|
|
|
if self.platform_admin and not restrict_admin_usage:
|
|
|
|
|
return True
|
|
|
|
|
|
2018-03-01 11:34:53 +00:00
|
|
|
if org_id:
|
|
|
|
|
return org_id in self.organisations
|
2018-08-06 11:09:57 +01:00
|
|
|
if not permissions:
|
2018-11-19 15:26:43 +00:00
|
|
|
return service_id in self.services
|
2018-08-06 11:09:57 +01:00
|
|
|
if service_id:
|
2018-03-01 11:34:53 +00:00
|
|
|
return any(x in self._permissions.get(service_id, []) for x in permissions)
|
2016-03-01 10:45:13 +00:00
|
|
|
|
2018-02-28 18:13:29 +00:00
|
|
|
def has_permission_for_service(self, service_id, permission):
|
|
|
|
|
return permission in self._permissions.get(service_id, [])
|
|
|
|
|
|
2019-03-29 16:44:57 +00:00
|
|
|
def has_template_folder_permission(self, template_folder, service=None):
|
|
|
|
|
from app import current_service
|
|
|
|
|
|
|
|
|
|
if service is None:
|
|
|
|
|
service = current_service
|
|
|
|
|
|
|
|
|
|
if not service.has_permission('edit_folder_permissions'):
|
|
|
|
|
return True
|
|
|
|
|
|
2019-03-11 14:13:56 +00:00
|
|
|
if self.platform_admin:
|
|
|
|
|
return True
|
|
|
|
|
|
2019-03-19 15:58:18 +00:00
|
|
|
# Top-level templates are always visible
|
2019-03-28 16:07:52 +00:00
|
|
|
if template_folder is None or template_folder['id'] is None:
|
2019-03-19 15:58:18 +00:00
|
|
|
return True
|
|
|
|
|
|
2019-03-11 14:13:56 +00:00
|
|
|
return self.id in template_folder.get("users_with_permission", [])
|
|
|
|
|
|
2019-04-04 17:55:37 +01:00
|
|
|
def template_folders_for_service(self, service=None):
|
|
|
|
|
"""
|
|
|
|
|
Returns list of template folders that a user can view for a given service
|
|
|
|
|
"""
|
|
|
|
|
if not service.has_permission('edit_folder_permissions'):
|
|
|
|
|
return service.all_template_folders
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
template_folder
|
|
|
|
|
for template_folder in service.all_template_folders
|
|
|
|
|
if self.id in template_folder.get("users_with_permission", [])
|
|
|
|
|
]
|
|
|
|
|
|
2019-01-15 17:31:55 +00:00
|
|
|
def belongs_to_service(self, service_id):
|
|
|
|
|
return str(service_id) in self.services
|
|
|
|
|
|
2019-01-07 14:49:33 +00:00
|
|
|
def belongs_to_service_or_403(self, service_id):
|
2019-01-15 17:31:55 +00:00
|
|
|
if not self.belongs_to_service(service_id):
|
2019-01-07 14:49:33 +00:00
|
|
|
abort(403)
|
|
|
|
|
|
2016-03-01 10:45:13 +00:00
|
|
|
def is_locked(self):
|
|
|
|
|
return self.failed_login_count >= self.max_failed_login_count
|
|
|
|
|
|
2019-04-04 11:18:22 +01:00
|
|
|
@property
|
|
|
|
|
def email_domain(self):
|
|
|
|
|
return self.email_address.split('@')[-1]
|
|
|
|
|
|
|
|
|
|
@cached_property
|
|
|
|
|
def default_organisation(self):
|
|
|
|
|
return Organisation(
|
|
|
|
|
organisations_client.get_organisation_by_domain(self.email_domain)
|
|
|
|
|
)
|
|
|
|
|
|
2019-05-07 10:31:16 +01:00
|
|
|
@property
|
|
|
|
|
def default_organisation_type(self):
|
|
|
|
|
if self.default_organisation:
|
|
|
|
|
return self.default_organisation.organisation_type
|
|
|
|
|
if self.has_nhs_email_address:
|
|
|
|
|
return 'nhs'
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def has_nhs_email_address(self):
|
|
|
|
|
return self.email_address.lower().endswith((
|
|
|
|
|
'@nhs.uk', '.nhs.uk', '@nhs.net', '.nhs.net',
|
|
|
|
|
))
|
|
|
|
|
|
2016-03-01 10:45:13 +00:00
|
|
|
def serialize(self):
|
2017-02-17 14:06:09 +00:00
|
|
|
dct = {
|
|
|
|
|
"id": self.id,
|
|
|
|
|
"name": self.name,
|
|
|
|
|
"email_address": self.email_address,
|
|
|
|
|
"mobile_number": self.mobile_number,
|
|
|
|
|
"password_changed_at": self.password_changed_at,
|
|
|
|
|
"state": self.state,
|
|
|
|
|
"failed_login_count": self.failed_login_count,
|
|
|
|
|
"permissions": [x for x in self._permissions],
|
2018-02-19 16:53:29 +00:00
|
|
|
"organisations": self.organisations,
|
2017-02-17 14:06:09 +00:00
|
|
|
"current_session_id": self.current_session_id
|
|
|
|
|
}
|
|
|
|
|
if hasattr(self, '_password'):
|
2016-03-01 10:45:13 +00:00
|
|
|
dct['password'] = self._password
|
|
|
|
|
return dct
|
|
|
|
|
|
|
|
|
|
def set_password(self, pwd):
|
|
|
|
|
self._password = pwd
|
2016-02-29 17:35:21 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvitedUser(object):
|
|
|
|
|
|
2019-03-12 14:15:46 +00:00
|
|
|
def __init__(self,
|
|
|
|
|
id,
|
|
|
|
|
service,
|
|
|
|
|
from_user,
|
|
|
|
|
email_address,
|
|
|
|
|
permissions,
|
|
|
|
|
status,
|
|
|
|
|
created_at,
|
|
|
|
|
auth_type,
|
2019-03-15 10:25:00 +00:00
|
|
|
folder_permissions):
|
2016-02-29 17:35:21 +00:00
|
|
|
self.id = id
|
|
|
|
|
self.service = str(service)
|
|
|
|
|
self.from_user = from_user
|
|
|
|
|
self.email_address = email_address
|
2016-03-03 15:26:18 +00:00
|
|
|
if isinstance(permissions, list):
|
|
|
|
|
self.permissions = permissions
|
|
|
|
|
else:
|
2016-03-11 12:50:25 +00:00
|
|
|
if permissions:
|
|
|
|
|
self.permissions = permissions.split(',')
|
|
|
|
|
else:
|
|
|
|
|
self.permissions = []
|
2016-02-29 17:35:21 +00:00
|
|
|
self.status = status
|
|
|
|
|
self.created_at = created_at
|
2017-11-01 15:36:27 +00:00
|
|
|
self.auth_type = auth_type
|
2018-03-01 16:59:01 +00:00
|
|
|
self.permissions = translate_permissions_from_db_to_admin_roles(self.permissions)
|
2019-04-03 15:53:30 +01:00
|
|
|
self.folder_permissions = folder_permissions
|
2016-02-29 17:35:21 +00:00
|
|
|
|
2017-10-15 15:02:01 +01:00
|
|
|
def has_permissions(self, *permissions):
|
2018-01-26 17:14:00 +00:00
|
|
|
if self.status == 'cancelled':
|
|
|
|
|
return False
|
2016-03-03 13:00:12 +00:00
|
|
|
return set(self.permissions) > set(permissions)
|
2016-03-02 15:25:04 +00:00
|
|
|
|
2018-02-28 18:13:29 +00:00
|
|
|
def has_permission_for_service(self, service_id, permission):
|
2018-08-06 11:10:37 +01:00
|
|
|
if self.status == 'cancelled':
|
2018-06-12 14:29:47 +01:00
|
|
|
return False
|
|
|
|
|
return self.service == service_id and permission in self.permissions
|
2018-02-28 18:13:29 +00:00
|
|
|
|
2016-03-03 15:26:18 +00:00
|
|
|
def __eq__(self, other):
|
|
|
|
|
return ((self.id,
|
|
|
|
|
self.service,
|
|
|
|
|
self.from_user,
|
|
|
|
|
self.email_address,
|
2017-11-01 15:36:27 +00:00
|
|
|
self.auth_type,
|
2016-03-03 15:26:18 +00:00
|
|
|
self.status) == (other.id,
|
|
|
|
|
other.service,
|
|
|
|
|
other.from_user,
|
|
|
|
|
other.email_address,
|
2017-11-01 15:36:27 +00:00
|
|
|
other.auth_type,
|
2016-03-03 15:26:18 +00:00
|
|
|
other.status))
|
|
|
|
|
|
|
|
|
|
def serialize(self, permissions_as_string=False):
|
|
|
|
|
data = {'id': self.id,
|
2016-03-02 15:25:04 +00:00
|
|
|
'service': self.service,
|
|
|
|
|
'from_user': self.from_user,
|
|
|
|
|
'email_address': self.email_address,
|
2016-03-03 15:26:18 +00:00
|
|
|
'status': self.status,
|
2017-11-01 15:36:27 +00:00
|
|
|
'created_at': str(self.created_at),
|
2019-03-15 10:25:00 +00:00
|
|
|
'auth_type': self.auth_type,
|
|
|
|
|
'folder_permissions': self.folder_permissions
|
2016-03-02 15:25:04 +00:00
|
|
|
}
|
2016-03-03 15:26:18 +00:00
|
|
|
if permissions_as_string:
|
|
|
|
|
data['permissions'] = ','.join(self.permissions)
|
|
|
|
|
else:
|
2018-03-01 16:59:01 +00:00
|
|
|
data['permissions'] = sorted(self.permissions)
|
2016-03-03 15:26:18 +00:00
|
|
|
return data
|
2017-02-17 14:06:09 +00:00
|
|
|
|
2019-04-04 17:55:37 +01:00
|
|
|
def template_folders_for_service(self, service=None):
|
|
|
|
|
# only used on the manage users page to display the count, so okay to not be fully fledged for now
|
|
|
|
|
return [{'id': x} for x in self.folder_permissions]
|
|
|
|
|
|
2017-02-17 14:06:09 +00:00
|
|
|
|
2018-02-19 16:53:29 +00:00
|
|
|
class InvitedOrgUser(object):
|
|
|
|
|
|
|
|
|
|
def __init__(self, id, organisation, invited_by, email_address, status, created_at):
|
|
|
|
|
self.id = id
|
|
|
|
|
self.organisation = str(organisation)
|
|
|
|
|
self.invited_by = invited_by
|
|
|
|
|
self.email_address = email_address
|
|
|
|
|
self.status = status
|
|
|
|
|
self.created_at = created_at
|
|
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
|
return ((self.id,
|
|
|
|
|
self.organisation,
|
|
|
|
|
self.invited_by,
|
|
|
|
|
self.email_address,
|
|
|
|
|
self.status) == (other.id,
|
|
|
|
|
other.organisation,
|
|
|
|
|
other.invited_by,
|
|
|
|
|
other.email_address,
|
|
|
|
|
other.status))
|
|
|
|
|
|
|
|
|
|
def serialize(self, permissions_as_string=False):
|
|
|
|
|
data = {'id': self.id,
|
|
|
|
|
'organisation': self.organisation,
|
|
|
|
|
'invited_by': self.invited_by,
|
|
|
|
|
'email_address': self.email_address,
|
|
|
|
|
'status': self.status,
|
|
|
|
|
'created_at': str(self.created_at)
|
|
|
|
|
}
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
2017-02-17 14:06:09 +00:00
|
|
|
class AnonymousUser(AnonymousUserMixin):
|
|
|
|
|
# set the anonymous user so that if a new browser hits us we don't error http://stackoverflow.com/a/19275188
|
|
|
|
|
def logged_in_elsewhere(self):
|
|
|
|
|
return False
|
2019-04-04 11:18:22 +01:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def default_organisation(self):
|
|
|
|
|
return Organisation(None)
|