Add full application management API, UI and test cases

This commit is contained in:
Joseph Schorr 2014-03-20 15:46:13 -04:00
parent a3eff7a2e8
commit f7c27f250b
16 changed files with 904 additions and 15 deletions

View file

@ -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):

View file

@ -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

View file

@ -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/<orgname>/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/<orgname>/applications/<client_id>')
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/<orgname>/applications/<client_id>/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()

View file

@ -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')

View file

@ -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;
}

View file

@ -1,6 +1,6 @@
<div class="application-info-element" style="padding-bottom: 18px">
<div class="auth-header">
<img src="//www.gravatar.com/avatar/{{ application.organization.gravatar }}?s=48&d=identicon">
<img src="//www.gravatar.com/avatar/{{ application.gravatar }}?s=48&d=identicon">
<h2><a href="{{ application.url }}" target="_blank">{{ application.name }}</a></h2>
<h4>
{{ application.organization.name }}

View file

@ -0,0 +1,24 @@
<div class="application-manager-element">
<div class="quay-spinner" ng-show="loading"></div>
<div class="container" ng-show="!loading">
<div class="side-controls">
<span class="popup-input-button" placeholder="'Application Name'" submitted="createApplication(value)">
<i class="fa fa-plus"></i> Create New Application
</span>
</div>
<table class="table">
<thead>
<th>Application Name</th>
<th>Application URI</th>
</thead>
<tr ng-repeat="app in applications">
<td><a href="/organization/{{ organization.name }}/application/{{ app.client_id }}">{{ app.name }}</a></td>
<td><a href="{{ app.application_uri }}" ng-if="app.application_uri" target="_blank">{{ app.application_uri }}</a></td>
</tr>
</table>
</div>
</div>

View file

@ -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,

View file

@ -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();
}

View file

@ -0,0 +1,165 @@
<div class="resource-view" resource="appResource" error-message="'Application not found'">
</div>
<div ng-show="application">
<div class="container manage-application">
<!-- Header -->
<div class="row">
<div class="col-md-12">
<div class="auth-header">
<img src="//www.gravatar.com/avatar/{{ application.gravatar_email | gravatar }}?s=48&d=identicon">
<h2>{{ application.name || '(Untitled)' }}</h2>
<h4>
<img src="//www.gravatar.com/avatar/{{ organization.gravatar }}?s=24&d=identicon" style="vertical-align: middle; margin-right: 4px;">
<span style="vertical-align: middle"><a href="/organization/{{ organization.name }}/admin">{{ organization.name }}</a></span>
</h4>
</div>
</div>
</div>
<div class="row" style="margin-top: 10px" ng-if="!application.redirect_uri">
<div class="alert alert-warning">
Warning: There is no OAuth Redirect setup for this application. Please enter it in the <strong>Settings</strong> tab.
</div>
</div>
<!-- Content -->
<div class="row" style="margin-top: 10px">
<!-- Side tabs -->
<div class="col-md-2">
<ul class="nav nav-pills nav-stacked">
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#settings">Settings</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#oauth">OAuth Information</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#delete">Delete Application</a></li>
</ul>
</div>
<!-- Content -->
<div class="col-md-10">
<div class="tab-content">
<!-- Settings tab -->
<div id="settings" class="tab-pane active">
<form method="put" name="applicationForm" id="applicationForm" ng-submit="updateApplication()">
<div class="form-group nested">
<label for="fieldAppName">Application Name</label>
<input type="text" class="form-control" id="fieldAppName" placeholder="Application Name" required ng-model="application.name">
<div class="description">The name of the application that is displayed to users</div>
</div>
<div class="form-group nested">
<label for="fieldAppURI">Homepage URL</label>
<input type="url" class="form-control" id="fieldAppURI" placeholder="Homepage URL" required ng-model="application.application_uri">
<div class="description">The URL to which the application will link in the authorization view</div>
</div>
<div class="form-group nested">
<label for="fieldAppDescription">Description (optional)</label>
<input type="text" class="form-control" id="fieldAppURI" placeholder="Description" ng-model="application.description">
<div class="description">The user friendly description of the application</div>
</div>
<div class="form-group nested">
<label for="fieldAppGravatar">Gravatar E-mail (optional)</label>
<input type="email" class="form-control" id="fieldAppGravatar" placeholder="Gravatar E-mail" ng-model="application.gravatar_email">
<div class="description">An e-mail address representing the <a href="http://en.gravatar.com/" target="_blank">Gravatar</a> for the application</div>
</div>
<div class="form-group nested" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee;">
<label for="fieldAppRedirect">Redirect/Callback URL</label>
<input type="url" class="form-control" id="fieldAppRedirect" placeholder="OAuth Redirect URL" ng-model="application.redirect_uri" required>
<div class="description">The application's OAuth redirection/callback URL, invoked once access has been granted.</div>
</div>
<div class="button-bar">
<button class="btn btn-success btn-large" type="submit" ng-disabled="applicationForm.$invalid || updating">
Update Application
</button>
<span class="quay-spinner" ng-show="updating"></span>
</div>
</form>
</div>
<!-- Delete tab -->
<div id="delete" class="tab-pane">
<div class="panel panel-default">
<div class="panel-body">
<div style="text-align: center">
<div class="alert alert-danger">Deleting an application <b>cannot be undone</b>. Any existing users of your application will <strong>break!</strong>. Here be dragons!</div>
<button class="btn btn-danger" ng-click="askDelete()">Delete Application</button>
</div>
</div>
</div>
</div>
<!-- OAuth tab -->
<div id="oauth" class="tab-pane">
<table style="margin-top: 20px;">
<thead>
<th style="width: 150px"></th>
<th style="width: 250px"></th>
<th></th>
</thead>
<tr>
<td>Client ID:</td>
<td style="width: 250px">
<div class="copy-box" hovering-message="true" value="application.client_id"></div>
</td>
</tr>
<tr>
<td>Client Secret: <i class="fa fa-lock fa-lg" title="Keep this secret!" bs-tooltip style="margin-left: 10px"></i></td>
<td>
{{ application.client_secret }}
</td>
<td style="padding-left: 10px">
<button class="btn btn-primary" ng-click="askResetClientSecret()">Reset Client Secret</button>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="resetSecretModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Reset Client Secret?</h4>
</div>
<div class="modal-body">
<div class="alert alert-info">
Note that resetting the Client Secret for this application will <strong>not</strong> invalidate any user tokens.
</div>
<div>Are you sure you want to reset your Client Secret? Any existing users of this Secret <strong>will break!</strong></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="resetClientSecret()">Yes, I'm sure</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="deleteAppModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Delete Application?</h4>
</div>
<div class="modal-body">
Are you <b>absolutely, positively</b> sure you would like to delete this application? This <b>cannot be undone</b>.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" ng-click="deleteApplication()">Delete Application</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

