Merge pull request #1754 from coreos-inc/team-add-perms

Better UI and permissions handling for robots and teams
This commit is contained in:
josephschorr 2016-09-06 17:21:19 -04:00 committed by GitHub
commit cd8b45e25b
21 changed files with 895 additions and 458 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

@ -1,7 +1,9 @@
from data.database import Team, TeamMember, TeamRole, User, TeamMemberInvite
from data.database import Team, TeamMember, TeamRole, User, TeamMemberInvite, RepositoryPermission
from data.model import (DataModelException, InvalidTeamException, UserAlreadyInTeam,
InvalidTeamMemberException, user, _basequery)
from util.validation import validate_username
from peewee import fn, JOIN_LEFT_OUTER
from util.morecollections import AttrDict
def create_team(name, org_obj, team_role_name, description=''):
@ -186,7 +188,50 @@ def get_matching_teams(team_prefix, organization):
def get_teams_within_org(organization):
return Team.select().where(Team.organization == 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()
.where(Team.organization == organization)
.join(TeamRole))
def _team_view(team):
return {
'id': team.id,
'name': team.name,
'description': team.description,
'role_name': team.role.name,
'repo_count': 0,
'member_count': 0,
}
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

@ -8,13 +8,12 @@ import features
from app import billing as stripe, avatar
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
related_user_resource, internal_only, require_user_admin, log_action,
related_user_resource, internal_only, require_user_admin, log_action,
show_if, path_param, require_scope)
from endpoints.exception import Unauthorized, NotFound
from endpoints.api.team import team_view
from endpoints.api.user import User, PrivateRepositories
from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission,
CreateRepositoryPermission)
CreateRepositoryPermission, ViewTeamPermission)
from auth.auth_context import get_authenticated_user
from auth import scopes
from data import model
@ -24,6 +23,18 @@ from data.billing import get_plan
logger = logging.getLogger(__name__)
def team_view(orgname, team):
return {
'name': team.name,
'description': team.description,
'role': team.role_name,
'avatar': avatar.get_data_for_team(team),
'can_view': ViewTeamPermission(orgname, team.name).can(),
'repo_count': team.repo_count,
'member_count': team.member_count,
}
def org_view(o, teams):
is_admin = AdministerOrganizationPermission(o.username).can()

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

@ -15,39 +15,3 @@
margin-left: 4px;
margin-right: 4px;
}
.create-entity-dialog-element label {
margin-top: 4px;
}
.create-entity-dialog-element .co-table {
margin-top: 20px;
}
.create-entity-dialog-element .fa-hdd-o {
margin-right: 4px;
vertical-align: middle;
}
.create-entity-dialog-element .co-filter-box {
display: block;
float: right;
margin-bottom: 20px;
}
.create-entity-dialog-element .co-filter-box .filter-message {
left: -180px;
top: 4px;
}
.create-entity-dialog-element .co-filter-box input {
width: 100%;
padding-top: 2px;
padding-bottom: 2px;
height: 28px;
}
.create-entity-dialog-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

