Merge branch 'swaggerlikeus' of ssh://bitbucket.org/yackob03/quay into swaggerlikeus

Conflicts:
	test/data/test.db
This commit is contained in:
jakedt 2014-03-25 12:43:09 -04:00
commit b81e48cb41
10 changed files with 262 additions and 3 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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