2019-05-21 15:53:48 +01:00
|
|
|
|
import uuid
|
2024-05-23 13:59:51 -07:00
|
|
|
|
from datetime import timedelta
|
2023-02-17 11:54:17 -05:00
|
|
|
|
from secrets import randbelow
|
2018-03-13 13:07:02 +00:00
|
|
|
|
|
2024-04-24 16:27:20 -04:00
|
|
|
|
import sqlalchemy
|
|
|
|
|
|
from flask import current_app
|
2024-10-11 11:30:35 -07:00
|
|
|
|
from sqlalchemy import delete, func, select, text
|
2018-03-13 13:07:02 +00:00
|
|
|
|
from sqlalchemy.orm import joinedload
|
|
|
|
|
|
|
2016-01-07 17:31:17 +00:00
|
|
|
|
from app import db
|
2021-04-14 07:11:01 +01:00
|
|
|
|
from app.dao.dao_utils import autocommit
|
2019-05-21 15:53:48 +01:00
|
|
|
|
from app.dao.permissions_dao import permission_dao
|
|
|
|
|
|
from app.dao.service_user_dao import dao_get_service_users_by_user_id
|
2025-08-28 11:12:49 -07:00
|
|
|
|
from app.enums import AuthType, PermissionType, UserState
|
2019-05-21 15:53:48 +01:00
|
|
|
|
from app.errors import InvalidRequest
|
2024-04-24 16:27:20 -04:00
|
|
|
|
from app.models import Organization, Service, User, VerifyCode
|
2024-05-23 13:59:51 -07:00
|
|
|
|
from app.utils import escape_special_characters, get_archived_db_column_value, utc_now
|
2017-06-13 18:11:13 +01:00
|
|
|
|
|
2016-01-21 17:29:24 +00:00
|
|
|
|
|
2016-11-07 17:42:39 +00:00
|
|
|
|
def _remove_values_for_keys_if_present(dict, keys):
|
|
|
|
|
|
for key in keys:
|
|
|
|
|
|
dict.pop(key, None)
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-02-17 11:54:17 -05:00
|
|
|
|
def create_secret_code(length=6):
|
2023-08-29 14:54:30 -07:00
|
|
|
|
random_number = randbelow(10**length)
|
2023-02-22 10:48:15 -05:00
|
|
|
|
return "{:0{length}d}".format(random_number, length=length)
|
2016-01-07 17:31:17 +00:00
|
|
|
|
|
|
|
|
|
|
|
2024-03-08 08:44:27 -08:00
|
|
|
|
def get_login_gov_user(login_uuid, email_address):
|
|
|
|
|
|
"""
|
|
|
|
|
|
We want to check to see if the user is registered with login.gov
|
|
|
|
|
|
If we can find the login.gov uuid in our user table, then they are.
|
|
|
|
|
|
|
|
|
|
|
|
Also, because we originally keyed off email address we might have a few
|
|
|
|
|
|
older users who registered with login.gov but we don't know what their
|
|
|
|
|
|
login.gov uuids are. Eventually the code that checks by email address
|
|
|
|
|
|
should be removed.
|
|
|
|
|
|
"""
|
2024-12-19 11:10:03 -08:00
|
|
|
|
stmt = select(User).where(User.login_uuid == login_uuid)
|
2024-10-11 11:30:35 -07:00
|
|
|
|
user = db.session.execute(stmt).scalars().first()
|
2024-03-08 08:44:27 -08:00
|
|
|
|
if user:
|
|
|
|
|
|
if user.email_address != email_address:
|
2024-04-24 16:27:20 -04:00
|
|
|
|
try:
|
|
|
|
|
|
save_user_attribute(user, {"email_address": email_address})
|
2024-08-15 10:40:26 -07:00
|
|
|
|
except sqlalchemy.exc.IntegrityError:
|
2024-04-24 16:27:20 -04:00
|
|
|
|
# We are trying to change the email address as a courtesy,
|
|
|
|
|
|
# based on the assumption that the user has somehow changed their
|
|
|
|
|
|
# address in login.gov.
|
|
|
|
|
|
# But if we cannot change the email address, at least we don't
|
|
|
|
|
|
# want to fail here, otherwise the user will be locked out.
|
2024-09-11 09:39:18 -07:00
|
|
|
|
current_app.logger.exception("Error getting login.gov user")
|
2024-04-24 16:27:20 -04:00
|
|
|
|
db.session.rollback()
|
|
|
|
|
|
|
2024-03-08 08:44:27 -08:00
|
|
|
|
return user
|
2024-04-24 16:27:20 -04:00
|
|
|
|
|
2025-08-21 12:35:03 -07:00
|
|
|
|
# Handle the case of the brand new user. We know their email from the
|
|
|
|
|
|
# invitation but need to related the login_uuid to it.
|
|
|
|
|
|
stmt = select(User).where(User.email_address.ilike(email_address))
|
|
|
|
|
|
user = db.session.execute(stmt).scalars().first()
|
|
|
|
|
|
|
2024-03-08 08:44:27 -08:00
|
|
|
|
if user:
|
|
|
|
|
|
save_user_attribute(user, {"login_uuid": login_uuid})
|
|
|
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2020-12-22 15:46:31 +00:00
|
|
|
|
def save_user_attribute(usr, update_dict=None):
|
2024-12-19 11:10:03 -08:00
|
|
|
|
db.session.query(User).where(User.id == usr.id).update(update_dict or {})
|
2016-11-07 17:42:39 +00:00
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-08-29 14:54:30 -07:00
|
|
|
|
def save_model_user(
|
2024-01-30 09:21:27 -05:00
|
|
|
|
user,
|
|
|
|
|
|
update_dict=None,
|
|
|
|
|
|
password=None,
|
|
|
|
|
|
validated_email_access=False,
|
2023-08-29 14:54:30 -07:00
|
|
|
|
):
|
2020-01-24 15:18:39 +00:00
|
|
|
|
if password:
|
|
|
|
|
|
user.password = password
|
2024-05-23 13:59:51 -07:00
|
|
|
|
user.password_changed_at = utc_now()
|
2020-01-24 15:18:39 +00:00
|
|
|
|
if validated_email_access:
|
2024-05-23 13:59:51 -07:00
|
|
|
|
user.email_access_validated_at = utc_now()
|
2016-01-11 17:19:06 +00:00
|
|
|
|
if update_dict:
|
2023-08-29 14:54:30 -07:00
|
|
|
|
_remove_values_for_keys_if_present(update_dict, ["id", "password_changed_at"])
|
2024-12-19 11:10:03 -08:00
|
|
|
|
db.session.query(User).where(User.id == user.id).update(update_dict or {})
|
2016-01-11 17:19:06 +00:00
|
|
|
|
else:
|
2020-01-24 15:18:39 +00:00
|
|
|
|
db.session.add(user)
|
2016-01-07 17:31:17 +00:00
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
2016-01-21 17:29:24 +00:00
|
|
|
|
def create_user_code(user, code, code_type):
|
2023-08-29 14:54:30 -07:00
|
|
|
|
verify_code = VerifyCode(
|
|
|
|
|
|
code_type=code_type,
|
2024-05-23 13:59:51 -07:00
|
|
|
|
expiry_datetime=utc_now() + timedelta(minutes=30),
|
2023-08-29 14:54:30 -07:00
|
|
|
|
user=user,
|
|
|
|
|
|
)
|
2016-01-21 17:29:24 +00:00
|
|
|
|
verify_code.code = code
|
|
|
|
|
|
db.session.add(verify_code)
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
return verify_code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_user_code(user, code, code_type):
|
|
|
|
|
|
# Get the most recent codes to try and reduce the
|
|
|
|
|
|
# time searching for the correct code.
|
2024-10-11 11:30:35 -07:00
|
|
|
|
stmt = (
|
|
|
|
|
|
select(VerifyCode)
|
2024-12-19 11:10:03 -08:00
|
|
|
|
.where(VerifyCode.user == user, VerifyCode.code_type == code_type)
|
2024-10-11 11:30:35 -07:00
|
|
|
|
.order_by(VerifyCode.created_at.desc())
|
2023-08-29 14:54:30 -07:00
|
|
|
|
)
|
2024-10-11 11:30:35 -07:00
|
|
|
|
codes = db.session.execute(stmt).scalars().all()
|
2017-11-03 09:51:50 +00:00
|
|
|
|
return next((x for x in codes if x.check_code(code)), None)
|
2016-01-21 17:29:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
2016-03-09 17:46:01 +00:00
|
|
|
|
def delete_codes_older_created_more_than_a_day_ago():
|
2024-12-20 08:09:19 -08:00
|
|
|
|
stmt = delete(VerifyCode).where(
|
2024-10-11 11:30:35 -07:00
|
|
|
|
VerifyCode.created_at < utc_now() - timedelta(hours=24)
|
2023-08-29 14:54:30 -07:00
|
|
|
|
)
|
2024-10-11 11:30:35 -07:00
|
|
|
|
|
|
|
|
|
|
deleted = db.session.execute(stmt)
|
2016-03-09 17:46:01 +00:00
|
|
|
|
db.session.commit()
|
|
|
|
|
|
return deleted
|
|
|
|
|
|
|
|
|
|
|
|
|
2016-01-21 17:29:24 +00:00
|
|
|
|
def use_user_code(id):
|
2024-10-11 11:30:35 -07:00
|
|
|
|
verify_code = db.session.get(VerifyCode, id)
|
2016-01-21 17:29:24 +00:00
|
|
|
|
verify_code.code_used = True
|
|
|
|
|
|
db.session.add(verify_code)
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
2016-01-12 10:39:49 +00:00
|
|
|
|
def delete_model_user(user):
|
|
|
|
|
|
db.session.delete(user)
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
2016-05-06 11:07:11 +01:00
|
|
|
|
def delete_user_verify_codes(user):
|
2024-12-19 11:10:03 -08:00
|
|
|
|
stmt = delete(VerifyCode).where(VerifyCode.user == user)
|
2024-10-11 11:30:35 -07:00
|
|
|
|
db.session.execute(stmt)
|
2016-05-06 11:07:11 +01:00
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
2017-02-15 16:18:05 +00:00
|
|
|
|
def count_user_verify_codes(user):
|
2024-12-20 08:09:19 -08:00
|
|
|
|
stmt = select(func.count(VerifyCode.id)).where(
|
2017-02-16 12:44:40 +00:00
|
|
|
|
VerifyCode.user == user,
|
2024-05-23 13:59:51 -07:00
|
|
|
|
VerifyCode.expiry_datetime > utc_now(),
|
2023-08-29 14:54:30 -07:00
|
|
|
|
VerifyCode.code_used.is_(False),
|
2017-02-16 12:44:40 +00:00
|
|
|
|
)
|
2024-10-11 11:39:48 -07:00
|
|
|
|
result = db.session.execute(stmt).scalar()
|
|
|
|
|
|
return result or 0
|
2017-02-15 16:18:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
2016-11-07 17:42:39 +00:00
|
|
|
|
def get_user_by_id(user_id=None):
|
2016-01-07 17:31:17 +00:00
|
|
|
|
if user_id:
|
2024-12-19 11:10:03 -08:00
|
|
|
|
stmt = select(User).where(User.id == user_id)
|
2024-10-11 11:30:35 -07:00
|
|
|
|
return db.session.execute(stmt).scalars().one()
|
|
|
|
|
|
return get_users()
|
2016-01-25 11:14:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
2023-07-26 09:13:57 -07:00
|
|
|
|
def get_users():
|
2024-10-11 11:30:35 -07:00
|
|
|
|
stmt = select(User)
|
|
|
|
|
|
return db.session.execute(stmt).scalars().all()
|
2023-07-26 09:13:57 -07:00
|
|
|
|
|
|
|
|
|
|
|
2016-02-23 11:03:59 +00:00
|
|
|
|
def get_user_by_email(email):
|
2024-12-20 08:09:19 -08:00
|
|
|
|
stmt = select(User).where(func.lower(User.email_address) == func.lower(email))
|
2024-10-11 11:30:35 -07:00
|
|
|
|
return db.session.execute(stmt).scalars().one()
|
2016-02-23 11:03:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
2018-07-09 17:25:13 +01:00
|
|
|
|
def get_users_by_partial_email(email):
|
2018-07-13 15:26:42 +01:00
|
|
|
|
email = escape_special_characters(email)
|
2024-12-20 08:09:19 -08:00
|
|
|
|
stmt = select(User).where(User.email_address.ilike("%{}%".format(email)))
|
2024-10-11 11:30:35 -07:00
|
|
|
|
return db.session.execute(stmt).scalars().all()
|
2018-07-09 17:25:13 +01:00
|
|
|
|
|
|
|
|
|
|
|
2016-01-25 11:14:23 +00:00
|
|
|
|
def increment_failed_login_count(user):
|
|
|
|
|
|
user.failed_login_count += 1
|
|
|
|
|
|
db.session.add(user)
|
|
|
|
|
|
db.session.commit()
|
2016-01-28 11:32:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def reset_failed_login_count(user):
|
|
|
|
|
|
if user.failed_login_count > 0:
|
|
|
|
|
|
user.failed_login_count = 0
|
|
|
|
|
|
db.session.add(user)
|
|
|
|
|
|
db.session.commit()
|
2017-02-07 11:05:15 +00:00
|
|
|
|
|
|
|
|
|
|
|
2021-08-17 16:59:51 +01:00
|
|
|
|
def update_user_password(user, password):
|
2017-02-16 15:20:30 +00:00
|
|
|
|
# reset failed login count - they've just reset their password so should be fine
|
2017-02-07 11:05:15 +00:00
|
|
|
|
user.password = password
|
2024-05-23 13:59:51 -07:00
|
|
|
|
user.password_changed_at = utc_now()
|
2017-02-07 11:05:15 +00:00
|
|
|
|
db.session.add(user)
|
|
|
|
|
|
db.session.commit()
|
2018-03-13 13:07:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_user_and_accounts(user_id):
|
2024-04-24 16:27:20 -04:00
|
|
|
|
# TODO: With sqlalchemy 2.0 change as below because of the breaking change
|
|
|
|
|
|
# at User.organizations.services, we need to verify that the below subqueryload
|
|
|
|
|
|
# that we have put is functionally doing the same thing as before
|
2024-10-11 11:30:35 -07:00
|
|
|
|
stmt = (
|
|
|
|
|
|
select(User)
|
2024-12-20 08:09:19 -08:00
|
|
|
|
.where(User.id == user_id)
|
2023-08-29 14:54:30 -07:00
|
|
|
|
.options(
|
2024-04-24 16:35:03 -04:00
|
|
|
|
# eagerly load the user's services and organizations, and also the service's org and vice versa
|
|
|
|
|
|
# (so we can see if the user knows about it)
|
2024-04-24 16:27:20 -04:00
|
|
|
|
joinedload(User.services).joinedload(Service.organization),
|
|
|
|
|
|
joinedload(User.organizations).subqueryload(Organization.services),
|
2023-08-29 14:54:30 -07:00
|
|
|
|
)
|
|
|
|
|
|
)
|
2024-10-11 12:00:04 -07:00
|
|
|
|
return db.session.execute(stmt).scalars().unique().one()
|
2019-05-21 15:53:48 +01:00
|
|
|
|
|
|
|
|
|
|
|
2021-04-14 07:11:01 +01:00
|
|
|
|
@autocommit
|
2019-05-21 15:53:48 +01:00
|
|
|
|
def dao_archive_user(user):
|
|
|
|
|
|
if not user_can_be_archived(user):
|
|
|
|
|
|
msg = "User can’t be removed from a service - check all services have another team member with manage_settings"
|
|
|
|
|
|
raise InvalidRequest(msg, 400)
|
|
|
|
|
|
|
|
|
|
|
|
permission_dao.remove_user_service_permissions_for_all_services(user)
|
|
|
|
|
|
|
|
|
|
|
|
service_users = dao_get_service_users_by_user_id(user.id)
|
|
|
|
|
|
for service_user in service_users:
|
|
|
|
|
|
db.session.delete(service_user)
|
|
|
|
|
|
|
2023-07-10 11:06:29 -07:00
|
|
|
|
user.organizations = []
|
2019-05-21 15:53:48 +01:00
|
|
|
|
|
2024-01-15 16:45:55 -05:00
|
|
|
|
user.auth_type = AuthType.EMAIL
|
2020-05-22 09:36:07 +01:00
|
|
|
|
user.email_address = get_archived_db_column_value(user.email_address)
|
2019-05-21 15:53:48 +01:00
|
|
|
|
user.mobile_number = None
|
|
|
|
|
|
user.password = str(uuid.uuid4())
|
|
|
|
|
|
# Changing the current_session_id signs the user out
|
2023-08-29 14:54:30 -07:00
|
|
|
|
user.current_session_id = "00000000-0000-0000-0000-000000000000"
|
2025-08-28 11:12:49 -07:00
|
|
|
|
user.state = UserState.INACTIVE
|
2019-05-21 15:53:48 +01:00
|
|
|
|
|
|
|
|
|
|
db.session.add(user)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def user_can_be_archived(user):
|
|
|
|
|
|
active_services = [x for x in user.services if x.active]
|
|
|
|
|
|
|
|
|
|
|
|
for service in active_services:
|
2023-08-29 14:54:30 -07:00
|
|
|
|
other_active_users = [
|
2025-08-28 11:12:49 -07:00
|
|
|
|
x for x in service.users if x.state == UserState.ACTIVE and x != user
|
2023-08-29 14:54:30 -07:00
|
|
|
|
]
|
2019-05-21 15:53:48 +01:00
|
|
|
|
|
|
|
|
|
|
if not other_active_users:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
2023-08-29 14:54:30 -07:00
|
|
|
|
if not any(
|
2024-02-21 13:47:04 -05:00
|
|
|
|
PermissionType.MANAGE_SETTINGS in user.get_permissions(service.id)
|
2023-08-29 14:54:30 -07:00
|
|
|
|
for user in other_active_users
|
|
|
|
|
|
):
|
2019-05-21 15:53:48 +01:00
|
|
|
|
# no-one else has manage settings
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
return True
|
2024-06-14 09:32:58 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def dao_report_users():
|
|
|
|
|
|
sql = """
|
|
|
|
|
|
select users.name, users.email_address, users.mobile_number, services.name as service_name
|
|
|
|
|
|
from users
|
|
|
|
|
|
inner join user_to_service on users.id=user_to_service.user_id
|
|
|
|
|
|
inner join services on services.id=user_to_service.service_id
|
|
|
|
|
|
where services.name not like '_archived%'
|
2024-08-14 10:29:30 -07:00
|
|
|
|
order by users.name asc
|
2024-06-14 09:32:58 -07:00
|
|
|
|
"""
|
|
|
|
|
|
return db.session.execute(text(sql))
|