@ -1,5 +1,19 @@
.teams-manager .popup-input-button {
.teams-manager .co-filter-box {
display: block;
float: right;
margin-bottom: 20px;
}
.teams-manager.co-filter-box .filter-message {
left: -180px;
top: 4px;
}
.teams-manager .co-filter-box input {
width: 100%;
padding-top: 2px;
padding-bottom: 2px;
height: 28px;
}
.teams-manager .manager-header {
@ -20,6 +34,10 @@
color: #ccc;
}
.teams-manager .co-table .avatar {
margin-right: 6px;
}
.teams-manager .cor-confirm-dialog .entity-reference .avatar {
margin-left: 4px;
margin-right: 0px;
@ -36,14 +54,20 @@
}
@media (max-width: 767px) {
.teams-manager .control-col {
padding-left: 55px;
padding-bottom: 10px;
.teams-manager .co-filter-box {
display: block;
float: none;
}
}
.teams-manager .header-col .info-icon {
.teams-manager .info-icon {
margin-left: 4px;
}
.teams-manager .popover-content {
color: black;
font-size: 16px;
text-transform: none;
}
.teams-manager .header-col .header-text {

View file

@ -634,13 +634,6 @@ i.toggle-icon:hover {
padding: 6px;
}
.info-icon {
display: inline-block;
float: right;
vertical-align: middle;
font-size: 20px;
}
.accordion-toggle {
cursor: pointer;
text-decoration: none !important;

View file

@ -8,81 +8,25 @@
<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'">
<span class="co-filter-box">
<span class="filter-message" ng-if="options.filter">
Showing {{ orderedRepositories.entries.length }} of {{ repositories.length }} repositories
</span>
<input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Repositories...">
</span>
<label>
Select repositories in
<span class="avatar" size="16" data="namespace.avatar"></span>
{{ info.namespace }}:
</label>
<table class="co-table" style="margin-bottom: 210px;">
<thead>
<td class="checkbox-col checkbox-menu-col">
<span class="cor-checkable-menu" controller="checkedRepos">
<div class="cor-checkable-menu-item" item-filter="allRepositoriesFilter(item)">
<i class="fa fa-check-square-o"></i>All Repositories
</div>
<div class="cor-checkable-menu-item" item-filter="noRepositoriesFilter(item)">
<i class="fa fa-square-o"></i>No Repositories
</div>
<div class="cor-checkable-menu-item" item-filter="missingPermsRepositoriesFilter(item)">
<i class="fa fa-circle-o"></i>Missing Permissions
</div>
</span>
</td>
<td ng-class="TableService.tablePredicateClass('name', options.predicate, options.reverse)">
<a ng-click="TableService.orderBy('name', options)">Repository Name</a>
</td>
<td>Permission</td>
<td ng-class="TableService.tablePredicateClass('last_modified_datetime', options.predicate, options.reverse)">
<a ng-click="TableService.orderBy('last_modified_datetime', options)">Last Updated</a>
</td>
</thead>
<tr class="co-checkable-row"
ng-repeat="repo in orderedRepositories.visibleEntries"
ng-class="checkedRepos.isChecked(repo, checkedRepos.checked) ? 'checked' : ''"
bindonce>
<td class="offset-check-col">
<span class="cor-checkable-item" controller="checkedRepos" item="repo"></span>
</td>
<td>
<i class="fa fa-hdd-o"></i>
<span bo-text="repo.name"></span>
</td>
<td>
<span class="role-group small" current-role="repo.permission"
roles="repoRolesOrNone"
role-changed="setRole(role, repo)"></span>
</td>
<td>
<span ng-if="repo.last_modified">
{{ repo.last_modified * 1000 | amCalendar }}
</span>
<span class="empty" ng-if="!repo.last_modified">(Empty Repository)</span>
</td>
</tr>
</table>
<div class="empty" ng-if="!orderedRepositories.entries.length"
style="margin-top: 20px;">
<div class="empty-primary-msg">No matching repositories found.</div>
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
</div>
<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"
has-changed-repositories="context.hasChangedRepositories"
has-checked-repositories="context.hasCheckedRepositories"
repositories-loaded="repositoriesLoaded(repositories)"
setting-permissions="settingPermissions()"
permissions-set="permissionsSet(repositories)"
set-permissions="context.setPermissionsCounter"
ng-if="entity"></div>
</div>
<div class="modal-body" ng-show="view == 'enterName'">
<form name="enterNameForm" ng-submit="createEntity()">
@ -94,9 +38,9 @@
</div>
</form>
</div> <!-- /.modal-body -->
<div class="modal-footer" ng-show="view == 'addperms'">
<button type="button" class="btn btn-primary" ng-click="addPermissions()"
ng-show="checkedRepos.checked.length">Add permissions</button>
<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 -->
<div class="modal-footer" ng-show="view == 'enterName'">

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

@ -0,0 +1,69 @@
<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
</span>
<input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Repositories...">
</span>
<label>
Select repositories in
<span class="avatar" size="16" data="namespaceInfo.avatar"></span>
{{ namespace }}:
</label>
<table class="co-table">
<thead>
<td class="checkbox-col checkbox-menu-col">
<span class="cor-checkable-menu" controller="checkedRepos">
<div class="cor-checkable-menu-item" item-filter="allRepositoriesFilter(item)">
<i class="fa fa-check-square-o"></i>All Repositories
</div>
<div class="cor-checkable-menu-item" item-filter="noRepositoriesFilter(item)">
<i class="fa fa-square-o"></i>No Repositories
</div>
<div class="cor-checkable-menu-item" item-filter="missingPermsRepositoriesFilter(item)">
<i class="fa fa-circle-o"></i>Missing Permissions
</div>
</span>
</td>
<td ng-class="TableService.tablePredicateClass('name', options.predicate, options.reverse)">
<a ng-click="TableService.orderBy('name', options)">Repository Name</a>
</td>
<td>Permission</td>
<td ng-class="TableService.tablePredicateClass('last_modified_datetime', options.predicate, options.reverse)">
<a ng-click="TableService.orderBy('last_modified_datetime', options)">Last Updated</a>
</td>
</thead>
<tr class="co-checkable-row"
ng-repeat="repo in orderedRepositories.visibleEntries"
ng-class="checkedRepos.isChecked(repo, checkedRepos.checked) ? 'checked' : ''"
bindonce>
<td class="offset-check-col">
<span class="cor-checkable-item" controller="checkedRepos" item="repo"></span>
</td>
<td>
<i class="fa fa-hdd-o"></i>
<span bo-text="repo.name"></span>
</td>
<td>
<span class="role-group small" current-role="repo.permission"
roles="repoRolesOrNone"
role-changed="setRole(role, repo)"></span>
</td>
<td>
<span ng-if="repo.last_modified">
{{ repo.last_modified * 1000 | amCalendar }}
</span>
<span class="empty" ng-if="!repo.last_modified">(Empty Repository)</span>
</td>
</tr>
</table>
<div class="empty" ng-if="!orderedRepositories.entries.length"
style="margin-top: 20px;">
<div class="empty-primary-msg">No matching repositories found.</div>
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
</div>
</div>

View file

@ -1,6 +1,14 @@
<div class="teams-manager-element">
<div class="feedback-bar" feedback="feedback"></div>
<div class="manager-header" header-title="Teams and Membership">
<div class="tab-header-controls visible-xs">
<button class="btn btn-primary"
ng-show="organization.is_admin"
ng-click="askCreateTeam()">
<i class="fa fa-plus" style="margin-right: 4px;"></i> Create New Team
</button>
</div>
<div class="tab-header-controls hidden-xs">
<div class="btn-group btn-group-sm" ng-show="organization.is_admin">
<button class="btn"
@ -17,76 +25,100 @@
<!-- Teams List -->
<div ng-show="!showingMembers">
<div class="row" style="margin-left: 0px; margin-right: 0px;">
<button class="btn btn-primary hidden-xs"
ng-show="organization.is_admin"
style="margin-bottom: 10px; float: right;"
ng-click="askCreateTeam()">
<i class="fa fa-plus" style="margin-right: 4px;"></i> Create New Team
</button>
</div>
<button class="btn btn-primary hidden-xs"
ng-show="organization.is_admin"
style="margin-bottom: 10px; "
ng-click="askCreateTeam()">
<i class="fa fa-plus" style="margin-right: 4px;"></i> Create New Team
</button>
<div class="row hidden-xs">
<div class="col-sm-7 col-md-8 header-col">
<span class="header-text">Team Summary</span>
</div>
<div class="col-md-4 col-sm-5 header-col" ng-show="organization.is_admin">
<span class="header-text">Team Permissions</span>
<i class="info-icon fa fa-info-circle" data-placement="bottom" data-original-title="" data-title=""
data-content="Global permissions for the team and its members<br><br><dl><dt>Member</dt><dd>Permissions are assigned on a per repository basis</dd><dt>Creator</dt><dd>A team can create its own repositories</dd><dt>Admin</dt><dd>A team has full control of the organization</dd></dl>"
data-html="true"
data-trigger="hover"
bs-popover></i>
</div>
</div>
<span class="co-filter-box">
<span class="filter-message" ng-if="options.filter">
Showing {{ orderedTeams.entries.length }} of {{ teams.length }} teams
</span>
<input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Teams...">
</span>
<div class="team-listing" ng-repeat="team in orderedTeams">
<div id="team-{{team.name}}" class="row">
<div class="col-sm-7 col-md-8">
<div class="team-title">
<span class="avatar" data="team.avatar" size="30"></span>
<span ng-show="team.can_view">
<a href="/organization/{{ organization.name }}/teams/{{ team.name }}">{{ team.name }}</a>
</span>
<span ng-show="!team.can_view">
{{ team.name }}
</span>
</div>
<table class="co-table" style="margin-top: 10px;">
<thead>
<td ng-class="TableService.tablePredicateClass('name', options.predicate, options.reverse)">
<a ng-click="TableService.orderBy('name', options)">Team Name</a>
</td>
<td ng-class="TableService.tablePredicateClass('member_count', options.predicate, options.reverse)">
<a ng-click="TableService.orderBy('member_count', options)">Members</a>
</td>
<td class="hidden-xs" ng-class="TableService.tablePredicateClass('repo_count', options.predicate, options.reverse)">
<a ng-click="TableService.orderBy('repo_count', options)">Repositories</a>
</td>
<td ng-class="TableService.tablePredicateClass('role_index', options.predicate, options.reverse)">
<a ng-click="TableService.orderBy('role_index', options)">Team Role</a>
<div class="team-description markdown-view" content="team.description" first-line-only="true"></div>
<i class="info-icon fa fa-info-circle" data-placement="bottom" data-original-title="" data-title=""
data-content="Global permissions for the team and its members<br><br><dl><dt>Member</dt><dd>Permissions are assigned on a per repository basis</dd><dt>Creator</dt><dd>A team can create its own repositories</dd><dt>Admin</dt><dd>A team has full control of the organization</dd></dl>"
data-html="true"
data-trigger="hover"
bs-popover></i>
</td>
<td class="options-col"></td>
</thead>
<div class="team-member-list hidden-xs" ng-if="members[team.name]">
<div class="cor-loader" ng-if="!members[team.name].members"></div>
<span class="team-member"
ng-repeat="member in members[team.name].members | orderBy:'is_robot' | limitTo: 20">
<span data-title="{{ member.name }}" bs-tooltip>
<a href="/user/{{ member.name }}" ng-if="!member.is_robot">
<span class="avatar" data="member.avatar" size="26"></span>
</a>
<i class="fa ci-robot fa-lg" ng-if="member.is_robot"></i>
</span>
</span>
<span class="team-member-more"
ng-if="members[team.name].members.length > 20">+ {{ members[team.name].members.length - 20 }} more team members.</span>
<span class="team-member-more"
ng-if="members[team.name].members && !members[team.name].members.length">(Empty Team)</span>
</div>
</div>
<tr class="co-checkable-row"
ng-repeat="team in orderedTeams.visibleEntries"
bindonce>
<td style="white-space: nowrap;">
<span class="avatar" data="team.avatar" size="24"></span>
<span bo-show="team.can_view">
<a href="/organization/{{ organization.name }}/teams/{{ team.name }}"><span bo-text="team.name"></span></a>
</span>
<span bo-show="!team.can_view" bo-text="team.name"></span>
</td>
<td>
<span bo-show="team.can_view">
<a href="/organization/{{ organization.name }}/teams/{{ team.name }}"><span bo-text="team.member_count"></span> <span class="hidden-xs">member<span bo-if="team.member_count != 1">s</span></span></a>
</span>
<span bo-show="!team.can_view">
<span bo-text="team.member_count"></span> <span class="hidden-xs">member<span bo-if="team.member_count != 1">s</span></span>
</span>
</td>
<td class="hidden-xs">
<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>
<div class="col-sm-5 col-md-4 control-col" ng-show="organization.is_admin">
<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>
<span class="cor-options-menu">
</td>
<td>
<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="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 }}
</span>
</span>
</div>
</div>
</td>
</tr>
</table>
<div class="empty" ng-if="!orderedTeams.entries.length"
style="margin-top: 20px;">
<div class="empty-primary-msg">No matching teams found.</div>
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
</div>
</div>
@ -149,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

@ -20,25 +20,9 @@ angular.module('quay').directive('createEntityDialog', function () {
'entityCreateCompleted': '&entityCreateCompleted'
},
controller: function($scope, $element, ApiService, UIService, TableService, RolesService, UserService) {
$scope.TableService = TableService;
$scope.options = {
'predicate': 'last_modified_datetime',
'reverse': false,
'filter': ''
};
var handleRepoCheckChange = function() {
$scope.repositories.forEach(function(repo) {
if ($scope.checkedRepos.isChecked(repo)) {
if (repo['permission'] == 'none') {
repo['permission'] = 'read';
}
} else {
repo['permission'] = 'none';
}
});
controller: function($scope, $element, ApiService, UIService, UserService) {
$scope.context = {
'setPermissionsCounter': 0
};
$scope.$on('$destroy', function() {
@ -47,16 +31,6 @@ angular.module('quay').directive('createEntityDialog', function () {
}
});
$scope.setRole = function(role, repo) {
repo['permission'] = role;
if (role == 'none') {
$scope.checkedRepos.uncheckItem(repo);
} else {
$scope.checkedRepos.checkItem(repo);
}
};
$scope.hide = function() {
$element.find('.modal').modal('hide');
if ($scope.entity) {
@ -68,6 +42,7 @@ angular.module('quay').directive('createEntityDialog', function () {
$scope.show = function() {
$scope.entityName = null;
$scope.entity = null;
$scope.entityForPermissions = null;
$scope.creating = false;
$scope.view = 'enterName';
$scope.enterNameForm.$setPristine(true);
@ -79,92 +54,13 @@ angular.module('quay').directive('createEntityDialog', function () {
document.body.appendChild($element[0]);
};
var setRepoState = function() {
if (!$scope.repositories) {
return;
}
$scope.orderedRepositories = TableService.buildOrderedItems($scope.repositories, $scope.options,
['name', 'permission'],
['last_modified_datetime']);
};
var entityCreateCallback = function(entity) {
$scope.entity = entity;
if (!entity || $scope.info.skip_permissions) {
$scope.entity = entity;
$scope.hide();
return;
}
// Load the repositories under the entity's namespace.
var params = {
'namespace': $scope.info.namespace,
'last_modified': true
};
ApiService.listRepos(null, params).then(function(resp) {
$scope.view = 'addperms';
$scope.entity = entity;
var repos = [];
resp['repositories'].forEach(function(repo) {
repos.push({
'namespace': repo.namespace,
'name': repo.name,
'last_modified': repo.last_modified,
'last_modified_datetime': TableService.getReversedTimestamp(repo.last_modified),
'permission': 'none'
});
});
if (repos.length == 0) {
$scope.hide();
return;
}
$scope.repositories = repos;
$scope.checkedRepos = UIService.createCheckStateController($scope.repositories, 'name');
$scope.checkedRepos.listen(handleRepoCheckChange);
if ($scope.info.repository) {
repos.forEach(function(repo) {
if (repo['namespace'] == $scope.info.repository.namespace &&
repo['name'] == $scope.info.repository.name) {
$scope.checkedRepos.checkItem(repo);
$scope.options.filter = $scope.info.repository.name;
}
});
}
setRepoState();
}, ApiService.errorDisplay('Could not load repositories'));
};
$scope.addPermissions = function() {
$scope.view = 'addingperms';
var repos = $scope.checkedRepos.checked;
var counter = 0;
var addPerm = function() {
if (counter >= repos.length) {
$scope.hide();
return;
}
var repo = repos[counter];
RolesService.setRepositoryRole(repo, repo.permission, $scope.entityKind, $scope.entity.name,
function(status) {
if (status) {
counter++;
addPerm();
} else {
$scope.hide();
}
});
};
addPerm();
};
$scope.createEntity = function() {
@ -175,21 +71,27 @@ angular.module('quay').directive('createEntityDialog', function () {
});
};
$scope.allRepositoriesFilter = function(item) {
return true;
$scope.permissionsSet = function(repositories) {
$scope.entity['repo_count'] = repositories.length;
$scope.hide();
};
$scope.noRepositoriesFilter = function(item) {
return false;
$scope.settingPermissions = function() {
$scope.view = 'settingperms';
};
$scope.missingPermsRepositoriesFilter = function(item) {
return !item.perm;
$scope.setPermissions = function() {
$scope.context.setPermissionsCounter++;
};
$scope.$watch('options.predicate', setRepoState);
$scope.$watch('options.reverse', setRepoState);
$scope.$watch('options.filter', setRepoState);
$scope.repositoriesLoaded = function(repositories) {
if (repositories && !repositories.length) {
$scope.hide();
return;
}
$scope.view = 'setperms';
};
$scope.$watch('entityNameRegex', function(r) {
if (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

@ -0,0 +1,235 @@
/**
* An element which displays a table for setting permissions for an entity to repositories under
* a namespace.
*/
angular.module('quay').directive('setRepoPermissions', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/set-repo-permissions.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'namespace': '=namespace',
'entityName': '=entityName',
'entityKind': '=entityKind',
'setPermissions': '=setPermissions',
'hasCheckedRepositories': '=hasCheckedRepositories',
'hasChangedRepositories': '=hasChangedRepositories',
'repositoriesLoaded': '&repositoriesLoaded',
'settingPermissions': '&settingPermissions',
'permissionsSet': '&permissionsSet',
},
controller: function($scope, $element, ApiService, UIService, TableService, RolesService, UserService) {
$scope.TableService = TableService;
$scope.options = {
'predicate': 'last_modified_datetime',
'reverse': false,
'filter': ''
};
$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) {
if ($scope.checkedRepos.isChecked(repo)) {
if (repo['permission'] == 'none') {
repo['permission'] = 'read';
}
} else {
repo['permission'] = 'none';
}
});
checkForChanges();
};
var setRepoState = function() {
if (!$scope.repositories) {
return;
}
$scope.orderedRepositories = TableService.buildOrderedItems(
$scope.repositories, $scope.options,
['name', 'permission'],
['last_modified_datetime']);
};
var loadRepositoriesAndPermissions = function() {
if (!$scope.namespace || !$scope.entityName || !$scope.entityKind) {
return;
}
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, along with the current repo
// permissions for the entity.
var params = {
'namespace': $scope.namespace,
'last_modified': true
};
ApiService.listRepos(null, params).then(function(resp) {
$scope.currentNamespace = $scope.namespace;
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': existingPermission,
'original_permission': existingPermission
});
});
if (repos.length == 0) {
$scope.repositoriesLoaded({'repositories': repos});
return;
}
$scope.repositories = repos;
$scope.checkedRepos = UIService.createCheckStateController($scope.repositories, '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 setPermissions = function() {
if (!$scope.checkedRepos || !$scope.namespace || !$scope.repositories) {
return;
}
$scope.settingPermissions();
var repos = $scope.repositories;
var counter = 0;
var setPerm = function() {
if (counter >= repos.length) {
$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++;
setPerm();
}
});
};
setPerm();
};
$scope.setRole = function(role, repo) {
repo['permission'] = role;
if (role == 'none') {
$scope.checkedRepos.uncheckItem(repo);
} else {
$scope.checkedRepos.checkItem(repo);
}
checkForChanges();
};
$scope.allRepositoriesFilter = function(item) {
return true;
};
$scope.noRepositoriesFilter = function(item) {
return false;
};
$scope.missingPermsRepositoriesFilter = function(item) {
return !item.perm;
};
$scope.$watch('options.predicate', setRepoState);
$scope.$watch('options.reverse', setRepoState);
$scope.$watch('options.filter', setRepoState);
$scope.$watch('namespace', loadRepositoriesAndPermissions);
$scope.$watch('entityName', loadRepositoriesAndPermissions);
$scope.$watch('entityKind', loadRepositoriesAndPermissions);
$scope.$watch('setPermissions', function(value) {
if (value) {
setPermissions();
}
});
}
};
return directiveDefinitionObject;
});

View file

@ -12,7 +12,15 @@ angular.module('quay').directive('teamsManager', function () {
'organization': '=organization',
'isEnabled': '=isEnabled'
},
controller: function($scope, $element, ApiService, $timeout, UserService) {
controller: function($scope, $element, ApiService, $timeout, UserService, TableService, UIService) {
$scope.TableService = TableService;
$scope.options = {
'predicate': 'ordered_team_index',
'reverse': false,
'filter': ''
};
$scope.teamRoles = [
{ 'id': 'member', 'title': 'Member', 'kind': 'default' },
{ 'id': 'creator', 'title': 'Creator', 'kind': 'success' },
@ -21,65 +29,42 @@ angular.module('quay').directive('teamsManager', function () {
UserService.updateUserIn($scope);
$scope.members = {};
$scope.orderedTeams = [];
$scope.teams = null;
$scope.orderedTeams = null;
$scope.showingMembers = false;
$scope.fullMemberList = null;
$scope.feedback = null;
$scope.createTeamInfo = null;
var loadTeamMembers = function() {
if (!$scope.organization || !$scope.isEnabled) { return; }
// Skip loading team members on mobile.
if (!window.matchMedia('(min-width: 768px)').matches) {
return;
var getRoleIndex = function(name) {
for (var i = 0; i < $scope.teamRoles.length; ++i) {
if ($scope.teamRoles[i]['id'] == name) {
return i;
}
}
for (var name in $scope.organization.teams) {
if (!$scope.organization.teams.hasOwnProperty(name) || $scope.members[name]) { continue; }
// Load fully async to prevent it from blocking the UI.
(function(teamname) {
$timeout(function() {
loadMembersOfTeam(teamname);
}, 1);
})(name);
}
return -1;
};
var loadMembersOfTeam = function(name) {
var params = {
'orgname': $scope.organization.name,
'teamname': name
};
$scope.members[name] = {};
ApiService.getOrganizationTeamMembers(null, params).then(function(resp) {
$scope.members[name].members = resp.members;
}, function() {
delete $scope.members[name];
});
};
var loadOrderedTeams = function() {
var setTeamsState = function() {
if (!$scope.organization || !$scope.organization.ordered_teams || !$scope.isEnabled) {
return;
}
$scope.orderedTeams = [];
$scope.organization.ordered_teams.map(function(name) {
$scope.orderedTeams.push($scope.organization.teams[name]);
$scope.teams = [];
$scope.organization.ordered_teams.map(function(name, index) {
var team = $scope.organization.teams[name];
team['ordered_team_index'] = $scope.organization.ordered_teams.length - index;
team['role_index'] = getRoleIndex(team['role']);
$scope.teams.push(team);
});
$scope.orderedTeams = TableService.buildOrderedItems(
$scope.teams, $scope.options,
['name'],
['ordered_team_index', 'member_count', 'repo_count', 'role_index']);
};
$scope.$watch('organization', loadOrderedTeams);
$scope.$watch('organization', loadTeamMembers);
$scope.$watch('isEnabled', loadOrderedTeams);
$scope.$watch('isEnabled', loadTeamMembers);
$scope.setRole = function(role, teamname) {
var previousRole = $scope.organization.teams[teamname].role;
$scope.organization.teams[teamname].role = role;
@ -115,9 +100,9 @@ angular.module('quay').directive('teamsManager', function () {
$scope.handleTeamCreated = function(created) {
var teamname = created.name;
created['member_count'] = 0;
$scope.organization.teams[teamname] = created;
$scope.members[teamname] = {};
$scope.members[teamname].members = [];
$scope.organization.ordered_teams.push(teamname);
$scope.orderedTeams.push(created);
@ -150,8 +135,8 @@ angular.module('quay').directive('teamsManager', function () {
$scope.organization.ordered_teams.splice(index, 1);
}
loadOrderedTeams();
delete $scope.organization.teams[teamname];
setTeamsState();
$scope.feedback = {
'kind': 'success',
@ -192,12 +177,7 @@ angular.module('quay').directive('teamsManager', function () {
ApiService.removeOrganizationMember(null, params).then(function(resp) {
// Reset the state of the directive.
$scope.members = {};
$scope.orderedTeams = [];
$scope.fullMemberList = null;
loadOrderedTeams();
loadTeamMembers();
$scope.showMembers(true);
callback(true);
@ -215,6 +195,27 @@ angular.module('quay').directive('teamsManager', function () {
$scope.askRemoveMember = function(memberInfo) {
$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);
$scope.$watch('options.predicate', setTeamsState);
$scope.$watch('options.reverse', setTeamsState);
$scope.$watch('options.filter', 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
@ -679,6 +680,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

@ -28,7 +28,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
@ -1158,6 +1159,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)