diff --git a/data/database.py b/data/database.py index 911256dc0..c56918071 100644 --- a/data/database.py +++ b/data/database.py @@ -38,9 +38,16 @@ class User(BaseModel): class Team(BaseModel): - name = CharField() + name = CharField(index=True) organization = ForeignKeyField(User, index=True) + class Meta: + database = db + indexes = ( + # A team name must be unique within an organization + (('name', 'organization'), True), + ) + class TeamMember(BaseModel): user = ForeignKeyField(User, index=True) @@ -97,13 +104,15 @@ class Role(BaseModel): class RepositoryPermission(BaseModel): - user = ForeignKeyField(User, index=True) + team = ForeignKeyField(Team, index=True, null=True) + user = ForeignKeyField(User, index=True, null=True) repository = ForeignKeyField(Repository, index=True) role = ForeignKeyField(Role) class Meta: database = db indexes = ( + (('team', 'repository'), True), (('user', 'repository'), True), ) @@ -116,7 +125,6 @@ class TeamPermission(BaseModel): class Meta: database = db indexes = ( - # A team may only have one permission level in an org (('team', 'organization'), True), ) diff --git a/data/model.py b/data/model.py index 67e47a09c..d65a028b2 100644 --- a/data/model.py +++ b/data/model.py @@ -22,6 +22,14 @@ class InvalidUsernameException(DataModelException): pass +class InvalidOrganizationException(DataModelException): + pass + + +class InvalidTeamException(DataModelException): + pass + + class InvalidPasswordException(DataModelException): pass @@ -73,6 +81,58 @@ def create_user(username, password, email): raise DataModelException(ex.message) +def create_organization(name, email, creating_user): + try: + # Create the org + new_org = create_user(name, None, email) + new_org.organization = True + new_org.save() + + # Create a team for the owners + owners_team = create_team('Owners', new_org) + + # Add the user who created the org to the owners + add_user_to_team(creating_user, owners_team) + + # Give the owners team admin access to the namespace + set_team_org_permission(owners_team, new_org, 'admin') + + return new_org + except InvalidUsernameException: + raise InvalidOrganizationException('Invalid organization name: %s' % name) + + +def create_team(name, org): + if not validate_username(name): + raise InvalidTeamException('Invalid team name: %s' % name) + + if not org.organization: + raise InvalidOrganizationException('User with name %s is not an org.' % + org.username) + + return Team.create(name=name, organization=org) + + +def add_user_to_team(user, team): + return TeamMember.create(user=user, team=team) + + +def set_team_org_permission(team, org, role_name): + new_role = Role.get(Role.name == role_name) + + # Fetch any existing permission for this user on the repo + try: + perm = TeamPermission.get(TeamPermission.team == team, + TeamPermission.organization == org) + perm.role = new_role + perm.save() + return perm + except TeamPermission.DoesNotExist: + new_perm = TeamPermission.create(team=team, organization=org, + role=new_role) + return new_perm + + def create_federated_user(username, email, service_name, service_id): new_user = create_user(username, None, email) new_user.verified = True @@ -166,35 +226,27 @@ def verify_user(username, password): # We weren't able to authorize the user return None -class dotdict(dict): - def __getattr__(self, name): - return self[name] - def get_user_organizations(username): - # TODO: return the orgs that the user is apart of. - return [dotdict({ - 'username': 'testorg', - 'email': 'testorg@quay.io' - })] + UserAlias = User.alias() + all_teams = User.select().join(Team).join(TeamMember) + with_user = all_teams.join(UserAlias, on=(UserAlias.id == TeamMember.user)) + return with_user.where(User.organization == True, + UserAlias.username == username) -def lookup_organization(name, username=None): - if name == 'testorg': - return dotdict({ - 'username': 'testorg', - 'email': 'testorg@quay.io' - }) +def get_organization(name): + try: + return User.get(username=name, organization=True) + except User.DoesNotExist: + raise InvalidOrganizationException('Organization does not exist: %s' % + name) + - return None - - -def get_user_teams(username, organization): - # TODO: return the teams that the user is apart of. - return [dotdict({ - 'id': 1234, - 'name': 'Owners' - })] +def get_user_teams_within_org(username, organization): + joined = Team.select().join(TeamMember).join(User) + return joined.where(Team.organization == organization, + User.username == username) def get_visible_repositories(username=None, include_public=True, limit=None, @@ -328,8 +380,10 @@ def create_repository(namespace, name, owner, visibility='private'): repo = Repository.create(namespace=namespace, name=name, visibility=private) admin = Role.get(name='admin') - permission = RepositoryPermission.create(user=owner, repository=repo, - role=admin) + + if owner and not owner.organization: + permission = RepositoryPermission.create(user=owner, repository=repo, + role=admin) return repo @@ -451,59 +505,101 @@ def delete_all_repository_tags(namespace_name, repository_name): RepositoryTag.delete().where(RepositoryTag.repository == repo) -def user_permission_repo_query(username, namespace_name, repository_name): - selected = RepositoryPermission.select(User, Repository, Role, +def __entity_permission_repo_query(entity_id, entity_table, + entity_id_property, namespace_name, + repository_name): + """ This method works for both users and teams. """ + selected = RepositoryPermission.select(entity_table, Repository, Role, RepositoryPermission) - with_user = selected.join(User) + with_user = selected.join(entity_table) with_role = with_user.switch(RepositoryPermission).join(Role) with_repo = with_role.switch(RepositoryPermission).join(Repository) return with_repo.where(Repository.name == repository_name, Repository.namespace == namespace_name, - User.username == username) + entity_id_property == entity_id) def get_user_reponame_permission(username, namespace_name, repository_name): - fetched = list(user_permission_repo_query(username, namespace_name, - repository_name)) + fetched = list(__entity_permission_repo_query(username, User, User.username, + namespace_name, + repository_name)) if not fetched: raise DataModelException('User does not have permission for repo.') return fetched[0] +def get_team_reponame_permission(team_name, namespace_name, repository_name): + fetched = list(__entity_permission_repo_query(team_name, Team, Team.name, + namespace_name, + repository_name)) + if not fetched: + raise DataModelException('Team does not have permission for repo.') + + return fetched[0] + + +def delete_user_permission(username, namespace_name, repository_name): + if username == namespace_name: + raise DataModelException('Namespace owner must always be admin.') + + fetched = list(__entity_permission_repo_query(username, User, User.username, + namespace_name, + repository_name)) + if not fetched: + raise DataModelException('User does not have permission for repo.') + + fetched[0].delete_instance() + + +def delete_team_permission(team_name, namespace_name, repository_name): + fetched = list(__entity_permission_repo_query(team_name, Team, Team.name, + namespace_name, + repository_name)) + if not fetched: + raise DataModelException('Team does not have permission for repo.') + + fetched[0].delete_instance() + + +def __set_entity_repo_permission(entity_id, entity_table, entity_id_property, + permission_entity_property, namespace_name, + repository_name, role_name): + entity = entity_table.get(entity_id_property == entity_id) + repo = Repository.get(Repository.name == repository_name, + Repository.namespace == namespace_name) + new_role = Role.get(Role.name == role_name) + + # Fetch any existing permission for this user on the repo + try: + entity_attr = getattr(RepositoryPermission, permission_entity_property) + perm = RepositoryPermission.get(entity_attr == entity, + RepositoryPermission.repository == repo) + perm.role = new_role + perm.save() + return perm + except RepositoryPermission.DoesNotExist: + set_entity_kwargs = {permission_entity_property: entity} + new_perm = RepositoryPermission.create(repository=repo, role=new_role, + **set_entity_kwargs) + return new_perm + + def set_user_repo_permission(username, namespace_name, repository_name, role_name): if username == namespace_name: raise DataModelException('Namespace owner must always be admin.') - user = User.get(User.username == username) - repo = Repository.get(Repository.name == repository_name, - Repository.namespace == namespace_name) - new_role = Role.get(Role.name == role_name) - - # Fetch any existing permission for this user on the repo - try: - perm = RepositoryPermission.get(RepositoryPermission.user == user, - RepositoryPermission.repository == repo) - perm.role = new_role - perm.save() - return perm - except RepositoryPermission.DoesNotExist: - new_perm = RepositoryPermission.create(repository=repo, user=user, - role=new_role) - return new_perm + return __set_entity_repo_permission(username, User, User.username, 'user', + namespace_name, repository_name, + role_name) -def delete_user_permission(username, namespace_name, repository_name): - if username == namespace_name: - raise DataModelException('Namespace owner must always be admin.') - - fetched = list(user_permission_repo_query(username, namespace_name, - repository_name)) - if not fetched: - raise DataModelException('User does not have permission for repo.') - - fetched[0].delete_instance() +def set_team_repo_permission(team_name, namespace_name, repository_name, + role_name): + return __set_entity_repo_permission(team_name, Team, Team.name, 'team', + namespace_name, repository_name, + role_name) def purge_repository(namespace_name, repository_name): diff --git a/endpoints/api.py b/endpoints/api.py index 1ebe4f2a2..53099d030 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -208,11 +208,11 @@ def get_organization(orgname): abort(404) user = current_user.db_user() - organization = model.lookup_organization(orgname, username = user.username) - if not organization: + org = model.get_organization(orgname) + if not org: abort(404) - teams = model.get_user_teams(user.username, organization) + teams = model.get_user_teams_within_org(user.username, org) return jsonify(org_view(organization, teams)) diff --git a/initdb.py b/initdb.py index c5afbdc93..8120ea48a 100644 --- a/initdb.py +++ b/initdb.py @@ -148,6 +148,19 @@ if __name__ == '__main__': 'Empty repository which is building.', False, [], (0, [], None)) + + org = model.create_organization('devtableorg', 'quay@devtable.com', + new_user_1) + + org_repo = __generate_repository(org, 'orgrepo', + 'Repository owned by an org.', False, + [], (4, [], ['latest', 'prod'])) + + reader_team = model.create_team('Readers', org) + model.set_team_repo_permission(reader_team.name, org_repo.namespace, + org_repo.name, 'read') + model.add_user_to_team(new_user_2, reader_team) + token = model.create_access_token(building, 'write') tag = 'ci.devtable.com:5000/%s/%s' % (building.namespace, building.name) build = model.create_repository_build(building, token, '123-45-6789', tag) diff --git a/test/data/test.db b/test/data/test.db index 28a606a85..242e6c95a 100644 Binary files a/test/data/test.db and b/test/data/test.db differ