Start fleshing out support for robots.
This commit is contained in:
parent
f8ae8ed6cd
commit
026ed7ffb4
5 changed files with 176 additions and 28 deletions
|
@ -22,6 +22,14 @@ def close_db(exc):
|
||||||
app.teardown_request(close_db)
|
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 BaseModel(Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
database = db
|
database = db
|
||||||
|
@ -30,10 +38,12 @@ class BaseModel(Model):
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
username = CharField(unique=True, index=True)
|
username = CharField(unique=True, index=True)
|
||||||
password_hash = CharField(null=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)
|
verified = BooleanField(default=False)
|
||||||
stripe_id = CharField(index=True, null=True)
|
stripe_id = CharField(index=True, null=True)
|
||||||
organization = BooleanField(default=False, index=True)
|
organization = BooleanField(default=False, index=True)
|
||||||
|
robot = BooleanField(default=False, index=True)
|
||||||
invoice_email = BooleanField(default=False)
|
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):
|
class Webhook(BaseModel):
|
||||||
public_id = CharField(default=random_string_generator(length=64),
|
public_id = CharField(default=random_string_generator(length=64),
|
||||||
unique=True, index=True)
|
unique=True, index=True)
|
||||||
|
|
102
data/model.py
102
data/model.py
|
@ -6,6 +6,7 @@ import json
|
||||||
|
|
||||||
from database import *
|
from database import *
|
||||||
from util.validation import *
|
from util.validation import *
|
||||||
|
from util.names import format_robot_username
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -27,6 +28,10 @@ class InvalidOrganizationException(DataModelException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidRobotException(DataModelException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InvalidTeamException(DataModelException):
|
class InvalidTeamException(DataModelException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -60,7 +65,7 @@ def create_user(username, password, email):
|
||||||
try:
|
try:
|
||||||
existing = User.get((User.username == username) | (User.email == email))
|
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
|
# A user already exists with either the same username or email
|
||||||
if existing.username == username:
|
if existing.username == username:
|
||||||
|
@ -104,6 +109,65 @@ def create_organization(name, email, creating_user):
|
||||||
raise InvalidOrganizationException('Invalid organization name: %s' % name)
|
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):
|
def convert_user_to_organization(user, admin_user):
|
||||||
# Change the user to an organization.
|
# Change the user to an organization.
|
||||||
user.organization = True
|
user.organization = True
|
||||||
|
@ -123,6 +187,7 @@ def convert_user_to_organization(user, admin_user):
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
def create_team(name, org, team_role_name, description=''):
|
def create_team(name, org, team_role_name, description=''):
|
||||||
if not validate_username(name):
|
if not validate_username(name):
|
||||||
raise InvalidTeamException('Invalid team name: %s' % name)
|
raise InvalidTeamException('Invalid team name: %s' % name)
|
||||||
|
@ -136,16 +201,16 @@ def create_team(name, org, team_role_name, description=''):
|
||||||
description=description)
|
description=description)
|
||||||
|
|
||||||
|
|
||||||
def __get_user_admin_teams(org_name, team_name, username):
|
def __get_user_admin_teams(org_name, username):
|
||||||
Org = User.alias()
|
Org = User.alias()
|
||||||
user_teams = Team.select().join(TeamMember).join(User)
|
user_teams = Team.select().join(TeamMember).join(User)
|
||||||
with_org = user_teams.switch(Team).join(Org,
|
with_org = user_teams.switch(Team).join(Org,
|
||||||
on=(Org.id == Team.organization))
|
on=(Org.id == Team.organization))
|
||||||
with_role = with_org.switch(Team).join(TeamRole)
|
with_role = with_org.switch(Team).join(TeamRole)
|
||||||
admin_teams = with_role.where(User.username == username,
|
admin_teams = with_role.where(User.username == username,
|
||||||
Org.username == org_name,
|
Org.username == org_name,
|
||||||
TeamRole.name == 'admin')
|
TeamRole.name == 'admin')
|
||||||
return admin_teams
|
return admin_teams
|
||||||
|
|
||||||
|
|
||||||
def remove_team(org_name, team_name, removed_by_username):
|
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):
|
def create_federated_user(username, email, service_name, service_id):
|
||||||
new_user = create_user(username, None, email)
|
new_user = create_user(username, None, email)
|
||||||
new_user.verified = True
|
new_user.verified = True
|
||||||
new_user.save()
|
new_user.save()
|
||||||
|
|
||||||
service = LoginService.get(LoginService.name == service_name)
|
service = LoginService.get(LoginService.name == service_name)
|
||||||
federated_user = FederatedLogin.create(user=new_user, service=service,
|
FederatedLogin.create(user=new_user, service=service,
|
||||||
service_ident=service_id)
|
service_ident=service_id)
|
||||||
|
|
||||||
|
return new_user
|
||||||
|
|
||||||
return new_user
|
|
||||||
|
|
||||||
def verify_federated_login(service_name, service_id):
|
def verify_federated_login(service_name, service_id):
|
||||||
selected = FederatedLogin.select(FederatedLogin, User)
|
selected = FederatedLogin.select(FederatedLogin, User)
|
||||||
|
|
|
@ -15,7 +15,7 @@ from data.queue import dockerfile_build_queue
|
||||||
from data.plans import USER_PLANS, BUSINESS_PLANS, get_plan
|
from data.plans import USER_PLANS, BUSINESS_PLANS, get_plan
|
||||||
from app import app
|
from app import app
|
||||||
from util.email import send_confirmation_email, send_recovery_email
|
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 util.gravatar import compute_hash
|
||||||
from auth.permissions import (ReadRepositoryPermission,
|
from auth.permissions import (ReadRepositoryPermission,
|
||||||
ModifyRepositoryPermission,
|
ModifyRepositoryPermission,
|
||||||
|
@ -1501,3 +1501,78 @@ def get_org_subscription(orgname):
|
||||||
})
|
})
|
||||||
|
|
||||||
abort(403)
|
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/<orgname>/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/<robot_shortname>', 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/<orgname>/robots/<robot_shortname>',
|
||||||
|
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/<robot_shortname>', 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/<orgname>/robots/<robot_shortname>',
|
||||||
|
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)
|
||||||
|
|
|
@ -109,6 +109,7 @@ def initialize_database():
|
||||||
Visibility.create(name='public')
|
Visibility.create(name='public')
|
||||||
Visibility.create(name='private')
|
Visibility.create(name='private')
|
||||||
LoginService.create(name='github')
|
LoginService.create(name='github')
|
||||||
|
LoginService.create(name='quayrobot')
|
||||||
|
|
||||||
|
|
||||||
def wipe_database():
|
def wipe_database():
|
||||||
|
|
|
@ -20,3 +20,7 @@ def parse_repository_name(f):
|
||||||
(namespace, repository) = parse_namespace_repository(repository)
|
(namespace, repository) = parse_namespace_repository(repository)
|
||||||
return f(namespace, repository, *args, **kwargs)
|
return f(namespace, repository, *args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def format_robot_username(parent_username, robot_shortname):
|
||||||
|
return '%s+%s' % (parent_username, robot_shortname)
|
||||||
|
|
Reference in a new issue