From f7c27f250bd0d0fd8817aa14c805afef4b7059f9 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 20 Mar 2014 15:46:13 -0400 Subject: [PATCH] Add full application management API, UI and test cases --- data/database.py | 2 + data/model/oauth.py | 31 ++- endpoints/api/organization.py | 241 ++++++++++++++++++++- initdb.py | 5 + static/css/quay.css | 17 +- static/directives/application-info.html | 2 +- static/directives/application-manager.html | 24 ++ static/js/app.js | 85 +++++++- static/js/controllers.js | 125 +++++++++++ static/partials/manage-application.html | 165 ++++++++++++++ static/partials/new-organization.html | 6 +- static/partials/org-admin.html | 6 + templates/oauthorize.html | 2 +- test/data/test.db | Bin 193536 -> 532480 bytes test/test_api_security.py | 106 ++++++++- test/test_api_usage.py | 102 ++++++++- 16 files changed, 904 insertions(+), 15 deletions(-) create mode 100644 static/directives/application-manager.html create mode 100644 static/partials/manage-application.html diff --git a/data/database.py b/data/database.py index 5962f85ca..b3d2eafff 100644 --- a/data/database.py +++ b/data/database.py @@ -277,8 +277,10 @@ class OAuthApplication(BaseModel): redirect_uri = CharField() application_uri = CharField() organization = ForeignKeyField(User) + name = CharField() description = TextField(default='') + gravatar_email = CharField(null=True) class OAuthAuthorizationCode(BaseModel): diff --git a/data/model/oauth.py b/data/model/oauth.py index 7dd4445c8..b10bfa372 100644 --- a/data/model/oauth.py +++ b/data/model/oauth.py @@ -4,7 +4,8 @@ from datetime import datetime, timedelta from oauth2lib.provider import AuthorizationProvider from oauth2lib import utils -from data.database import OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken, User +from data.database import (OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken, User, + random_string_generator) from auth import scopes @@ -184,3 +185,31 @@ def get_application_for_client_id(client_id): return OAuthApplication.get(client_id=client_id) except OAuthApplication.DoesNotExist: return None + +def reset_client_secret(application): + application.client_secret = random_string_generator(length=40)() + application.save() + return application + +def lookup_application(org, client_id): + try: + return OAuthApplication.get(organization = org, client_id=client_id) + except OAuthApplication.DoesNotExist: + return None + + +def delete_application(org, client_id): + application = lookup_application(org, client_id) + if not application: + return + + application.delete_instance(recursive=True, delete_nullable=True) + return application + +def list_applications_for_org(org): + query = (OAuthApplication + .select() + .join(User) + .where(OAuthApplication.organization == org)) + + return query diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py index b10011aed..dda11bf36 100644 --- a/endpoints/api/organization.py +++ b/endpoints/api/organization.py @@ -5,7 +5,7 @@ from flask import request from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, related_user_resource, internal_only, Unauthorized, NotFound, - require_user_admin) + require_user_admin, log_action) from endpoints.api.team import team_view from endpoints.api.user import User, PrivateRepositories from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission, @@ -271,9 +271,248 @@ class ApplicationInformation(ApiResource): if not application: raise NotFound() + org_hash = compute_hash(application.organization.email) + gravatar = compute_hash(application.gravatar_email) if application.gravatar_email else org_hash + return { 'name': application.name, 'description': application.description, 'uri': application.application_uri, + 'gravatar': gravatar, 'organization': org_view(application.organization, []) } + + +def app_view(application): + is_admin = AdministerOrganizationPermission(application.organization.username).can() + + return { + 'name': application.name, + 'description': application.description, + 'application_uri': application.application_uri, + + 'client_id': application.client_id, + 'client_secret': application.client_secret if is_admin else None, + 'redirect_uri': application.redirect_uri if is_admin else None, + 'gravatar_email': application.gravatar_email if is_admin else None, + } + + +@resource('/v1/organization//applications') +class OrganizationApplications(ApiResource): + """ Resource for managing applications defined by an organizations. """ + schemas = { + 'NewApp': { + 'id': 'NewApp', + 'type': 'object', + 'description': 'Description of a new organization application.', + 'required': [ + 'name', + ], + 'properties': { + 'name': { + 'type': 'string', + 'description': 'The name of the application', + }, + 'redirect_uri': { + 'type': 'string', + 'description': 'The URI for the application\'s OAuth redirect', + }, + 'application_uri': { + 'type': 'string', + 'description': 'The URI for the application\'s homepage', + }, + 'description': { + 'type': ['string', 'null'], + 'description': 'The human-readable description for the application', + }, + 'gravatar_email': { + 'type': ['string', 'null'], + 'description': 'The e-mail address of the gravatar to use for the application', + } + }, + }, + } + + + @nickname('getOrganizationApplications') + def get(self, orgname): + """ List the applications for the specified organization """ + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + try: + org = model.get_organization(orgname) + except model.InvalidOrganizationException: + raise NotFound() + + applications = model.oauth.list_applications_for_org(org) + return {'applications': [app_view(application) for application in applications]} + + raise Unauthorized() + + @nickname('createOrganizationApplication') + @validate_json_request('NewApp') + def post(self, orgname): + """ Creates a new application under this organization. """ + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + try: + org = model.get_organization(orgname) + except model.InvalidOrganizationException: + raise NotFound() + + app_data = request.get_json() + application = model.oauth.create_application( + org, app_data['name'], + app_data.get('application_uri', ''), + app_data.get('redirect_uri', ''), + description = app_data.get('description', ''), + gravatar_email = app_data.get('gravatar_email', None),) + + + app_data.update({ + 'application_name': application.name, + 'client_id': application.client_id + }) + + log_action('create_application', orgname, app_data) + + return app_view(application) + raise Unauthorized() + + +@resource('/v1/organization//applications/') +class OrganizationApplicationResource(ApiResource): + """ Resource for managing an application defined by an organizations. """ + schemas = { + 'UpdateApp': { + 'id': 'UpdateApp', + 'type': 'object', + 'description': 'Description of an updated application.', + 'required': [ + 'name', + 'redirect_uri', + 'application_uri' + ], + 'properties': { + 'name': { + 'type': 'string', + 'description': 'The name of the application', + }, + 'redirect_uri': { + 'type': 'string', + 'description': 'The URI for the application\'s OAuth redirect', + }, + 'application_uri': { + 'type': 'string', + 'description': 'The URI for the application\'s homepage', + }, + 'description': { + 'type': ['string', 'null'], + 'description': 'The human-readable description for the application', + }, + 'gravatar_email': { + 'type': ['string', 'null'], + 'description': 'The e-mail address of the gravatar to use for the application', + } + }, + }, + } + + @nickname('getOrganizationApplication') + def get(self, orgname, client_id): + """ Retrieves the application with the specified client_id under the specified organization """ + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + try: + org = model.get_organization(orgname) + except model.InvalidOrganizationException: + raise NotFound() + + application = model.oauth.lookup_application(org, client_id) + if not application: + raise NotFound() + + return app_view(application) + + raise Unauthorized() + + @nickname('updateOrganizationApplication') + @validate_json_request('UpdateApp') + def put(self, orgname, client_id): + """ Updates an application under this organization. """ + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + try: + org = model.get_organization(orgname) + except model.InvalidOrganizationException: + raise NotFound() + + application = model.oauth.lookup_application(org, client_id) + if not application: + raise NotFound() + + app_data = request.get_json() + application.name = app_data['name'] + application.application_uri = app_data['application_uri'] + application.redirect_uri = app_data['redirect_uri'] + application.description = app_data.get('description', '') + application.gravatar_email = app_data.get('gravatar_email', None) + application.save() + + app_data.update({ + 'application_name': application.name, + 'client_id': application.client_id + }) + + log_action('update_application', orgname, app_data) + + return app_view(application) + raise Unauthorized() + + + @nickname('deleteOrganizationApplication') + def delete(self, orgname, client_id): + """ Deletes the application under this organization. """ + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + try: + org = model.get_organization(orgname) + except model.InvalidOrganizationException: + raise NotFound() + + application = model.oauth.delete_application(org, client_id) + if not application: + raise NotFound() + + log_action('delete_application', orgname, + {'application_name': application.name, 'client_id': client_id}) + + return 'Deleted', 204 + raise Unauthorized() + + +@resource('/v1/organization//applications//resetclientsecret') +@internal_only +class OrganizationApplicationResetClientSecret(ApiResource): + """ Custom verb for resetting the client secret of an application. """ + @nickname('resetOrganizationApplicationClientSecret') + def post(self, orgname, client_id): + """ Resets the client secret of the application. """ + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + try: + org = model.get_organization(orgname) + except model.InvalidOrganizationException: + raise NotFound() + + application = model.oauth.lookup_application(org, client_id) + if not application: + raise NotFound() + + application = model.oauth.reset_client_secret(application) + log_action('reset_application_client_secret', orgname, + {'application_name': application.name, 'client_id': client_id}) + + return app_view(application) + raise Unauthorized() diff --git a/initdb.py b/initdb.py index b37fc9225..266d462d9 100644 --- a/initdb.py +++ b/initdb.py @@ -225,6 +225,11 @@ def initialize_database(): LogEntryKind.create(name='setup_repo_trigger') LogEntryKind.create(name='delete_repo_trigger') + LogEntryKind.create(name='create_application') + LogEntryKind.create(name='update_application') + LogEntryKind.create(name='delete_application') + LogEntryKind.create(name='reset_application_client_secret') + NotificationKind.create(name='password_required') NotificationKind.create(name='over_private_usage') diff --git a/static/css/quay.css b/static/css/quay.css index 1613d7a5f..a03721505 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -2801,7 +2801,7 @@ p.editable:hover i { margin-bottom: 10px; } -.create-org .step-container .description { +.form-group .description { margin-top: 10px; display: block; color: #888; @@ -2809,7 +2809,7 @@ p.editable:hover i { margin-left: 10px; } -.create-org .form-group input { +.form-group.nested input { margin-top: 10px; margin-left: 10px; } @@ -3469,7 +3469,7 @@ pre.command:before { display: block !important; } -.auth-header img { +.auth-header > img { float: left; margin-top: 8px; margin-right: 20px; @@ -3560,3 +3560,14 @@ pre.command:before { .auth-container .button-bar button { margin: 6px; } + +.manage-application #oauth td { + padding: 6px; + padding-bottom: 20px; +} + +.manage-application .button-bar { + margin-top: 10px; + padding-top: 20px; + border-top: 1px solid #eee; +} \ No newline at end of file diff --git a/static/directives/application-info.html b/static/directives/application-info.html index 32472ae74..bbaf56454 100644 --- a/static/directives/application-info.html +++ b/static/directives/application-info.html @@ -1,6 +1,6 @@
- +

{{ application.name }}

{{ application.organization.name }} diff --git a/static/directives/application-manager.html b/static/directives/application-manager.html new file mode 100644 index 000000000..df870d7a5 --- /dev/null +++ b/static/directives/application-manager.html @@ -0,0 +1,24 @@ +
+
+ +
+
+ + Create New Application + +
+ + + + + + + + + + + +
Application NameApplication URI
{{ app.name }}{{ app.application_uri }}
+
+ +
diff --git a/static/js/app.js b/static/js/app.js index 86f962cf3..71c46e6db 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -237,7 +237,9 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu 'robot': 'wrench', 'tag': 'tag', 'role': 'th-large', - 'original_role': 'th-large' + 'original_role': 'th-large', + 'application_name': 'cloud', + 'client_id': 'chain' }; var description = value_or_func; @@ -1088,6 +1090,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu when('/organization/:orgname/admin', {templateUrl: '/static/partials/org-admin.html', controller: OrgAdminCtrl, reloadOnSearch: false}). when('/organization/:orgname/teams/:teamname', {templateUrl: '/static/partials/team-view.html', controller: TeamViewCtrl}). when('/organization/:orgname/logs/:membername', {templateUrl: '/static/partials/org-member-logs.html', controller: OrgMemberLogsCtrl}). + when('/organization/:orgname/application/:clientid', {templateUrl: '/static/partials/manage-application.html', + controller: ManageApplicationCtrl, reloadOnSearch: false}). when('/v1/', {title: 'Activation information', templateUrl: '/static/partials/v1-page.html', controller: V1Ctrl}). when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}). otherwise({redirectTo: '/'}); @@ -1751,7 +1755,12 @@ quayApp.directive('logsView', function () { var triggerDescription = TriggerDescriptionBuilder.getDescription( metadata['service'], metadata['config']); return 'Delete build trigger - ' + triggerDescription; - } + }, + 'create_application': 'Create application {application_name} with client ID {client_id}', + 'update_application': 'Update application to {application_name} for client ID {client_id}', + 'delete_application': 'Delete application {application_name} with client ID {client_id}', + 'reset_application_client_secret': 'Reset the Client Secret of application {application_name} ' + + 'with client ID {client_id}' }; var logKinds = { @@ -1785,7 +1794,11 @@ quayApp.directive('logsView', function () { 'modify_prototype_permission': 'Modify default permission', 'delete_prototype_permission': 'Delete default permission', 'setup_repo_trigger': 'Setup build trigger', - 'delete_repo_trigger': 'Delete build trigger' + 'delete_repo_trigger': 'Delete build trigger', + 'create_application': 'Create Application', + 'update_application': 'Update Application', + 'delete_application': 'Delete Application', + 'reset_application_client_secret': 'Reset Client Secret' }; var getDateString = function(date) { @@ -1878,6 +1891,72 @@ quayApp.directive('logsView', function () { return directiveDefinitionObject; }); + +quayApp.directive('applicationManager', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/application-manager.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'organization': '=organization', + 'visible': '=visible' + }, + controller: function($scope, $element, ApiService) { + $scope.loading = false; + + $scope.createApplication = function(appName) { + if (!appName) { return; } + + var params = { + 'orgname': $scope.organization.name + }; + + var data = { + 'name': appName + }; + + ApiService.createOrganizationApplication(data, params).then(function(resp) { + $scope.applications.push(resp); + }, function(resp) { + bootbox.dialog({ + "message": resp['message'] || 'The application could not be created', + "title": "Cannot create application", + "buttons": { + "close": { + "label": "Close", + "className": "btn-primary" + } + } + }); + }); + }; + + var update = function() { + if (!$scope.organization || !$scope.visible) { return; } + if ($scope.loading) { return; } + + $scope.loading = true; + + var params = { + 'orgname': $scope.organization.name + }; + + ApiService.getOrganizationApplications(null, params).then(function(resp) { + $scope.loading = false; + $scope.applications = resp['applications']; + }); + }; + + $scope.$watch('organization', update); + $scope.$watch('visible', update); + } + }; + return directiveDefinitionObject; +}); + + quayApp.directive('robotsManager', function () { var directiveDefinitionObject = { priority: 0, diff --git a/static/js/controllers.js b/static/js/controllers.js index 1575dae90..3d186ce88 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -2139,12 +2139,17 @@ function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, U $scope.invoiceLoading = true; $scope.logsShown = 0; $scope.invoicesShown = 0; + $scope.applicationsShown = 0; $scope.changingOrganization = false; $scope.loadLogs = function() { $scope.logsShown++; }; + $scope.loadApplications = function() { + $scope.applicationsShown++; + }; + $scope.loadInvoices = function() { $scope.invoicesShown++; }; @@ -2429,4 +2434,124 @@ function OrgMemberLogsCtrl($scope, $routeParams, $rootScope, $timeout, Restangul // Load the org info and the member info. loadOrganization(); loadMemberInfo(); +} + + +function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $timeout, ApiService) { + var orgname = $routeParams.orgname; + var clientId = $routeParams.clientid; + + $scope.updating = false; + + $scope.askResetClientSecret = function() { + $('#resetSecretModal').modal({}); + }; + + $scope.askDelete = function() { + $('#deleteAppModal').modal({}); + }; + + $scope.deleteApplication = function() { + var params = { + 'orgname': orgname, + 'client_id': clientId + }; + + $('#deleteAppModal').modal('hide'); + + ApiService.deleteOrganizationApplication(null, params).then(function(resp) { + $timeout(function() { + $location.path('/organization/' + orgname + '/admin'); + }, 500); + }, function(resp) { + bootbox.dialog({ + "message": resp.message || 'Could not delete application', + "title": "Cannot delete application", + "buttons": { + "close": { + "label": "Close", + "className": "btn-primary" + } + } + }); + }); + }; + + $scope.updateApplication = function() { + $scope.updating = true; + var params = { + 'orgname': orgname, + 'client_id': clientId + }; + + ApiService.updateOrganizationApplication($scope.application, params).then(function(resp) { + $scope.application = resp; + $scope.updating = false; + }, function(resp) { + $scope.updating = false; + bootbox.dialog({ + "message": resp.message || 'Could not update application', + "title": "Cannot update application", + "buttons": { + "close": { + "label": "Close", + "className": "btn-primary" + } + } + }); + }); + }; + + $scope.resetClientSecret = function() { + var params = { + 'orgname': orgname, + 'client_id': clientId + }; + + $('#resetSecretModal').modal('hide'); + + ApiService.resetOrganizationApplicationClientSecret(null, params).then(function(resp) { + $scope.application = resp; + }, function(resp) { + bootbox.dialog({ + "message": resp.message || 'Could not reset client secret', + "title": "Cannot reset client secret", + "buttons": { + "close": { + "label": "Close", + "className": "btn-primary" + } + } + }); + }); + }; + + var loadOrganization = function() { + $scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) { + $scope.organization = org; + return org; + }); + }; + + var loadApplicationInfo = function() { + var params = { + 'orgname': orgname, + 'client_id': clientId + }; + + $scope.appResource = ApiService.getOrganizationApplicationAsResource(params).get(function(resp) { + $scope.application = resp; + + $rootScope.title = 'Manage Application ' + $scope.application.name + ' (' + $scope.orgname + ')'; + $rootScope.description = 'Manage the details of application ' + $scope.application.name + + ' under organization ' + $scope.orgname; + + return resp; + }); + }; + + + // Load the organization and application info. + loadOrganization(); + loadApplicationInfo(); } \ No newline at end of file diff --git a/static/partials/manage-application.html b/static/partials/manage-application.html new file mode 100644 index 000000000..6741aa67d --- /dev/null +++ b/static/partials/manage-application.html @@ -0,0 +1,165 @@ +
+
+ +
+
+ +
+
+
+ +

{{ application.name || '(Untitled)' }}

+

+ + {{ organization.name }} +

+
+
+
+ +
+
+ Warning: There is no OAuth Redirect setup for this application. Please enter it in the Settings tab. +
+
+ + +
+ + + + +
+
+ +
+
+
+ + +
The name of the application that is displayed to users
+
+ +
+ + +
The URL to which the application will link in the authorization view
+
+ +
+ + +
The user friendly description of the application
+
+ +
+ + +
An e-mail address representing the Gravatar for the application
+
+ +
+ + +
The application's OAuth redirection/callback URL, invoked once access has been granted.
+
+ +
+ + +
+
+
+ + +
+
+
+
+
Deleting an application cannot be undone. Any existing users of your application will break!. Here be dragons!
+ +
+
+
+
+ + +
+ + + + + + + + + + + + + + + +
Client ID: +
+
Client Secret: + {{ application.client_secret }} + + +
+
+
+ +
+
+
+
+ + + + + + diff --git a/static/partials/new-organization.html b/static/partials/new-organization.html index 05abfe185..be99bae98 100644 --- a/static/partials/new-organization.html +++ b/static/partials/new-organization.html @@ -50,7 +50,7 @@

Setup the new organization

-
+
This will also be the namespace for your repositories
-
+
@@ -66,7 +66,7 @@
-
+
Choose your organization's plan
diff --git a/static/partials/org-admin.html b/static/partials/org-admin.html index 99c3f0730..139579aa8 100644 --- a/static/partials/org-admin.html +++ b/static/partials/org-admin.html @@ -12,6 +12,7 @@
  • Members
  • Robot Accounts
  • Default Permissions
  • +
  • Applications
  • Billing
  • Billing History
  • @@ -60,6 +61,11 @@
    + +
    +
    +
    +
    diff --git a/templates/oauthorize.html b/templates/oauthorize.html index 1346bd297..2629d6a2e 100644 --- a/templates/oauthorize.html +++ b/templates/oauthorize.html @@ -13,7 +13,7 @@
    - +

    {{ application.name }}

    {{ application.organization.name }} diff --git a/test/data/test.db b/test/data/test.db index 0063d6055e0c4fe9c873563b126c736051b70d88..4e24e334a2297efadfb5689091f399fab4b43303 100644 GIT binary patch delta 16920 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 QiWULq9~SxlAkIMl2R}^^!T0>6$3e3cR;@FRzR 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 gE-{!uGzZbR6