diff --git a/auth/auth.py b/auth/auth.py index 3616792ad..a81876e54 100644 --- a/auth/auth.py +++ b/auth/auth.py @@ -135,8 +135,15 @@ def process_token(auth): logger.warning('Invalid token format: %s' % auth) abort(401, message='Invalid token format: %(auth)s', issue='invalid-auth-token', auth=auth) - token_vals = {val[0]: val[1] for val in + def safe_get(lst, index, default_value): + try: + return lst[index] + except IndexError: + return default_value + + token_vals = {val[0]: safe_get(val, 1, '') for val in (detail.split('=') for detail in token_details)} + if 'signature' not in token_vals: logger.warning('Token does not contain signature: %s' % auth) abort(401, message='Token does not contain a valid signature: %(auth)s', diff --git a/data/model/legacy.py b/data/model/legacy.py index 92a130dca..d9b2079d8 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -1830,6 +1830,16 @@ def get_active_users(): def get_active_user_count(): return get_active_users().count() + +def detach_external_login(user, service_name): + try: + service = LoginService.get(name = service_name) + except LoginService.DoesNotExist: + return + + FederatedLogin.delete().where(FederatedLogin.user == user, + FederatedLogin.service == service).execute() + def delete_user(user): user.delete_instance(recursive=True, delete_nullable=True) diff --git a/data/model/oauth.py b/data/model/oauth.py index 309e2122a..51bfc053e 100644 --- a/data/model/oauth.py +++ b/data/model/oauth.py @@ -46,7 +46,7 @@ class DatabaseAuthorizationProvider(AuthorizationProvider): def validate_redirect_uri(self, client_id, redirect_uri): try: app = OAuthApplication.get(client_id=client_id) - if app.redirect_uri and redirect_uri.startswith(app.redirect_uri): + if app.redirect_uri and redirect_uri and redirect_uri.startswith(app.redirect_uri): return True return False except OAuthApplication.DoesNotExist: diff --git a/endpoints/api/user.py b/endpoints/api/user.py index ddf05aafa..43a08508a 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -408,6 +408,19 @@ class Signout(ApiResource): return {'success': True} + +@resource('/v1/detachexternal/') +@internal_only +class DetachExternal(ApiResource): + """ Resource for detaching an external login. """ + @require_user_admin + @nickname('detachExternalLogin') + def post(self, servicename): + """ Request that the current user be detached from the external login service. """ + model.detach_external_login(get_authenticated_user(), servicename) + return {'success': True} + + @resource("/v1/recovery") @internal_only class Recovery(ApiResource): diff --git a/endpoints/index.py b/endpoints/index.py index 4017d47e9..5c8d7058a 100644 --- a/endpoints/index.py +++ b/endpoints/index.py @@ -66,6 +66,9 @@ def generate_headers(role='read'): @index.route('/users/', methods=['POST']) def create_user(): user_data = request.get_json() + if not 'username' in user_data: + abort(400, 'Missing username') + username = user_data['username'] password = user_data.get('password', '') diff --git a/static/js/app.js b/static/js/app.js index 6a6bf27e3..9ebe2a3e1 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -556,7 +556,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading // If an error occurred, report it and done. if (ping < 0) { cached['pings'] = [-1]; - invokeCallback($scope, pings, callback); + invokeCallback($scope, [-1], callback); return; } @@ -1518,7 +1518,12 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading }; notificationService.getPage = function(notification) { - var page = notificationKinds[notification['kind']]['page']; + var kindInfo = notificationKinds[notification['kind']]; + if (!kindInfo) { + return null; + } + + var page = kindInfo['page']; if (typeof page != 'string') { page = page(notification['metadata']); } diff --git a/static/js/controllers.js b/static/js/controllers.js index f259ead68..7010dc4eb 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1781,6 +1781,18 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use UIService.showFormError('#changePasswordForm', result); }); }; + + $scope.detachExternalLogin = function(kind) { + var params = { + 'servicename': kind + }; + + ApiService.detachExternalLogin(null, params).then(function() { + $scope.hasGithubLogin = false; + $scope.hasGoogleLogin = false; + UserService.load(); + }, ApiService.errorDisplay('Count not detach service')); + }; } function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService) { diff --git a/static/partials/user-admin.html b/static/partials/user-admin.html index c4d3b94a0..7d6dd8dfb 100644 --- a/static/partials/user-admin.html +++ b/static/partials/user-admin.html @@ -177,10 +177,14 @@
{{githubLogin}} +
Account attached to Github Account +
@@ -197,10 +201,14 @@
{{ googleLogin }} +
Account attached to Google Account +
diff --git a/storage/cloud.py b/storage/cloud.py index 824c57534..06dd8a2a9 100644 --- a/storage/cloud.py +++ b/storage/cloud.py @@ -205,6 +205,9 @@ class _CloudStorage(BaseStorage): path = self._init_path(path) key = self._key_class(self._cloud_bucket, path) k = self._cloud_bucket.lookup(key) + if k is None: + raise IOError('No such key: \'{0}\''.format(path)) + return k.etag[1:-1][:7] diff --git a/test/test_api_security.py b/test/test_api_security.py index 3c33ad712..1f4a18a84 100644 --- a/test/test_api_security.py +++ b/test/test_api_security.py @@ -24,7 +24,7 @@ from endpoints.api.repoemail import RepositoryAuthorizedEmail from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout, Signin, User, UserAuthorizationList, UserAuthorization, UserNotification, - VerifyUser) + VerifyUser, DetachExternal) from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList from endpoints.api.logs import UserLogs, OrgLogs, RepositoryLogs @@ -435,6 +435,24 @@ class TestSignin(ApiTestCase): self._run_test('POST', 403, 'devtable', {u'username': 'E9RY', u'password': 'LQ0N'}) +class TestDetachExternal(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(DetachExternal, servicename='someservice') + + def test_post_anonymous(self): + self._run_test('POST', 401, None, {}) + + def test_post_freshuser(self): + self._run_test('POST', 200, 'freshuser', {}) + + def test_post_reader(self): + self._run_test('POST', 200, 'reader', {}) + + def test_post_devtable(self): + self._run_test('POST', 200, 'devtable', {}) + + class TestVerifyUser(ApiTestCase): def setUp(self): ApiTestCase.setUp(self) diff --git a/tools/reparsedockerfile.py b/tools/reparsedockerfile.py index 19dce6cd4..09ac3955f 100644 --- a/tools/reparsedockerfile.py +++ b/tools/reparsedockerfile.py @@ -15,4 +15,9 @@ for index, command in reversed(list(enumerate(parsed_dockerfile.commands))): parsed_dockerfile.commands.insert(new_command_index, env_command) break +image_and_tag_tuple = parsed_dockerfile.get_image_and_tag() +print image_and_tag_tuple +if image_and_tag_tuple is None or image_and_tag_tuple[0] is None: + raise Exception('Missing FROM command in Dockerfile') + print serialize_dockerfile(parsed_dockerfile)