diff --git a/data/database.py b/data/database.py index 1a8291d5a..9d1744c0a 100644 --- a/data/database.py +++ b/data/database.py @@ -22,6 +22,14 @@ def close_db(exc): app.teardown_request(close_db) +def random_string_generator(length=16): + def random_string(): + random = SystemRandom() + return ''.join([random.choice(string.ascii_uppercase + string.digits) + for _ in range(length)]) + return random_string + + class BaseModel(Model): class Meta: database = db @@ -30,10 +38,12 @@ class BaseModel(Model): class User(BaseModel): username = CharField(unique=True, index=True) password_hash = CharField(null=True) - email = CharField(unique=True, index=True) + email = CharField(unique=True, index=True, + default=random_string_generator(length=64)) verified = BooleanField(default=False) stripe_id = CharField(index=True, null=True) organization = BooleanField(default=False, index=True) + robot = BooleanField(default=False, index=True) invoice_email = BooleanField(default=False) @@ -123,14 +133,6 @@ class RepositoryPermission(BaseModel): ) -def random_string_generator(length=16): - def random_string(): - random = SystemRandom() - return ''.join([random.choice(string.ascii_uppercase + string.digits) - for x in range(length)]) - return random_string - - class Webhook(BaseModel): public_id = CharField(default=random_string_generator(length=64), unique=True, index=True) diff --git a/data/model.py b/data/model.py index 28c910d28..a18dc0af4 100644 --- a/data/model.py +++ b/data/model.py @@ -6,6 +6,7 @@ import json from database import * from util.validation import * +from util.names import format_robot_username logger = logging.getLogger(__name__) @@ -27,6 +28,10 @@ class InvalidOrganizationException(DataModelException): pass +class InvalidRobotException(DataModelException): + pass + + class InvalidTeamException(DataModelException): pass @@ -60,7 +65,7 @@ def create_user(username, password, email): try: existing = User.get((User.username == username) | (User.email == email)) - logger.debug('Existing user with same username or 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: @@ -104,6 +109,65 @@ def create_organization(name, email, creating_user): raise InvalidOrganizationException('Invalid organization name: %s' % name) +def create_robot(robot_shortname, parent): + if not validate_username(robot_shortname): + raise InvalidRobotException('The name for the robot \'%s\' is invalid.' % + robot_shortname) + + 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 + + try: + created = User.create(username=username, robot=True) + + service = LoginService.get(name='quayrobot') + password = created.email + FederatedLogin.create(user=created, service=service, + service_ident=password) + + return created, password + except Exception as ex: + raise DataModelException(ex.message) + + +def verify_robot(robot_username, password): + joined = User.select().join(FederatedLogin).join(LoginService) + found = list(joined.where(FederatedLogin.service_ident == password, + LoginService.name == 'quayrobot', + User.username == robot_username)) + if not found: + msg = ('Could not find robot with username: %s and supplied password.' % + robot_username) + raise InvalidRobotException(msg) + + return found[0] + + +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_entity_robots(entity_name): + selected = User.select(User.username, FederatedLogin.service_ident) + joined = selected.join(FederatedLogin) + return joined.where(User.robot == True, + User.username ** (entity_name + '+%')).tuples() + + def convert_user_to_organization(user, admin_user): # Change the user to an organization. user.organization = True @@ -123,6 +187,7 @@ def convert_user_to_organization(user, admin_user): return user + def create_team(name, org, team_role_name, description=''): if not validate_username(name): raise InvalidTeamException('Invalid team name: %s' % name) @@ -136,16 +201,16 @@ def create_team(name, org, team_role_name, description=''): description=description) -def __get_user_admin_teams(org_name, team_name, username): - Org = User.alias() - user_teams = Team.select().join(TeamMember).join(User) - with_org = user_teams.switch(Team).join(Org, - on=(Org.id == Team.organization)) - with_role = with_org.switch(Team).join(TeamRole) - admin_teams = with_role.where(User.username == username, - Org.username == org_name, - TeamRole.name == 'admin') - return admin_teams +def __get_user_admin_teams(org_name, username): + Org = User.alias() + user_teams = Team.select().join(TeamMember).join(User) + with_org = user_teams.switch(Team).join(Org, + on=(Org.id == Team.organization)) + with_role = with_org.switch(Team).join(TeamRole) + admin_teams = with_role.where(User.username == username, + Org.username == org_name, + TeamRole.name == 'admin') + return admin_teams def remove_team(org_name, team_name, removed_by_username): @@ -228,15 +293,16 @@ def set_team_org_permission(team, team_role_name, set_by_username): def create_federated_user(username, email, service_name, service_id): - new_user = create_user(username, None, email) - new_user.verified = True - new_user.save() + new_user = create_user(username, None, email) + new_user.verified = True + new_user.save() - service = LoginService.get(LoginService.name == service_name) - federated_user = FederatedLogin.create(user=new_user, service=service, - service_ident=service_id) + service = LoginService.get(LoginService.name == service_name) + FederatedLogin.create(user=new_user, service=service, + service_ident=service_id) + + return new_user - return new_user def verify_federated_login(service_name, service_id): selected = FederatedLogin.select(FederatedLogin, User) diff --git a/endpoints/api.py b/endpoints/api.py index b8ab44c77..185459094 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -15,7 +15,7 @@ from data.queue import dockerfile_build_queue from data.plans import USER_PLANS, BUSINESS_PLANS, get_plan from app import app from util.email import send_confirmation_email, send_recovery_email -from util.names import parse_repository_name +from util.names import parse_repository_name, format_robot_username from util.gravatar import compute_hash from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission, @@ -1501,3 +1501,78 @@ def get_org_subscription(orgname): }) abort(403) + + +def robot_view(name, password): + return { + 'name': name, + 'password': password, + } + + +@app.route('/api/user/robots', methods=['GET']) +@api_login_required +def get_user_robots(): + user = current_user.db_user() + robots = model.list_entity_robots(user.username) + return jsonify({ + 'robots': [robot_view(name, password) for name, password in robots] + }) + + +@app.route('/api/organization//robots', methods=['GET']) +@api_login_required +def get_org_robots(orgname): + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + robots = model.list_entity_robots(orgname) + return jsonify({ + 'robots': [robot_view(name, password) for name, password in robots] + }) + + abort(403) + + +@app.route('/api/user/robots/', methods=['PUT']) +@api_login_required +def create_robot(robot_shortname): + parent = current_user.db_user() + robot, password = model.create_robot(robot_shortname, parent) + resp = jsonify(robot_view(robot.username, password)) + resp.status_code = 201 + return resp + + +@app.route('/api/organization//robots/', + methods=['PUT']) +@api_login_required +def create_org_robot(orgname, robot_shortname): + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + parent = model.get_organization(orgname) + robot, password = model.create_robot(robot_shortname, parent) + resp = jsonify(robot_view(robot.username, password)) + resp.status_code = 201 + return resp + + abort(403) + + +@app.route('/api/user/robots/', methods=['DELETE']) +@api_login_required +def delete_robot(robot_shortname): + parent = current_user.db_user() + model.delete_robot(format_robot_username(parent.username, robot_shortname)) + return make_response('No Content', 204) + + +@app.route('/api/organization//robots/', + methods=['DELETE']) +@api_login_required +def delete_org_robot(orgname, robot_shortname): + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + model.delete_robot(format_robot_username(orgname, robot_shortname)) + return make_response('No Content', 204) + + abort(403) diff --git a/initdb.py b/initdb.py index aebdbe684..5a2fbeb40 100644 --- a/initdb.py +++ b/initdb.py @@ -109,6 +109,7 @@ def initialize_database(): Visibility.create(name='public') Visibility.create(name='private') LoginService.create(name='github') + LoginService.create(name='quayrobot') def wipe_database(): diff --git a/util/names.py b/util/names.py index 25616a346..7e48468bb 100644 --- a/util/names.py +++ b/util/names.py @@ -20,3 +20,7 @@ def parse_repository_name(f): (namespace, repository) = parse_namespace_repository(repository) return f(namespace, repository, *args, **kwargs) return wrapper + + +def format_robot_username(parent_username, robot_shortname): + return '%s+%s' % (parent_username, robot_shortname)