import bcrypt import logging import json import uuid from flask_login import UserMixin from peewee import JOIN, IntegrityError, fn from uuid import uuid4 from datetime import datetime, timedelta from enum import Enum from data.database import (User, LoginService, FederatedLogin, RepositoryPermission, TeamMember, Team, Repository, TupleSelector, TeamRole, Namespace, Visibility, EmailConfirmation, Role, db_for_update, random_string_generator, UserRegion, ImageStorageLocation, ServiceKeyApproval, OAuthApplication, RepositoryBuildTrigger, UserPromptKind, UserPrompt, UserPromptTypes, DeletedNamespace, RobotAccountMetadata, NamespaceGeoRestriction) from data.model import (DataModelException, InvalidPasswordException, InvalidRobotException, InvalidUsernameException, InvalidEmailAddressException, TooManyLoginAttemptsException, db_transaction, notification, config, repository, _basequery) from data.text import prefix_search from util.names import format_robot_username, parse_robot_username from util.validation import (validate_username, validate_email, validate_password, INVALID_PASSWORD_MESSAGE) from util.backoff import exponential_backoff from util.timedeltastring import convert_to_timedelta logger = logging.getLogger(__name__) EXPONENTIAL_BACKOFF_SCALE = timedelta(seconds=1) def hash_password(password, salt=None): salt = salt or bcrypt.gensalt() return bcrypt.hashpw(password.encode('utf-8'), salt) def create_user(username, password, email, auto_verify=False, email_required=True, prompts=tuple(), is_possible_abuser=False): """ Creates a regular user, if allowed. """ if not validate_password(password): raise InvalidPasswordException(INVALID_PASSWORD_MESSAGE) created = create_user_noverify(username, email, email_required=email_required, prompts=prompts, is_possible_abuser=is_possible_abuser) created.password_hash = hash_password(password) created.verified = auto_verify created.save() return created def create_user_noverify(username, email, email_required=True, prompts=tuple(), is_possible_abuser=False): if email_required: if not validate_email(email): raise InvalidEmailAddressException('Invalid email address: %s' % email) else: # If email addresses are not required and none was specified, then we just use a unique # ID to ensure that the database consistency check remains intact. email = email or str(uuid.uuid4()) (username_valid, username_issue) = validate_username(username) if not username_valid: raise InvalidUsernameException('Invalid namespace %s: %s' % (username, username_issue)) try: existing = User.get((User.username == username) | (User.email == email)) logger.info('Existing user with same username or email.') # A user already exists with either the same username or email if existing.username == username: raise InvalidUsernameException('Username has already been taken: %s' % username) raise InvalidEmailAddressException('Email has already been used: %s' % email) except User.DoesNotExist: # This is actually the happy path logger.debug('Email and username are unique!') # Create the user. try: default_expr_s = _convert_to_s(config.app_config['DEFAULT_TAG_EXPIRATION']) default_max_builds = config.app_config.get('DEFAULT_NAMESPACE_MAXIMUM_BUILD_COUNT') threat_max_builds = config.app_config.get('THREAT_NAMESPACE_MAXIMUM_BUILD_COUNT') if is_possible_abuser and threat_max_builds is not None: default_max_builds = threat_max_builds new_user = User.create(username=username, email=email, removed_tag_expiration_s=default_expr_s, maximum_queued_builds_count=default_max_builds) for prompt in prompts: create_user_prompt(new_user, prompt) return new_user except Exception as ex: raise DataModelException(ex.message) def increase_maximum_build_count(user, maximum_queued_builds_count): """ Increases the maximum number of allowed builds on the namespace, if greater than that already present. """ if (user.maximum_queued_builds_count is not None and maximum_queued_builds_count > user.maximum_queued_builds_count): user.maximum_queued_builds_count = maximum_queued_builds_count user.save() def is_username_unique(test_username): try: User.get((User.username == test_username)) return False except User.DoesNotExist: return True def change_password(user, new_password): if not validate_password(new_password): raise InvalidPasswordException(INVALID_PASSWORD_MESSAGE) pw_hash = hash_password(new_password) user.invalid_login_attempts = 0 user.password_hash = pw_hash invalidate_all_sessions(user) # Remove any password required notifications for the user. notification.delete_notifications_by_kind(user, 'password_required') def get_default_user_prompts(features): prompts = set() if features.USER_METADATA: prompts.add(UserPromptTypes.ENTER_NAME) prompts.add(UserPromptTypes.ENTER_COMPANY) return prompts def has_user_prompts(user): try: UserPrompt.select().where(UserPrompt.user == user).get() return True except UserPrompt.DoesNotExist: return False def has_user_prompt(user, prompt_name): prompt_kind = UserPromptKind.get(name=prompt_name) try: UserPrompt.get(user=user, kind=prompt_kind) return True except UserPrompt.DoesNotExist: return False def create_user_prompt(user, prompt_name): prompt_kind = UserPromptKind.get(name=prompt_name) return UserPrompt.create(user=user, kind=prompt_kind) def remove_user_prompt(user, prompt_name): prompt_kind = UserPromptKind.get(name=prompt_name) UserPrompt.delete().where(UserPrompt.user == user, UserPrompt.kind == prompt_kind).execute() def get_user_prompts(user): query = UserPrompt.select().where(UserPrompt.user == user).join(UserPromptKind) return [prompt.kind.name for prompt in query] def change_username(user_id, new_username): (username_valid, username_issue) = validate_username(new_username) if not username_valid: raise InvalidUsernameException('Invalid username %s: %s' % (new_username, username_issue)) with db_transaction(): # Reload the user for update user = db_for_update(User.select().where(User.id == user_id)).get() # Rename the robots for robot in db_for_update(_list_entity_robots(user.username, include_metadata=False)): _, robot_shortname = parse_robot_username(robot.username) new_robot_name = format_robot_username(new_username, robot_shortname) robot.username = new_robot_name robot.save() # Rename the user user.username = new_username user.save() # Remove any prompts for username. remove_user_prompt(user, 'confirm_username') return user def change_invoice_email_address(user, invoice_email_address): # Note: We null out the address if it is an empty string. user.invoice_email_address = invoice_email_address or None user.save() def change_send_invoice_email(user, invoice_email): user.invoice_email = invoice_email user.save() def _convert_to_s(timespan_string): """ Returns the given timespan string (e.g. `2w` or `45s`) into seconds. """ return convert_to_timedelta(timespan_string).total_seconds() def change_user_tag_expiration(user, tag_expiration_s): """ Changes the tag expiration on the given user/org. Note that the specified expiration must be within the configured TAG_EXPIRATION_OPTIONS or this method will raise a DataModelException. """ allowed_options = [_convert_to_s(o) for o in config.app_config['TAG_EXPIRATION_OPTIONS']] if tag_expiration_s not in allowed_options: raise DataModelException('Invalid tag expiration option') user.removed_tag_expiration_s = tag_expiration_s user.save() def update_email(user, new_email, auto_verify=False): try: user.email = new_email user.verified = auto_verify user.save() except IntegrityError: raise DataModelException('E-mail address already used') def update_enabled(user, set_enabled): user.enabled = set_enabled user.save() def create_robot(robot_shortname, parent, description='', unstructured_metadata=None): (username_valid, username_issue) = validate_username(robot_shortname) if not username_valid: raise InvalidRobotException('The name for the robot \'%s\' is invalid: %s' % (robot_shortname, username_issue)) username = format_robot_username(parent.username, robot_shortname) try: User.get(User.username == username) msg = 'Existing robot with name: %s' % username logger.info(msg) raise InvalidRobotException(msg) except User.DoesNotExist: pass service = LoginService.get(name='quayrobot') try: with db_transaction(): created = User.create(username=username, robot=True) password = created.email FederatedLogin.create(user=created, service=service, service_ident=password) RobotAccountMetadata.create(robot_account=created, description=description[0:255], unstructured_json=unstructured_metadata or {}) return created, password except Exception as ex: raise DataModelException(ex.message) def get_or_create_robot_metadata(robot): defaults = dict(description='', unstructured_json={}) metadata, _ = RobotAccountMetadata.get_or_create(robot_account=robot, defaults=defaults) return metadata def update_robot_metadata(robot, description='', unstructured_json=None): """ Updates the description and user-specified unstructured metadata associated with a robot account to that specified. """ metadata = get_or_create_robot_metadata(robot) metadata.description = description metadata.unstructured_json = unstructured_json or metadata.unstructured_json or {} metadata.save() def get_robot_and_metadata(robot_shortname, parent): robot_username = format_robot_username(parent.username, robot_shortname) robot, metadata = lookup_robot_and_metadata(robot_username) return robot, robot.email, metadata def lookup_robot(robot_username): try: return (User .select(User, FederatedLogin) .join(FederatedLogin) .join(LoginService) .where(LoginService.name == 'quayrobot', User.username == robot_username, User.robot == True) .get()) except User.DoesNotExist: raise InvalidRobotException('Could not find robot with username: %s' % robot_username) def lookup_robot_and_metadata(robot_username): robot = lookup_robot(robot_username) return robot, get_or_create_robot_metadata(robot) def get_matching_robots(name_prefix, username, limit=10): admined_orgs = (_basequery.get_user_organizations(username) .switch(Team) .join(TeamRole) .where(TeamRole.name == 'admin')) prefix_checks = False for org in admined_orgs: org_search = prefix_search(User.username, org.username + '+' + name_prefix) prefix_checks = prefix_checks | org_search user_search = prefix_search(User.username, username + '+' + name_prefix) prefix_checks = prefix_checks | user_search return User.select().where(prefix_checks).limit(limit) def verify_robot(robot_username, password): result = parse_robot_username(robot_username) if result is None: raise InvalidRobotException('%s is an invalid robot name' % robot_username) # Find the matching robot. query = (User .select() .join(FederatedLogin) .join(LoginService) .where(FederatedLogin.service_ident == password, LoginService.name == 'quayrobot', User.username == robot_username)) try: robot = query.get() except User.DoesNotExist: msg = ('Could not find robot with username: %s and supplied password.' % robot_username) raise InvalidRobotException(msg) # Find the owner user and ensure it is not disabled. try: owner = User.get(User.username == result[0]) except User.DoesNotExist: raise InvalidRobotException('Robot %s owner does not exist' % robot_username) if not owner.enabled: raise InvalidRobotException('This user has been disabled. Please contact your administrator.') # Mark that the robot was accessed. _basequery.update_last_accessed(robot) return robot def regenerate_robot_token(robot_shortname, parent): robot_username = format_robot_username(parent.username, robot_shortname) robot, metadata = lookup_robot_and_metadata(robot_username) password = random_string_generator(length=64)() robot.email = password robot.uuid = str(uuid4()) service = LoginService.get(name='quayrobot') login = FederatedLogin.get(FederatedLogin.user == robot, FederatedLogin.service == service) login.service_ident = password login.save() robot.save() return robot, password, metadata def delete_robot(robot_username): try: robot = User.get(username=robot_username, robot=True) robot.delete_instance(recursive=True, delete_nullable=True) except User.DoesNotExist: raise InvalidRobotException('Could not find robot with username: %s' % robot_username) def list_namespace_robots(namespace): """ Returns all the robots found under the given namespace. """ return _list_entity_robots(namespace) def _list_entity_robots(entity_name, include_metadata=True): """ Return the list of robots for the specified entity. This MUST return a query, not a materialized list so that callers can use db_for_update. """ if include_metadata: query = (User .select(User, FederatedLogin, RobotAccountMetadata) .join(FederatedLogin) .switch(User) .join(RobotAccountMetadata, JOIN.LEFT_OUTER) .where(User.robot == True, User.username ** (entity_name + '+%'))) else: query = (User .select(User, FederatedLogin) .join(FederatedLogin) .where(User.robot == True, User.username ** (entity_name + '+%'))) return query def list_entity_robot_permission_teams(entity_name, limit=None, include_permissions=False): query = (_list_entity_robots(entity_name)) fields = [User.username, User.creation_date, User.last_accessed, FederatedLogin.service_ident, RobotAccountMetadata.description, RobotAccountMetadata.unstructured_json] if include_permissions: query = (query .join(RepositoryPermission, JOIN.LEFT_OUTER, on=(RepositoryPermission.user == FederatedLogin.user)) .join(Repository, JOIN.LEFT_OUTER) .switch(User) .join(TeamMember, JOIN.LEFT_OUTER) .join(Team, JOIN.LEFT_OUTER)) fields.append(Repository.name) fields.append(Team.name) query = query.limit(limit).order_by(User.last_accessed.desc()) return TupleSelector(query, fields) def update_user_metadata(user, metadata=None): """ Updates the metadata associated with the user, including his/her name and company. """ metadata = metadata if metadata is not None else {} with db_transaction(): if 'given_name' in metadata: user.given_name = metadata['given_name'] if 'family_name' in metadata: user.family_name = metadata['family_name'] if 'company' in metadata: user.company = metadata['company'] if 'location' in metadata: user.location = metadata['location'] user.save() # Remove any prompts associated with the user's metadata being needed. remove_user_prompt(user, UserPromptTypes.ENTER_NAME) remove_user_prompt(user, UserPromptTypes.ENTER_COMPANY) def _get_login_service(service_id): try: return LoginService.get(LoginService.name == service_id) except LoginService.DoesNotExist: return LoginService.create(name=service_id) def create_federated_user(username, email, service_id, service_ident, set_password_notification, metadata={}, email_required=True, confirm_username=True, prompts=tuple()): prompts = set(prompts) if confirm_username: prompts.add(UserPromptTypes.CONFIRM_USERNAME) new_user = create_user_noverify(username, email, email_required=email_required, prompts=prompts) new_user.verified = True new_user.save() FederatedLogin.create(user=new_user, service=_get_login_service(service_id), service_ident=service_ident, metadata_json=json.dumps(metadata)) if set_password_notification: notification.create_notification('password_required', new_user) return new_user def attach_federated_login(user, service_id, service_ident, metadata=None): service = _get_login_service(service_id) FederatedLogin.create(user=user, service=service, service_ident=service_ident, metadata_json=json.dumps(metadata or {})) return user def verify_federated_login(service_id, service_ident): try: found = (FederatedLogin .select(FederatedLogin, User) .join(LoginService) .switch(FederatedLogin).join(User) .where(FederatedLogin.service_ident == service_ident, LoginService.name == service_id) .get()) # Mark that the user was accessed. _basequery.update_last_accessed(found.user) return found.user except FederatedLogin.DoesNotExist: return None def list_federated_logins(user): selected = FederatedLogin.select(FederatedLogin.service_ident, LoginService.name, FederatedLogin.metadata_json) joined = selected.join(LoginService) return joined.where(LoginService.name != 'quayrobot', FederatedLogin.user == user) def lookup_federated_login(user, service_name): try: return list_federated_logins(user).where(LoginService.name == service_name).get() except FederatedLogin.DoesNotExist: return None def create_confirm_email_code(user, new_email=None): if new_email: if not validate_email(new_email): raise InvalidEmailAddressException('Invalid email address: %s' % new_email) code = EmailConfirmation.create(user=user, email_confirm=True, new_email=new_email) return code def confirm_user_email(code): try: code = EmailConfirmation.get(EmailConfirmation.code == code, EmailConfirmation.email_confirm == True) except EmailConfirmation.DoesNotExist: raise DataModelException('Invalid email confirmation code') user = code.user user.verified = True old_email = None new_email = code.new_email if new_email and new_email != old_email: if find_user_by_email(new_email): raise DataModelException('E-mail address already used') old_email = user.email user.email = new_email user.save() code.delete_instance() return user, new_email, old_email def create_reset_password_email_code(email): try: user = User.get(User.email == email) except User.DoesNotExist: raise InvalidEmailAddressException('Email address was not found') if user.organization: raise InvalidEmailAddressException('Organizations can not have passwords') code = EmailConfirmation.create(user=user, pw_reset=True) return code def validate_reset_code(code): # Find the reset code. try: code = EmailConfirmation.get(EmailConfirmation.code == code, EmailConfirmation.pw_reset == True) except EmailConfirmation.DoesNotExist: return None # Make sure the code is not expired. max_lifetime_duration = convert_to_timedelta(config.app_config['USER_RECOVERY_TOKEN_LIFETIME']) if code.created + max_lifetime_duration < datetime.now(): code.delete_instance() return None # Verify the user and return the code. user = code.user if not user.verified: user.verified = True user.save() code.delete_instance() return user def find_user_by_email(email): try: return User.get(User.email == email) except User.DoesNotExist: return None def get_nonrobot_user(username): try: return User.get(User.username == username, User.organization == False, User.robot == False) except User.DoesNotExist: return None def get_user(username): try: return User.get(User.username == username, User.organization == False) except User.DoesNotExist: return None def get_namespace_user(username): try: return User.get(User.username == username) except User.DoesNotExist: return None def get_user_or_org(username): try: return User.get(User.username == username, User.robot == False) except User.DoesNotExist: return None def get_user_by_id(user_db_id): try: return User.get(User.id == user_db_id, User.organization == False) except User.DoesNotExist: return None def get_user_map_by_ids(namespace_ids): id_user = {namespace_id: None for namespace_id in namespace_ids} users = User.select().where(User.id << namespace_ids, User.organization == False) for user in users: id_user[user.id] = user return id_user def get_namespace_user_by_user_id(namespace_user_db_id): try: return User.get(User.id == namespace_user_db_id, User.robot == False) except User.DoesNotExist: raise InvalidUsernameException('User with id does not exist: %s' % namespace_user_db_id) def get_namespace_by_user_id(namespace_user_db_id): try: return User.get(User.id == namespace_user_db_id, User.robot == False).username except User.DoesNotExist: raise InvalidUsernameException('User with id does not exist: %s' % namespace_user_db_id) def get_user_by_uuid(user_uuid): try: return User.get(User.uuid == user_uuid, User.organization == False) except User.DoesNotExist: return None def get_user_or_org_by_customer_id(customer_id): try: return User.get(User.stripe_id == customer_id) except User.DoesNotExist: return None def invalidate_all_sessions(user): """ Invalidates all existing user sessions by rotating the user's UUID. """ if not user: return user.uuid = str(uuid4()) user.save() def get_matching_user_namespaces(namespace_prefix, username, limit=10): namespace_user = get_namespace_user(username) namespace_user_id = namespace_user.id if namespace_user is not None else None namespace_search = prefix_search(Namespace.username, namespace_prefix) base_query = (Namespace .select() .distinct() .join(Repository, on=(Repository.namespace_user == Namespace.id)) .join(RepositoryPermission, JOIN.LEFT_OUTER) .where(namespace_search)) return _basequery.filter_to_repos_for_user(base_query, namespace_user_id).limit(limit) def get_matching_users(username_prefix, robot_namespace=None, organization=None, limit=20, exact_matches_only=False): user_search = prefix_search(User.username, username_prefix) if exact_matches_only: user_search = (User.username == username_prefix) direct_user_query = (user_search & (User.organization == False) & (User.robot == False)) if robot_namespace: robot_prefix = format_robot_username(robot_namespace, username_prefix) robot_search = prefix_search(User.username, robot_prefix) direct_user_query = ((robot_search & (User.robot == True)) | direct_user_query) query = (User .select(User.id, User.username, User.email, User.robot) .group_by(User.id, User.username, User.email, User.robot) .where(direct_user_query)) if organization: query = (query .select(User.id, User.username, User.email, User.robot, fn.Sum(Team.id)) .join(TeamMember, JOIN.LEFT_OUTER) .join(Team, JOIN.LEFT_OUTER, on=((Team.id == TeamMember.team) & (Team.organization == organization))) .order_by(User.robot.desc())) class MatchingUserResult(object): def __init__(self, *args): self.id = args[0] self.username = args[1] self.email = args[2] self.robot = args[3] if organization: self.is_org_member = (args[3] != None) else: self.is_org_member = None return (MatchingUserResult(*args) for args in query.tuples().limit(limit)) def verify_user(username_or_email, password): """ Verifies that the given username/email + password pair is valid. If the username or e-mail address is invalid, returns None. If the password specified does not match for the given user, either returns None or raises TooManyLoginAttemptsException if there have been too many invalid login attempts. Returns the user object if the login was valid. """ # Make sure we didn't get any unicode for the username. try: str(username_or_email) except ValueError: return None # Fetch the user with the matching username or e-mail address. try: fetched = User.get((User.username == username_or_email) | (User.email == username_or_email)) except User.DoesNotExist: return None # If the user has any invalid login attempts, check to see if we are within the exponential # backoff window for the user. If so, we raise an exception indicating that the user cannot # login. now = datetime.utcnow() if fetched.invalid_login_attempts > 0: can_retry_at = exponential_backoff(fetched.invalid_login_attempts, EXPONENTIAL_BACKOFF_SCALE, fetched.last_invalid_login) if can_retry_at > now: retry_after = can_retry_at - now raise TooManyLoginAttemptsException('Too many login attempts.', retry_after.total_seconds()) # Hash the given password and compare it to the specified password. if (fetched.password_hash and hash_password(password, fetched.password_hash) == fetched.password_hash): # If the user previously had any invalid login attempts, clear them out now. if fetched.invalid_login_attempts > 0: (User .update(invalid_login_attempts=0) .where(User.id == fetched.id) .execute()) # Mark that the user was accessed. _basequery.update_last_accessed(fetched) # Return the valid user. return fetched # Otherwise, update the user's invalid login attempts. (User .update(invalid_login_attempts=User.invalid_login_attempts+1, last_invalid_login=now) .where(User.id == fetched.id) .execute()) # We weren't able to authorize the user return None def get_all_repo_users(namespace_name, repository_name): return (RepositoryPermission .select(User, Role, RepositoryPermission) .join(User) .switch(RepositoryPermission) .join(Role) .switch(RepositoryPermission) .join(Repository) .join(Namespace, on=(Repository.namespace_user == Namespace.id)) .where(Namespace.username == namespace_name, Repository.name == repository_name)) def get_all_repo_users_transitive_via_teams(namespace_name, repository_name): return (User .select() .distinct() .join(TeamMember) .join(Team) .join(RepositoryPermission) .join(Repository) .join(Namespace, on=(Repository.namespace_user == Namespace.id)) .where(Namespace.username == namespace_name, Repository.name == repository_name)) def get_all_repo_users_transitive(namespace_name, repository_name): # Load the users found via teams and directly via permissions. via_teams = get_all_repo_users_transitive_via_teams(namespace_name, repository_name) directly = [perm.user for perm in get_all_repo_users(namespace_name, repository_name)] # Filter duplicates. user_set = set() def check_add(u): if u.username in user_set: return False user_set.add(u.username) return True return [user for user in list(directly) + list(via_teams) if check_add(user)] def get_private_repo_count(username): return (Repository .select() .join(Visibility) .switch(Repository) .join(Namespace, on=(Repository.namespace_user == Namespace.id)) .where(Namespace.username == username, Visibility.name == 'private') .count()) def get_active_users(disabled=True, deleted=False): query = (User .select() .where(User.organization == False, User.robot == False)) if not disabled: query = query.where(User.enabled == True) if not deleted: query = query.where(User.id.not_in(DeletedNamespace.select(DeletedNamespace.namespace))) return query def get_active_user_count(): return get_active_users().count() def get_robot_count(): return User.select().where(User.robot == True).count() def detach_external_login(user, service_name): try: service = LoginService.get(name=service_name) except LoginService.DoesNotExist: return FederatedLogin.delete().where(FederatedLogin.user == user, FederatedLogin.service == service).execute() def get_solely_admined_organizations(user_obj): """ Returns the organizations admined solely by the given user. """ orgs = (User.select() .where(User.organization == True) .join(Team) .join(TeamRole) .where(TeamRole.name == 'admin') .switch(Team) .join(TeamMember) .where(TeamMember.user == user_obj) .distinct()) # Filter to organizations where the user is the sole admin. solely_admined = [] for org in orgs: admin_user_count = (TeamMember.select() .join(Team) .join(TeamRole) .where(Team.organization == org, TeamRole.name == 'admin') .switch(TeamMember) .join(User) .where(User.robot == False) .distinct() .count()) if admin_user_count == 1: solely_admined.append(org) return solely_admined def mark_namespace_for_deletion(user, queues, namespace_gc_queue, force=False): """ Marks a namespace (as referenced by the given user) for deletion. A queue item will be added to delete the namespace's repositories and storage, while the namespace itself will be renamed, disabled, and delinked from other tables. """ if not user.enabled: return None if not force and not user.organization: # Ensure that the user is not the sole admin for any organizations. If so, then the user # cannot be deleted before those organizations are deleted or reassigned. organizations = get_solely_admined_organizations(user) if len(organizations) > 0: message = 'Cannot delete %s as you are the only admin for organizations: ' % user.username for index, org in enumerate(organizations): if index > 0: message = message + ', ' message = message + org.username raise DataModelException(message) # Delete all queue items for the user. for queue in queues: queue.delete_namespaced_items(user.username) # Delete non-repository related items. This operation is very quick, so we can do so here. _delete_user_linked_data(user) with db_transaction(): original_username = user.username user = db_for_update(User.select().where(User.id == user.id)).get() # Mark the namespace as deleted and ready for GC. try: marker = DeletedNamespace.create(namespace=user, original_username=original_username, original_email=user.email) except IntegrityError: return # Disable the namespace itself, and replace its various unique fields with UUIDs. user.enabled = False user.username = str(uuid4()) user.email = str(uuid4()) user.save() # Add a queueitem to delete the namespace. marker.queue_id = namespace_gc_queue.put([str(user.id)], json.dumps({ 'marker_id': marker.id, 'original_username': original_username, })) marker.save() return marker.id def delete_namespace_via_marker(marker_id, queues): """ Deletes a namespace referenced by the given DeletedNamespace marker ID. """ try: marker = DeletedNamespace.get(id=marker_id) except DeletedNamespace.DoesNotExist: return delete_user(marker.namespace, queues) def delete_user(user, queues): """ Deletes a user/organization/robot. Should *not* be called by any user-facing API. Instead, mark_namespace_for_deletion should be used, and the queue should call this method. """ # Delete all queue items for the user. for queue in queues: queue.delete_namespaced_items(user.username) # Delete any repositories under the user's namespace. for repo in list(Repository.select().where(Repository.namespace_user == user)): repository.purge_repository(user.username, repo.name) # Delete non-repository related items. _delete_user_linked_data(user) # Delete the user itself. user.delete_instance(recursive=True, delete_nullable=True) def _delete_user_linked_data(user): if user.organization: # Delete the organization's teams. with db_transaction(): for team in Team.select().where(Team.organization == user): team.delete_instance(recursive=True) # Delete any OAuth approvals and tokens associated with the user. with db_transaction(): for app in OAuthApplication.select().where(OAuthApplication.organization == user): app.delete_instance(recursive=True) else: # Remove the user from any teams in which they are a member. TeamMember.delete().where(TeamMember.user == user).execute() # Delete any repository buildtriggers where the user is the connected user. with db_transaction(): triggers = RepositoryBuildTrigger.select().where(RepositoryBuildTrigger.connected_user == user) for trigger in triggers: trigger.delete_instance(recursive=True, delete_nullable=False) # Delete any robots owned by this user. with db_transaction(): robots = list(list_namespace_robots(user.username)) for robot in robots: robot.delete_instance(recursive=True, delete_nullable=True) # Null out any service key approvals. We technically lose information here, but its better than # falling and only occurs if a superuser is being deleted. ServiceKeyApproval.update(approver=None).where(ServiceKeyApproval.approver == user).execute() def get_pull_credentials(robotname): try: robot = lookup_robot(robotname) except InvalidRobotException: return None try: login_info = FederatedLogin.get(user=robot) except FederatedLogin.DoesNotExist: return None return { 'username': robot.username, 'password': login_info.service_ident, 'registry': '%s://%s/v1/' % (config.app_config['PREFERRED_URL_SCHEME'], config.app_config['SERVER_HOSTNAME']), } def get_region_locations(user): """ Returns the locations defined as preferred storage for the given user. """ query = UserRegion.select().join(ImageStorageLocation).where(UserRegion.user == user) return set([region.location.name for region in query]) def get_federated_logins(user_ids, service_name): """ Returns all federated logins for the given user ids under the given external service. """ if not user_ids: return [] return (FederatedLogin .select() .join(User) .switch(FederatedLogin) .join(LoginService) .where(FederatedLogin.user << user_ids, LoginService.name == service_name)) def list_namespace_geo_restrictions(namespace_name): """ Returns all of the defined geographic restrictions for the given namespace. """ return (NamespaceGeoRestriction .select() .join(User) .where(User.username == namespace_name)) def get_minimum_user_id(): return User.select(fn.Min(User.id)).tuples().get()[0] class LoginWrappedDBUser(UserMixin): def __init__(self, user_uuid, db_user=None): self._uuid = user_uuid self._db_user = db_user def db_user(self): if not self._db_user: self._db_user = get_user_by_uuid(self._uuid) return self._db_user @property def is_authenticated(self): return self.db_user() is not None @property def is_active(self): return self.db_user() and self.db_user().verified def get_id(self): return unicode(self._uuid)