View file

@ -50,7 +50,7 @@
<h3>Setup the new organization</h3>
<form method="post" name="newOrgForm" id="newOrgForm" ng-submit="createNewOrg()">
<div class="form-group">
<div class="form-group nested">
<label for="orgName">Organization Name</label>
<input id="orgName" name="orgName" type="text" class="form-control" placeholder="Organization Name"
ng-model="org.name" required autofocus data-trigger="manual" data-content="{{ createError }}"
@ -58,7 +58,7 @@
<span class="description">This will also be the namespace for your repositories</span>
</div>
<div class="form-group">
<div class="form-group nested">
<label for="orgName">Organization Email</label>
<input id="orgEmail" name="orgEmail" type="email" class="form-control" placeholder="Organization Email"
ng-model="org.email" required>
@ -66,7 +66,7 @@
</div>
<!-- Plans Table -->
<div class="form-group plan-group">
<div class="form-group nested plan-group">
<strong>Choose your organization's plan</strong>
<div class="plans-table" plans="plans" current-plan="currentPlan"></div>
</div>

View file

@ -12,6 +12,7 @@
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#members" ng-click="loadMembers()">Members</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#prototypes">Default Permissions</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#applications" ng-click="loadApplications()">Applications</a></li>
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing</a></li>
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a></li>
</ul>
@ -60,6 +61,11 @@
<div class="logs-view" organization="organization" visible="logsShown"></div>
</div>
<!-- Applications tab -->
<div id="applications" class="tab-pane">
<div class="application-manager" organization="organization" visible="applicationsShown"></div>
</div>
<!-- Billing Options tab -->
<div id="billingoptions" class="tab-pane">
<div class="billing-options" organization="organization"></div>

View file

