Add full application management API, UI and test cases
This commit is contained in:
parent
a3eff7a2e8
commit
f7c27f250b
16 changed files with 904 additions and 15 deletions
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 }}
|
||||
|
|
24
static/directives/application-manager.html
Normal file
24
static/directives/application-manager.html
Normal 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>
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
165
static/partials/manage-application.html
Normal file
165
static/partials/manage-application.html
Normal 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">×</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">×</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 -->
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
Reference in a new issue