Merge branch 'swaggerlikeus' of ssh://bitbucket.org/yackob03/quay into swaggerlikeus
Conflicts: test/data/test.db
This commit is contained in:
commit
b81e48cb41
10 changed files with 262 additions and 3 deletions
|
@ -291,6 +291,7 @@ class OAuthAuthorizationCode(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class OAuthAccessToken(BaseModel):
|
class OAuthAccessToken(BaseModel):
|
||||||
|
uuid = CharField(default=uuid_generator, index=True)
|
||||||
application = ForeignKeyField(OAuthApplication)
|
application = ForeignKeyField(OAuthApplication)
|
||||||
authorized_user = ForeignKeyField(User)
|
authorized_user = ForeignKeyField(User)
|
||||||
scope = CharField()
|
scope = CharField()
|
||||||
|
|
|
@ -233,6 +233,26 @@ def delete_application(org, client_id):
|
||||||
application.delete_instance(recursive=True, delete_nullable=True)
|
application.delete_instance(recursive=True, delete_nullable=True)
|
||||||
return application
|
return application
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_access_token_for_user(user, token_uuid):
|
||||||
|
try:
|
||||||
|
return OAuthAccessToken.get(OAuthAccessToken.authorized_user == user,
|
||||||
|
OAuthAccessToken.uuid == token_uuid)
|
||||||
|
except OAuthAccessToken.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def list_access_tokens_for_user(user):
|
||||||
|
query = (OAuthAccessToken
|
||||||
|
.select()
|
||||||
|
.join(OAuthApplication)
|
||||||
|
.switch(OAuthAccessToken)
|
||||||
|
.join(User)
|
||||||
|
.where(OAuthAccessToken.authorized_user == user))
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
def list_applications_for_org(org):
|
def list_applications_for_org(org):
|
||||||
query = (OAuthApplication
|
query = (OAuthApplication
|
||||||
.select()
|
.select()
|
||||||
|
@ -240,3 +260,11 @@ def list_applications_for_org(org):
|
||||||
.where(OAuthApplication.organization == org))
|
.where(OAuthApplication.organization == org))
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
def create_access_token_for_testing(user, client_id, scope):
|
||||||
|
expires_at = datetime.now() + timedelta(seconds=10000)
|
||||||
|
application = get_application_for_client_id(client_id)
|
||||||
|
OAuthAccessToken.create(application=application, authorized_user=user, scope=scope,
|
||||||
|
token_type='token', access_token='test',
|
||||||
|
expires_at=expires_at, refresh_token='', data='')
|
||||||
|
|
|
@ -386,3 +386,56 @@ class UserNotificationList(ApiResource):
|
||||||
return {
|
return {
|
||||||
'notifications': [notification_view(notification) for notification in notifications]
|
'notifications': [notification_view(notification) for notification in notifications]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def authorization_view(access_token):
|
||||||
|
oauth_app = access_token.application
|
||||||
|
return {
|
||||||
|
'application': {
|
||||||
|
'name': oauth_app.name,
|
||||||
|
'description': oauth_app.description,
|
||||||
|
'url': oauth_app.application_uri,
|
||||||
|
'gravatar': compute_hash(oauth_app.gravatar_email or oauth_app.organization.email),
|
||||||
|
'organization': {
|
||||||
|
'name': oauth_app.organization.username,
|
||||||
|
'gravatar': compute_hash(oauth_app.organization.email)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'scopes': scopes.get_scope_information(access_token.scope),
|
||||||
|
'uuid': access_token.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
@resource('/v1/user/authorizations')
|
||||||
|
@internal_only
|
||||||
|
class UserAuthorizationList(ApiResource):
|
||||||
|
@require_user_admin
|
||||||
|
@nickname('listUserAuthorizations')
|
||||||
|
def get(self):
|
||||||
|
access_tokens = model.oauth.list_access_tokens_for_user(get_authenticated_user())
|
||||||
|
|
||||||
|
return {
|
||||||
|
'authorizations': [authorization_view(token) for token in access_tokens]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/user/authorizations/<access_token_uuid>')
|
||||||
|
@internal_only
|
||||||
|
class UserAuthorization(ApiResource):
|
||||||
|
@require_user_admin
|
||||||
|
@nickname('getUserAuthorization')
|
||||||
|
def get(self, access_token_uuid):
|
||||||
|
access_token = model.oauth.lookup_access_token_for_user(get_authenticated_user(), access_token_uuid)
|
||||||
|
if not access_token:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
return authorization_view(access_token)
|
||||||
|
|
||||||
|
@require_user_admin
|
||||||
|
@nickname('deleteUserAuthorization')
|
||||||
|
def delete(self, access_token_uuid):
|
||||||
|
access_token = model.oauth.lookup_access_token_for_user(get_authenticated_user(), access_token_uuid)
|
||||||
|
if not access_token:
|
||||||
|
raise NotFound()
|
||||||
|
|
||||||
|
access_token.delete_instance(recursive=True, delete_nullable=True)
|
||||||
|
return 'Deleted', 204
|
||||||
|
|
|
@ -361,6 +361,8 @@ def populate_database():
|
||||||
client_id='deadpork',
|
client_id='deadpork',
|
||||||
description = 'This is another test application')
|
description = 'This is another test application')
|
||||||
|
|
||||||
|
model.oauth.create_access_token_for_testing(new_user_1, 'deadbeef', 'repo:admin')
|
||||||
|
|
||||||
model.create_robot('neworgrobot', org)
|
model.create_robot('neworgrobot', org)
|
||||||
|
|
||||||
owners = model.get_organization_team('buynlarge', 'owners')
|
owners = model.get_organization_team('buynlarge', 'owners')
|
||||||
|
|
|
@ -3575,3 +3575,18 @@ pre.command:before {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
border-top: 1px solid #eee;
|
border-top: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-info .by:before {
|
||||||
|
content: "by";
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-info .by {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-info .scope {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
|
@ -1628,12 +1628,42 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
|
||||||
$scope.org = {};
|
$scope.org = {};
|
||||||
$scope.githubRedirectUri = KeyService.githubRedirectUri;
|
$scope.githubRedirectUri = KeyService.githubRedirectUri;
|
||||||
$scope.githubClientId = KeyService.githubClientId;
|
$scope.githubClientId = KeyService.githubClientId;
|
||||||
|
$scope.authorizedApps = null;
|
||||||
|
|
||||||
$('.form-change').popover();
|
$('.form-change').popover();
|
||||||
|
|
||||||
$scope.logsShown = 0;
|
$scope.logsShown = 0;
|
||||||
$scope.invoicesShown = 0;
|
$scope.invoicesShown = 0;
|
||||||
|
|
||||||
|
$scope.loadAuthedApps = function() {
|
||||||
|
if ($scope.authorizedApps) { return; }
|
||||||
|
|
||||||
|
ApiService.listUserAuthorizations().then(function(resp) {
|
||||||
|
$scope.authorizedApps = resp['authorizations'];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.deleteAccess = function(accessTokenInfo) {
|
||||||
|
var params = {
|
||||||
|
'access_token_uuid': accessTokenInfo['uuid']
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.deleteUserAuthorization(null, params).then(function(resp) {
|
||||||
|
$scope.authorizedApps.splice($scope.authorizedApps.indexOf(accessTokenInfo), 1);
|
||||||
|
}, function(resp) {
|
||||||
|
bootbox.dialog({
|
||||||
|
"message": resp.message || 'Could not revoke authorization',
|
||||||
|
"title": "Cannot revoke authorization",
|
||||||
|
"buttons": {
|
||||||
|
"close": {
|
||||||
|
"label": "Close",
|
||||||
|
"className": "btn-primary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
$scope.loadLogs = function() {
|
$scope.loadLogs = function() {
|
||||||
if (!$scope.hasPaidBusinessPlan) { return; }
|
if (!$scope.hasPaidBusinessPlan) { return; }
|
||||||
$scope.logsShown++;
|
$scope.logsShown++;
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#email">Account E-mail</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#email">Account E-mail</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Change Password</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Change Password</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#github">GitHub Login</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#github">GitHub Login</a></li>
|
||||||
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#authorized" ng-click="loadAuthedApps()">Authorized Applications</a></li>
|
||||||
<li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li>
|
<li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#migrate" id="migrateTab">Convert to Organization</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#migrate" id="migrateTab">Convert to Organization</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -41,6 +42,55 @@
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
|
<!-- Authorized applications tab -->
|
||||||
|
<div id="authorized" class="tab-pane">
|
||||||
|
<div class="quay-spinner" ng-show="!authorizedApps"></div>
|
||||||
|
|
||||||
|
<div class="panel" ng-show="authorizedApps != null">
|
||||||
|
<div class="panel-body" ng-show="!authorizedApps.length">
|
||||||
|
You have not authorized any external applications
|
||||||
|
</div>
|
||||||
|
<div class="panel-body" ng-show="authorizedApps.length">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
These are the applications you have authorized to view information and perform actions on Quay.io on your behalf.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<th>Application Name</th>
|
||||||
|
<th>Authorized Permissions</th>
|
||||||
|
<th style="width: 150px">Revoke</th>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tr class="auth-info" ng-repeat="authInfo in authorizedApps">
|
||||||
|
<td>
|
||||||
|
<img src="//www.gravatar.com/avatar/{{ authInfo.gravatar }}?s=16&d=identicon">
|
||||||
|
<a href="{{ authInfo.application.url }}" ng-if="authInfo.application.url" target="_blank"
|
||||||
|
title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
|
||||||
|
{{ authInfo.application.name }}
|
||||||
|
</a>
|
||||||
|
<span ng-if="!authInfo.application.url" title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
|
||||||
|
{{ authInfo.application.name }}
|
||||||
|
</span>
|
||||||
|
<span class="by">{{ authInfo.application.organization.name }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="label label-default scope"
|
||||||
|
ng-class="{'repo:admin': 'label-primary', 'repo:write': 'label-success', 'repo:create': 'label-success'}[scopeInfo.scope]"
|
||||||
|
ng-repeat="scopeInfo in authInfo.scopes" title="{{ scopeInfo.description }}" bs-tooltip>
|
||||||
|
{{ scopeInfo.scope }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="delete-ui" delete-title="'Revoke Authorization'" button-title="'Revoke'" perform-delete="deleteAccess(authInfo)"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Logs tab -->
|
<!-- Logs tab -->
|
||||||
<div id="logs" class="tab-pane">
|
<div id="logs" class="tab-pane">
|
||||||
<div class="logs-view" user="user" visible="logsShown"></div>
|
<div class="logs-view" user="user" visible="logsShown"></div>
|
||||||
|
|
Binary file not shown.
|
@ -17,7 +17,7 @@ from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, Bu
|
||||||
BuildTriggerList)
|
BuildTriggerList)
|
||||||
from endpoints.api.webhook import Webhook, WebhookList
|
from endpoints.api.webhook import Webhook, WebhookList
|
||||||
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout,
|
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout,
|
||||||
Signin, User)
|
Signin, User, UserAuthorizationList, UserAuthorization)
|
||||||
from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList
|
from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList
|
||||||
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
|
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
|
||||||
from endpoints.api.logs import UserLogs, OrgLogs, RepositoryLogs
|
from endpoints.api.logs import UserLogs, OrgLogs, RepositoryLogs
|
||||||
|
@ -3039,5 +3039,56 @@ class TestOrganizationApplicationResetClientSecret(ApiTestCase):
|
||||||
self._run_test('POST', 200, 'devtable', None)
|
self._run_test('POST', 200, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserAuthorizationList(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(UserAuthorizationList)
|
||||||
|
|
||||||
|
def test_get_anonymous(self):
|
||||||
|
self._run_test('GET', 401, None, None)
|
||||||
|
|
||||||
|
def test_get_freshuser(self):
|
||||||
|
self._run_test('GET', 200, 'freshuser', None)
|
||||||
|
|
||||||
|
def test_get_reader(self):
|
||||||
|
self._run_test('GET', 200, 'reader', None)
|
||||||
|
|
||||||
|
def test_get_devtable(self):
|
||||||
|
self._run_test('GET', 200, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserAuthorization(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(UserAuthorization, access_token_uuid='fake')
|
||||||
|
|
||||||
|
def test_get_anonymous(self):
|
||||||
|
self._run_test('GET', 401, None, None)
|
||||||
|
|
||||||
|
def test_get_freshuser(self):
|
||||||
|
self._run_test('GET', 404, 'freshuser', None)
|
||||||
|
|
||||||
|
def test_get_reader(self):
|
||||||
|
self._run_test('GET', 404, 'reader', None)
|
||||||
|
|
||||||
|
def test_get_devtable(self):
|
||||||
|
self._run_test('GET', 404, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_anonymous(self):
|
||||||
|
self._run_test('DELETE', 401, None, None)
|
||||||
|
|
||||||
|
def test_delete_freshuser(self):
|
||||||
|
self._run_test('DELETE', 404, 'freshuser', None)
|
||||||
|
|
||||||
|
def test_delete_reader(self):
|
||||||
|
self._run_test('DELETE', 404, 'reader', None)
|
||||||
|
|
||||||
|
def test_delete_devtable(self):
|
||||||
|
self._run_test('DELETE', 404, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -18,7 +18,9 @@ from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, Bu
|
||||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||||
BuildTriggerList)
|
BuildTriggerList)
|
||||||
from endpoints.api.webhook import Webhook, WebhookList
|
from endpoints.api.webhook import Webhook, WebhookList
|
||||||
from endpoints.api.user import PrivateRepositories, ConvertToOrganization, Signout, Signin, User
|
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Signout, Signin, User,
|
||||||
|
UserAuthorizationList, UserAuthorization)
|
||||||
|
|
||||||
from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList
|
from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList
|
||||||
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
|
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
|
||||||
from endpoints.api.logs import UserLogs, OrgLogs
|
from endpoints.api.logs import UserLogs, OrgLogs
|
||||||
|
@ -1624,5 +1626,32 @@ class TestBuildTriggers(ApiTestCase):
|
||||||
self.assertEquals("build-name", start_json['display_name'])
|
self.assertEquals("build-name", start_json['display_name'])
|
||||||
self.assertEquals(['bar'], start_json['job_config']['docker_tags'])
|
self.assertEquals(['bar'], start_json['job_config']['docker_tags'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestUserAuthorizations(ApiTestCase):
|
||||||
|
def test_list_get_delete_user_authorizations(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
json = self.getJsonResponse(UserAuthorizationList)
|
||||||
|
|
||||||
|
self.assertEquals(1, len(json['authorizations']))
|
||||||
|
|
||||||
|
authorization = json['authorizations'][0]
|
||||||
|
|
||||||
|
assert 'uuid' in authorization
|
||||||
|
assert 'scopes' in authorization
|
||||||
|
assert 'application' in authorization
|
||||||
|
|
||||||
|
# Retrieve the authorization.
|
||||||
|
get_json = self.getJsonResponse(UserAuthorization, params=dict(access_token_uuid = authorization['uuid']))
|
||||||
|
self.assertEquals(authorization, get_json)
|
||||||
|
|
||||||
|
# Delete the authorization.
|
||||||
|
self.deleteResponse(UserAuthorization, params=dict(access_token_uuid = authorization['uuid']))
|
||||||
|
|
||||||
|
# Verify it has been deleted.
|
||||||
|
self.getJsonResponse(UserAuthorization, params=dict(access_token_uuid = authorization['uuid']),
|
||||||
|
expected_code=404)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
Reference in a new issue