@ -13,7 +13,7 @@
<div class="container auth-container" ng-if="!user.anonymous">
<div class="auth-header">
<img src="//www.gravatar.com/avatar/{{ application.organization.gravatar }}?s=48&d=identicon">
<img src="//www.gravatar.com/avatar/{{ application.gravatar }}?s=48&d=identicon">
<h2><a href="{{ application.url }}" target="_blank">{{ application.name }}</a></h2>
<h4>
{{ application.organization.name }}

Binary file not shown.

View file

@ -26,7 +26,9 @@ from endpoints.api.billing import (UserInvoiceList, UserCard, UserPlan, ListPlan
from endpoints.api.discovery import DiscoveryResource
from endpoints.api.organization import (OrganizationList, OrganizationMember,
OrgPrivateRepositories, OrgnaizationMemberList,
Organization, ApplicationInformation)
Organization, ApplicationInformation,
OrganizationApplications, OrganizationApplicationResource,
OrganizationApplicationResetClientSecret)
from endpoints.api.repository import RepositoryList, RepositoryVisibility, Repository
from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission,
RepositoryTeamPermissionList, RepositoryUserPermissionList)
@ -2937,3 +2939,105 @@ class TestApplicationInformation3lgi(ApiTestCase):
def test_get_devtable(self):
self._run_test('GET', 404, 'devtable', None)
class TestOrganizationApplications(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(OrganizationApplications, orgname="buynlarge")
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)
def test_post_anonymous(self):
self._run_test('POST', 401, None, {u'name': 'foo'})
def test_post_freshuser(self):
self._run_test('POST', 403, 'freshuser', {u'name': 'foo'})
def test_post_reader(self):
self._run_test('POST', 403, 'reader', {u'name': 'foo'})
def test_post_devtable(self):
self._run_test('POST', 200, 'devtable', {u'name': 'foo'})
class TestOrganizationApplicationResource(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(OrganizationApplicationResource, orgname="buynlarge", client_id="deadbeef")
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)
def test_put_anonymous(self):
self._run_test('PUT', 401, None,
{u'name': 'foo', u'application_uri': 'foo', u'redirect_uri': 'foo'})
def test_put_freshuser(self):
self._run_test('PUT', 403, 'freshuser',
{u'name': 'foo', u'application_uri': 'foo', u'redirect_uri': 'foo'})
def test_put_reader(self):
self._run_test('PUT', 403, 'reader',
{u'name': 'foo', u'application_uri': 'foo', u'redirect_uri': 'foo'})
def test_put_devtable(self):
self._run_test('PUT', 200, 'devtable',
{u'name': 'foo', u'application_uri': 'foo', u'redirect_uri': 'foo'})
def test_delete_anonymous(self):
self._run_test('DELETE', 401, None, None)
def test_delete_freshuser(self):
self._run_test('DELETE', 403, 'freshuser', None)
def test_delete_reader(self):
self._run_test('DELETE', 403, 'reader', None)
def test_delete_devtable(self):
self._run_test('DELETE', 204, 'devtable', None)
class TestOrganizationApplicationResetClientSecret(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(OrganizationApplicationResetClientSecret,
orgname="buynlarge", client_id="deadbeef")
def test_post_anonymous(self):
self._run_test('POST', 401, None, None)
def test_post_freshuser(self):
self._run_test('POST', 403, 'freshuser', None)
def test_post_reader(self):
self._run_test('POST', 403, 'reader', None)
def test_post_devtable(self):
self._run_test('POST', 200, 'devtable', None)
if __name__ == '__main__':
unittest.main()

View file

@ -27,7 +27,9 @@ from endpoints.api.billing import (UserCard, UserPlan, ListPlans, OrganizationCa
from endpoints.api.discovery import DiscoveryResource
from endpoints.api.organization import (OrganizationList, OrganizationMember,
OrgPrivateRepositories, OrgnaizationMemberList,
Organization)
Organization, ApplicationInformation,
OrganizationApplications, OrganizationApplicationResource,
OrganizationApplicationResetClientSecret)
from endpoints.api.repository import RepositoryList, RepositoryVisibility, Repository
from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission,
RepositoryTeamPermissionList, RepositoryUserPermissionList)
@ -55,6 +57,7 @@ NEW_USER_DETAILS = {
'email': 'bobby@tables.com',
}
FAKE_APPLICATION_CLIENT_ID = 'deadbeef'
class ApiTestCase(unittest.TestCase):
def setUp(self):
@ -1391,6 +1394,103 @@ class TestLogs(ApiTestCase):
self.assertEquals(READ_ACCESS_USER, log['performer']['name'])
class TestApplicationInformation(ApiTestCase):
def test_get_info(self):
json = self.getJsonResponse(ApplicationInformation, params=dict(client_id=FAKE_APPLICATION_CLIENT_ID))
assert 'name' in json
assert 'uri' in json
assert 'organization' in json
def test_get_invalid_info(self):
self.getJsonResponse(ApplicationInformation, params=dict(client_id='invalid-code'),
expected_code=404)
class TestOrganizationApplications(ApiTestCase):
def test_list_create_applications(self):
self.login(ADMIN_ACCESS_USER)
json = self.getJsonResponse(OrganizationApplications, params=dict(orgname=ORGANIZATION))
self.assertEquals(2, len(json['applications']))
self.assertEquals(FAKE_APPLICATION_CLIENT_ID, json['applications'][0]['client_id'])
# Add a new application.
json = self.postJsonResponse(OrganizationApplications, params=dict(orgname=ORGANIZATION),
data=dict(name="Some cool app", description="foo"))
self.assertEquals("Some cool app", json['name'])
self.assertEquals("foo", json['description'])
# Retrieve the apps list again
list_json = self.getJsonResponse(OrganizationApplications, params=dict(orgname=ORGANIZATION))
self.assertEquals(3, len(list_json['applications']))
self.assertEquals(json, list_json['applications'][2])
class TestOrganizationApplicationResource(ApiTestCase):
def test_get_edit_delete_application(self):
self.login(ADMIN_ACCESS_USER)
# Retrieve the application.
json = self.getJsonResponse(OrganizationApplicationResource,
params=dict(orgname=ORGANIZATION, client_id=FAKE_APPLICATION_CLIENT_ID))
self.assertEquals(FAKE_APPLICATION_CLIENT_ID, json['client_id'])
# Edit the application.
edit_json = self.putJsonResponse(OrganizationApplicationResource,
params=dict(orgname=ORGANIZATION, client_id=FAKE_APPLICATION_CLIENT_ID),
data=dict(name="Some App", description="foo", application_uri="bar",
redirect_uri="baz", gravatar_email="meh"))
self.assertEquals(FAKE_APPLICATION_CLIENT_ID, edit_json['client_id'])
self.assertEquals("Some App", edit_json['name'])
self.assertEquals("foo", edit_json['description'])
self.assertEquals("bar", edit_json['application_uri'])
self.assertEquals("baz", edit_json['redirect_uri'])
self.assertEquals("meh", edit_json['gravatar_email'])
# Retrieve the application again.
json = self.getJsonResponse(OrganizationApplicationResource,
params=dict(orgname=ORGANIZATION, client_id=FAKE_APPLICATION_CLIENT_ID))
self.assertEquals(json, edit_json)
# Delete the application.
self.deleteResponse(OrganizationApplicationResource,
params=dict(orgname=ORGANIZATION, client_id=FAKE_APPLICATION_CLIENT_ID))
# Make sure the application is gone.
self.getJsonResponse(OrganizationApplicationResource,
params=dict(orgname=ORGANIZATION, client_id=FAKE_APPLICATION_CLIENT_ID),
expected_code=404)
class TestOrganizationApplicationResetClientSecret(ApiTestCase):
def test_reset_client_secret(self):
self.login(ADMIN_ACCESS_USER)
# Retrieve the application.
json = self.getJsonResponse(OrganizationApplicationResource,
params=dict(orgname=ORGANIZATION, client_id=FAKE_APPLICATION_CLIENT_ID))
self.assertEquals(FAKE_APPLICATION_CLIENT_ID, json['client_id'])
# Reset the client secret.
reset_json = self.postJsonResponse(OrganizationApplicationResetClientSecret,
params=dict(orgname=ORGANIZATION, client_id=FAKE_APPLICATION_CLIENT_ID))
self.assertEquals(FAKE_APPLICATION_CLIENT_ID, reset_json['client_id'])
self.assertNotEquals(reset_json['client_secret'], json['client_secret'])
# Verify it was changed in the DB.
json = self.getJsonResponse(OrganizationApplicationResource,
params=dict(orgname=ORGANIZATION, client_id=FAKE_APPLICATION_CLIENT_ID))
self.assertEquals(reset_json['client_secret'], json['client_secret'])
class FakeBuildTrigger(BuildTriggerBase):
@classmethod
def service_name(cls):