- Fix tests

- Add new endpoints for retrieving the repo permissions for a robot account
- Have the robots list return the number of repositories for which there are permissions
- Other UI fixes
This commit is contained in:
Joseph Schorr 2015-03-31 18:50:43 -04:00
parent bb81c05c03
commit 1f5e6df678
16 changed files with 356 additions and 60 deletions

View file

@ -312,9 +312,23 @@ def _list_entity_robots(entity_name):
def list_entity_robot_tuples(entity_name):
return (_list_entity_robots(entity_name)
.select(User.username, FederatedLogin.service_ident)
.join(RepositoryPermission, JOIN_LEFT_OUTER,
on=(RepositoryPermission.user == FederatedLogin.user))
.join(Repository, JOIN_LEFT_OUTER)
.switch(User)
.group_by(User, FederatedLogin)
.select(User.username, FederatedLogin.service_ident, fn.Count(Repository.id))
.tuples())
def list_robot_permissions(robot_name):
return (RepositoryPermission.select(RepositoryPermission, User, Repository)
.join(Repository)
.join(Visibility)
.switch(RepositoryPermission)
.join(Role)
.switch(RepositoryPermission)
.join(User)
.where(User.username == robot_name, User.robot == True))
def convert_user_to_organization(user, admin_user):
# Change the user to an organization.

View file

