Merge branch 'master' of https://bitbucket.org/yackob03/quay
This commit is contained in:
commit
aac43bb1c4
24 changed files with 742 additions and 215 deletions
14
auth/auth.py
14
auth/auth.py
|
@ -48,6 +48,20 @@ def process_basic_auth(auth):
|
|||
except model.DataModelException:
|
||||
logger.debug('Invalid token: %s' % credentials[1])
|
||||
|
||||
elif '+' in credentials[0]:
|
||||
logger.debug('Trying robot auth with credentials %s' % str(credentials))
|
||||
# Use as robot auth
|
||||
try:
|
||||
robot = model.verify_robot(credentials[0], credentials[1])
|
||||
logger.debug('Successfully validated robot: %s' % credentials[0])
|
||||
ctx = _request_ctx_stack.top
|
||||
ctx.authenticated_user = robot
|
||||
|
||||
identity_changed.send(app, identity=Identity(robot.username, 'username'))
|
||||
return
|
||||
except model.InvalidRobotException:
|
||||
logger.debug('Invalid robot or password for robot: %s' % credentials[0])
|
||||
|
||||
else:
|
||||
authenticated = model.verify_user(credentials[0], credentials[1])
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,7 +201,7 @@ def create_team(name, org, team_role_name, description=''):
|
|||
description=description)
|
||||
|
||||
|
||||
def __get_user_admin_teams(org_name, team_name, username):
|
||||
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,
|
||||
|
@ -233,11 +298,12 @@ def create_federated_user(username, email, service_name, service_id):
|
|||
new_user.save()
|
||||
|
||||
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)
|
||||
|
||||
return new_user
|
||||
|
||||
|
||||
def verify_federated_login(service_name, service_id):
|
||||
selected = FederatedLogin.select(FederatedLogin, User)
|
||||
with_service = selected.join(LoginService)
|
||||
|
@ -313,11 +379,19 @@ def get_matching_teams(team_prefix, organization):
|
|||
return query.limit(10)
|
||||
|
||||
|
||||
def get_matching_users(username_prefix, organization=None):
|
||||
def get_matching_users(username_prefix, robot_namespace=None,
|
||||
organization=None):
|
||||
Org = User.alias()
|
||||
users_no_orgs = (User.username ** (username_prefix + '%') &
|
||||
(User.organization == False))
|
||||
query = User.select(User.username, Org.username).where(users_no_orgs)
|
||||
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.robot == True)))
|
||||
|
||||
query = User.select(User.username, Org.username, User.robot).where(direct_user_query)
|
||||
|
||||
if organization:
|
||||
with_team = query.join(TeamMember, JOIN_LEFT_OUTER).join(Team,
|
||||
|
@ -330,6 +404,7 @@ def get_matching_users(username_prefix, organization=None):
|
|||
class MatchingUserResult(object):
|
||||
def __init__(self, *args):
|
||||
self.username = args[0]
|
||||
self.is_robot = args[2]
|
||||
if organization:
|
||||
self.is_org_member = (args[1] == organization.username)
|
||||
else:
|
||||
|
@ -552,7 +627,7 @@ def get_all_repo_teams(namespace_name, repository_name):
|
|||
|
||||
|
||||
def get_all_repo_users(namespace_name, repository_name):
|
||||
select = RepositoryPermission.select(User.username, Role.name,
|
||||
select = RepositoryPermission.select(User.username, User.robot, Role.name,
|
||||
RepositoryPermission)
|
||||
with_user = select.join(User)
|
||||
with_role = with_user.switch(RepositoryPermission).join(Role)
|
||||
|
|
160
endpoints/api.py
160
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,
|
||||
|
@ -44,6 +44,10 @@ def api_login_required(f):
|
|||
current_user.db_user().organization):
|
||||
abort(401)
|
||||
|
||||
if (current_user and current_user.db_user() and
|
||||
current_user.db_user().robot):
|
||||
abort(401)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_view
|
||||
|
||||
|
@ -260,20 +264,26 @@ def get_matching_users(prefix):
|
|||
def get_matching_entities(prefix):
|
||||
teams = []
|
||||
|
||||
organization_name = request.args.get('organization', None)
|
||||
namespace_name = request.args.get('namespace', None)
|
||||
robot_namespace = None
|
||||
organization = None
|
||||
if organization_name:
|
||||
permission = OrganizationMemberPermission(organization_name)
|
||||
if permission.can():
|
||||
try:
|
||||
organization = model.get_organization(organization_name)
|
||||
except model.InvalidOrganizationException:
|
||||
pass
|
||||
organization = model.get_organization(namespace_name)
|
||||
|
||||
if organization:
|
||||
# namespace name was an org
|
||||
permission = OrganizationMemberPermission(namespace_name)
|
||||
if permission.can():
|
||||
robot_namespace = namespace_name
|
||||
|
||||
if request.args.get('includeTeams', False):
|
||||
teams = model.get_matching_teams(prefix, organization)
|
||||
|
||||
users = model.get_matching_users(prefix, organization)
|
||||
except model.InvalidOrganizationException:
|
||||
# namespace name was a user
|
||||
if current_user.db_user().username == namespace_name:
|
||||
robot_namespace = namespace_name
|
||||
|
||||
users = model.get_matching_users(prefix, robot_namespace, organization)
|
||||
|
||||
def entity_team_view(team):
|
||||
result = {
|
||||
|
@ -287,10 +297,11 @@ def get_matching_entities(prefix):
|
|||
user_json = {
|
||||
'name': user.username,
|
||||
'kind': 'user',
|
||||
'is_robot': user.is_robot,
|
||||
}
|
||||
|
||||
if user.is_org_member is not None:
|
||||
user_json['is_org_member'] = user.is_org_member
|
||||
user_json['is_org_member'] = user.is_robot or user.is_org_member
|
||||
|
||||
return user_json
|
||||
|
||||
|
@ -449,7 +460,8 @@ def get_organization_private_allowed(orgname):
|
|||
|
||||
def member_view(member):
|
||||
return {
|
||||
'username': member.username
|
||||
'username': member.username,
|
||||
'is_robot': member.robot,
|
||||
}
|
||||
|
||||
|
||||
|
@ -911,8 +923,13 @@ def role_view(repo_perm_obj):
|
|||
}
|
||||
|
||||
|
||||
def wrap_role_view_org(role_json, org_member):
|
||||
role_json['is_org_member'] = org_member
|
||||
def wrap_role_view_user(role_json, user):
|
||||
role_json['is_robot'] = user.robot
|
||||
return role_json
|
||||
|
||||
|
||||
def wrap_role_view_org(role_json, user, org_members):
|
||||
role_json['is_org_member'] = user.robot or user.username in org_members
|
||||
return role_json
|
||||
|
||||
|
||||
|
@ -1021,22 +1038,30 @@ def list_repo_team_permissions(namespace, repository):
|
|||
def list_repo_user_permissions(namespace, repository):
|
||||
permission = AdministerRepositoryPermission(namespace, repository)
|
||||
if permission.can():
|
||||
# Determine how to wrap the permissions
|
||||
role_view_func = role_view
|
||||
# Lookup the organization (if any).
|
||||
org = None
|
||||
try:
|
||||
model.get_organization(namespace) # Will raise an error if not org
|
||||
org_members = model.get_organization_member_set(namespace)
|
||||
def wrapped_role_view(repo_perm):
|
||||
unwrapped = role_view(repo_perm)
|
||||
return wrap_role_view_org(unwrapped,
|
||||
repo_perm.user.username in org_members)
|
||||
|
||||
role_view_func = wrapped_role_view
|
||||
|
||||
org = model.get_organization(namespace) # Will raise an error if not org
|
||||
except model.InvalidOrganizationException:
|
||||
# This repository isn't under an org
|
||||
pass
|
||||
|
||||
# Determine how to wrap the role(s).
|
||||
def wrapped_role_view(repo_perm):
|
||||
return wrap_role_view_user(role_view(repo_perm), repo_perm.user)
|
||||
|
||||
role_view_func = wrapped_role_view
|
||||
|
||||
if org:
|
||||
org_members = model.get_organization_member_set(namespace)
|
||||
current_func = role_view_func
|
||||
|
||||
def wrapped_role_org_view(repo_perm):
|
||||
return wrap_role_view_org(current_func(repo_perm), repo_perm.user, org_members)
|
||||
|
||||
role_view_func = wrapped_role_org_view
|
||||
|
||||
# Load and return the permissions.
|
||||
repo_perms = model.get_all_repo_users(namespace, repository)
|
||||
return jsonify({
|
||||
'permissions': {perm.user.username: role_view_func(perm)
|
||||
|
@ -1056,13 +1081,12 @@ def get_user_permissions(namespace, repository, username):
|
|||
permission = AdministerRepositoryPermission(namespace, repository)
|
||||
if permission.can():
|
||||
perm = model.get_user_reponame_permission(username, namespace, repository)
|
||||
perm_view = role_view(perm)
|
||||
perm_view = wrap_role_view_user(role_view(perm), perm.user)
|
||||
|
||||
try:
|
||||
model.get_organization(namespace)
|
||||
org_members = model.get_organization_member_set(namespace)
|
||||
perm_view = wrap_role_view_org(perm_view,
|
||||
perm.user.username in org_members)
|
||||
perm_view = wrap_role_view_org(perm_view, perm.user, org_members)
|
||||
except model.InvalidOrganizationException:
|
||||
# This repository is not part of an organization
|
||||
pass
|
||||
|
@ -1101,13 +1125,12 @@ def change_user_permissions(namespace, repository, username):
|
|||
|
||||
perm = model.set_user_repo_permission(username, namespace, repository,
|
||||
new_permission['role'])
|
||||
perm_view = role_view(perm)
|
||||
perm_view = wrap_role_view_user(role_view(perm), perm.user)
|
||||
|
||||
try:
|
||||
model.get_organization(namespace)
|
||||
org_members = model.get_organization_member_set(namespace)
|
||||
perm_view = wrap_role_view_org(perm_view,
|
||||
perm.user.username in org_members)
|
||||
perm_view = wrap_role_view_org(perm_view, perm.user, org_members)
|
||||
except model.InvalidOrganizationException:
|
||||
# This repository is not part of an organization
|
||||
pass
|
||||
|
@ -1519,3 +1542,78 @@ def get_org_subscription(orgname):
|
|||
})
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
def robot_view(name, token):
|
||||
return {
|
||||
'name': name,
|
||||
'token': token,
|
||||
}
|
||||
|
||||
|
||||
@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)
|
||||
|
|
|
@ -62,6 +62,13 @@ def create_user():
|
|||
except model.InvalidTokenException:
|
||||
abort(401)
|
||||
|
||||
elif '+' in username:
|
||||
try:
|
||||
model.verify_robot(username, password)
|
||||
return make_response('Verified', 201)
|
||||
except model.InvalidRobotException:
|
||||
abort(401)
|
||||
|
||||
existing_user = model.get_user(username)
|
||||
if existing_user:
|
||||
verified = model.verify_user(username, password)
|
||||
|
|
|
@ -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():
|
||||
|
@ -131,6 +132,8 @@ def populate_database():
|
|||
new_user_1.verified = True
|
||||
new_user_1.save()
|
||||
|
||||
model.create_robot('dtrobot', new_user_1)
|
||||
|
||||
new_user_2 = model.create_user('public', 'password',
|
||||
'jacob.moshenko@gmail.com')
|
||||
new_user_2.verified = True
|
||||
|
@ -187,6 +190,8 @@ def populate_database():
|
|||
org.stripe_id = TEST_STRIPE_ID
|
||||
org.save()
|
||||
|
||||
model.create_robot('neworgrobot', org)
|
||||
|
||||
owners = model.get_organization_team('buynlarge', 'owners')
|
||||
owners.description = 'Owners have unfetterd access across the entire org.'
|
||||
owners.save()
|
||||
|
|
|
@ -3,6 +3,11 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
#input-box {
|
||||
padding: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -16,6 +21,45 @@ html, body {
|
|||
border-bottom: 1px dashed #aaa;
|
||||
}
|
||||
|
||||
.docker-auth-dialog .token-dialog-body .well {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.docker-auth-dialog .token-view {
|
||||
background: transparent;
|
||||
display: block;
|
||||
border: 0px transparent;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.docker-auth-dialog .download-cfg {
|
||||
float: left;
|
||||
padding-top: 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.docker-auth-dialog .download-cfg .fa-download {
|
||||
margin-right: 10px;
|
||||
font-size: 25px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#copyClipboard {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#copyClipboard.zeroclipboard-is-hover {
|
||||
background: #428bca;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#clipboardCopied.hovering {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
padding-bottom: 70px;
|
||||
}
|
||||
|
@ -32,10 +76,38 @@ html, body {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.footer-container.fixed {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.button-hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.robots-manager-element {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.robots-manager-element .alert {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.robots-manager-element .robot a {
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.robots-manager-element .robot .prefix {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.robots-manager-element .robot i {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.billing-options-element .current-card {
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
|
@ -950,21 +1022,6 @@ p.editable:hover i {
|
|||
width: 300px;
|
||||
}
|
||||
|
||||
.repo #copyClipboard {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.repo #copyClipboard.zeroclipboard-is-hover {
|
||||
background: #428bca;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.repo #clipboardCopied.hovering {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
.repo-image-view .id-container {
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
|
@ -1007,7 +1064,7 @@ p.editable:hover i {
|
|||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.repo #clipboardCopied {
|
||||
#clipboardCopied {
|
||||
font-size: 0.8em;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
|
@ -1018,7 +1075,7 @@ p.editable:hover i {
|
|||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.repo #clipboardCopied.animated {
|
||||
#clipboardCopied.animated {
|
||||
-webkit-animation: fadeOut 4s ease-in-out 0s 1 forwards;
|
||||
-moz-animation: fadeOut 4s ease-in-out 0s 1 forwards;
|
||||
-ms-animation: fadeOut 4s ease-in-out 0s 1 forwards;
|
||||
|
@ -1105,21 +1162,17 @@ p.editable:hover i {
|
|||
width: 300px;
|
||||
}
|
||||
|
||||
.repo-admin .token-dialog-body .well {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.repo-admin .token-view {
|
||||
background: transparent;
|
||||
display: block;
|
||||
border: 0px transparent;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.repo-admin .panel {
|
||||
display: inline-block;
|
||||
width: 620px;
|
||||
width: 720px;
|
||||
}
|
||||
|
||||
.repo-admin .prefix {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.repo-admin .admin-search {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.repo-admin .user i.fa-user {
|
||||
|
@ -1127,6 +1180,11 @@ p.editable:hover i {
|
|||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.repo-admin .user i.fa-wrench {
|
||||
margin-left: 1px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.repo-admin .team i.fa-group {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
@ -1258,18 +1316,6 @@ p.editable:hover i {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.repo .download-cfg {
|
||||
float: left;
|
||||
padding-top: 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.repo .download-cfg .icon-download {
|
||||
margin-right: 10px;
|
||||
font-size: 25px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.navbar-nav > li > .user-dropdown {
|
||||
padding-top: 9px;
|
||||
padding-bottom: 9px;
|
||||
|
@ -1725,16 +1771,16 @@ p.editable:hover i {
|
|||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.org-admin #members .side-controls {
|
||||
.side-controls {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.org-admin #members .result-count {
|
||||
.side-controls .result-count {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.org-admin #members .filter-input {
|
||||
.side-controls .filter-input {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
|
30
static/directives/docker-auth-dialog.html
Normal file
30
static/directives/docker-auth-dialog.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="dockerauthmodal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">
|
||||
<span ng-transclude></span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body token-dialog-body">
|
||||
<div class="alert alert-info">The docker <u>username</u> is <b>{{ username }}</b> and the <u>password</u> is the token below. You may use any value for email.</div>
|
||||
<div class="well well-sm">
|
||||
<input id="token-view" class="token-view" type="text" value="{{ token }}" onClick="this.select();" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<span class="download-cfg" ng-show="isDownloadSupported()">
|
||||
<i class="fa fa-download"></i>
|
||||
<a href="javascript:void(0)" ng-click="downloadCfg(shownRobot)">Download .dockercfg file</a>
|
||||
</span>
|
||||
<div id="clipboardCopied" style="display: none">
|
||||
Copied to clipboard
|
||||
</div>
|
||||
<button id="copyClipboard" type="button" class="btn btn-primary" data-clipboard-target="token-view">Copy to clipboard</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
5
static/directives/popup-input-button.html
Normal file
5
static/directives/popup-input-button.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
<button class="btn btn-success" data-trigger="click" bs-popover="'static/directives/popup-input-dialog.html'"
|
||||
data-placement="bottom" ng-click="popupShown()">
|
||||
<span ng-transclude></span>
|
||||
</button>
|
||||
|
4
static/directives/popup-input-dialog.html
Normal file
4
static/directives/popup-input-dialog.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<form name="popupinput" ng-submit="inputSubmit(); hide()" novalidate>
|
||||
<input id="input-box" type="text form-control" placeholder="{{ placeholder }}" ng-blur="hide()"
|
||||
ng-pattern="getRegexp(pattern)" ng-model="inputValue" ng-trim="false" ng-minlength="2" required>
|
||||
</form>
|
40
static/directives/robots-manager.html
Normal file
40
static/directives/robots-manager.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
<div class="robots-manager-element">
|
||||
<i class="fa fa-spinner fa-spin fa-3x" ng-show="loading"></i>
|
||||
<div class="alert alert-info">Robot accounts allow for delegating access in multiple repositories to role-based accounts that you manage</div>
|
||||
|
||||
<div class="container" ng-show="!loading">
|
||||
<div class="side-controls">
|
||||
<span class="popup-input-button" pattern="'^[a-zA-Z][a-zA-Z0-9]+$'" placeholder="'Robot Account Name'"
|
||||
submitted="createRobot(value)">
|
||||
<i class="fa fa-wrench"></i> Create Robot Account
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Robot Account Name</th>
|
||||
<th style="width: 150px"></th>
|
||||
</thead>
|
||||
|
||||
<tr ng-repeat="robotInfo in robots">
|
||||
<td class="robot">
|
||||
<i class="fa fa-wrench"></i>
|
||||
<a ng-click="showRobot(robotInfo)">
|
||||
<span class="prefix">{{ getPrefix(robotInfo.name) }}+</span>{{ getShortenedName(robotInfo.name) }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="delete-ui" tabindex="0">
|
||||
<span class="delete-ui-button" ng-click="deleteRobot(robotInfo)"><button class="btn btn-danger">Delete</button></span>
|
||||
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Robot Account"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="docker-auth-dialog" username="shownRobot.name" token="shownRobot.token"
|
||||
shown="!!shownRobot" counter="showRobotCounter">
|
||||
<i class="fa fa-wrench"></i> {{ shownRobot.name }}
|
||||
</div>
|
||||
</div>
|
228
static/js/app.js
228
static/js/app.js
|
@ -428,9 +428,9 @@ quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'an
|
|||
// WARNING WARNING WARNING
|
||||
$routeProvider.
|
||||
when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl,
|
||||
hideFooter: true, reloadOnSearch: false}).
|
||||
fixFooter: true, reloadOnSearch: false}).
|
||||
when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl,
|
||||
hideFooter: true}).
|
||||
fixFooter: true}).
|
||||
when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl}).
|
||||
when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl}).
|
||||
when('/repository/', {title: 'Repositories', description: 'Public and private docker repositories list',
|
||||
|
@ -550,7 +550,7 @@ quayApp.directive('plansTable', function () {
|
|||
priority: 0,
|
||||
templateUrl: '/static/directives/plans-table.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'plans': '=plans',
|
||||
|
@ -566,6 +566,208 @@ quayApp.directive('plansTable', function () {
|
|||
});
|
||||
|
||||
|
||||
quayApp.directive('dockerAuthDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/docker-auth-dialog.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'username': '=username',
|
||||
'token': '=token',
|
||||
'shown': '=shown',
|
||||
'counter': '=counter'
|
||||
},
|
||||
controller: function($scope, $element, Restangular) {
|
||||
$scope.isDownloadSupported = function() {
|
||||
try { return !!new Blob(); } catch(e){}
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.downloadCfg = function() {
|
||||
var auth = $.base64.encode($scope.username + ":" + $scope.token);
|
||||
config = {
|
||||
"https://quay.io/v1/": {
|
||||
"auth": auth,
|
||||
"email": ""
|
||||
}
|
||||
};
|
||||
|
||||
var file = JSON.stringify(config, null, ' ');
|
||||
var blob = new Blob([file]);
|
||||
saveAs(blob, '.dockercfg');
|
||||
};
|
||||
|
||||
var show = function(r) {
|
||||
if (!$scope.shown || !$scope.username || !$scope.token) {
|
||||
$('#dockerauthmodal').modal('hide');
|
||||
return;
|
||||
}
|
||||
|
||||
$('#copyClipboard').clipboardCopy();
|
||||
$('#dockerauthmodal').modal({});
|
||||
};
|
||||
|
||||
$scope.$watch('counter', show);
|
||||
$scope.$watch('shown', show);
|
||||
$scope.$watch('username', show);
|
||||
$scope.$watch('token', show);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('robotsManager', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/robots-manager.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization',
|
||||
'user': '=user'
|
||||
},
|
||||
controller: function($scope, $element, Restangular) {
|
||||
$scope.robots = null;
|
||||
$scope.loading = false;
|
||||
$scope.shownRobot = null;
|
||||
$scope.showRobotCounter = 0;
|
||||
|
||||
$scope.showRobot = function(info) {
|
||||
$scope.shownRobot = info;
|
||||
$scope.showRobotCounter++;
|
||||
};
|
||||
|
||||
$scope.getShortenedName = function(name) {
|
||||
var plus = name.indexOf('+');
|
||||
return name.substr(plus + 1);
|
||||
};
|
||||
|
||||
$scope.getPrefix = function(name) {
|
||||
var plus = name.indexOf('+');
|
||||
return name.substr(0, plus);
|
||||
};
|
||||
|
||||
$scope.createRobot = function(name) {
|
||||
if (!name) { return; }
|
||||
|
||||
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'robots', name) :
|
||||
getRestUrl('user/robots', name);
|
||||
var createRobot = Restangular.one(url);
|
||||
createRobot.customPUT().then(function(resp) {
|
||||
$scope.robots.push(resp);
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data : 'The robot account could not be created',
|
||||
"title": "Cannot create robot account",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteRobot = function(info) {
|
||||
var shortName = $scope.getShortenedName(info.name);
|
||||
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'robots', shortName) :
|
||||
getRestUrl('user/robots', shortName);
|
||||
|
||||
var deleteRobot = Restangular.one(url);
|
||||
deleteRobot.customDELETE().then(function(resp) {
|
||||
for (var i = 0; i < $scope.robots.length; ++i) {
|
||||
if ($scope.robots[i].name == info.name) {
|
||||
$scope.robots.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, function() {
|
||||
bootbox.dialog({
|
||||
"message": 'The selected robot account could not be deleted',
|
||||
"title": "Cannot delete robot account",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
if (!$scope.user && !$scope.organization) { return; }
|
||||
if ($scope.loading) { return; }
|
||||
|
||||
$scope.loading = true;
|
||||
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'robots') : 'user/robots';
|
||||
var getRobots = Restangular.one(url);
|
||||
getRobots.customGET($scope.obj).then(function(resp) {
|
||||
$scope.robots = resp.robots;
|
||||
$scope.loading = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('organization', update);
|
||||
$scope.$watch('user', update);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('popupInputButton', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/popup-input-button.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'placeholder': '=placeholder',
|
||||
'pattern': '=pattern',
|
||||
'submitted': '&submitted'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.popupShown = function() {
|
||||
setTimeout(function() {
|
||||
var box = $('#input-box');
|
||||
box[0].value = '';
|
||||
box.focus();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
$scope.getRegexp = function(pattern) {
|
||||
if (!pattern) {
|
||||
pattern = '.*';
|
||||
}
|
||||
return new RegExp(pattern);
|
||||
};
|
||||
|
||||
$scope.inputSubmit = function() {
|
||||
var box = $('#input-box');
|
||||
if (box.hasClass('ng-invalid')) { return; }
|
||||
|
||||
var entered = box[0].value;
|
||||
if (!entered) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.submitted) {
|
||||
$scope.submitted({'value': entered});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('organizationHeader', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
@ -738,9 +940,10 @@ quayApp.directive('entitySearch', function () {
|
|||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization',
|
||||
'namespace': '=namespace',
|
||||
'inputTitle': '=inputTitle',
|
||||
'entitySelected': '=entitySelected'
|
||||
'entitySelected': '=entitySelected',
|
||||
'includeTeams': '=includeTeams'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
if (!$scope.entitySelected) { return; }
|
||||
|
@ -748,15 +951,16 @@ quayApp.directive('entitySearch', function () {
|
|||
number++;
|
||||
|
||||
var input = $element[0].firstChild;
|
||||
$scope.organization = $scope.organization || '';
|
||||
$scope.namespace = $scope.namespace || '';
|
||||
$(input).typeahead({
|
||||
name: 'entities' + number,
|
||||
remote: {
|
||||
url: '/api/entities/%QUERY',
|
||||
replace: function (url, uriEncodedQuery) {
|
||||
url = url.replace('%QUERY', uriEncodedQuery);
|
||||
if ($scope.organization) {
|
||||
url += '?organization=' + encodeURIComponent($scope.organization);
|
||||
url += '?namespace=' + encodeURIComponent($scope.namespace);
|
||||
if ($scope.includeTeams) {
|
||||
url += '&includeTeams=true'
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
@ -775,14 +979,16 @@ quayApp.directive('entitySearch', function () {
|
|||
},
|
||||
template: function (datum) {
|
||||
template = '<div class="entity-mini-listing">';
|
||||
if (datum.entity.kind == 'user') {
|
||||
if (datum.entity.kind == 'user' && !datum.entity.is_robot) {
|
||||
template += '<i class="fa fa-user fa-lg"></i>';
|
||||
} else if (datum.entity.kind == 'user' && datum.entity.is_robot) {
|
||||
template += '<i class="fa fa-wrench fa-lg"></i>';
|
||||
} else if (datum.entity.kind == 'team') {
|
||||
template += '<i class="fa fa-group fa-lg"></i>';
|
||||
}
|
||||
template += '<span class="name">' + datum.value + '</span>';
|
||||
|
||||
if (datum.entity.is_org_member !== undefined && !datum.entity.is_org_member) {
|
||||
if (datum.entity.is_org_member !== undefined && !datum.entity.is_org_member && datum.kind == 'user') {
|
||||
template += '<div class="alert-warning warning">This user is outside your organization</div>';
|
||||
}
|
||||
|
||||
|
@ -1216,7 +1422,7 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', '$http',
|
|||
$rootScope.description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.';
|
||||
}
|
||||
|
||||
$rootScope.hideFooter = !!current.$$route.hideFooter;
|
||||
$rootScope.fixFooter = !!current.$$route.fixFooter;
|
||||
});
|
||||
|
||||
var initallyChecked = false;
|
||||
|
|
|
@ -422,30 +422,19 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
|||
'html': true
|
||||
});
|
||||
|
||||
$('#copyClipboard').clipboardCopy();
|
||||
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
|
||||
$scope.permissions = {'team': [], 'user': []};
|
||||
|
||||
$scope.isDownloadSupported = function() {
|
||||
try { return !!new Blob(); } catch(e){}
|
||||
return false;
|
||||
$scope.getPrefix = function(name) {
|
||||
var plus = name.indexOf('+');
|
||||
return name.substr(0, plus + 1);
|
||||
};
|
||||
|
||||
$scope.downloadCfg = function(token) {
|
||||
var auth = $.base64.encode("$token:" + token.code);
|
||||
config = {
|
||||
"https://quay.io/v1/": {
|
||||
"auth": auth,
|
||||
"email": ""
|
||||
}
|
||||
};
|
||||
|
||||
var file = JSON.stringify(config, null, ' ');
|
||||
var blob = new Blob([file]);
|
||||
saveAs(blob, '.dockercfg');
|
||||
$scope.getShortenedName = function(name) {
|
||||
var plus = name.indexOf('+');
|
||||
return name.substr(plus + 1);
|
||||
};
|
||||
|
||||
$scope.grantRole = function() {
|
||||
|
@ -468,7 +457,7 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
|||
// Need the $scope.apply for both the permission stuff to change and for
|
||||
// the XHR call to be made.
|
||||
$scope.$apply(function() {
|
||||
$scope.addRole(entity.name, 'read', entity.kind, entity.is_org_member)
|
||||
$scope.addRole(entity.name, 'read', entity.kind);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -486,15 +475,14 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
|||
});
|
||||
};
|
||||
|
||||
$scope.addRole = function(entityName, role, kind, is_org_member) {
|
||||
$scope.addRole = function(entityName, role, kind) {
|
||||
var permission = {
|
||||
'role': role,
|
||||
'is_org_member': is_org_member
|
||||
};
|
||||
|
||||
var permissionPost = Restangular.one(getRestUrl('repository', namespace, name, 'permissions', kind, entityName));
|
||||
permissionPost.customPOST(permission).then(function() {
|
||||
$scope.permissions[kind][entityName] = permission;
|
||||
permissionPost.customPOST(permission).then(function(result) {
|
||||
$scope.permissions[kind][entityName] = result;
|
||||
}, function(result) {
|
||||
$('#cannotchangeModal').modal({});
|
||||
});
|
||||
|
@ -555,9 +543,11 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
|||
});
|
||||
};
|
||||
|
||||
$scope.shownTokenCounter = 0;
|
||||
|
||||
$scope.showToken = function(tokenCode) {
|
||||
$scope.shownToken = $scope.tokens[tokenCode];
|
||||
$('#tokenmodal').modal({});
|
||||
$scope.shownTokenCounter++;
|
||||
};
|
||||
|
||||
$scope.askChangeAccess = function(newAccess) {
|
||||
|
@ -1104,17 +1094,7 @@ function OrgViewCtrl($rootScope, $scope, Restangular, $routeParams) {
|
|||
});
|
||||
};
|
||||
|
||||
$scope.createTeamShown = function() {
|
||||
setTimeout(function() {
|
||||
$('#create-team-box').focus();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
$scope.createTeam = function() {
|
||||
var box = $('#create-team-box');
|
||||
if (box.hasClass('ng-invalid')) { return; }
|
||||
|
||||
var teamname = box[0].value.toLowerCase();
|
||||
$scope.createTeam = function(teamname) {
|
||||
if (!teamname) {
|
||||
return;
|
||||
}
|
||||
|
@ -1247,7 +1227,7 @@ function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) {
|
|||
'html': true
|
||||
});
|
||||
|
||||
var orgname = $routeParams.orgname;
|
||||
$scope.orgname = $routeParams.orgname;
|
||||
var teamname = $routeParams.teamname;
|
||||
|
||||
$rootScope.title = 'Loading...';
|
||||
|
@ -1258,7 +1238,7 @@ function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) {
|
|||
if ($scope.members[member.name]) { return; }
|
||||
|
||||
$scope.$apply(function() {
|
||||
var addMember = Restangular.one(getRestUrl('organization', orgname, 'team', teamname, 'members', member.name));
|
||||
var addMember = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname, 'members', member.name));
|
||||
addMember.customPOST().then(function(resp) {
|
||||
$scope.members[member.name] = resp;
|
||||
}, function() {
|
||||
|
@ -1268,7 +1248,7 @@ function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) {
|
|||
};
|
||||
|
||||
$scope.removeMember = function(username) {
|
||||
var removeMember = Restangular.one(getRestUrl('organization', orgname, 'team', teamname, 'members', username));
|
||||
var removeMember = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname, 'members', username));
|
||||
removeMember.customDELETE().then(function(resp) {
|
||||
delete $scope.members[username];
|
||||
}, function() {
|
||||
|
@ -1279,7 +1259,7 @@ function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) {
|
|||
$scope.updateForDescription = function(content) {
|
||||
$scope.organization.teams[teamname].description = content;
|
||||
|
||||
var updateTeam = Restangular.one(getRestUrl('organization', orgname, 'team', teamname));
|
||||
var updateTeam = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname));
|
||||
var data = $scope.organization.teams[teamname];
|
||||
updateTeam.customPUT(data).then(function(resp) {
|
||||
}, function() {
|
||||
|
@ -1288,7 +1268,7 @@ function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) {
|
|||
};
|
||||
|
||||
var loadOrganization = function() {
|
||||
var getOrganization = Restangular.one(getRestUrl('organization', orgname))
|
||||
var getOrganization = Restangular.one(getRestUrl('organization', $scope.orgname))
|
||||
getOrganization.get().then(function(resp) {
|
||||
$scope.organization = resp;
|
||||
$scope.team = $scope.organization.teams[teamname];
|
||||
|
@ -1301,12 +1281,12 @@ function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) {
|
|||
};
|
||||
|
||||
var loadMembers = function() {
|
||||
var getMembers = Restangular.one(getRestUrl('organization', orgname, 'team', teamname, 'members'));
|
||||
var getMembers = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname, 'members'));
|
||||
getMembers.get().then(function(resp) {
|
||||
$scope.members = resp.members;
|
||||
$scope.canEditMembers = resp.can_edit;
|
||||
$scope.loading = !$scope.organization || !$scope.members;
|
||||
$rootScope.title = teamname + ' (' + orgname + ')';
|
||||
$rootScope.title = teamname + ' (' + $scope.orgname + ')';
|
||||
$rootScope.description = 'Team management page for team ' + teamname + ' under organization ' + orgname;
|
||||
}, function() {
|
||||
$scope.organization = null;
|
||||
|
|
|
@ -86,7 +86,7 @@ ImageHistoryTree.prototype.updateDimensions_ = function() {
|
|||
$('#' + container).removeOverscroll();
|
||||
var viewportHeight = $(window).height();
|
||||
var boundingBox = document.getElementById(container).getBoundingClientRect();
|
||||
document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top - 30) + 'px';
|
||||
document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top - 110) + 'px';
|
||||
$('#' + container).overscroll();
|
||||
|
||||
// Update the tree.
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<form name="newteamform" ng-submit="createTeam(); hide()" novalidate>
|
||||
<input id="create-team-box" type="text form-control" placeholder="Team Name" ng-blur="hide()" ng-pattern="/^[a-zA-Z][a-zA-Z0-9]+$/" ng-model="newTeamName" ng-trim="false" ng-minlength="2" required>
|
||||
</form>
|
|
@ -50,11 +50,32 @@ Email: my@email.com</pre>
|
|||
</div>
|
||||
<br>
|
||||
|
||||
<h3>Using robot accounts <span class="label label-info">Requires Admin Access</span></h3>
|
||||
<div class="container">
|
||||
<div class="description-overview">
|
||||
There are many circumstances where permissions for repositories need to be shared across those repositories (continuous integration, etc).
|
||||
To support this case, Quay allows the use of <b>robot accounts</b> which can be created in the user/organization's admin view and can be
|
||||
shared by multiple repositories that are owned by that user or organization.
|
||||
</div>
|
||||
|
||||
<ul class="description-list">
|
||||
<li>Robot accounts can be managed in the user or organization admin's interface
|
||||
<li><b>Adding a robot account:</b> Click "Create Robot Account" and enter a name for the account. The username will become <b>namespace+accountname</b> where "namespace" is the name of the user or organiaztion.
|
||||
<li><b>Setting permissions:</b> Permissions can be granted to a robot account in a repository by adding that account like any other user or team.
|
||||
<li><b>Deleting a robot account:</b> A robot account can be deleted by clicking the <b>X</b> and then clicking <b>Delete</b>
|
||||
<li><b>Using a robot account:</b> To use the robot account, the following credentials can be used:
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Username</dt><dd>namespace+accountname (Example: mycompany+deploy)</dd>
|
||||
<dt>Password</dt><dd>(token value can be found by clicking on the robot account in the admin panel)</dd>
|
||||
<dt>Email</dt><dd>This value is ignored, any value may be used.</dd>
|
||||
</dl>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>Using access tokens in place of users <span class="label label-info">Requires Admin Access</span></h3>
|
||||
<div class="container">
|
||||
<div class="description-overview">
|
||||
There are many circumstances where it makes sense to <b>not</b> use a user's username and password (deployment scripts, etc).
|
||||
To support this case, Quay allows the use of <b>access tokens</b> which can be created on a repository and have read and/or write
|
||||
For per-repository token authentication, Quay allows the use of <b>access tokens</b> which can be created on a repository and have read and/or write
|
||||
permissions, without any passwords.
|
||||
</div>
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#members" ng-click="loadMembers()">Members</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
|
||||
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing</a></li>
|
||||
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a></li>
|
||||
</ul>
|
||||
|
@ -28,6 +29,11 @@
|
|||
<div class="plan-manager" organization="orgname" plan-changed="planChanged(plan)"></div>
|
||||
</div>
|
||||
|
||||
<!-- Robot accounts tab -->
|
||||
<div id="robots" class="tab-pane">
|
||||
<div class="robots-manager" organization="organization"></div>
|
||||
</div>
|
||||
|
||||
<!-- Billing Options tab -->
|
||||
<div id="billingoptions" class="tab-pane">
|
||||
<div class="billing-options" organization="organization"></div>
|
||||
|
|
|
@ -9,7 +9,12 @@
|
|||
<div class="org-view container" ng-show="!loading && organization">
|
||||
<div class="organization-header" organization="organization">
|
||||
<div class="header-buttons" ng-show="organization.is_admin">
|
||||
<button class="btn btn-success" data-trigger="click" bs-popover="'static/partials/create-team-dialog.html'" data-placement="bottom" ng-click="createTeamShown()"><i class="fa fa-group"></i> Create Team</button>
|
||||
|
||||
<span class="popup-input-button" pattern="'^[a-zA-Z][a-zA-Z0-9]+$'" placeholder="'Team Name'"
|
||||
submitted="createTeam(value)">
|
||||
<i class="fa fa-group"></i> Create Team
|
||||
</span>
|
||||
|
||||
<a class="btn btn-default" href="/organization/{{ organization.name }}/admin"><i class="fa fa-gear"></i> Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -34,16 +34,16 @@
|
|||
<div id="permissions" class="tab-pane active">
|
||||
<!-- User Access Permissions -->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">User <span ng-show="repo.is_organization">and Team</span> Access Permissions
|
||||
<div class="panel-heading">Access Permissions
|
||||
|
||||
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Allow any number of users or teams to read, write or administer this repository"></i>
|
||||
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Allow any number of users, robot accounts or teams to read, write or administer this repository"></i>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
|
||||
<table class="permissions">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>User<span ng-show="repo.is_organization">/Team</span></td>
|
||||
<td style="min-width: 400px;">User<span ng-show="repo.is_organization">/Team</span>/Robot Account</td>
|
||||
<td>Permissions</td>
|
||||
<td style="width: 95px;"></td>
|
||||
</tr>
|
||||
|
@ -52,7 +52,7 @@
|
|||
<!-- Team Permissions -->
|
||||
<tr ng-repeat="(name, permission) in permissions['team']">
|
||||
<td class="team entity">
|
||||
<i class="fa fa-group"></i>
|
||||
<i class="fa fa-group" title="Team" bs-tooltip="tooltip.title"></i>
|
||||
<span><a href="/organization/{{ repo.namespace }}/teams/{{ name }}">{{name}}</a></span>
|
||||
</td>
|
||||
<td class="user-permissions">
|
||||
|
@ -69,8 +69,9 @@
|
|||
<!-- User Permissions -->
|
||||
<tr ng-repeat="(name, permission) in permissions['user']">
|
||||
<td class="{{ 'user entity ' + (permission.is_org_member ? '' : 'outside') }}">
|
||||
<i class="fa fa-user"></i>
|
||||
<span>{{name}}</span>
|
||||
<i class="fa fa-user" ng-show="!permission.is_robot" title="User" bs-tooltip="tooltip.title"></i>
|
||||
<i class="fa fa-wrench" ng-show="permission.is_robot" title="Robot Account" bs-tooltip="tooltip.title"></i>
|
||||
<span class="prefix">{{getPrefix(name)}}</span><span>{{getShortenedName(name)}}</span>
|
||||
<i class="fa fa-exclamation-triangle" ng-show="permission.is_org_member === false" data-trigger="hover" bs-popover="{'content': 'This user is not a member of the organization'}"></i>
|
||||
</td>
|
||||
<td class="user-permissions">
|
||||
|
@ -87,8 +88,8 @@
|
|||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<span class="entity-search" organization="repo.namespace" input-title="'Add a ' + (repo.is_organization ? 'team or ' : '') + 'user...'" entity-selected="addNewPermission"></span>
|
||||
<td colspan="2" class="admin-search">
|
||||
<span class="entity-search" namespace="repo.namespace" include-teams="true" input-title="'Add a ' + (repo.is_organization ? 'team or ' : '') + 'user...'" entity-selected="addNewPermission"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -106,7 +107,7 @@
|
|||
<table class="permissions">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Token Description</td>
|
||||
<td style="min-width: 400px;">Token Description</td>
|
||||
<td>Permissions</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
|
@ -132,10 +133,10 @@
|
|||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<td class="admin-search">
|
||||
<input type="text" class="form-control" placeholder="New token description" ng-model="newToken.friendlyName"required>
|
||||
</td>
|
||||
<td>
|
||||
<td class="admin-search">
|
||||
<button type="submit" ng-disabled="createTokenForm.$invalid" class="btn btn-sm btn-default">Create</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -237,6 +238,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="docker-auth-dialog" username="shownToken.friendlyName" token="shownToken.code"
|
||||
shown="!!shownToken" counter="shownTokenCounter">
|
||||
<i class="fa fa-key"></i> {{ shownToken.friendlyName }}
|
||||
</div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="cannotchangeModal">
|
||||
|
@ -256,36 +261,6 @@
|
|||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="tokenmodal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title"><i class="fa fa-key"></i> {{ shownToken.friendlyName }}</h4>
|
||||
</div>
|
||||
<div class="modal-body token-dialog-body">
|
||||
<div class="alert alert-info">The docker <u>username</u> is <b>$token</b> and the <u>password</u> is the token. You may use any value for email.</div>
|
||||
<div class="well well-sm">
|
||||
<input id="token-view" class="token-view" type="text" value="{{ shownToken.code }}" onClick="this.select();" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<span class="download-cfg" ng-show="isDownloadSupported()">
|
||||
<i class="icon-download"></i>
|
||||
<a href="javascript:void(0)" ng-click="downloadCfg(shownToken)">Download .dockercfg file</a>
|
||||
</span>
|
||||
<div id="clipboardCopied" style="display: none">
|
||||
Copied to clipboard
|
||||
</div>
|
||||
<button id="copyClipboard" type="button" class="btn btn-primary" data-clipboard-target="token-view">Copy to clipboard</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="makepublicModal">
|
||||
<div class="modal-dialog">
|
||||
|
|
|
@ -20,7 +20,8 @@
|
|||
<table class="permissions">
|
||||
<tr ng-repeat="(name, member) in members">
|
||||
<td class="user entity">
|
||||
<i class="fa fa-user"></i>
|
||||
<i class="fa fa-user" ng-show="!member.is_robot"></i>
|
||||
<i class="fa fa-wrench" ng-show="member.is_robot"></i>
|
||||
<span>{{ member.username }}</span>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -33,7 +34,7 @@
|
|||
|
||||
<tr ng-show="canEditMembers">
|
||||
<td colspan="2">
|
||||
<span class="entity-search" organization="''" input-title="'Add a user...'" entity-selected="addNewMember"></span>
|
||||
<span class="entity-search" namespace="orgname" include-teams="false" input-title="'Add a user...'" entity-selected="addNewMember"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li>
|
||||
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing">Billing Options</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Set Password</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#migrate" id="migrateTab">Convert to Organization</a></li>
|
||||
</ul>
|
||||
|
@ -59,6 +60,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Robot accounts tab -->
|
||||
<div id="robots" class="tab-pane">
|
||||
<div class="robots-manager" user="user"></div>
|
||||
</div>
|
||||
|
||||
<!-- Billing options tab -->
|
||||
<div id="billing" class="tab-pane">
|
||||
<div class="billing-options" user="user"></div>
|
||||
|
|
|
@ -74,17 +74,17 @@ b._i.push([a,e,d])};b.__SV=1.2}})(document,window.mixpanel||[]);
|
|||
mixpanel.init(isProd ? "50ff2b2569faa3a51c8f5724922ffb7e" : "38014a0f27e7bdc3ff8cc7cc29c869f9", { track_pageview : false, debug: !isProd });</script><!-- end Mixpanel -->
|
||||
</head>
|
||||
<body>
|
||||
<div ng-class="!hideFooter ? 'wrapper' : ''">
|
||||
<div ng-class="!fixFooter ? 'wrapper' : ''">
|
||||
<nav class="navbar navbar-default header-bar" role="navigation"></nav>
|
||||
|
||||
{% block body_content %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
<div ng-class="!hideFooter ? 'push' : ''"></div>
|
||||
<div ng-class="!fixFooter ? 'push' : ''"></div>
|
||||
</div>
|
||||
|
||||
<div class="footer-container" ng-show="!hideFooter">
|
||||
<div class="footer-container" ng-class="fixFooter ? 'fixed' : ''">
|
||||
<nav class="page-footer visible-lg visible-md">
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
|
|
Binary file not shown.
|
@ -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)
|
||||
|
|
Reference in a new issue