From e17c3590a7d7065744f6fdee4285cbcd52c1f179 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 21 Jan 2014 14:18:20 -0500 Subject: [PATCH] - Add model functions for working with prototypes - Add API calls for working with prototypes - Get UI for prototypes working (minus add) --- data/database.py | 1 + data/model.py | 47 ++++++++++ endpoints/api.py | 108 ++++++++++++++++++++++- initdb.py | 8 ++ static/css/quay.css | 10 +++ static/directives/prototype-manager.html | 59 +++++++++++++ static/js/app.js | 91 +++++++++++++++++++ static/partials/org-admin.html | 6 ++ test/data/test.db | Bin 123904 -> 376832 bytes 9 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 static/directives/prototype-manager.html 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 4ca6469e4..f351e5ff8 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 36011e3bdae87e11bf6eed47feaa5f165d407a95..b062c877add6f1d98ebd207985dac5e680fff685 100644 GIT binary patch delta 11587 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