diff --git a/data/database.py b/data/database.py index 5b1a672b5..2afd3f79c 100644 --- a/data/database.py +++ b/data/database.py @@ -135,6 +135,7 @@ class RepositoryPermission(BaseModel): class PermissionPrototype(BaseModel): org = ForeignKeyField(User, index=True, related_name='orgpermissionproto') + uuid = CharField() activating_user = ForeignKeyField(User, index=True, null=True, related_name='userpermissionproto') delegate_user = ForeignKeyField(User, related_name='receivingpermission', diff --git a/data/model.py b/data/model.py index e211b4ea2..138df3fcd 100644 --- a/data/model.py +++ b/data/model.py @@ -4,6 +4,7 @@ import datetime import dateutil.parser import operator import json +import uuid from datetime import timedelta @@ -671,6 +672,52 @@ def get_all_user_permissions(user): (UserThroughTeam.id == user)) +def delete_prototype_permission(org, uid): + found = get_prototype_permission(org, uid) + if not found: + return None + + found.delete_instance() + return found + + +def get_prototype_permission(org, uid): + found = None + try: + return PermissionPrototype.get(PermissionPrototype.org == org, PermissionPrototype.uuid == uid) + except PermissionPrototype.DoesNotExist: + return None + + +def get_prototype_permissions(org): + ActivatingUser = User.alias() + DelegateUser = User.alias() + where = PermissionPrototype.select().where(PermissionPrototype.org == org) + join1 = where.join(ActivatingUser, JOIN_LEFT_OUTER, on=(ActivatingUser.id == PermissionPrototype.activating_user)) + join2 = join1.join(DelegateUser, JOIN_LEFT_OUTER, on=(DelegateUser.id == PermissionPrototype.delegate_user)) + join3 = join2.join(Team, JOIN_LEFT_OUTER, on=(Team.id == PermissionPrototype.delegate_team)) + join4 = join3.join(Role, JOIN_LEFT_OUTER, on=(Role.id == PermissionPrototype.role)) + return join4 + + +def update_prototype_permission(org, uid, role_name): + found = get_prototype_permission(org, uid) + if not found: + return None + + new_role = Role.get(Role.name == role_name) + found.role = new_role + found.save() + return found + + +def add_prototype_permission(org, role_name, activating_user, delegate_user=None, delegate_team=None): + new_role = Role.get(Role.name == role_name) + uid = uuid.uuid4() + return PermissionPrototype.create(org=org, uuid=uid, role=new_role, activating_user=activating_user, + delegate_user=delegate_user, delegate_team=delegate_team) + + def get_org_wide_permissions(user): Org = User.alias() team_with_role = Team.select(Team, Org, TeamRole).join(TeamRole) diff --git a/endpoints/api.py b/endpoints/api.py index a17dd5cd6..9bd0352bb 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -541,6 +541,113 @@ def change_organization_details(orgname): abort(403) +def prototype_view(p): + def user_view(u): + return { + 'name': u.username, + 'is_robot': u.robot, + 'kind': 'user' + } + + def team_view(t): + return { + 'name': t.name, + 'kind': 'team' + } + + return { + 'activating_user': user_view(p.activating_user), + 'delegate': user_view(p.delegate_user) if p.delegate_user else team_view(p.delegate_team), + 'role': p.role.name, + 'id': p.uuid + } + +@app.route('/api/organization//prototypes', methods=['GET']) +@api_login_required +def get_organization_prototype_permissions(orgname): + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + try: + org = model.get_organization(orgname) + except model.InvalidOrganizationException: + abort(404) + + permissions = model.get_prototype_permissions(org) + return jsonify({'prototypes': [prototype_view(p) for p in permissions]}) + + abort(403) + + +@app.route('/api/organization//prototypes', methods=['POST']) +@api_login_required +def create_organization_prototype_permission(orgname): + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + try: + org = model.get_organization(orgname) + except model.InvalidOrganizationException: + abort(404) + + details = request.get_json() + activating_user = details['activating_user']['name'] + + delegate = details['delegate'] + delegate_kind = delegate['kind'] + delegate_name = delegate['name'] + + delegate_user = delegate_name if delegate_kind == 'user' else None + delegate_team = delegate_name if delegate_kind == 'team' else None + + role_name = details['role'] + + if not delegate_user and not delegate_team: + abort(400) + + prototype = model.add_prototype_permission(org, role_name, activating_user, delegate_user, delegate_team) + return jsonify(protoype_view(prototype)) + + abort(403) + + +@app.route('/api/organization//prototypes/', methods=['DELETE']) +@api_login_required +def delete_organization_prototype_permission(orgname, prototypeid): + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + try: + org = model.get_organization(orgname) + except model.InvalidOrganizationException: + abort(404) + + prototype = model.delete_prototype_permission(org, prototypeid) + if not prototype: + abort(404) + + return make_response('Deleted', 204) + + abort(403) + + +@app.route('/api/organization//prototypes/', methods=['PUT']) +@api_login_required +def update_organization_prototype_permission(orgname, prototypeid): + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + try: + org = model.get_organization(orgname) + except model.InvalidOrganizationException: + abort(404) + + details = request.get_json() + role_name = details['role'] + prototype = model.update_prototype_permission(org, prototypeid, role_name) + if not prototype: + abort(404) + + return jsonify(prototype_view(prototype)) + + abort(403) + @app.route('/api/organization//members', methods=['GET']) @api_login_required @@ -1140,7 +1247,6 @@ def get_filedrop_url(): 'file_id': file_id }) - def role_view(repo_perm_obj): return { 'role': repo_perm_obj.role.name, diff --git a/initdb.py b/initdb.py index a92a3548d..15eed9183 100644 --- a/initdb.py +++ b/initdb.py @@ -153,6 +153,10 @@ def initialize_database(): LogEntryKind.create(name='org_remove_team_member') LogEntryKind.create(name='org_set_team_description') LogEntryKind.create(name='org_set_team_role') + + LogEntryKind.create(name='org_create_prototype_permission') + LogEntryKind.create(name='org_modify_prototype_permission') + LogEntryKind.create(name='org_delete_prototype_permission') def wipe_database(): @@ -261,6 +265,10 @@ def populate_database(): build.status_url = 'http://localhost:5000/test/build/status' build.save() + model.add_prototype_permission(org, 'read', activating_user=new_user_1, delegate_user=new_user_2) + model.add_prototype_permission(org, 'read', activating_user=new_user_1, delegate_team=reader_team) + model.add_prototype_permission(org, 'write', activating_user=new_user_2, delegate_user=new_user_1) + today = datetime.today() week_ago = today - timedelta(6) six_ago = today - timedelta(5) diff --git a/static/css/quay.css b/static/css/quay.css index 697810594..7ca8a5f27 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -2256,6 +2256,16 @@ p.editable:hover i { color: steelblue; } +.prototype-manager-element thead th { + padding: 4px; + color: #666; +} + +.prototype-manager-element td { + padding: 10px !important; + vertical-align: middle !important; +} + .org-list h2 { margin-bottom: 20px; } diff --git a/static/directives/prototype-manager.html b/static/directives/prototype-manager.html new file mode 100644 index 000000000..c82d5c9c4 --- /dev/null +++ b/static/directives/prototype-manager.html @@ -0,0 +1,59 @@ +
+
+ +
+
+ Default permissions provide a means of specifying additional permissions that should be granted automatically to a repository based on the user or robot creating the repository. +
+ +
+ +
+ + + + + + + + + + + + + + + +
+ + Creating User/Robot + + + + Delegated User/Team + + Permission
+ + + + + + + + + + +
+
+ + +
diff --git a/static/js/app.js b/static/js/app.js index 616f67888..0f0db3a27 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1479,6 +1479,97 @@ quayApp.directive('robotsManager', function () { }); +quayApp.directive('prototypeManager', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/prototype-manager.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'organization': '=organization' + }, + controller: function($scope, $element, ApiService) { + $scope.loading = false; + + $scope.roles = [ + { 'id': 'read', 'title': 'Read', 'kind': 'success' }, + { 'id': 'write', 'title': 'Write', 'kind': 'success' }, + { 'id': 'admin', 'title': 'Admin', 'kind': 'primary' } + ]; + + $scope.setRole = function(role, prototype) { + var params = { + 'orgname': $scope.organization.name, + 'prototypeid': prototype.id + }; + + var data = { + 'id': prototype.id, + 'role': role + }; + + ApiService.updateOrganizationPrototypePermission(data, params).then(function(resp) { + prototype.role = role; + }, function(resp) { + bootbox.dialog({ + "message": resp.data ? resp.data : 'The permission could not be modified', + "title": "Cannot modify permission", + "buttons": { + "close": { + "label": "Close", + "className": "btn-primary" + } + } + }); + }); + }; + + $scope.deletePrototype = function(prototype) { + $scope.loading = true; + + var params = { + 'orgname': $scope.organization.name, + 'prototypeid': prototype.id + }; + + ApiService.deleteOrganizationPrototypePermission(null, params).then(function(resp) { + $scope.prototypes.splice($scope.prototypes.indexOf(prototype), 1); + $scope.loading = false; + }, function(resp) { + bootbox.dialog({ + "message": resp.data ? resp.data : 'The permission could not be deleted', + "title": "Cannot delete permission", + "buttons": { + "close": { + "label": "Close", + "className": "btn-primary" + } + } + }); + }); + }; + + var update = function() { + if (!$scope.organization) { return; } + if ($scope.loading) { return; } + + var params = {'orgname': $scope.organization.name}; + + $scope.loading = true; + ApiService.getOrganizationPrototypePermissions(null, params).then(function(resp) { + $scope.prototypes = resp.prototypes; + $scope.loading = false; + }); + }; + + $scope.$watch('organization', update); + } + }; + return directiveDefinitionObject; +}); + + quayApp.directive('popupInputButton', function () { var directiveDefinitionObject = { priority: 0, diff --git a/static/partials/org-admin.html b/static/partials/org-admin.html index 927caae26..7938ceb7b 100644 --- a/static/partials/org-admin.html +++ b/static/partials/org-admin.html @@ -11,6 +11,7 @@
  • Usage Logs
  • Members
  • Robot Accounts
  • +
  • Default Permissions
  • Billing
  • Billing History
  • @@ -49,6 +50,11 @@
    + +
    +
    +
    +
    diff --git a/test/data/test.db b/test/data/test.db index 36011e3bd..b062c877a 100644 Binary files a/test/data/test.db and b/test/data/test.db differ