Add repo permissions dialog for existing teams and robots

Fixes #1686
This commit is contained in:
Joseph Schorr 2016-08-22 14:42:35 -04:00
parent b5efc57655
commit 391d70d9ec
18 changed files with 496 additions and 224 deletions

View file

@ -5,6 +5,17 @@ from data.database import (RepositoryPermission, User, Repository, Visibility, R
from data.model import DataModelException, _basequery
def list_team_permissions(team):
return (RepositoryPermission
.select(RepositoryPermission)
.join(Repository)
.join(Visibility)
.switch(RepositoryPermission)
.join(Role)
.switch(RepositoryPermission)
.where(RepositoryPermission.team == team))
def list_robot_permissions(robot_name):
return (RepositoryPermission
.select(RepositoryPermission, User, Repository)

View file

@ -191,29 +191,47 @@ def get_teams_within_org(organization):
""" Returns a AttrDict of team info (id, name, description), its role under the org,
the number of repositories on which it has permission, and the number of members.
"""
query = (Team.select(Team.id, Team.name, Team.description, TeamRole.name,
fn.Count(RepositoryPermission.id), fn.Count(TeamMember.id))
query = (Team.select()
.where(Team.organization == organization)
.join(TeamRole)
.switch(Team)
.join(RepositoryPermission, JOIN_LEFT_OUTER)
.switch(Team)
.join(TeamMember, JOIN_LEFT_OUTER)
.group_by(Team.id)
.tuples())
.join(TeamRole))
def _team_view(team_tuple):
return AttrDict({
'id': team_tuple[0],
'name': team_tuple[1],
'description': team_tuple[2],
'role_name': team_tuple[3],
def _team_view(team):
return {
'id': team.id,
'name': team.name,
'description': team.description,
'role_name': team.role.name,
'repo_count': team_tuple[4],
'member_count': team_tuple[5],
})
'repo_count': 0,
'member_count': 0,
}
return [_team_view(team_tuple) for team_tuple in query]
teams = {team.id: _team_view(team) for team in query}
if not teams:
# Just in case. Should ideally never happen.
return []
# Add repository permissions count.
permission_tuples = (RepositoryPermission.select(RepositoryPermission.team,
fn.Count(RepositoryPermission.id))
.where(RepositoryPermission.team << teams.keys())
.group_by(RepositoryPermission.team)
.tuples())
for perm_tuple in permission_tuples:
teams[perm_tuple[0]]['repo_count'] = perm_tuple[1]
# Add the member count.
members_tuples = (TeamMember.select(TeamMember.team,
fn.Count(TeamMember.id))
.where(TeamMember.team << teams.keys())
.group_by(TeamMember.team)
.tuples())
for member_tuple in members_tuples:
teams[member_tuple[0]]['member_count'] = member_tuple[1]
return [AttrDict(team_info) for team_info in teams.values()]
def get_user_teams_within_org(username, organization):

View file

@ -5,7 +5,7 @@ from flask import request
import features
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
log_action, internal_only, require_scope, path_param, query_param,
log_action, internal_only, require_scope, path_param, query_param,
truthy_bool, parse_args, require_user_admin, show_if)
from endpoints.exception import Unauthorized, NotFound
from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission
@ -15,6 +15,15 @@ from data import model
from util.useremails import send_org_invite_email
from app import avatar
def permission_view(permission):
return {
'repository': {
'name': permission.repository.name,
'is_public': permission.repository.visibility.name == 'public'
},
'role': permission.role.name
}
def try_accept_invite(code, user):
(team, inviter) = model.team.confirm_team_invite(code, user)
@ -346,6 +355,30 @@ class InviteTeamMember(ApiResource):
raise Unauthorized()
@resource('/v1/organization/<orgname>/team/<teamname>/permissions')
@path_param('orgname', 'The name of the organization')
@path_param('teamname', 'The name of the team')
class TeamPermissions(ApiResource):
""" Resource for listing the permissions an org's team has in the system. """
@nickname('getTeamPermissions')
def get(self, orgname, teamname):
""" Returns the list of repository permissions for the org's team. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
try:
team = model.team.get_organization_team(orgname, teamname)
except model.InvalidTeamException:
raise NotFound()
permissions = model.permission.list_team_permissions(team)
return {
'permissions': [permission_view(permission) for permission in permissions]
}
raise Unauthorized()
@resource('/v1/teaminvite/<code>')
@internal_only
@show_if(features.MAILING)

View file

@ -1,35 +0,0 @@
.add-repo-permissions-element label {
margin-top: 4px;
}
.add-repo-permissions-element .co-table {
margin-top: 20px;
}
.add-repo-permissions-element .fa-hdd-o {
margin-right: 4px;
vertical-align: middle;
}
.add-repo-permissions-element .co-filter-box {
display: block;
float: right;
margin-bottom: 20px;
}
.add-repo-permissions-element .co-filter-box .filter-message {
left: -180px;
top: 4px;
}
.add-repo-permissions-element .co-filter-box input {
width: 100%;
padding-top: 2px;
padding-bottom: 2px;
height: 28px;
}
.add-repo-permissions-element label .avatar {
vertical-align: text-bottom;
margin-left: 4px;
}

View file

@ -0,0 +1,35 @@
.set-repo-permissions-element label {
margin-top: 4px;
}
.set-repo-permissions-element .co-table {
margin-top: 20px;
}
.set-repo-permissions-element .fa-hdd-o {
margin-right: 4px;
vertical-align: middle;
}
.set-repo-permissions-element .co-filter-box {
display: block;
float: right;
margin-bottom: 20px;
}
.set-repo-permissions-element .co-filter-box .filter-message {
left: -180px;
top: 4px;
}
.set-repo-permissions-element .co-filter-box input {
width: 100%;
padding-top: 2px;
padding-bottom: 2px;
height: 28px;
}
.set-repo-permissions-element label .avatar {
vertical-align: text-bottom;
margin-left: 4px;
}

View file

@ -8,23 +8,24 @@
<i class="fa {{ entityIcon }}"></i>
Create {{ entityTitle }}
</h4>
<h4 class="modal-title" ng-show="view == 'addperms' || view == 'addingperms'">
<h4 class="modal-title" ng-show="view == 'setperms' || view == 'settingperms'">
Add permissions for <i class="fa {{ entityIcon }}"></i> {{ entity.name }}
</h4>
</div> <!-- /.model-header -->
<div class="modal-body" ng-show="view == 'creating' || view == 'addingperms'">
<div class="modal-body" ng-show="view == 'creating' || view == 'settingperms'">
<div class="cor-loader"></div>
</div>
<div class="modal-body co-modal-body-scrollable" ng-show="view == 'addperms'">
<div class="add-repo-permissions" namespace="info.namespace"
<div class="modal-body co-modal-body-scrollable" ng-show="view == 'setperms'">
<div class="set-repo-permissions"
namespace="info.namespace"
entity-name="entity.name"
entity-kind="entityKind"
checked-repository="info.repository"
has-changed-repositories="context.hasChangedRepositories"
has-checked-repositories="context.hasCheckedRepositories"
repositories-loaded="repositoriesLoaded(repositories)"
adding-permissions="addingPermissions()"
permissions-added="permissionsAdded(repositories)"
add-permissions="context.addPermissionsCounter"
setting-permissions="settingPermissions()"
permissions-set="permissionsSet(repositories)"
set-permissions="context.setPermissionsCounter"
ng-if="entity"></div>
</div>
<div class="modal-body" ng-show="view == 'enterName'">
@ -37,8 +38,8 @@
</div>
</form>
</div> <!-- /.modal-body -->
<div class="modal-footer" ng-show="view == 'addperms'">
<button type="button" class="btn btn-primary" ng-click="addPermissions()"
<div class="modal-footer" ng-show="view == 'setperms'">
<button type="button" class="btn btn-primary" ng-click="setPermissions()"
ng-show="context.hasCheckedRepositories">Add permissions</button>
<button type="button" class="btn btn-default" ng-click="hide()">Close</button>
</div> <!-- /.footer-body -->

View file

@ -35,99 +35,66 @@
<thead>
<td>Robot Account Name</td>
<td ng-if="organization">Teams</td>
<td>Direct Repository Permissions</td>
<td>Repositories</td>
<td class="options-col"></td>
</thead>
<tbody ng-repeat="robotInfo in robots | filter:robotFilter | orderBy:getShortenedRobotName" bindonce>
<tr ng-class="robotInfo.showing_permissions ? 'open' : 'closed'">
<td class="robot">
<i class="fa ci-robot hidden-xs"></i>
<a ng-click="showRobot(robotInfo)">
<span class="prefix" bo-text="getPrefix(robotInfo.name) + '+'"></span><span bo-text="getShortenedName(robotInfo.name)"></span>
</a>
</td>
<td bo-if="organization">
<span class="empty" bo-if="robotInfo.teams.length == 0">
No teams
</span>
<span class="empty" bo-if="robotInfo.teams.length > 0">
<span ng-repeat="team in robotInfo.teams"
data-title="Team {{ team.name }}" bs-tooltip>
<span class="anchor" is-only-text="!organization.is_admin" href="/organization/{{ organization.name }}/teams/{{ team.name }}">
<span class="avatar" size="24" data="team.avatar"></span>
</span>
<tr ng-repeat="robotInfo in robots | filter:robotFilter | orderBy:getShortenedRobotName" bindonce>
<td class="robot">
<i class="fa ci-robot hidden-xs"></i>
<a ng-click="showRobot(robotInfo)">
<span class="prefix" bo-text="getPrefix(robotInfo.name) + '+'"></span><span bo-text="getShortenedName(robotInfo.name)"></span>
</a>
</td>
<td bo-if="organization">
<span class="empty" bo-if="robotInfo.teams.length == 0">
No teams
</span>
<span class="empty" bo-if="robotInfo.teams.length > 0">
<span ng-repeat="team in robotInfo.teams"
data-title="Team {{ team.name }}" bs-tooltip>
<span class="anchor" is-only-text="!organization.is_admin" href="/organization/{{ organization.name }}/teams/{{ team.name }}">
<span class="avatar" size="24" data="team.avatar"></span>
</span>
</span>
</td>
<td>
<span class="empty" bo-if="robotInfo.repositories.length == 0">
</span>
</span>
</td>
<td>
<span class="empty" ng-if="robotInfo.repositories.length == 0">
<a is-only-text="!organization.is_admin" ng-click="setPermissions(robotInfo)">
No repositories
</span>
</a>
</span>
<span class="member-perm-summary" bo-if="robotInfo.repositories.length > 0">
<span 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>
<a class="hidden-xs" is-only-text="!organization.is_admin"
ng-click="showPermissions(robotInfo)">
<span bo-text="robotInfo.repositories.length"></span>
<span bo-if="robotInfo.repositories.length == 1">repository</span>
<span bo-if="robotInfo.repositories.length > 1">repositories</span>
</a>
<span class="visible-xs">
<span bo-text="robotInfo.repositories.length"></span>
<span bo-if="robotInfo.repositories.length == 1">repository</span>
<span bo-if="robotInfo.repositories.length > 1">repositories</span>
</span>
<span class="member-perm-summary" ng-if="robotInfo.repositories.length > 0">
<a is-only-text="!organization.is_admin" ng-click="setPermissions(robotInfo)">
{{ robotInfo.repositories.length }}
<span ng-if="robotInfo.repositories.length == 1">repository</span>
<span ng-if="robotInfo.repositories.length > 1">repositories</span>
</a>
</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>
</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="askDeleteRobot(robotInfo)">
<i class="fa fa-times"></i> Delete Robot {{ robotInfo.name }}
</span>
<span class="cor-option" option-click="setPermissions(robotInfo)">
<i class="fa fa-hdd-o"></i> Set Repository Permissions
</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 }}?tab=settings">{{ 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="repoRoles"
read-only="true"></span>
</div>
</td>
</tr>
</table>
</div>
</td>
</tr>
</tbody>
<span class="cor-option" option-click="askDeleteRobot(robotInfo)">
<i class="fa fa-times"></i> Delete Robot {{ robotInfo.name }}
</span>
</span>
</td>
</tr>
</table>
</div>
<!-- Set repo permissions dialog -->
<div class="set-repo-permissions-dialog" info="setRepoPermissionsInfo"
permissions-set="handlePermissionsSet(info, repositories)"></div>
<div class="create-robot-dialog" info="createRobotInfo" robot-created="robotCreated()"></div>
<div class="robot-credentials-dialog" info="robotDisplayInfo"></div>
</div>

View file

@ -0,0 +1,35 @@
<div class="set-repo-permissions-dialog-element">
<div class="modal fade co-dialog wider">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" ng-click="hide()" aria-hidden="true">&times;</button>
<h4 class="modal-title">
Set permissions for <i class="fa {{ info.entityIcon }}"></i> {{ info.entityName }}
</h4>
</div> <!-- /.model-header -->
<div class="modal-body" ng-show="working">
<div class="cor-loader"></div>
</div>
<div class="modal-body co-modal-body-scrollable" ng-show="!working" style="padding-bottom: 210px;">
<div class="set-repo-permissions"
namespace="context.info.namespace"
entity-name="context.info.entityName"
entity-kind="context.info.entityKind"
has-changed-repositories="context.hasChangedRepositories"
has-checked-repositories="context.hasCheckedRepositories"
setting-permissions="settingPermissions()"
permissions-set="permissionsSetComplete(repositories)"
set-permissions="setPermissionsCounter"
ng-if="context.info.namespace && context.info.entityName && context.info.entityKind">
</div>
</div> <!-- /.modal-body -->
<div class="modal-footer" ng-show="!working">
<button type="button" class="btn btn-primary" ng-click="setPermissions()"
ng-show="context.hasChangedRepositories">Update permissions</button>
<button type="button" class="btn btn-default" ng-click="hide()">Close</button>
</div> <!-- /.footer-body -->
</div>
</div>
</div>
</div>

View file

@ -1,4 +1,4 @@
<div class="add-repo-permissions-element">
<div class="set-repo-permissions-element">
<span class="co-filter-box">
<span class="filter-message" ng-if="options.filter">
Showing {{ orderedRepositories.entries.length }} of {{ repositories.length }} repositories

View file

@ -81,19 +81,31 @@
</span>
</td>
<td class="hidden-xs">
<span bo-text="team.repo_count"></span> repositories
<span class="empty" ng-if="team.repo_count == 0">
<a is-only-text="!organization.is_admin" ng-click="setRepoPermissions(team.name)">
No repositories
</a>
</span>
<span class="member-perm-summary" ng-if="team.repo_count > 0">
<a is-only-text="!organization.is_admin" ng-click="setRepoPermissions(team.name)">
{{ team.repo_count }}
<span ng-if="team.repo_count == 1">repository</span>
<span ng-if="team.repo_count > 1">repositories</span>
</a>
</span>
</td>
<td>
<span class="role-group" current-role="team.role" pull-left="true"
role-changed="setRole(role, team.name)" roles="teamRoles"></span>
</td>
<td>
<span class="cor-options-menu">
<span class="cor-options-menu" ng-show="organization.is_admin">
<span class="cor-option" option-click="viewTeam(team.name)">
<i class="fa fa-user"></i> Manage Team Members
</span>
<span class="cor-option" option-click="addRepoPermissions(team.name)">
<i class="fa fa-hdd-o"></i> Add Repository Permissions
<span class="cor-option" option-click="setRepoPermissions(team.name)">
<i class="fa fa-hdd-o"></i> Set Repository Permissions
</span>
<span class="cor-option" option-click="askDeleteTeam(team.name)">
<i class="fa fa-times"></i> Delete Team {{ team.name }}
@ -169,8 +181,13 @@
</table>
</div>
<!-- Create team dialog -->
<div class="create-team-dialog" info="createTeamInfo" team-created="handleTeamCreated(team)"></div>
<!-- Set repo permissions dialog -->
<div class="set-repo-permissions-dialog" info="setRepoPermissionsInfo"
permissions-set="handlePermissionsSet(info, repositories)"></div>
<!-- Remove member confirm -->
<div class="cor-confirm-dialog"
dialog-context="removeMemberInfo"

View file

@ -22,7 +22,7 @@ angular.module('quay').directive('createEntityDialog', function () {
controller: function($scope, $element, ApiService, UIService, UserService) {
$scope.context = {
'addPermissionsCounter': 0
'setPermissionsCounter': 0
};
$scope.$on('$destroy', function() {
@ -71,17 +71,17 @@ angular.module('quay').directive('createEntityDialog', function () {
});
};
$scope.permissionsAdded = function(repositories) {
$scope.permissionsSet = function(repositories) {
$scope.entity['repo_count'] = repositories.length;
$scope.hide();
};
$scope.addingPermissions = function() {
$scope.view = 'addingperms';
$scope.settingPermissions = function() {
$scope.view = 'settingperms';
};
$scope.addPermissions = function() {
$scope.context.addPermissionsCounter++;
$scope.setPermissions = function() {
$scope.context.setPermissionsCounter++;
};
$scope.repositoriesLoaded = function(repositories) {
@ -90,7 +90,7 @@ angular.module('quay').directive('createEntityDialog', function () {
return;
}
$scope.view = 'addperms';
$scope.view = 'setperms';
};
$scope.$watch('entityNameRegex', function(r) {

View file

@ -33,15 +33,6 @@ angular.module('quay').directive('robotsManager', function () {
locationListener && locationListener();
});
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.filterToRobot = function(robotName) {
if ($scope.robotFilter == robotName) {
return;
@ -56,14 +47,6 @@ angular.module('quay').directive('robotsManager', function () {
$scope.robotFilter = robotName;
};
$scope.showPermissions = function(robotInfo) {
robotInfo.showing_permissions = !robotInfo.showing_permissions;
if (robotInfo.showing_permissions) {
loadRobotPermissions(robotInfo);
}
};
$scope.showRobot = function(info) {
$scope.robotDisplayInfo = {
'name': info.name
@ -126,6 +109,21 @@ angular.module('quay').directive('robotsManager', function () {
});
};
$scope.setPermissions = function(info) {
var namespace = $scope.organization ? $scope.organization.name : $scope.user.username;
$scope.setRepoPermissionsInfo = {
'namespace': namespace,
'entityName': info.name,
'entityKind': 'robot',
'entityIcon': 'ci-robot'
};
};
$scope.handlePermissionsSet = function(info, repositories) {
var index = $scope.findRobotIndexByName(info.entityName);
$scope.robots[index]['repositories'] = repositories;
};
$scope.robotCreated = function() {
update();
};

View file

@ -0,0 +1,60 @@
/**
* An element which displays a dialog for setting permissions for an entity to repositories under
* a namespace.
*/
angular.module('quay').directive('setRepoPermissionsDialog', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/set-repo-permissions-dialog.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'info': '=info',
'permissionsSet': '&permissionsSet',
},
controller: function($scope, $element) {
$scope.setPermissionsCounter = 0;
$scope.loading = false;
$scope.context = {};
$scope.setPermissions = function() {
$scope.setPermissionsCounter++;
};
$scope.settingPermissions = function() {
$scope.working = true;
};
$scope.show = function() {
$scope.setPermissionsCounter = 0;
$scope.working = false;
$element.find('.modal').modal({});
};
$scope.hide = function() {
$scope.working = false;
$scope.context.info = null;
$scope.context.hasChangedRepositories = false;
$scope.context.hasCheckedRepositories = false;
$element.find('.modal').modal('hide');
};
$scope.permissionsSetComplete = function(repositories) {
$scope.hide();
$scope.permissionsSet({'repositories': repositories, 'info': $scope.info});
};
$scope.$watch('info', function(info) {
if (info) {
$scope.context.info = info;
$scope.show();
}
});
}
};
return directiveDefinitionObject;
});

View file

@ -1,11 +1,11 @@
/**
* An element which displays a table for adding permissions for an entity to repositories under
* An element which displays a table for setting permissions for an entity to repositories under
* a namespace.
*/
angular.module('quay').directive('addRepoPermissions', function () {
angular.module('quay').directive('setRepoPermissions', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/add-repo-permissions.html',
templateUrl: '/static/directives/set-repo-permissions.html',
replace: false,
transclude: true,
restrict: 'C',
@ -14,15 +14,14 @@ angular.module('quay').directive('addRepoPermissions', function () {
'entityName': '=entityName',
'entityKind': '=entityKind',
'checkedRepository': '=checkedRepository',
'addPermissions': '=addPermissions',
'setPermissions': '=setPermissions',
'hasCheckedRepositories': '=hasCheckedRepositories',
'hasChangedRepositories': '=hasChangedRepositories',
'repositoriesLoaded': '&repositoriesLoaded',
'addingPermissions': '&addingPermissions',
'permissionsAdded': '&permissionsAdded',
'settingPermissions': '&settingPermissions',
'permissionsSet': '&permissionsSet',
},
controller: function($scope, $element, ApiService, UIService, TableService, RolesService, UserService) {
@ -36,6 +35,20 @@ angular.module('quay').directive('addRepoPermissions', function () {
$scope.repositories = null;
$scope.currentNamespace = null;
$scope.currentEntityName = null;
var checkForChanges = function() {
var hasChanges = false;
$scope.repositories.forEach(function(repo) {
if (repo['permission'] != repo['original_permission']) {
hasChanges = true;
}
});
$scope.hasCheckedRepositories = !!$scope.checkedRepos.checked.length;
$scope.hasChangedRepositories = hasChanges;
};
var handleRepoCheckChange = function() {
$scope.repositories.forEach(function(repo) {
@ -48,7 +61,7 @@ angular.module('quay').directive('addRepoPermissions', function () {
}
});
$scope.hasCheckedRepositories = !!$scope.checkedRepos.checked.length;
checkForChanges();
};
var setRepoState = function() {
@ -62,18 +75,43 @@ angular.module('quay').directive('addRepoPermissions', function () {
['last_modified_datetime']);
};
var loadRepositories = function() {
var loadRepositoriesAndPermissions = function() {
if (!$scope.namespace || !$scope.entityName || !$scope.entityKind) {
return;
}
if ($scope.namespace == $scope.currentNamespace) {
if (($scope.entityName == $scope.currentEntityName) &&
($scope.namespace == $scope.currentNamespace)) {
return;
}
$scope.currentNamespace = $scope.namespace;
$scope.currentEntityName = $scope.entityName;
// Load the repository permissions for the entity first. We then load the full repo list
// and compare.
RolesService.getRepoPermissions($scope.namespace, $scope.entityKind, $scope.entityName,
function(permissions) {
if (permissions == null) {
$scope.currentNamespace = null;
$scope.currentEntityName = null;
return;
}
var existingPermissionsMap = {};
permissions.forEach(function(existingPermission) {
existingPermissionsMap[existingPermission.repository.name] = existingPermission.role;
});
loadRepositories(existingPermissionsMap);
});
};
var loadRepositories = function(existingPermissionsMap) {
$scope.namespaceInfo = UserService.getNamespace($scope.namespace);
// Load the repositories under the entity's namespace.
// Load the repositories under the entity's namespace, along with the current repo
// permissions for the entity.
var params = {
'namespace': $scope.namespace,
'last_modified': true
@ -84,12 +122,15 @@ angular.module('quay').directive('addRepoPermissions', function () {
var repos = [];
resp['repositories'].forEach(function(repo) {
var existingPermission = existingPermissionsMap[repo.name] || 'none';
repos.push({
'namespace': repo.namespace,
'name': repo.name,
'last_modified': repo.last_modified,
'last_modified_datetime': TableService.getReversedTimestamp(repo.last_modified),
'permission': 'none'
'permission': existingPermission,
'original_permission': existingPermission
});
});
@ -100,53 +141,55 @@ angular.module('quay').directive('addRepoPermissions', function () {
$scope.repositories = repos;
$scope.checkedRepos = UIService.createCheckStateController($scope.repositories, 'name');
$scope.checkedRepos.listen(handleRepoCheckChange);
if ($scope.checkedRepository) {
repos.forEach(function(repo) {
if (repo['namespace'] == $scope.checkedRepository.namespace &&
repo['name'] == $scope.checkedRepository.name) {
$scope.checkedRepos.checkItem(repo);
$scope.options.filter = $scope.checkedRepository.name;
}
});
}
repos.forEach(function(repo) {
if (repo.permission != 'none') {
$scope.checkedRepos.checkItem(repo);
}
});
$scope.checkedRepos.listen(handleRepoCheckChange);
setRepoState();
$scope.repositoriesLoaded({'repositories': repos});
}, ApiService.errorDisplay('Could not load repositories'));
};
var addPermissions = function() {
var setPermissions = function() {
if (!$scope.checkedRepos || !$scope.namespace || !$scope.repositories) {
return;
}
$scope.addingPermissions();
$scope.settingPermissions();
var repos = $scope.checkedRepos.checked;
var repos = $scope.repositories;
var counter = 0;
var addPerm = function() {
var setPerm = function() {
if (counter >= repos.length) {
$scope.permissionsAdded({'repositories': repos});
$scope.permissionsSet({'repositories': $scope.checkedRepos.checked});
$scope.checkedRepos.setChecked([]);
return;
}
var repo = repos[counter];
if (repo['permission'] == repo['original_permission']) {
// Skip changing it.
counter++;
setPerm();
return;
}
RolesService.setRepositoryRole(repo, repo.permission, $scope.entityKind,
$scope.entityName,
function(status) {
if (status) {
counter++;
addPerm();
} else {
$scope.permissionsAdded();
}
});
$scope.entityName, function(status) {
if (status) {
counter++;
setPerm();
}
});
};
addPerm();
setPerm();
};
$scope.setRole = function(role, repo) {
@ -157,6 +200,8 @@ angular.module('quay').directive('addRepoPermissions', function () {
} else {
$scope.checkedRepos.checkItem(repo);
}
checkForChanges();
};
$scope.allRepositoriesFilter = function(item) {
@ -175,13 +220,13 @@ angular.module('quay').directive('addRepoPermissions', function () {
$scope.$watch('options.reverse', setRepoState);
$scope.$watch('options.filter', setRepoState);
$scope.$watch('namespace', loadRepositories);
$scope.$watch('entityName', loadRepositories);
$scope.$watch('entityKind', loadRepositories);
$scope.$watch('namespace', loadRepositoriesAndPermissions);
$scope.$watch('entityName', loadRepositoriesAndPermissions);
$scope.$watch('entityKind', loadRepositoriesAndPermissions);
$scope.$watch('addPermissions', function(value) {
$scope.$watch('setPermissions', function(value) {
if (value) {
addPermissions();
setPermissions();
}
});
}

View file

@ -196,6 +196,20 @@ angular.module('quay').directive('teamsManager', function () {
$scope.removeMemberInfo = $.extend({}, memberInfo);
};
$scope.setRepoPermissions = function(teamName) {
$scope.setRepoPermissionsInfo = {
'namespace': $scope.organization.name,
'entityName': teamName,
'entityKind': 'team',
'entityIcon': 'fa-group'
};
};
$scope.handlePermissionsSet = function(info, repositories) {
var team = $scope.organization.teams[info.entityName];
team['repo_count'] = repositories.length;
};
$scope.$watch('organization', setTeamsState);
$scope.$watch('isEnabled', setTeamsState);

View file

@ -1,7 +1,8 @@
/**
* Service which defines the various role groups.
*/
angular.module('quay').factory('RolesService', ['UtilService', 'Restangular', 'ApiService', function(UtilService, Restangular, ApiService) {
angular.module('quay').factory('RolesService', ['UtilService', 'Restangular', 'ApiService', 'UserService',
function(UtilService, Restangular, ApiService, UserService) {
var roleService = {};
roleService.repoRolesOrNone = [
@ -20,14 +21,22 @@ angular.module('quay').factory('RolesService', ['UtilService', 'Restangular', 'A
{ 'id': 'admin', 'title': 'Admin', 'kind': 'primary', 'description': 'Full admin access to the organization' }
];
var getPermissionEndpoint = function(repository, entityName, kind) {
var getPermissionEndpoint = function(repository, entityName, entityKind) {
if (entityKind == 'robot') {
entityKind = 'user';
}
var namespace = repository.namespace;
var name = repository.name;
var url = UtilService.getRestUrl('repository', namespace, name, 'permissions', kind, entityName);
var url = UtilService.getRestUrl('repository', namespace, name, 'permissions', entityKind, entityName);
return Restangular.one(url);
};
roleService.deleteRepositoryRole = function(repository, entityKind, entityName, callback) {
if (entityKind == 'robot') {
entityKind = 'user';
}
var errorDisplay = ApiService.errorDisplay('Cannot change permission', function(resp) {
callback(false);
});
@ -39,6 +48,15 @@ angular.module('quay').factory('RolesService', ['UtilService', 'Restangular', 'A
};
roleService.setRepositoryRole = function(repository, role, entityKind, entityName, callback) {
if (role == 'none') {
roleService.deleteRepositoryRole(repository, entityKind, entityName, callback);
return;
}
if (entityKind == 'robot') {
entityKind = 'user';
}
var errorDisplay = ApiService.errorDisplay('Cannot change permission', function(resp) {
callback(false);
});
@ -53,5 +71,30 @@ angular.module('quay').factory('RolesService', ['UtilService', 'Restangular', 'A
}, errorDisplay);
};
roleService.getRepoPermissions = function(namespace, entityKind, entityName, callback) {
var errorHandler = ApiService.errorDisplay('Could not load permissions', callback);
if (entityKind == 'team') {
var params = {
'orgname': namespace,
'teamname': entityName
};
ApiService.getTeamPermissions(null, params).then(function(resp) {
callback(resp.permissions);
}, errorHandler);
} else if (entityKind == 'robot') {
var parts = entityName.split('+');
var shortName = parts[1];
var orgname = UserService.isOrganization(namespace) ? namespace : null;
ApiService.getRobotPermissions(orgname, null, {'robot_shortname': shortName}).then(function(resp) {
callback(resp.permissions);
}, errorHandler);
} else {
throw Error('Unknown entity kind ' + entityKind);
}
};
return roleService;
}]);

View file

@ -10,7 +10,8 @@ from data import model
from initdb import setup_database_for_testing, finished_database_for_testing
from endpoints.api import api_bp, api
from endpoints.api.team import TeamMember, TeamMemberList, OrganizationTeam, TeamMemberInvite
from endpoints.api.team import (TeamMember, TeamMemberList, OrganizationTeam, TeamMemberInvite,
TeamPermissions)
from endpoints.api.tag import RepositoryTagImages, RepositoryTag, ListRepositoryTags, RevertTag
from endpoints.api.search import EntitySearch
from endpoints.api.image import RepositoryImage, RepositoryImageList
@ -678,6 +679,24 @@ class TestTeamMemberBuynlargeDevtableOwners(ApiTestCase):
self._run_test('DELETE', 400, 'devtable', None)
class TestTeamPermissionsBuynlarge(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(TeamPermissions, orgname="buynlarge", teamname="readers")
def test_get_anonymous(self):
self._run_test('GET', 401, None, None)
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', 200, 'devtable', None)
class TestTeamMemberListBuynlargeReaders(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)

View file

@ -27,7 +27,8 @@ from data import database, model
from data.database import RepositoryActionCount, Repository as RepositoryTable
from test.helpers import assert_action_logged
from endpoints.api.team import TeamMember, TeamMemberList, TeamMemberInvite, OrganizationTeam
from endpoints.api.team import (TeamMember, TeamMemberList, TeamMemberInvite, OrganizationTeam,
TeamPermissions)
from endpoints.api.tag import RepositoryTagImages, RepositoryTag, RevertTag, ListRepositoryTags
from endpoints.api.search import EntitySearch, ConductSearch
from endpoints.api.image import RepositoryImage, RepositoryImageList
@ -1155,6 +1156,16 @@ class TestDeleteOrganizationTeam(ApiTestCase):
self.assertEquals(msg, data['message'])
class TestTeamPermissions(ApiTestCase):
def test_team_permissions(self):
self.login(ADMIN_ACCESS_USER)
resp = self.getJsonResponse(TeamPermissions,
params=dict(orgname=ORGANIZATION, teamname='readers'))
self.assertEquals(1, len(resp['permissions']))
class TestGetOrganizationTeamMembers(ApiTestCase):
def test_invalidteam(self):
self.login(ADMIN_ACCESS_USER)