@ -7,6 +7,7 @@ from auth.permissions import AdministerOrganizationPermission
from auth.auth_context import get_authenticated_user
from auth import scopes
from data import model
from app import avatar
def prototype_view(proto, org_members):
@ -16,6 +17,7 @@ def prototype_view(proto, org_members):
'is_robot': user.robot,
'kind': 'user',
'is_org_member': user.robot or user.username in org_members,
'avatar': avatar.get_data_for_user(user)
}
if proto.delegate_user:
@ -24,6 +26,7 @@ def prototype_view(proto, org_members):
delegate_view = {
'name': proto.delegate_team.name,
'kind': 'team',
'avatar': avatar.get_data_for_team(proto.delegate_team)
}
return {

View file

@ -6,12 +6,24 @@ from auth.auth_context import get_authenticated_user
from auth import scopes
from data import model
from util.names import format_robot_username
from flask import abort
def robot_view(name, token):
def robot_view(name, token, count=None):
return {
'name': name,
'token': token,
'permission_count': count
}
def permission_view(permission):
return {
'repository': {
'name': permission.repository.name,
'is_public': permission.repository.visibility.name == 'public'
},
'role': permission.role.name
}
@ -26,7 +38,7 @@ class UserRobotList(ApiResource):
user = get_authenticated_user()
robots = model.list_entity_robot_tuples(user.username)
return {
'robots': [robot_view(name, password) for name, password in robots]
'robots': [robot_view(name, password, count) for name, password, count in robots]
}
@ -75,7 +87,7 @@ class OrgRobotList(ApiResource):
if permission.can():
robots = model.list_entity_robot_tuples(orgname)
return {
'robots': [robot_view(name, password) for name, password in robots]
'robots': [robot_view(name, password, count) for name, password, count in robots]
}
raise Unauthorized()
@ -125,6 +137,47 @@ class OrgRobot(ApiResource):
raise Unauthorized()
@resource('/v1/user/robots/<robot_shortname>/permissions')
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
@internal_only
class UserRobotPermissions(ApiResource):
""" Resource for listing the permissions a user's robot has in the system. """
@require_user_admin
@nickname('getUserRobotPermissions')
def get(self, robot_shortname):
""" Returns the list of repository permissions for the user's robot. """
parent = get_authenticated_user()
robot, password = model.get_robot(robot_shortname, parent)
permissions = model.list_robot_permissions(robot.username)
return {
'permissions': [permission_view(permission) for permission in permissions]
}
@resource('/v1/organization/<orgname>/robots/<robot_shortname>/permissions')
@path_param('orgname', 'The name of the organization')
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
@related_user_resource(UserRobotPermissions)
class OrgRobotPermissions(ApiResource):
""" Resource for listing the permissions an org's robot has in the system. """
@require_user_admin
@nickname('getOrgRobotPermissions')
def get(self, orgname, robot_shortname):
""" Returns the list of repository permissions for the org's robot. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
parent = model.get_organization(orgname)
robot, password = model.get_robot(robot_shortname, parent)
permissions = model.list_robot_permissions(robot.username)
return {
'permissions': [permission_view(permission) for permission in permissions]
}
abort(403)
@resource('/v1/user/robots/<robot_shortname>/regenerate')
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
@internal_only

View file

@ -175,8 +175,8 @@ class User(ApiResource):
'description': 'The user\'s email address',
},
'avatar': {
'type': 'string',
'description': 'Avatar hash representing the user\'s icon'
'type': 'object',
'description': 'Avatar data representing the user\'s icon'
},
'organizations': {
'type': 'array',

View file

@ -818,6 +818,17 @@
width: 30px;
}
.co-table td.caret-col {
width: 10px;
padding-left: 6px;
padding-right: 0px;
color: #aaa;
}
.co-table td.caret-col i.fa {
cursor: pointer;
}
.co-table .add-row-spacer td {
padding: 5px;
}

View file

@ -0,0 +1,86 @@
.robots-manager-element .manager-header {
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.robots-manager-element .manager-header h3 {
margin-bottom: 10px;
}
.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;
}
.robots-manager-element .popup-input-button i.fa {
margin-right: 4px;
}
.robots-manager-element .empty {
color: #ccc;
}
.robots-manager-element tr.open td {
border-bottom: 1px solid transparent;
}
.robots-manager-element .permissions-table-wrapper {
margin-left: 0px;
border-left: 2px solid #ccc;
padding-left: 20px;
}
.robots-manager-element .permissions-table tbody tr:last-child td {
border-bottom: 0px;
}
.robots-manager-element .permissions-display-row {
position: relative;
padding-bottom: 20px;
}
.robots-manager-element .permissions-display-row td:first-child {
min-width: 300px;
}
.robots-manager-element .repo-circle {
color: #999;
display: inline-block;
position: relative;
background: #eee;
padding: 4px;
border-radius: 50%;
display: inline-block;
width: 46px;
height: 46px;
margin-right: 6px;
}
.robots-manager-element .repo-circle .fa-hdd-o {
font-size: 1.7em;
}
.robots-manager-element .repo-circle.no-background .fa-hdd-o {
font-size: 1.7em;
}
.robots-manager-element .repo-circle .fa-lock {
width: 16px;
height: 16px;
line-height: 16px;
font-size: 12px !important;
}
.robots-manager-element .repo-circle.no-background .fa-lock {
bottom: 5px;
right: 2px;
}

View file

@ -6,4 +6,20 @@
.org-view h3 {
margin-bottom: 20px;
margin-top: 0px;
}
.org-view .section-description-header {
padding-left: 40px;
position: relative;
margin-bottom: 20px;
}
.org-view .section-description-header:before {
font-family: FontAwesome;
content: "\f05a";
position: absolute;
top: 2px;
left: 6px;
font-size: 27px;
color: #888;
}

View file

@ -525,27 +525,6 @@ i.toggle-icon:hover {
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;
}
.logs-view-element .header {
padding-bottom: 10px;
border-bottom: 1px solid #eee;

View file

@ -1,32 +1,102 @@
<div class="robots-manager-element">
<div class="quay-spinner" ng-show="loading"></div>
<div class="alert alert-info">Robot accounts allow for delegating access in multiple repositories to role-based accounts that you manage</div>
<div class="cor-loader" ng-show="loading"></div>
<div ng-show="!loading">
<div class="side-controls">
<span class="popup-input-button" pattern="ROBOT_PATTERN" placeholder="'Robot Account Name'"
submitted="createRobot(value)">
<i class="fa fa-wrench"></i> Create Robot Account
</span>
<div class="manager-header">
<div class="side-controls">
<span class="popup-input-button" pattern="ROBOT_PATTERN"
placeholder="'Robot Account Name'"
submitted="createRobot(value)">
<i class="fa fa-plus"></i> Create Robot Account
</span>
</div>
<h3>Robot Accounts</h3>
</div>
<table class="table">
<div class="manager-header section-description-header">
Robot Accounts are named tokens that can be granted permissions on multiple repositories
under this <span ng-if="organization">organization</span><span ng-if="!organization">user namespace</span>. They are typically used in environments where credentials will
be shared, such as deployment systems.
</div>
<div class="empty" ng-if="!robots.length">
<div class="empty-primary-msg">No robot accounts defined.</div>
<div class="empty-secondary-msg">
Click the "Create Robot Account" button above to create a robot account.
</div>
</div>
<table class="co-table" ng-if="robots.length">
<thead>
<th>Robot Account Name</th>
<th style="width: 150px"></th>
<td class="caret-col" ng-if="organization.is_admin"></td>
<td>Robot Account Name</td>
<td>Repository Permissions</td>
<td class="options-col"></td>
</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" delete-title="'Delete Robot Account'" perform-delete="deleteRobot(robotInfo)"></span>
</td>
</tr>
<tbody ng-repeat="robotInfo in robots">
<tr ng-class="robotInfo.showing_permissions ? 'open' : 'closed'">
<td class="caret-col" ng-if="organization.is_admin">
<span ng-if="robotInfo.permission_count > 0" ng-click="showPermissions(robotInfo)">
<i class="fa"
ng-class="robotInfo.showing_permissions ? 'fa-caret-down' : 'fa-caret-right'"
data-title="View Permissions List" bs-tooltip></i>
</span>
</td>
<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="empty" ng-if="robotInfo.permission_count == 0">(No permissions on any repositories)</span>
<span ng-if="robotInfo.permission_count > 0">
Permissions on
<span class="anchor" href="javascript:void(0)" is-text-only="!organization.is_admin" ng-click="showPermissions(robotInfo)">{{ robotInfo.permission_count }}
<span ng-if="robotInfo.permission_count == 1">repository</span>
<span ng-if="robotInfo.permission_count > 1">repositories</span>
</span>
</span>
</td>
<td class="options-col">
<span class="cor-options-menu">
<span class="cor-option" option-click="showRobot(robotInfo)">
<i class="fa fa-key"></i> View Credentials
</span>
<span class="cor-option" option-click="deleteRobot(robotInfo)">
<i class="fa fa-times"></i> Delete Robot {{ robotInfo.name }}
</span>
</span>
</td>
</tr>
<tr ng-if="robotInfo.showing_permissions">
<td class="permissions-display-row" colspan="4">
<span class="cor-loader" ng-if="robotInfo.loading_permissions"></span>
<div class="permissions-table-wrapper">
<table class="permissions-table" ng-if="!robotInfo.loading_permissions">
<thead>
<td>Repository</td>
<td>Permission</td>
</thead>
<tr ng-repeat="permission in robotInfo.permissions">
<td>
<span class="repo-icon repo-circle no-background" repo="permission.repository"></span>
<a ng-href="/repository/{{ getPrefix(robotInfo.name) }}/{{ permission.repository.name }}">{{ getPrefix(robotInfo.name) }}/{{ permission.repository.name }}</a>
</td>
<td>
<div class="btn-group btn-group-sm">
<span class="role-group" current-role="permission.role" roles="roles"
read-only="true"></span>
</div>
</td>
</tr>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</div>

View file

@ -1,5 +1,6 @@
<div class="btn-group btn-group-sm">
<button ng-repeat="role in roles"
type="button" class="btn" ng-click="setRole(role.id)"
ng-class="(currentRole == role.id) ? ('active btn-' + role.kind) : 'btn-default'">{{ role.title }}</button>
ng-class="(currentRole == role.id) ? ('active btn-' + role.kind) : 'btn-default'"
ng-disabled="readOnly">{{ role.title }}</button>
</div>

View file

@ -13,6 +13,7 @@ angular.module('quay').directive('repositoryPermissionsTable', function () {
'repository': '=repository'
},
controller: function($scope, $element, ApiService, Restangular, UtilService) {
// TODO(jschorr): move this to a service.
$scope.roles = [
{ 'id': 'read', 'title': 'Read', 'kind': 'success' },
{ 'id': 'write', 'title': 'Write', 'kind': 'success' },

View file

@ -14,11 +14,36 @@ angular.module('quay').directive('robotsManager', function () {
},
controller: function($scope, $element, ApiService, $routeParams, CreateService) {
$scope.ROBOT_PATTERN = ROBOT_PATTERN;
// TODO(jschorr): move this to a service.
$scope.roles = [
{ 'id': 'read', 'title': 'Read', 'kind': 'success' },
{ 'id': 'write', 'title': 'Write', 'kind': 'success' },
{ 'id': 'admin', 'title': 'Admin', 'kind': 'primary' }
];
$scope.robots = null;
$scope.loading = false;
$scope.shownRobot = null;
$scope.showRobotCounter = 0;
var loadRobotPermissions = function(info) {
var shortName = $scope.getShortenedName(info.name);
info.loading_permissions = true;
ApiService.getRobotPermissions($scope.organization, null, {'robot_shortname': shortName}).then(function(resp) {
info.permissions = resp.permissions;
info.loading_permissions = false;
}, ApiService.errorDisplay('Could not load robot permissions'));
};
$scope.showPermissions = function(robotInfo) {
robotInfo.showing_permissions = !robotInfo.showing_permissions;
if (robotInfo.showing_permissions) {
loadRobotPermissions(robotInfo);
}
};
$scope.regenerateToken = function(username) {
if (!username) { return; }

View file

@ -12,6 +12,7 @@ angular.module('quay').directive('roleGroup', function () {
scope: {
'roles': '=roles',
'currentRole': '=currentRole',
'readOnly': '=readOnly',
'roleChanged': '&roleChanged'
},
controller: function($scope, $element) {

View file

@ -18,10 +18,10 @@
<span class="cor-tab" tab-title="Teams" tab-target="#teams">
<i class="fa fa-users"></i>
</span>
<span class="cor-tab" tab-title="Robot Accounts" tab-target="#robots" ng-if="isAdmin">
<span class="cor-tab" tab-title="Robot Accounts" tab-target="#robots" ng-show="isAdmin">
<i class="fa fa-wrench"></i>
</span>
<span class="cor-tab" tab-title="Default Permissions" tab-target="#default" ng-if="isAdmin">
<span class="cor-tab" tab-title="Default Permissions" tab-target="#default" ng-show="isAdmin">
<i class="fa ci-stamp"></i>
</span>
<span class="cor-tab" tab-title="Billing" tab-target="#usage"
@ -33,15 +33,15 @@
<i class="fa ci-invoice"></i>
</span>
<span class="cor-tab" tab-title="Usage Logs" tab-target="#logs"
tab-init="showLogs()" ng-if="isAdmin">
tab-init="showLogs()" ng-show="isAdmin">
<i class="fa fa-bar-chart"></i>
</span>
<span class="cor-tab" tab-title="Applications" tab-target="#applications"
tab-init="showApplications()"ng-if="isAdmin">
tab-init="showApplications()" ng-show="isAdmin">
<i class="fa ci-application"></i>
</span>
<span class="cor-tab" tab-title="Organization Settings" tab-target="#settings"
ng-if="isAdmin">
ng-show="isAdmin">
<i class="fa fa-gears"></i>
</span>
</div> <!-- /cor-tabs -->
@ -65,7 +65,6 @@
<!-- Robot Accounts -->
<div id="robots" class="tab-pane">
<h3>Robot Accounts</h3>
<div class="robots-manager" organization="organization"></div>
</div>

View file

@ -17,7 +17,8 @@ from endpoints.api.image import RepositoryImageChanges, RepositoryImage, Reposit
from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, RepositoryBuildLogs,
RepositoryBuildList, RepositoryBuildResource)
from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobot,
RegenerateOrgRobot, RegenerateUserRobot)
RegenerateOrgRobot, RegenerateUserRobot, UserRobotPermissions,
OrgRobotPermissions)
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
@ -3335,10 +3336,28 @@ class TestRegenerateOrgRobot(ApiTestCase):
self._run_test('POST', 400, 'devtable', None)
class TestOrganizationBuynlarge(ApiTestCase):
class TestUserRobotPermissions(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(Organization, orgname="buynlarge")
self._set_url(UserRobotPermissions, robot_shortname="robotname")
def test_get_anonymous(self):
self._run_test('GET', 401, None, None)
def test_get_freshuser(self):
self._run_test('GET', 400, 'freshuser', None)
def test_get_reader(self):
self._run_test('GET', 400, 'reader', None)
def test_get_devtable(self):
self._run_test('GET', 400, 'devtable', None)
class TestOrgRobotPermissions(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(OrgRobotPermissions, orgname="buynlarge", robot_shortname="robotname")
def test_get_anonymous(self):
self._run_test('GET', 401, None, None)
@ -3346,6 +3365,24 @@ class TestOrganizationBuynlarge(ApiTestCase):
def test_get_freshuser(self):
self._run_test('GET', 403, 'freshuser', None)
def test_get_reader(self):
self._run_test('GET', 403, 'reader', None)
def test_get_devtable(self):
self._run_test('GET', 400, 'devtable', None)
class TestOrganizationBuynlarge(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(Organization, orgname="buynlarge")
def test_get_anonymous(self):
self._run_test('GET', 200, None, None)
def test_get_freshuser(self):
self._run_test('GET', 200, 'freshuser', None)
def test_get_reader(self):
self._run_test('GET', 200, 'reader', None)

View file

@ -552,12 +552,12 @@ class TestGetOrganization(ApiTestCase):
def test_unknownorg(self):
self.login(ADMIN_ACCESS_USER)
self.getResponse(Organization, params=dict(orgname='notvalid'),
expected_code=403)
expected_code=404)
def test_cannotaccess(self):
self.login(NO_ACCESS_USER)
self.getResponse(Organization, params=dict(orgname=ORGANIZATION),
expected_code=403)
expected_code=200)
def test_getorganization(self):
self.login(READ_ACCESS_USER)