diff --git a/auth/auth.py b/auth/auth.py index ed0c8d82a..66ba4b921 100644 --- a/auth/auth.py +++ b/auth/auth.py @@ -25,7 +25,7 @@ def _load_user_from_cookie(): if not current_user.is_anonymous(): logger.debug('Loading user from cookie: %s', current_user.get_id()) set_authenticated_user_deferred(current_user.get_id()) - loaded = QuayDeferredPermissionUser(current_user.get_id(), 'user_db_id', {scopes.DIRECT_LOGIN}) + loaded = QuayDeferredPermissionUser(current_user.get_id(), 'user_uuid', {scopes.DIRECT_LOGIN}) identity_changed.send(app, identity=loaded) return current_user.db_user() return None @@ -58,7 +58,7 @@ def _validate_and_apply_oauth_token(token): set_authenticated_user(validated.authorized_user) set_validated_oauth_token(validated) - new_identity = QuayDeferredPermissionUser(validated.authorized_user.id, 'user_db_id', scope_set) + new_identity = QuayDeferredPermissionUser(validated.authorized_user.uuid, 'user_uuid', scope_set) identity_changed.send(app, identity=new_identity) @@ -97,8 +97,8 @@ def process_basic_auth(auth): robot = model.verify_robot(credentials[0], credentials[1]) logger.debug('Successfully validated robot: %s' % credentials[0]) set_authenticated_user(robot) - - deferred_robot = QuayDeferredPermissionUser(robot.id, 'user_db_id', {scopes.DIRECT_LOGIN}) + + deferred_robot = QuayDeferredPermissionUser(robot.uuid, 'user_uuid', {scopes.DIRECT_LOGIN}) identity_changed.send(app, identity=deferred_robot) return except model.InvalidRobotException: @@ -111,7 +111,7 @@ def process_basic_auth(auth): logger.debug('Successfully validated user: %s' % authenticated.username) set_authenticated_user(authenticated) - new_identity = QuayDeferredPermissionUser(authenticated.id, 'user_db_id', + new_identity = QuayDeferredPermissionUser(authenticated.uuid, 'user_uuid', {scopes.DIRECT_LOGIN}) identity_changed.send(app, identity=new_identity) return diff --git a/auth/auth_context.py b/auth/auth_context.py index 6c587f901..4f9cc4a3d 100644 --- a/auth/auth_context.py +++ b/auth/auth_context.py @@ -10,13 +10,13 @@ logger = logging.getLogger(__name__) def get_authenticated_user(): user = getattr(_request_ctx_stack.top, 'authenticated_user', None) if not user: - db_id = getattr(_request_ctx_stack.top, 'authenticated_db_id', None) - if not db_id: - logger.debug('No authenticated user or deferred database id.') + user_uuid = getattr(_request_ctx_stack.top, 'authenticated_user_uuid', None) + if not user_uuid: + logger.debug('No authenticated user or deferred database uuid.') return None logger.debug('Loading deferred authenticated user.') - loaded = model.get_user_by_id(db_id) + loaded = model.get_user_by_uuid(user_uuid) set_authenticated_user(loaded) user = loaded @@ -30,10 +30,10 @@ def set_authenticated_user(user_or_robot): ctx.authenticated_user = user_or_robot -def set_authenticated_user_deferred(user_or_robot_db_id): - logger.debug('Deferring loading of authenticated user object: %s', user_or_robot_db_id) +def set_authenticated_user_deferred(user_or_robot_uuid): + logger.debug('Deferring loading of authenticated user object with uuid: %s', user_or_robot_uuid) ctx = _request_ctx_stack.top - ctx.authenticated_db_id = user_or_robot_db_id + ctx.authenticated_user_uuid = user_or_robot_uuid def get_validated_oauth_token(): diff --git a/auth/permissions.py b/auth/permissions.py index eb9059c22..eee8d75ff 100644 --- a/auth/permissions.py +++ b/auth/permissions.py @@ -58,8 +58,8 @@ SCOPE_MAX_USER_ROLES.update({ class QuayDeferredPermissionUser(Identity): - def __init__(self, db_id, auth_type, scopes): - super(QuayDeferredPermissionUser, self).__init__(db_id, auth_type) + def __init__(self, id, auth_type, scopes): + super(QuayDeferredPermissionUser, self).__init__(id, auth_type) self._permissions_loaded = False self._scope_set = scopes @@ -88,14 +88,14 @@ class QuayDeferredPermissionUser(Identity): def can(self, permission): if not self._permissions_loaded: logger.debug('Loading user permissions after deferring.') - user_object = model.get_user_by_id(self.id) + user_object = model.get_user_by_uuid(self.id) # Add the superuser need, if applicable. if (user_object.username is not None and user_object.username in app.config.get('SUPER_USERS', [])): self.provides.add(_SuperUserNeed()) - # Add the user specific permissions, only for non-oauth permission + # Add the user specific permissions, only for non-oauth permission user_grant = _UserNeed(user_object.username, self._user_role_for_scopes('admin')) logger.debug('User permission: {0}'.format(user_grant)) self.provides.add(user_grant) @@ -217,7 +217,7 @@ class ViewTeamPermission(Permission): team_admin = _TeamNeed(org_name, team_name, 'admin') team_creator = _TeamNeed(org_name, team_name, 'creator') team_member = _TeamNeed(org_name, team_name, 'member') - admin_org = _OrganizationNeed(org_name, 'admin') + admin_org = _OrganizationNeed(org_name, 'admin') super(ViewTeamPermission, self).__init__(team_admin, team_creator, team_member, admin_org) @@ -228,11 +228,11 @@ def on_identity_loaded(sender, identity): # We have verified an identity, load in all of the permissions if isinstance(identity, QuayDeferredPermissionUser): - logger.debug('Deferring permissions for user: %s', identity.id) + logger.debug('Deferring permissions for user with uuid: %s', identity.id) - elif identity.auth_type == 'user_db_id': - logger.debug('Switching username permission to deferred object: %s', identity.id) - switch_to_deferred = QuayDeferredPermissionUser(identity.id, 'user_db_id', {scopes.DIRECT_LOGIN}) + elif identity.auth_type == 'user_uuid': + logger.debug('Switching username permission to deferred object with uuid: %s', identity.id) + switch_to_deferred = QuayDeferredPermissionUser(identity.id, 'user_uuid', {scopes.DIRECT_LOGIN}) identity_changed.send(app, identity=switch_to_deferred) elif identity.auth_type == 'token': diff --git a/data/database.py b/data/database.py index c3204f161..0ccdbf54f 100644 --- a/data/database.py +++ b/data/database.py @@ -26,7 +26,7 @@ SCHEME_RANDOM_FUNCTION = { 'mysql+pymysql': fn.Rand, 'sqlite': fn.Random, 'postgresql': fn.Random, - 'postgresql+psycopg2': fn.Random, + 'postgresql+psycopg2': fn.Random, } class CallableProxy(Proxy): @@ -137,6 +137,7 @@ class BaseModel(ReadSlaveModel): class User(BaseModel): + uuid = CharField(default=uuid_generator) username = CharField(unique=True, index=True) password_hash = CharField(null=True) email = CharField(unique=True, index=True, @@ -212,7 +213,7 @@ class FederatedLogin(BaseModel): user = QuayUserField(allows_robots=True, index=True) service = ForeignKeyField(LoginService, index=True) service_ident = CharField() - metadata_json = TextField(default='{}') + metadata_json = TextField(default='{}') class Meta: database = db @@ -250,7 +251,7 @@ class Repository(BaseModel): # Therefore, we define our own deletion order here and use the dependency system to verify it. ordered_dependencies = [RepositoryAuthorizedEmail, RepositoryTag, Image, LogEntry, RepositoryBuild, RepositoryBuildTrigger, RepositoryNotification, - RepositoryPermission, AccessToken] + RepositoryPermission, AccessToken] for query, fk in self.dependencies(search_nullable=True): model = fk.model_class @@ -457,7 +458,7 @@ class LogEntry(BaseModel): kind = ForeignKeyField(LogEntryKind, index=True) account = QuayUserField(index=True, related_name='account') performer = QuayUserField(allows_robots=True, index=True, null=True, - related_name='performer') + related_name='performer') repository = ForeignKeyField(Repository, index=True, null=True) datetime = DateTimeField(default=datetime.now, index=True) ip = CharField(null=True) @@ -537,7 +538,7 @@ class RepositoryAuthorizedEmail(BaseModel): # create a unique index on email and repository (('email', 'repository'), True), ) - + all_models = [User, Repository, Image, AccessToken, Role, RepositoryPermission, Visibility, diff --git a/data/migrations/versions/17f11e265e13_add_uuid_field_to_user.py b/data/migrations/versions/17f11e265e13_add_uuid_field_to_user.py new file mode 100644 index 000000000..19b79df5e --- /dev/null +++ b/data/migrations/versions/17f11e265e13_add_uuid_field_to_user.py @@ -0,0 +1,26 @@ +"""add uuid field to user + +Revision ID: 17f11e265e13 +Revises: 204abf14783d +Create Date: 2014-11-11 14:32:54.866188 + +""" + +# revision identifiers, used by Alembic. +revision = '17f11e265e13' +down_revision = '204abf14783d' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +def upgrade(tables): + ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('uuid', sa.String(length=255), nullable=False)) + ### end Alembic commands ### + + +def downgrade(tables): + ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'uuid') + ### end Alembic commands ### diff --git a/data/model/legacy.py b/data/model/legacy.py index c06df82de..46a81bf67 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -132,7 +132,7 @@ def create_user(username, password, email, auto_verify=False): created = _create_user(username, email) created.password_hash = hash_password(password) - created.verified = auto_verify + created.verified = auto_verify created.save() return created @@ -194,7 +194,7 @@ def create_organization(name, email, creating_user): return new_org except InvalidUsernameException: msg = ('Invalid organization name: %s Organization names must consist ' + - 'solely of lower case letters, numbers, and underscores. ' + + 'solely of lower case letters, numbers, and underscores. ' + '[a-z0-9_]') % name raise InvalidOrganizationException(msg) @@ -380,7 +380,7 @@ def remove_team(org_name, team_name, removed_by_username): def add_or_invite_to_team(inviter, team, user=None, email=None, requires_invite=True): # If the user is a member of the organization, then we simply add the # user directly to the team. Otherwise, an invite is created for the user/email. - # We return None if the user was directly added and the invite object if the user was invited. + # We return None if the user was directly added and the invite object if the user was invited. if user and requires_invite: orgname = team.organization.username @@ -390,7 +390,7 @@ def add_or_invite_to_team(inviter, team, user=None, email=None, requires_invite= if not user.username.startswith(orgname + '+'): raise InvalidTeamMemberException('Cannot add the specified robot to this team, ' + 'as it is not a member of the organization') - else: + else: Org = User.alias() found = User.select(User.username) found = found.where(User.username == user.username).join(TeamMember).join(Team) @@ -525,7 +525,7 @@ def confirm_user_email(code): code = EmailConfirmation.get(EmailConfirmation.code == code, EmailConfirmation.email_confirm == True) except EmailConfirmation.DoesNotExist: - raise DataModelException('Invalid email confirmation code.') + raise DataModelException('Invalid email confirmation code.') user = code.user user.verified = True @@ -534,11 +534,11 @@ def confirm_user_email(code): new_email = code.new_email if new_email: if find_user_by_email(new_email): - raise DataModelException('E-mail address already used.') - + raise DataModelException('E-mail address already used.') + old_email = user.email user.email = new_email - + user.save() code.delete_instance() @@ -614,13 +614,20 @@ def get_namespace_by_user_id(namespace_user_db_id): 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 get_matching_teams(team_prefix, organization): +def get_matching_teams(team_prefix, organization): query = Team.select().where(Team.name ** (team_prefix + '%'), Team.organization == organization) return query.limit(10) @@ -628,13 +635,13 @@ def get_matching_teams(team_prefix, organization): def get_matching_users(username_prefix, robot_namespace=None, organization=None): - direct_user_query = (User.username ** (username_prefix + '%') & + direct_user_query = (User.username ** (username_prefix + '%') & (User.organization == False) & (User.robot == False)) if robot_namespace: robot_prefix = format_robot_username(robot_namespace, username_prefix) direct_user_query = (direct_user_query | - (User.username ** (robot_prefix + '%') & + (User.username ** (robot_prefix + '%') & (User.robot == True))) query = (User @@ -1198,7 +1205,7 @@ def __translate_ancestry(old_ancestry, translations, repository, username, prefe translations[old_id] = image_in_repo.id return translations[old_id] - # Select all the ancestor Docker IDs in a single query. + # Select all the ancestor Docker IDs in a single query. old_ids = [int(id_str) for id_str in old_ancestry.split('/')[1:-1]] query = Image.select(Image.id, Image.docker_image_id).where(Image.id << old_ids) old_images = {i.id: i.docker_image_id for i in query} @@ -1592,7 +1599,7 @@ def garbage_collect_storage(storage_id_whitelist): storage_id_whitelist, (ImageStorage, ImageStoragePlacement, ImageStorageLocation)) - + paths_to_remove = placements_query_to_paths_set(placements_to_remove.clone()) # Remove the placements for orphaned storages @@ -1607,7 +1614,7 @@ def garbage_collect_storage(storage_id_whitelist): orphaned_storages = list(orphaned_storage_query(ImageStorage.select(ImageStorage.id), storage_id_whitelist, (ImageStorage.id,))) - if len(orphaned_storages) > 0: + if len(orphaned_storages) > 0: (ImageStorage .delete() .where(ImageStorage.id << orphaned_storages) @@ -1967,7 +1974,7 @@ def create_repository_build(repo, access_token, job_config_obj, dockerfile_id, def get_pull_robot_name(trigger): if not trigger.pull_robot: return None - + return trigger.pull_robot.username @@ -2146,14 +2153,14 @@ def list_notifications(user, kind_name=None, id_filter=None, include_dismissed=F AdminTeamMember.team)) .join(AdminUser, JOIN_LEFT_OUTER, on=(AdminTeamMember.user == AdminUser.id)) - .where((Notification.target == user) | + .where((Notification.target == user) | ((AdminUser.id == user) & (TeamRole.name == 'admin'))) .order_by(Notification.created) .desc()) if not include_dismissed: query = query.switch(Notification).where(Notification.dismissed == False) - + if kind_name: query = (query .switch(Notification) @@ -2278,7 +2285,7 @@ def confirm_email_authorization_for_repo(code): .where(RepositoryAuthorizedEmail.code == code) .get()) except RepositoryAuthorizedEmail.DoesNotExist: - raise DataModelException('Invalid confirmation code.') + raise DataModelException('Invalid confirmation code.') found.confirmed = True found.save() @@ -2310,7 +2317,7 @@ def lookup_team_invite(code, user=None): raise DataModelException('Invalid confirmation code.') if user and found.user != user: - raise DataModelException('Invalid confirmation code.') + raise DataModelException('Invalid confirmation code.') return found @@ -2330,7 +2337,7 @@ def confirm_team_invite(code, user): # If the invite is for a specific user, we have to confirm that here. if found.user is not None and found.user != user: - message = """This invite is intended for user "%s". + message = """This invite is intended for user "%s". Please login to that account and try again.""" % found.user.username raise DataModelException(message) diff --git a/endpoints/common.py b/endpoints/common.py index a21c6c8ca..12093b288 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -85,23 +85,19 @@ def param_required(param_name): @login_manager.user_loader -def load_user(user_db_id): - logger.debug('User loader loading deferred user id: %s' % user_db_id) - try: - user_db_id_int = int(user_db_id) - return _LoginWrappedDBUser(user_db_id_int) - except ValueError: - return None +def load_user(user_uuid): + logger.debug('User loader loading deferred user with uuid: %s' % user_uuid) + return _LoginWrappedDBUser(user_uuid) class _LoginWrappedDBUser(UserMixin): - def __init__(self, user_db_id, db_user=None): - self._db_id = user_db_id + 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 = model.get_user_by_id(self._db_id) + self._db_user = model.get_user_by_uuid(self._uuid) return self._db_user def is_authenticated(self): @@ -111,13 +107,13 @@ class _LoginWrappedDBUser(UserMixin): return self.db_user().verified def get_id(self): - return unicode(self._db_id) + return unicode(self._uuid) def common_login(db_user): - if login_user(_LoginWrappedDBUser(db_user.id, db_user)): - logger.debug('Successfully signed in as: %s' % db_user.username) - new_identity = QuayDeferredPermissionUser(db_user.id, 'user_db_id', {scopes.DIRECT_LOGIN}) + if login_user(_LoginWrappedDBUser(db_user.uuid, db_user)): + logger.debug('Successfully signed in as: %s (%s)' % (db_user.username, db_user.uuid)) + new_identity = QuayDeferredPermissionUser(db_user.uuid, 'user_uuid', {scopes.DIRECT_LOGIN}) identity_changed.send(app, identity=new_identity) session['login_time'] = datetime.datetime.now() return True @@ -279,4 +275,4 @@ def start_build(repository, dockerfile_id, tags, build_name, subdir, manual, spawn_notification(repository, 'build_queued', event_data, subpage='build?current=%s' % build_request.uuid, pathargs=['build', build_request.uuid]) - return build_request \ No newline at end of file + return build_request diff --git a/test/data/test.db b/test/data/test.db index b58ef5c9e..c3c2c33bc 100644 Binary files a/test/data/test.db and b/test/data/test.db differ