From 026ed7ffb46ceefd755eed00d76d1d1f69530b13 Mon Sep 17 00:00:00 2001 From: yackob03 Date: Wed, 20 Nov 2013 16:13:03 -0500 Subject: [PATCH 1/8] Start fleshing out support for robots. --- data/database.py | 20 +++++----- data/model.py | 102 ++++++++++++++++++++++++++++++++++++++--------- endpoints/api.py | 77 ++++++++++++++++++++++++++++++++++- initdb.py | 1 + util/names.py | 4 ++ 5 files changed, 176 insertions(+), 28 deletions(-) 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) From b407c1d9fb926089c3dd0fa5651c14134d343b25 Mon Sep 17 00:00:00 2001 From: yackob03 Date: Wed, 20 Nov 2013 18:23:59 -0500 Subject: [PATCH 2/8] Add robots to the entity search. --- data/model.py | 17 +++++++++++++---- endpoints/api.py | 28 +++++++++++++++++----------- initdb.py | 4 ++++ static/js/app.js | 14 ++++++++------ static/js/controllers.js | 14 +++++++------- static/partials/repo-admin.html | 2 +- static/partials/team-view.html | 2 +- test/data/test.db | Bin 100352 -> 101376 bytes 8 files changed, 51 insertions(+), 30 deletions(-) diff --git a/data/model.py b/data/model.py index a18dc0af4..822605b92 100644 --- a/data/model.py +++ b/data/model.py @@ -379,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, @@ -396,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: diff --git a/endpoints/api.py b/endpoints/api.py index 185459094..202e723dd 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -260,20 +260,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) + try: + organization = model.get_organization(namespace_name) + + # namespace name was an org + permission = OrganizationMemberPermission(namespace_name) if permission.can(): - try: - organization = model.get_organization(organization_name) - except model.InvalidOrganizationException: - pass + robot_namespace = namespace_name - if organization: - teams = model.get_matching_teams(prefix, organization) + 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 = { @@ -286,7 +292,7 @@ def get_matching_entities(prefix): def user_view(user): user_json = { 'name': user.username, - 'kind': 'user', + 'kind': 'robot' if user.is_robot else 'user', } if user.is_org_member is not None: diff --git a/initdb.py b/initdb.py index 5a2fbeb40..e810d8ab7 100644 --- a/initdb.py +++ b/initdb.py @@ -132,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 @@ -188,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() diff --git a/static/js/app.js b/static/js/app.js index 26d7aa5da..04a887b8c 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -604,9 +604,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; } @@ -614,15 +615,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; }, @@ -648,7 +650,7 @@ quayApp.directive('entitySearch', function () { } template += '' + datum.value + ''; - 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 += '
This user is outside your organization
'; } diff --git a/static/js/controllers.js b/static/js/controllers.js index 8bba4f712..a83f375a2 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1296,7 +1296,7 @@ function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) { 'html': true }); - var orgname = $routeParams.orgname; + $scope.orgname = $routeParams.orgname; var teamname = $routeParams.teamname; $rootScope.title = 'Loading...'; @@ -1307,7 +1307,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() { @@ -1317,7 +1317,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() { @@ -1328,7 +1328,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() { @@ -1337,7 +1337,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]; @@ -1350,12 +1350,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 + ')'; }, function() { $scope.organization = null; $scope.members = null; diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index d9016b83a..89442c88f 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -91,7 +91,7 @@ - + diff --git a/static/partials/team-view.html b/static/partials/team-view.html index 6d7084b3e..1d8d66d5c 100644 --- a/static/partials/team-view.html +++ b/static/partials/team-view.html @@ -33,7 +33,7 @@ - + diff --git a/test/data/test.db b/test/data/test.db index 6a2cb0c869049a5802a332d49c5cb6c7124f7aea..fdb9329a01d83bc1012a996d0e100dbe7a96a2a9 100644 GIT binary patch delta 3810 zcmbtXd2kcg8Q<@{)gBfY3^MYGWk+BeUpj0_#tz(+GwB~4 zJnP?n``)+T`;OoD-s;f9+@VLghtrd`F$^Q8;byqjrP2IjO zM{lFmS>v^K+gog&W`D2U($Z$~G}Jgv=2l-zo8RMf_q12n)zq3??H(JSz+^Ecf>{*E z-^+_8`Zz68*5Zz0liy)= z9%$V&6y9&!KhZkcSYE%+wBKj9?iv~F8rEDlFU zw@Z?L+A}sV7?KJCV`{uUXOU(LXH+iNKdL0Vj z5WI;##Ro|e8`d`@a{BCK2=0gkqXGO5Z=Cg)s=%3H2tQrILAzVSM&CN~?cpjT&W zn#$=rUu&{)DbDt~>JC!gluq7mDoBZk{Hl_-+abS-U#B6T!!$<>2;{AXHRS!*a&!W^ z&Ye}7*QtU&|Pgej>&i3U{{B@soO}xr(hH*m+yntBLgkHms z%GrfrLlema+&V}lYutZD!)wUafDzKkWS}Nf&*|4CLsYH*m^2Wk^$$sXI|7QFK{ZyB ze+079QaF89G7R0;iVTD$MI}>JsgzQw?H(x~)~dCARH}hAGA|h*i+nDfC5gcl@~t3G zih@S+mto^n|4q#Eh{YiCQ`b<~3M@ zm50v`YFM2TDaZI2dc6$w$cui3n<$$r(QpP?5zK>Z^5$?V@dnpJCfOG>!9A)|v%wmC zSXfJPkLH1qv>m-noiiUZkxR$cl8IyaU?AT=W+kEH0{P>ydTOKKcs0p5VIq$mubDb@ z4QFT&=2@UlAjGnwD8`TR5QAUDAU9kzr*h}w@-_>gVz~+6JL)d z34g-(sOd)n;%FH}gyJ-Kh+!Ur4e$)*%NKYbrNIcE%+4WyI9>sHYTYd-yp#b)P8eVV zdFsR#wADy@P8*?sJaD=k){&P^8=#QfcQu7vKRpBM$+@SBp@>|cP9Zm*vO+G|aHa%` zRYTD;Ra1|#LX3Up)QS+tV_xLw=NyQfC{TeiL!-eW9~ZF(@mzdbBWTny5wkoSAHpJD zRHK}tq*1IOj@F!6cI~ju>Z+$rRFUr2BCzq^z45bzWvi5;X&Cy!ypj15evVV{VH#Z?skmUIcIlZx zmp^);fXW`4^?NTgfqgOe3FL_xE%iHDu0+7eRauZg`eqZ!pD(20!`>3Iy_ z0WgstzMBi>>WhDSw-C%EW!^;Q<}*lO-gI(~i*awhh;1C7z_B6#C#t;CXoPrsYj|~7 z!^Iz2jryn*Pig+x$8S&Jt#zJue|4|VVR6(ndVLKY_Uaa!yQi$7yt~t3YH4$LYwE2% z)!sV4r?t1UzN5an)7$B=^!7H^+efH6_;DB_HsL# zx-^HY?skEx%dD`BWj5p$A0BeUSw%3q$9Z32+cWpL{d{+^|J{N4fcBgO=XBa!9$goqtISE@AP&nqrtK z$cN`p8_vfkX=IP1@d`PxmKt7}1)1`5Yw5??Ci&7@>ZD6f$^tKJmWQ&S92%E;K%UNm zT0Gf#ul&tSNP{N%=}dST1uNy8Od7FUjcjWs6roU+{G$xWgJ$`68Q?rQ$3D&KKse7D I;Pc>r0O5V`v;Y7A delta 3020 zcmbtWdvFuS9lqc0-AN~dABlx5Kd|g5*aj@?X;~P8E#rqQ+gN^LhX9g%mSpQ;B#ZD! zAO-_TI_*F<>9v_Og}6yOOwu$`9_ihw_5@Tcj5Wq-$N0WhNtgrTIMEJ0XbZ? z58anb&UI8k0lD7Mn5t&;^Le<;;HPjsx`-Y?rEoc2z`hGA8!s^BOr6pskZ(F_4{YO9 zOvMaWBQQ#3EIFMBjft^%KyUWyO=kT_eZu1&Z5ecR$IQuYPi%T}=TxgWZ0>3?j5<2D znCo}-kM3=6F?93|wok|R+C9CY-Tl#+xoJA+jZP*E{#ay`RWaHn{ zN8f?^q{-lkc#O8rJ#I^nb@SA8ysyD2PPz=O@qX{P&28*;P9+Vy!aaQ>-4m4DbQ}3r ztYXS%xGH(_<8d(&OeVhM57|up9S#1(bbZ9!H#rtITKt9)!_JZMfuME7GTa{RHcYfP zAE;~bCW9@a-8Q__Gr2bsjon5+hB9;CAk$AJec_OQY3>F`S8v!g>Yp0h6YgvuwRMM* zVrw$cKV{w&jTrg@_Fzxlu1KhPcfga_6YQ932-I2nC%pcc&k%_vf?{+s))a_%Lt#Td zmB{#kSj4Z?rHP2g~*l0qI@2w8JxzS;P>(C_(S{-b^iCUcR4NxUvf`0?2QLR zJ{MQOS3c)dSPNMfvGRLg3X0{9?D|m-E)%|(qa3k3zQo{L_@DS=d=>uB1I2l+OunY)erI7vX-P=EVmwt8zH?P1Z@Fx@7&C`)+(9%1(#na!=(@u#PnTU~z zX!giJi5NjnCZ4Hb=((Rc2RG23C-Omq`twMws|@ZSZdZLtKCh`zLoyT{6L-tOv0ap9 zms^FTsX|R&a>*IPI= zN%p(ziNUQT8{ArQ-o2)1=0IU48xMIi&YKp!5&0@2{;`h=v+I43!EaH4p2GzH3_nQM z8^QfYq~x1*#cZe-?}YWaVg#a|1-uYDbiq@Fp=;F zWAXTxqor^4zmNw!IpwdU_#3|^75ogB^N7WetyD^8l%eMvPyoYl5j~5S<0L6yooHN5 zHj8@Dl4;SYUcqbbRzoZ!vApW-@mN?SVX=h#RV-I)culz){9pA(y%CX|6N`v>Tnj~{ zYrF=^GSYry+({YFjq6|~xi)?Zipi6KYA7fBXSF2nu!39)l%eqwGB{gJ-Ut*EX?7Kq zlKEL9d2m)q+ADGid)S!@f1H!cqa0QXLhSyi7*Fu2-Pf_eBDyRF0Hr_McM~gE%whTA z<^hgfnc{8vOwuq#^gU= zBiYIS{zNgEKkCFY6&2)>6OH6sM~&pg6AE(kgprc3J*k6g(tYyYR6d(O$NoT>%Q1A5 z8fIRB7f=;?k$R;SZM}<}I;n>m^7csw9a(rr2W!aj(?!Jj%o(`*u{i^*mAta%?Q<4b zMfT0rL2X9)g}Ektu&0V_J#7U;`uJ%rq@I5+ZDPL&yJ z+oJlvcrGuNlJ$#~nKAcM-ha&(lB0`jmY&-lyU3^cU%ZJcIh9-l1v$=GWhUBa)X}P# zY0_#YfONea3e)VHw{lV!Pvd${q2gHqfEO}R$a2bTG;)fJnd7sMESGu2RA@O?(;vT| z!r{%Pk&Xddo6+WI8EkO08C`unPMdSU&GSM`AVJ#TR>;)q@NdDCl^3DY#nlO>eS5}jkFO-tc zukgflb!F;<1>E%i&O7xNR+(n6-@^HsSu#fY2L@+Q7y1)T2pvA8GLycmtD#ZOg7gzt zT>uvH$A2`C2d*x~lkwUHu#)JtbwqQmoIH9hkNoc1>f(c?Hibr`fmuzaoQERbfS7@e zuY4CcNPUu{1n=>w!nE$z_poP26IE!t9J}|~DQq=UxVM(VFx{G%p}Q#zR#50KqtK_N z;F13bd-5oB%bP1MllIx_Gi)g`Zm1|Y$x3#ba8l|;buu7MTpiIgy&aNzz+Lf?gN^8LZ?Mrn` NB`qt4P0#5e_#gZ3EkytT From e69591c7d6fc423f54465fb805d170a02e8e1eef Mon Sep 17 00:00:00 2001 From: yackob03 Date: Wed, 20 Nov 2013 19:43:19 -0500 Subject: [PATCH 3/8] Add the ability to login with a robot, use the wrench icon for robots all over the place. --- auth/auth.py | 14 ++++++++++++++ data/model.py | 2 +- endpoints/api.py | 17 ++++++++++++----- endpoints/index.py | 7 +++++++ static/js/app.js | 4 +++- static/js/controllers.js | 9 ++++----- static/partials/repo-admin.html | 5 +++-- static/partials/team-view.html | 3 ++- 8 files changed, 46 insertions(+), 15 deletions(-) diff --git a/auth/auth.py b/auth/auth.py index 2d77d6094..a20e20129 100644 --- a/auth/auth.py +++ b/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]) diff --git a/data/model.py b/data/model.py index 822605b92..9fe51db65 100644 --- a/data/model.py +++ b/data/model.py @@ -627,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) diff --git a/endpoints/api.py b/endpoints/api.py index 202e723dd..aee4947d9 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -292,7 +292,8 @@ def get_matching_entities(prefix): def user_view(user): user_json = { 'name': user.username, - 'kind': 'robot' if user.is_robot else 'user', + 'kind': 'user', + 'is_robot': user.is_robot, } if user.is_org_member is not None: @@ -455,7 +456,8 @@ def get_organization_private_allowed(orgname): def member_view(member): return { - 'username': member.username + 'username': member.username, + 'is_robot': member.robot, } @@ -917,6 +919,11 @@ def role_view(repo_perm_obj): } +def wrap_role_view_user(role_json, user): + role_json['is_robot'] = user.robot + return role_json + + def wrap_role_view_org(role_json, org_member): role_json['is_org_member'] = org_member return role_json @@ -1033,7 +1040,7 @@ def list_repo_user_permissions(namespace, repository): 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) + unwrapped = wrap_role_view_user(role_view(repo_perm), repo_perm.user) return wrap_role_view_org(unwrapped, repo_perm.user.username in org_members) @@ -1062,7 +1069,7 @@ 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) @@ -1107,7 +1114,7 @@ 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) diff --git a/endpoints/index.py b/endpoints/index.py index 207ace697..13525d5e9 100644 --- a/endpoints/index.py +++ b/endpoints/index.py @@ -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) diff --git a/static/js/app.js b/static/js/app.js index 04a887b8c..376c0feb3 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -643,8 +643,10 @@ quayApp.directive('entitySearch', function () { }, template: function (datum) { template = '
'; - if (datum.entity.kind == 'user') { + if (datum.entity.kind == 'user' && !datum.entity.is_robot) { template += ''; + } else if (datum.entity.kind == 'user' && datum.entity.is_robot) { + template += ''; } else if (datum.entity.kind == 'team') { template += ''; } diff --git a/static/js/controllers.js b/static/js/controllers.js index a83f375a2..4172835ee 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -527,7 +527,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); }); }; @@ -545,15 +545,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({}); }); diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index 89442c88f..3d169340f 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -71,8 +71,9 @@ - - + + + {{name}} diff --git a/static/partials/team-view.html b/static/partials/team-view.html index 1d8d66d5c..fcb275c9a 100644 --- a/static/partials/team-view.html +++ b/static/partials/team-view.html @@ -20,7 +20,8 @@
- + + {{ member.username }} From 098b3b1b331c1c1705b2772c5333e3a5f0ffcfa0 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 22 Nov 2013 18:20:51 -0500 Subject: [PATCH 4/8] Start on robots UI --- endpoints/api.py | 4 +- static/css/quay.css | 26 +++++++++-- static/directives/robots-manager.html | 31 +++++++++++++ static/js/app.js | 66 +++++++++++++++++++++++++++ static/partials/org-admin.html | 6 +++ static/partials/user-admin.html | 6 +++ 6 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 static/directives/robots-manager.html diff --git a/endpoints/api.py b/endpoints/api.py index aee4947d9..b245d131f 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -1516,10 +1516,10 @@ def get_org_subscription(orgname): abort(403) -def robot_view(name, password): +def robot_view(name, token): return { 'name': name, - 'password': password, + 'token': token, } diff --git a/static/css/quay.css b/static/css/quay.css index bf3f1124a..8ff8c4880 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -6,6 +6,26 @@ visibility: hidden; } +.robots-manager-element { + max-width: 800px; +} + +.robots-manager-element .alert { + margin-bottom: 20px; +} + +.robots-manager-element .robot { + font-size: 16px; +} + +.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; @@ -1671,16 +1691,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; } diff --git a/static/directives/robots-manager.html b/static/directives/robots-manager.html new file mode 100644 index 000000000..21e916296 --- /dev/null +++ b/static/directives/robots-manager.html @@ -0,0 +1,31 @@ +
+ +
Robot accounts allow for delegating access in multiple repositories to tokens
+ +
+
+ +
+ + + + + + + + + + + +
Robot Account Name
+ + {{ getPrefix(robotInfo.name) }}+{{ getShortenedName(robotInfo.name) }} + + + + + +
+ +
+
diff --git a/static/js/app.js b/static/js/app.js index 376c0feb3..be084ac50 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -529,6 +529,72 @@ quayApp.directive('plansTable', function () { }); + +quayApp.directive('robotsManager', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/robots-manager.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'organization': '=organization', + 'user': '=user' + }, + controller: function($scope, $element, Restangular) { + $scope.robots = null; + $scope.loading = false; + + $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.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.slice(i, 1); + return; + } + } + }, function() { + + }); + }; + + 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('organizationHeader', function () { var directiveDefinitionObject = { priority: 0, diff --git a/static/partials/org-admin.html b/static/partials/org-admin.html index 46e3af4ea..3fead806a 100644 --- a/static/partials/org-admin.html +++ b/static/partials/org-admin.html @@ -15,6 +15,7 @@ @@ -28,6 +29,11 @@
+ +
+
+
+
diff --git a/static/partials/user-admin.html b/static/partials/user-admin.html index e6c7123e6..bdf222523 100644 --- a/static/partials/user-admin.html +++ b/static/partials/user-admin.html @@ -28,6 +28,7 @@ @@ -59,6 +60,11 @@
+ +
+
+
+
From 12eb932da1bd76188f1cd8af55bc89e5d072c4e0 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 22 Nov 2013 20:14:44 -0500 Subject: [PATCH 5/8] Get robots UI working --- endpoints/api.py | 42 ++++--- static/css/quay.css | 105 +++++++++------- static/directives/docker-auth-dialog.html | 30 +++++ static/directives/popup-input-button.html | 5 + static/directives/popup-input-dialog.html | 4 + static/directives/robots-manager.html | 17 ++- static/js/app.js | 144 +++++++++++++++++++++- static/js/controllers.js | 39 ++---- static/partials/create-team-dialog.html | 3 - static/partials/org-view.html | 7 +- static/partials/repo-admin.html | 56 +++------ 11 files changed, 309 insertions(+), 143 deletions(-) create mode 100644 static/directives/docker-auth-dialog.html create mode 100644 static/directives/popup-input-button.html create mode 100644 static/directives/popup-input-dialog.html delete mode 100644 static/partials/create-team-dialog.html diff --git a/endpoints/api.py b/endpoints/api.py index 07c3fd7f8..22300cb52 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -297,7 +297,7 @@ def get_matching_entities(prefix): } 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 @@ -924,8 +924,8 @@ def wrap_role_view_user(role_json, user): return role_json -def wrap_role_view_org(role_json, org_member): - role_json['is_org_member'] = org_member +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 @@ -1034,22 +1034,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 = wrap_role_view_user(role_view(repo_perm), repo_perm.user) - 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) @@ -1074,8 +1082,7 @@ def get_user_permissions(namespace, repository, username): 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 @@ -1119,8 +1126,7 @@ def change_user_permissions(namespace, repository, username): 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 diff --git a/static/css/quay.css b/static/css/quay.css index b7d63acc3..62e7cb7a8 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -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; } @@ -44,8 +88,9 @@ html, body { margin-bottom: 20px; } -.robots-manager-element .robot { +.robots-manager-element .robot a { font-size: 16px; + cursor: pointer; } .robots-manager-element .robot .prefix { @@ -970,21 +1015,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; @@ -1027,7 +1057,7 @@ p.editable:hover i { margin-top: 28px; } -.repo #clipboardCopied { +#clipboardCopied { font-size: 0.8em; display: inline-block; margin-right: 10px; @@ -1038,7 +1068,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; @@ -1125,21 +1155,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 { @@ -1147,6 +1173,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; } @@ -1278,18 +1309,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; diff --git a/static/directives/docker-auth-dialog.html b/static/directives/docker-auth-dialog.html new file mode 100644 index 000000000..dcb71a25b --- /dev/null +++ b/static/directives/docker-auth-dialog.html @@ -0,0 +1,30 @@ + + diff --git a/static/directives/popup-input-button.html b/static/directives/popup-input-button.html new file mode 100644 index 000000000..005c037bc --- /dev/null +++ b/static/directives/popup-input-button.html @@ -0,0 +1,5 @@ + + diff --git a/static/directives/popup-input-dialog.html b/static/directives/popup-input-dialog.html new file mode 100644 index 000000000..6632b1999 --- /dev/null +++ b/static/directives/popup-input-dialog.html @@ -0,0 +1,4 @@ +
+ +
diff --git a/static/directives/robots-manager.html b/static/directives/robots-manager.html index 21e916296..ef787a3dd 100644 --- a/static/directives/robots-manager.html +++ b/static/directives/robots-manager.html @@ -1,10 +1,13 @@
-
Robot accounts allow for delegating access in multiple repositories to tokens
+
Robot accounts allow for delegating access in multiple repositories to role-based accounts that you manage
-
- +
+ + Create Robot Account +
@@ -16,7 +19,9 @@
- {{ getPrefix(robotInfo.name) }}+{{ getShortenedName(robotInfo.name) }} + + {{ getPrefix(robotInfo.name) }}+{{ getShortenedName(robotInfo.name) }} + @@ -26,6 +31,10 @@
+
+
+ {{ shownRobot.name }}
diff --git a/static/js/app.js b/static/js/app.js index 7561fd2e6..a2d30a5fb 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -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,13 +566,65 @@ 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: true, + transclude: false, restrict: 'C', scope: { 'organization': '=organization', @@ -581,6 +633,13 @@ quayApp.directive('robotsManager', function () { 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('+'); @@ -592,6 +651,28 @@ quayApp.directive('robotsManager', function () { 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) : @@ -601,12 +682,21 @@ quayApp.directive('robotsManager', function () { deleteRobot.customDELETE().then(function(resp) { for (var i = 0; i < $scope.robots.length; ++i) { if ($scope.robots[i].name == info.name) { - $scope.robots.slice(i, 1); + $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" + } + } + }); }); }; @@ -631,6 +721,52 @@ quayApp.directive('robotsManager', function () { }); +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 = { diff --git a/static/js/controllers.js b/static/js/controllers.js index 2d6dd6ccc..c3edc3733 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -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() { @@ -554,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) { @@ -1103,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; } diff --git a/static/partials/create-team-dialog.html b/static/partials/create-team-dialog.html deleted file mode 100644 index 41a599189..000000000 --- a/static/partials/create-team-dialog.html +++ /dev/null @@ -1,3 +0,0 @@ -
- -
diff --git a/static/partials/org-view.html b/static/partials/org-view.html index c1011d74f..2d05e1426 100644 --- a/static/partials/org-view.html +++ b/static/partials/org-view.html @@ -9,7 +9,12 @@
- + + + Create Team + + Settings
diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index 34f378cb5..df7ff652c 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -34,16 +34,16 @@
-
User and Team Access Permissions +
Access Permissions - +
- + @@ -52,7 +52,7 @@ - @@ -107,7 +107,7 @@
User/TeamUser/Team/Robot Account Permissions
- + {{name}} @@ -69,9 +69,9 @@
- - - {{name}} + + + {{getPrefix(name)}}{{getShortenedName(name)}} @@ -88,7 +88,7 @@
+
- + @@ -133,10 +133,10 @@ - - @@ -238,6 +238,10 @@ +
+ {{ shownToken.friendlyName }} +
- - - -
Token DescriptionToken Description Permissions
+ +