From c82d1ffe98ea62063a1afc0c451503544cc262f9 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 24 Mar 2014 20:57:02 -0400 Subject: [PATCH] Add ability for users to see their authorized applications and revoke the access --- data/database.py | 1 + data/model/oauth.py | 28 ++++++++++++++++ endpoints/api/user.py | 55 +++++++++++++++++++++++++++++++- initdb.py | 2 ++ static/css/quay.css | 15 +++++++++ static/js/controllers.js | 30 +++++++++++++++++ static/partials/user-admin.html | 50 +++++++++++++++++++++++++++++ test/data/test.db | Bin 532480 -> 536576 bytes test/test_api_security.py | 53 +++++++++++++++++++++++++++++- test/test_api_usage.py | 31 +++++++++++++++++- 10 files changed, 262 insertions(+), 3 deletions(-) diff --git a/data/database.py b/data/database.py index b3d2eafff..e032ec248 100644 --- a/data/database.py +++ b/data/database.py @@ -291,6 +291,7 @@ class OAuthAuthorizationCode(BaseModel): class OAuthAccessToken(BaseModel): + uuid = CharField(default=uuid_generator, index=True) application = ForeignKeyField(OAuthApplication) authorized_user = ForeignKeyField(User) scope = CharField() diff --git a/data/model/oauth.py b/data/model/oauth.py index 2e83019e4..cde11524c 100644 --- a/data/model/oauth.py +++ b/data/model/oauth.py @@ -221,6 +221,26 @@ def delete_application(org, client_id): application.delete_instance(recursive=True, delete_nullable=True) 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): query = (OAuthApplication .select() @@ -228,3 +248,11 @@ def list_applications_for_org(org): .where(OAuthApplication.organization == org)) 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='') diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 7b935ef26..343b4a010 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -385,4 +385,57 @@ class UserNotificationList(ApiResource): notifications = model.list_notifications(get_authenticated_user()) return { 'notifications': [notification_view(notification) for notification in notifications] - } \ No newline at end of file + } + + +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/') +@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 diff --git a/initdb.py b/initdb.py index 266d462d9..a4b1709f0 100644 --- a/initdb.py +++ b/initdb.py @@ -361,6 +361,8 @@ def populate_database(): client_id='deadpork', description = 'This is another test application') + model.oauth.create_access_token_for_testing(new_user_1, 'deadbeef', 'repo:admin') + model.create_robot('neworgrobot', org) owners = model.get_organization_team('buynlarge', 'owners') diff --git a/static/css/quay.css b/static/css/quay.css index 6d461b6e7..ac7c01e66 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -3574,4 +3574,19 @@ pre.command:before { margin-top: 10px; padding-top: 20px; 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; } \ No newline at end of file diff --git a/static/js/controllers.js b/static/js/controllers.js index dad3c47c1..a81da70c3 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1628,12 +1628,42 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use $scope.org = {}; $scope.githubRedirectUri = KeyService.githubRedirectUri; $scope.githubClientId = KeyService.githubClientId; + $scope.authorizedApps = null; $('.form-change').popover(); $scope.logsShown = 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() { if (!$scope.hasPaidBusinessPlan) { return; } $scope.logsShown++; diff --git a/static/partials/user-admin.html b/static/partials/user-admin.html index ffc70257c..09414cfd4 100644 --- a/static/partials/user-admin.html +++ b/static/partials/user-admin.html @@ -33,6 +33,7 @@
  • Account E-mail
  • Change Password
  • GitHub Login
  • +
  • Authorized Applications
  • Usage Logs
  • Convert to Organization
  • @@ -41,6 +42,55 @@
    + +
    +
    + +
    +
    + You have not authorized any external applications +
    +
    +
    + These are the applications you have authorized to view information and perform actions on Quay.io on your behalf. +
    + + + + + + + + + + + + + +
    Application NameAuthorized PermissionsRevoke
    + + + {{ authInfo.application.name }} + + + {{ authInfo.application.name }} + + {{ authInfo.application.organization.name }} + + + {{ scopeInfo.scope }} + + + +
    +
    +
    + +
    +
    diff --git a/test/data/test.db b/test/data/test.db index 4e24e334a2297efadfb5689091f399fab4b43303..a7e891deb0b6d35076867db9d65122d974b9afc6 100644 GIT binary patch delta 7537 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 delta 7349 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