diff --git a/application.py b/application.py index bb2b40ca1..0923f5d40 100644 --- a/application.py +++ b/application.py @@ -4,7 +4,6 @@ import os from app import app as application from data.model import db as model_db - # Initialize logging application.config['LOGGING_CONFIG']() diff --git a/config.py b/config.py index 683e13d0f..8c1844821 100644 --- a/config.py +++ b/config.py @@ -16,7 +16,7 @@ from test import analytics as fake_analytics class FlaskConfig(object): SECRET_KEY = '1cb18882-6d12-440d-a4cc-b7430fb5f884' - + JSONIFY_PRETTYPRINT_REGULAR = False class FlaskProdConfig(FlaskConfig): SESSION_COOKIE_SECURE = True diff --git a/data/model.py b/data/model.py index 72c4e1861..a27036d3a 100644 --- a/data/model.py +++ b/data/model.py @@ -109,7 +109,10 @@ def create_organization(name, email, creating_user): return new_org except InvalidUsernameException: - raise InvalidOrganizationException('Invalid organization name: %s' % name) + msg = ('Invalid organization name: %s Organization names must consist ' + + 'solely of lower case letters, numbers, and underscores. ' + + '[a-z0-9_]') % name + raise InvalidOrganizationException(msg) def create_robot(robot_shortname, parent): @@ -970,6 +973,41 @@ def delete_tag_and_images(namespace_name, repository_name, tag_name): store.remove(repository_path) +def garbage_collect_repository(namespace_name, repository_name): + # Get a list of all images used by tags in the repository + tag_query = (RepositoryTag + .select(RepositoryTag, Image) + .join(Image) + .switch(RepositoryTag) + .join(Repository) + .where(Repository.name == repository_name, + Repository.namespace == namespace_name)) + + referenced_anscestors = set() + for tag in tag_query: + # The anscestor list is in the format '/1/2/3/', extract just the ids + anscestor_id_strings = tag.image.ancestors.split('/')[1:-1] + ancestor_list = [int(img_id_str) for img_id_str in anscestor_id_strings] + referenced_anscestors = referenced_anscestors.union(set(ancestor_list)) + referenced_anscestors.add(tag.image.id) + + all_repo_images = get_repository_images(namespace_name, repository_name) + all_images = {int(img.id):img for img in all_repo_images} + to_remove = set(all_images.keys()).difference(referenced_anscestors) + + logger.info('Cleaning up unreferenced images: %s', to_remove) + + for image_id_to_remove in to_remove: + image_to_remove = all_images[image_id_to_remove] + image_path = store.image_path(namespace_name, repository_name, + image_to_remove.docker_image_id) + image_to_remove.delete_instance() + logger.debug('Deleting image storage: %s' % image_path) + store.remove(image_path) + + return len(to_remove) + + def get_tag_image(namespace_name, repository_name, tag_name): joined = Image.select().join(RepositoryTag).join(Repository) fetched = list(joined.where(Repository.name == repository_name, @@ -1021,7 +1059,8 @@ def create_or_update_tag(namespace_name, repository_name, tag_name, (namespace_name, repository_name)) try: - image = Image.get(Image.docker_image_id == tag_docker_image_id) + image = Image.get(Image.docker_image_id == tag_docker_image_id, + Image.repository == repo) except Image.DoesNotExist: raise DataModelException('Invalid image with id: %s' % tag_docker_image_id) diff --git a/endpoints/api.py b/endpoints/api.py index 027503f45..7022a97dd 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -262,7 +262,6 @@ def convert_user_to_organization(): @internal_api_call def change_user_details(): user = current_user.db_user() - user_data = request.get_json() try: @@ -315,6 +314,8 @@ def create_new_user(): @internal_api_call def signin_user(): signin_data = request.get_json() + if not signin_data: + abort(404) username = signin_data['username'] password = signin_data['password'] @@ -421,6 +422,7 @@ def get_matching_entities(prefix): team_data = [entity_team_view(team) for team in teams] user_data = [user_view(user) for user in users] + return jsonify({ 'results': team_data + user_data }) @@ -446,11 +448,16 @@ def create_organization(): existing = None try: - existing = (model.get_organization(org_data['name']) or - model.get_user(org_data['name'])) + existing = model.get_organization(org_data['name']) except model.InvalidOrganizationException: pass + if not existing: + try: + existing = model.get_user(org_data['name']) + except model.InvalidUserException: + pass + if existing: msg = 'A user or organization with this name already exists' return request_error(message=msg) @@ -604,9 +611,9 @@ def create_organization_prototype_permission(orgname): 'name' in details['activating_user']): activating_username = details['activating_user']['name'] - delegate = details['delegate'] - delegate_kind = delegate['kind'] - delegate_name = delegate['name'] + delegate = details['delegate'] if 'delegate' in details else {} + delegate_kind = delegate.get('kind', None) + delegate_name = delegate.get('name', None) delegate_username = delegate_name if delegate_kind == 'user' else None delegate_teamname = delegate_name if delegate_kind == 'team' else None @@ -622,7 +629,7 @@ def create_organization_prototype_permission(orgname): return request_error(message='Unknown activating user') if not delegate_user and not delegate_team: - return request_error(message='Missing delagate user or team') + return request_error(message='Missing delegate user or team') role_name = details['role'] @@ -1278,7 +1285,11 @@ def create_webhook(namespace, repository): def get_webhook(namespace, repository, public_id): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): - webhook = model.get_webhook(namespace, repository, public_id) + try: + webhook = model.get_webhook(namespace, repository, public_id) + except model.InvalidWebhookException: + abort(404) + return jsonify(webhook_view(webhook)) abort(403) # Permission denied @@ -1673,7 +1684,11 @@ def list_repo_tokens(namespace, repository): def get_tokens(namespace, repository, code): permission = AdministerRepositoryPermission(namespace, repository) if permission.can(): - perm = model.get_repo_delegate_token(namespace, repository, code) + try: + perm = model.get_repo_delegate_token(namespace, repository, code) + except model.InvalidTokenException: + abort(404) + return jsonify(token_view(perm)) abort(403) # Permission denied @@ -1810,6 +1825,8 @@ def set_card(user, token): cus.save() except stripe.CardError as e: return carderror_response(e) + except stripe.InvalidRequestError as e: + return carderror_response(e) return get_card(user) diff --git a/endpoints/index.py b/endpoints/index.py index 40a226270..5106873cb 100644 --- a/endpoints/index.py +++ b/endpoints/index.py @@ -255,6 +255,8 @@ def update_images(namespace, repository): event = app.config['USER_EVENTS'].get_event(username) event.publish_event_data('docker-cli', user_data) + num_removed = model.garbage_collect_repository(namespace, repository) + # Generate a job for each webhook that has been added to this repo webhooks = model.list_webhooks(namespace, repository) for webhook in webhooks: @@ -270,7 +272,8 @@ def update_images(namespace, repository): 'homepage': 'https://quay.io/repository/%s' % repo_string, 'visibility': repo.visibility.name, 'updated_tags': updated_tags, - 'pushed_image_count': len(image_with_checksums), + 'pushed_image_count': len(image_with_checksums), + 'pruned_image_count': num_removed, } webhook_queue.put(json.dumps(webhook_data)) diff --git a/endpoints/tags.py b/endpoints/tags.py index f6b0e1163..a4ed782ab 100644 --- a/endpoints/tags.py +++ b/endpoints/tags.py @@ -73,22 +73,8 @@ def delete_tag(namespace, repository, tag): if permission.can(): model.delete_tag(namespace, repository, tag) + model.garbage_collect_repository(namespace, repository) - return make_response('Deleted', 204) - - abort(403) - - -@tags.route('/repositories//tags', - methods=['DELETE']) -@process_auth -@parse_repository_name -def delete_repository_tags(namespace, repository): - permission = ModifyRepositoryPermission(namespace, repository) - - if permission.can(): - model.delete_all_repository_tags(namespace, repository) - - return make_response('Deleted', 204) + return make_response('Deleted', 200) abort(403) diff --git a/endpoints/web.py b/endpoints/web.py index d4df03036..cc56eeac6 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -94,6 +94,12 @@ def contact(): return index('') +@web.route('/about/') +@no_cache +def about(): + return index('') + + @web.route('/new/') @no_cache def new(): diff --git a/static/css/quay.css b/static/css/quay.css index 0db3d8359..9fbf00d6f 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -2898,4 +2898,36 @@ pre.command:before { .form-inline { display: inline-block; margin-left: 10px; +} + +/*********************************************/ + +.contact-options .option-twitter .fa-circle { + color: #00b0ed; +} + +.contact-options .option-phone .fa-circle { + color: #1dd924; +} + +.contact-options .option-irc .fa-circle { + color: #e52f00; +} + +.contact-options .option-email .fa-circle { + color: #1b72f1; +} + +.about-us .row { + margin-bottom: 30px; +} + +.about-basic-icon { + display: inline-block; + margin-right: 10px; + color: #555; +} + +.about-basic-text { + display: inline-block; } \ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js index a2473e151..7cfe13e24 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -427,6 +427,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu var planService = {}; var listeners = []; + var previousSubscribeFailure = false; + planService.getFreePlan = function() { return 'free'; }; @@ -616,12 +618,15 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu if (orgname && !planService.isOrgCompatible(plan)) { return; } planService.getCardInfo(orgname, function(cardInfo) { - if (plan.price > 0 && !cardInfo.last4) { + if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4)) { planService.showSubscribeDialog($scope, orgname, planId, callbacks); return; } + previousSubscribeFailure = false; + planService.setSubscription(orgname, planId, callbacks['success'], function(resp) { + previousSubscribeFailure = true; planService.handleCardError(resp); callbacks['failure'](resp); }); @@ -784,11 +789,12 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu controller: TutorialCtrl}). when('/contact/', {title: 'Contact Us', description:'Different ways for you to get a hold of us when you need us most.', templateUrl: '/static/partials/contact.html', controller: ContactCtrl}). + when('/about/', {title: 'About Us', description:'Information about the Quay.io team and the company.', templateUrl: '/static/partials/about.html'}). when('/plans/', {title: 'Plans and Pricing', description: 'Plans and pricing for private docker repositories on Quay.io', templateUrl: '/static/partials/plans.html', controller: PlansCtrl}). when('/security/', {title: 'Security', description: 'Security features used when transmitting and storing data', - templateUrl: '/static/partials/security.html', controller: SecurityCtrl}). - when('/signin/', {title: 'Sign In', description: 'Sign into Quay.io', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}). + templateUrl: '/static/partials/security.html'}). + when('/signin/', {title: 'Sign In', description: 'Sign into Quay.io', templateUrl: '/static/partials/signin.html'}). when('/new/', {title: 'Create new repository', description: 'Create a new public or private docker repository, optionally constructing from a dockerfile', templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}). when('/organizations/', {title: 'Organizations', description: 'Private docker repository hosting for businesses and organizations', diff --git a/static/js/controllers.js b/static/js/controllers.js index adbf680da..c14b5d7b4 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -15,8 +15,14 @@ $.fn.clipboardCopy = function() { }); }; -function SigninCtrl($scope) { -}; +function GuideCtrl() { +} + +function SecurityCtrl($scope) { +} + +function ContactCtrl($scope) { +} function PlansCtrl($scope, $location, UserService, PlanService) { // Load the list of plans. @@ -42,9 +48,6 @@ function PlansCtrl($scope, $location, UserService, PlanService) { }; } -function GuideCtrl($scope) { -} - function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService) { $scope.tour = { 'title': 'Quay.io Tutorial', @@ -178,12 +181,6 @@ function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService) { }; } -function SecurityCtrl($scope) { -} - -function ContactCtrl($scope) { -} - function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { $scope.namespace = null; $scope.page = 1; @@ -756,6 +753,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope $scope.permissions = {'team': [], 'user': []}; $scope.logsShown = 0; $scope.deleting = false; + $scope.permissionCache = {}; $scope.buildEntityForPermission = function(name, permission, kind) { diff --git a/static/partials/about.html b/static/partials/about.html new file mode 100644 index 000000000..f59865cae --- /dev/null +++ b/static/partials/about.html @@ -0,0 +1,78 @@ +
+

+ About Us +

+ +
+
+

The Basics

+
+
+
+
+ Founded
+ 2012 +
+
+
+
+
+ Location
+ New York City, NY +
+
+
+
+
+ Worker Bees
+ 2 +
+
+
+ +
+
+

Our Story

+

Quay.io was originally created out of necessesity when we wanted to use Docker containers with DevTable IDE. We were using Docker containers to host and isolate server processes invoked on behalf of our users and often running their code. We started by building the Docker image dynamically whenever we spun up a new host node. The image was monolithic. It was too large, took too long to build, and was hard to manage conflicts. It was everything that Docker wasn't supposed to be. When we decided to split it up into pre-built images and host them somewhere, we noticed that there wasn't a good place to host images securely. Determined to scratch our own itch, we built Quay.io, and officially launched it as an aside in our presentation to the Docker New York City Meetup on October 2nd, 2013.

+

Since that time, our users have demanded that Quay.io become our main focus. Our customers rely on us to make sure they can store and distribute their Docker images, and we understand that solemn responsibility. Our customers have been fantastic with giving us great feedback and suggestions. We are working as hard as we can to deliver on the promise and execute our vision of what a top notch Docker registry should be. We thank you for taking this journey with us.

+
+
+ +
+
+

The Team

+ Our team is composed of two software engineers turned entrepreneurs: +
+
+
+
+

Jacob Moshenko
+ Co-Founder

+
+ +
+ +
+
+

Jacob graduated from The University of Michigan with a Bachelors in Computer Engineering. From there he allowed his love of flight and mountains to lure him to Seattle where he took a job with Boeing Commercial Airplanes working on the world's most accurate flight simulator. When he realized how much he also loved web development, he moved to Amazon to work on the e-commerce back-end. Finally, desiring to move to New York City, he moved to Google, where he worked on several products related to Google APIs.

+
+
+
+
+

Joseph Schorr
+ Co-Founder

+
+ +
+ +
+
+

Joseph graduated from University of Pennsylvania with a Bachelors and Masters in Computer Science. After a record setting (probably) five internships with Google, he took a full time position there to continue his work on exciting products such as Google Spreadsheets, the Google Closure Compiler, and Google APIs. Joseph was one of the original duo responsible for inventing the language and framework on which DevTable is built.

+
+
+
+
+

With a combined 10 years experience building tools for software engineers, our founding team knows what it takes to make software engineers happy doing their work. Combined with our love for the web, we are ready to make a difference in the way people think about software development in the cloud.

+
+
+
\ No newline at end of file diff --git a/static/partials/contact.html b/static/partials/contact.html index fe7a7ea33..bdc7dd159 100644 --- a/static/partials/contact.html +++ b/static/partials/contact.html @@ -5,7 +5,7 @@
-
+
@@ -14,7 +14,7 @@

support@quay.io

-
+
@@ -23,7 +23,7 @@

Freenode: #quayio

-
+
@@ -32,7 +32,7 @@

888-930-3475

-
+
diff --git a/static/partials/new-organization.html b/static/partials/new-organization.html index 79d29e3f9..05abfe185 100644 --- a/static/partials/new-organization.html +++ b/static/partials/new-organization.html @@ -54,7 +54,7 @@ + data-placement="right" data-container="body"> This will also be the namespace for your repositories
diff --git a/static/sitemap.xml b/static/sitemap.xml index db9440376..f947dd3ce 100644 --- a/static/sitemap.xml +++ b/static/sitemap.xml @@ -11,12 +11,20 @@ https://quay.io/organizations/ - monthly + weekly https://quay.io/repository/ always + + https://quay.io/contact/ + monthly + + + https://quay.io/about/ + monthly + https://quay.io/tos monthly diff --git a/templates/base.html b/templates/base.html index 5ee39f371..e240d737e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -117,10 +117,11 @@ var isProd = document.location.hostname === 'quay.io';
diff --git a/test/data/test.db b/test/data/test.db index 6062f0e97..ef37436e4 100644 Binary files a/test/data/test.db and b/test/data/test.db differ diff --git a/test/specs.py b/test/specs.py index e0a037655..65fb4332b 100644 --- a/test/specs.py +++ b/test/specs.py @@ -231,9 +231,9 @@ def build_specs(): TestSpec(url_for('api.get_webhook', repository=PUBLIC_REPO, public_id=FAKE_WEBHOOK), admin_code=403), TestSpec(url_for('api.get_webhook', repository=ORG_REPO, - public_id=FAKE_WEBHOOK), admin_code=400), + public_id=FAKE_WEBHOOK), admin_code=404), TestSpec(url_for('api.get_webhook', repository=PRIVATE_REPO, - public_id=FAKE_WEBHOOK), admin_code=400), + public_id=FAKE_WEBHOOK), admin_code=404), TestSpec(url_for('api.list_webhooks', repository=PUBLIC_REPO), admin_code=403), @@ -382,9 +382,9 @@ def build_specs(): TestSpec(url_for('api.get_tokens', repository=PUBLIC_REPO, code=FAKE_TOKEN), admin_code=403), TestSpec(url_for('api.get_tokens', repository=ORG_REPO, code=FAKE_TOKEN), - admin_code=400), + admin_code=404), TestSpec(url_for('api.get_tokens', repository=PRIVATE_REPO, - code=FAKE_TOKEN), admin_code=400), + code=FAKE_TOKEN), admin_code=404), TestSpec(url_for('api.create_token', repository=PUBLIC_REPO), admin_code=403).set_method('POST'), @@ -587,13 +587,4 @@ def build_index_specs(): IndexTestSpec(url_for('tags.delete_tag', repository=ORG_REPO, tag=FAKE_TAG_NAME), NO_REPO, 403, 403, 403, 400).set_method('DELETE'), - - IndexTestSpec(url_for('tags.delete_repository_tags', - repository=PUBLIC_REPO), - NO_REPO, 403, 403, 403, 403).set_method('DELETE'), - IndexTestSpec(url_for('tags.delete_repository_tags', - repository=PRIVATE_REPO), - NO_REPO, 403, 403, 403, 204).set_method('DELETE'), - IndexTestSpec(url_for('tags.delete_repository_tags', repository=ORG_REPO), - NO_REPO, 403, 403, 403, 204).set_method('DELETE'), ] diff --git a/test/test_api_usage.py b/test/test_api_usage.py new file mode 100644 index 000000000..fca9f2eed --- /dev/null +++ b/test/test_api_usage.py @@ -0,0 +1,1337 @@ +import unittest +import json as py_json + +from flask import url_for +from endpoints.api import api +from app import app +from initdb import setup_database_for_testing, finished_database_for_testing +from data import model + +app.register_blueprint(api, url_prefix='/api') + +NO_ACCESS_USER = 'freshuser' +READ_ACCESS_USER = 'reader' +ADMIN_ACCESS_USER = 'devtable' +PUBLIC_USER = 'public' + +ORG_REPO = 'orgrepo' + +ORGANIZATION = 'buynlarge' + +NEW_USER_DETAILS = { + 'username': 'bobby', + 'password': 'password', + 'email': 'bobby@tables.com', +} + + +class ApiTestCase(unittest.TestCase): + def setUp(self): + setup_database_for_testing(self) + self.app = app.test_client() + self.ctx = app.test_request_context() + self.ctx.__enter__() + + def tearDown(self): + finished_database_for_testing(self) + self.ctx.__exit__(True, None, None) + + def getJsonResponse(self, method_name, params={}): + rv = self.app.get(url_for(method_name, **params)) + self.assertEquals(200, rv.status_code) + data = rv.data + parsed = py_json.loads(data) + return parsed + + def postResponse(self, method_name, params={}, data={}, expected_code=200): + rv = self.app.post(url_for(method_name, **params), + data=py_json.dumps(data), + headers={"Content-Type": "application/json"}) + self.assertEquals(rv.status_code, expected_code) + return rv.data + + def getResponse(self, method_name, params={}, expected_code=200): + rv = self.app.get(url_for(method_name, **params)) + self.assertEquals(rv.status_code, expected_code) + return rv.data + + def deleteResponse(self, method_name, params={}, expected_code=204): + rv = self.app.delete(url_for(method_name, **params)) + self.assertEquals(rv.status_code, expected_code) + return rv.data + + def postJsonResponse(self, method_name, params={}, data={}, + expected_code=200): + rv = self.app.post(url_for(method_name, **params), + data=py_json.dumps(data), + headers={"Content-Type": "application/json"}) + + if rv.status_code != expected_code: + print 'Mismatch data for method %s: %s' % (method_name, rv.data) + + self.assertEquals(rv.status_code, expected_code) + data = rv.data + parsed = py_json.loads(data) + return parsed + + def putJsonResponse(self, method_name, params={}, data={}, + expected_code=200): + rv = self.app.put(url_for(method_name, **params), data=py_json.dumps(data), + headers={"Content-Type": "application/json"}) + + if rv.status_code != expected_code: + print 'Mismatch data for method %s: %s' % (method_name, rv.data) + + self.assertEquals(rv.status_code, expected_code) + data = rv.data + parsed = py_json.loads(data) + return parsed + + def login(self, username, password='password'): + return self.postJsonResponse('api.signin_user', + data=dict(username=username, + password=password)) + + +class TestDiscovery(ApiTestCase): + def test_discovery(self): + """ Basic sanity check that discovery returns valid JSON in the expected + format. """ + json = self.getJsonResponse('api.discovery') + found = set([]) + for method_info in json['endpoints']: + found.add(method_info['name']) + + assert 'discovery' in found + + +class TestPlans(ApiTestCase): + def test_plans(self): + """ Basic sanity check that the plans are returned in the expectedformat. + """ + json = self.getJsonResponse('api.list_plans') + found = set([]) + for method_info in json['plans']: + found.add(method_info['stripeId']) + + assert 'free' in found + + +class TestLoggedInUser(ApiTestCase): + def test_guest(self): + json = self.getJsonResponse('api.get_logged_in_user') + assert json['anonymous'] == True + + def test_user(self): + self.login(READ_ACCESS_USER) + json = self.getJsonResponse('api.get_logged_in_user') + assert json['anonymous'] == False + assert json['username'] == READ_ACCESS_USER + + +class TestGetUserPrivateCount(ApiTestCase): + def test_nonallowed(self): + self.login(READ_ACCESS_USER) + json = self.getJsonResponse('api.get_user_private_count') + assert json['privateCount'] == 0 + assert json['reposAllowed'] == 0 + + def test_allowed(self): + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse('api.get_user_private_count') + assert json['privateCount'] == 6 + assert json['reposAllowed'] > 0 + + +class TestConvertToOrganization(ApiTestCase): + def test_sameadminuser(self): + self.login(READ_ACCESS_USER) + json = self.postJsonResponse('api.convert_user_to_organization', + data={'adminUser': READ_ACCESS_USER, + 'adminPassword': 'password'}, + expected_code=400) + + self.assertEqual('The admin user is not valid', json['message']) + + def test_invalidadminuser(self): + self.login(READ_ACCESS_USER) + json = self.postJsonResponse('api.convert_user_to_organization', + data={'adminUser': 'unknownuser', + 'adminPassword': 'password'}, + expected_code=400) + + self.assertEqual('The admin user credentials are not valid', + json['message']) + + def test_invalidadminpassword(self): + self.login(READ_ACCESS_USER) + json = self.postJsonResponse('api.convert_user_to_organization', + data={'adminUser': ADMIN_ACCESS_USER, + 'adminPassword': 'invalidpass'}, + expected_code=400) + + self.assertEqual('The admin user credentials are not valid', + json['message']) + + def test_convert(self): + self.login(READ_ACCESS_USER) + json = self.postJsonResponse('api.convert_user_to_organization', + data={'adminUser': ADMIN_ACCESS_USER, + 'adminPassword': 'password', + 'plan': 'free'}) + + self.assertEqual(True, json['success']) + + # Verify the organization exists. + organization = model.get_organization(READ_ACCESS_USER) + assert organization is not None + + # Verify the admin user is the org's admin. + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse('api.get_organization', + params=dict(orgname=READ_ACCESS_USER)) + + self.assertEquals(READ_ACCESS_USER, json['name']) + self.assertEquals(True, json['is_admin']) + + +class TestChangeUserDetails(ApiTestCase): + def test_changepassword(self): + self.login(READ_ACCESS_USER) + self.putJsonResponse('api.change_user_details', + data=dict(password='newpasswordiscool')) + self.login(READ_ACCESS_USER, password='newpasswordiscool') + + def test_changeinvoiceemail(self): + self.login(READ_ACCESS_USER) + + json = self.putJsonResponse('api.change_user_details', + data=dict(invoice_email=True)) + self.assertEquals(True, json['invoice_email']) + + json = self.putJsonResponse('api.change_user_details', + data=dict(invoice_email=False)) + self.assertEquals(False, json['invoice_email']) + + +class TestCreateNewUser(ApiTestCase): + def test_existingusername(self): + json = self.postJsonResponse('api.create_new_user', + data=dict(username=READ_ACCESS_USER, + password='password', + email='test@example.com'), + expected_code=400) + + self.assertEquals('The username already exists', json['message']) + + def test_createuser(self): + data = self.postResponse('api.create_new_user', + data=NEW_USER_DETAILS, + expected_code=201) + self.assertEquals('Created', data) + + +class TestSignout(ApiTestCase): + def test_signout(self): + self.login(READ_ACCESS_USER) + + json = self.getJsonResponse('api.get_logged_in_user') + assert json['username'] == READ_ACCESS_USER + + self.postResponse('api.logout') + + json = self.getJsonResponse('api.get_logged_in_user') + assert json['anonymous'] == True + + +class TestGetMatchingEntities(ApiTestCase): + def test_notinorg(self): + self.login(NO_ACCESS_USER) + + json = self.getJsonResponse('api.get_matching_entities', + params=dict(prefix='o', namespace=ORGANIZATION, + includeTeams=True)) + + names = set([r['name'] for r in json['results']]) + assert 'outsideorg' in names + assert not 'owners' in names + + def test_inorg(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_matching_entities', + params=dict(prefix='o', namespace=ORGANIZATION, + includeTeams=True)) + + names = set([r['name'] for r in json['results']]) + assert 'outsideorg' in names + assert 'owners' in names + + +class TestCreateOrganization(ApiTestCase): + def test_existinguser(self): + self.login(ADMIN_ACCESS_USER) + + json = self.postJsonResponse('api.create_organization', + data=dict(name=ADMIN_ACCESS_USER), + expected_code=400) + + self.assertEquals('A user or organization with this name already exists', + json['message']) + + def test_existingorg(self): + self.login(ADMIN_ACCESS_USER) + + json = self.postJsonResponse('api.create_organization', + data=dict(name=ORGANIZATION), + expected_code=400) + + self.assertEquals('A user or organization with this name already exists', + json['message']) + + def test_createorg(self): + self.login(ADMIN_ACCESS_USER) + + data = self.postResponse('api.create_organization', + data=dict(name='neworg', + email='test@example.com'), + expected_code=201) + + self.assertEquals('Created', data) + + # Ensure the org was created. + organization = model.get_organization('neworg') + assert organization is not None + + # Verify the admin user is the org's admin. + json = self.getJsonResponse('api.get_organization', + params=dict(orgname='neworg')) + self.assertEquals('neworg', json['name']) + self.assertEquals(True, json['is_admin']) + + +class TestGetOrganization(ApiTestCase): + def test_unknownorg(self): + self.login(ADMIN_ACCESS_USER) + self.getResponse('api.get_organization', params=dict(orgname='notvalid'), + expected_code=403) + + def test_cannotaccess(self): + self.login(NO_ACCESS_USER) + self.getResponse('api.get_organization', params=dict(orgname=ORGANIZATION), + expected_code=403) + + def test_getorganization(self): + self.login(READ_ACCESS_USER) + json = self.getJsonResponse('api.get_organization', + params=dict(orgname=ORGANIZATION)) + + self.assertEquals(ORGANIZATION, json['name']) + self.assertEquals(False, json['is_admin']) + + def test_getorganization_asadmin(self): + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse('api.get_organization', + params=dict(orgname=ORGANIZATION)) + + self.assertEquals(ORGANIZATION, json['name']) + self.assertEquals(True, json['is_admin']) + + +class TestChangeOrganizationDetails(ApiTestCase): + def test_changeinvoiceemail(self): + self.login(ADMIN_ACCESS_USER) + + json = self.putJsonResponse('api.change_organization_details', + params=dict(orgname=ORGANIZATION), + data=dict(invoice_email=True)) + + self.assertEquals(True, json['invoice_email']) + + json = self.putJsonResponse('api.change_organization_details', + params=dict(orgname=ORGANIZATION), + data=dict(invoice_email=False)) + self.assertEquals(False, json['invoice_email']) + + + def test_changemail(self): + self.login(ADMIN_ACCESS_USER) + + json = self.putJsonResponse('api.change_organization_details', + params=dict(orgname=ORGANIZATION), + data=dict(email='newemail@example.com')) + + self.assertEquals('newemail@example.com', json['email']) + + +class TestGetOrganizationPrototypes(ApiTestCase): + def test_getprototypes(self): + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse('api.get_organization_prototype_permissions', + params=dict(orgname=ORGANIZATION)) + + assert len(json['prototypes']) > 0 + + +class TestCreateOrganizationPrototypes(ApiTestCase): + def test_invaliduser(self): + self.login(ADMIN_ACCESS_USER) + + json = self.postJsonResponse('api.create_organization_prototype_permission', + params=dict(orgname=ORGANIZATION), + data=dict(activating_user={'name': 'unknownuser'}, + role='read', + delegate={'kind': 'team', 'name': 'owners'}), + expected_code=400) + + self.assertEquals('Unknown activating user', json['message']) + + + def test_missingdelegate(self): + self.login(ADMIN_ACCESS_USER) + + json = self.postJsonResponse('api.create_organization_prototype_permission', + params=dict(orgname=ORGANIZATION), + data=dict(role='read'), + expected_code=400) + + self.assertEquals('Missing delegate user or team', json['message']) + + def test_createprototype(self): + self.login(ADMIN_ACCESS_USER) + + json = self.postJsonResponse('api.create_organization_prototype_permission', + params=dict(orgname=ORGANIZATION), + data=dict(role='read', + delegate={'kind': 'team', + 'name': 'readers'})) + + self.assertEquals('read', json['role']) + pid = json['id'] + + # Verify the prototype exists. + json = self.getJsonResponse('api.get_organization_prototype_permissions', + params=dict(orgname=ORGANIZATION)) + + ids = set([p['id'] for p in json['prototypes']]) + assert pid in ids + + +class TestDeleteOrganizationPrototypes(ApiTestCase): + def test_deleteprototype(self): + self.login(ADMIN_ACCESS_USER) + + # Get the existing prototypes + json = self.getJsonResponse('api.get_organization_prototype_permissions', + params=dict(orgname=ORGANIZATION)) + + ids = [p['id'] for p in json['prototypes']] + pid = ids[0] + + # Delete a prototype. + self.deleteResponse('api.delete_organization_prototype_permission', + params=dict(orgname=ORGANIZATION, prototypeid=pid)) + + # Verify the prototype no longer exists. + json = self.getJsonResponse('api.get_organization_prototype_permissions', + params=dict(orgname=ORGANIZATION)) + + newids = [p['id'] for p in json['prototypes']] + assert not pid in newids + + +class TestUpdateOrganizationPrototypes(ApiTestCase): + def test_updateprototype(self): + self.login(ADMIN_ACCESS_USER) + + # Get the existing prototypes + json = self.getJsonResponse('api.get_organization_prototype_permissions', + params=dict(orgname=ORGANIZATION)) + + ids = [p['id'] for p in json['prototypes']] + pid = ids[0] + + # Update a prototype. + json = self.putJsonResponse('api.delete_organization_prototype_permission', + params=dict(orgname=ORGANIZATION, + prototypeid=pid), data=dict(role='admin')) + + self.assertEquals('admin', json['role']) + + +class TestGetOrganiaztionMembers(ApiTestCase): + def test_getmembers(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_organization_members', + params=dict(orgname=ORGANIZATION)) + + assert ADMIN_ACCESS_USER in json['members'] + assert READ_ACCESS_USER in json['members'] + assert not NO_ACCESS_USER in json['members'] + + def test_getspecificmember(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_organization_member', + params=dict(orgname=ORGANIZATION, + membername=ADMIN_ACCESS_USER)) + + self.assertEquals(ADMIN_ACCESS_USER, json['member']['name']) + self.assertEquals('user', json['member']['kind']) + + assert 'owners' in json['member']['teams'] + + +class TestGetOrganizationPrivateAllowed(ApiTestCase): + def test_existingorg(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_organization_private_allowed', + params=dict(orgname=ORGANIZATION)) + + self.assertEquals(True, json['privateAllowed']) + assert not 'reposAllowed' in json + + + def test_neworg(self): + self.login(ADMIN_ACCESS_USER) + + data = self.postResponse('api.create_organization', + data=dict(name='neworg', + email='test@example.com'), + expected_code=201) + + json = self.getJsonResponse('api.get_organization_private_allowed', + params=dict(orgname='neworg')) + + self.assertEquals(False, json['privateAllowed']) + + +class TestUpdateOrganizationTeam(ApiTestCase): + def test_updateexisting(self): + self.login(ADMIN_ACCESS_USER) + + data = self.postJsonResponse('api.update_organization_team', + params=dict(orgname=ORGANIZATION, + teamname='readers'), + data=dict(description='My cool team', + role='creator')) + + self.assertEquals('My cool team', data['description']) + self.assertEquals('creator', data['role']) + + def test_attemptchangeroleonowners(self): + self.login(ADMIN_ACCESS_USER) + + self.postResponse('api.update_organization_team', + params=dict(orgname=ORGANIZATION, teamname='owners'), + data=dict(role = 'creator'), + expected_code=400) + + def test_createnewteam(self): + self.login(ADMIN_ACCESS_USER) + + data = self.putJsonResponse('api.update_organization_team', + params=dict(orgname=ORGANIZATION, + teamname='newteam'), + data=dict(description='My cool team', + role='member'), + expected_code=201) + + self.assertEquals('My cool team', data['description']) + self.assertEquals('member', data['role']) + + # Verify the team was created. + json = self.getJsonResponse('api.get_organization', + params=dict(orgname=ORGANIZATION)) + assert 'newteam' in json['teams'] + + +class TestDeleteOrganizationTeam(ApiTestCase): + def test_deleteteam(self): + self.login(ADMIN_ACCESS_USER) + + self.deleteResponse('api.delete_organization_team', + params=dict(orgname=ORGANIZATION, teamname='readers')) + + # Make sure the team was deleted + json = self.getJsonResponse('api.get_organization', + params=dict(orgname=ORGANIZATION)) + assert not 'readers' in json['teams'] + + def test_attemptdeleteowners(self): + self.login(ADMIN_ACCESS_USER) + + self.deleteResponse('api.delete_organization_team', + params=dict(orgname=ORGANIZATION, teamname='owners'), + expected_code=400) + + +class TestGetOrganizationTeamMembers(ApiTestCase): + def test_invalidteam(self): + self.login(ADMIN_ACCESS_USER) + + self.getResponse('api.get_organization_team_members', + params=dict(orgname=ORGANIZATION, teamname='notvalid'), + expected_code=404) + + def test_getmembers(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_organization_team_members', + params=dict(orgname=ORGANIZATION, + teamname='readers')) + + assert READ_ACCESS_USER in json['members'] + + +class TestUpdateOrganizationTeamMember(ApiTestCase): + def test_addmember(self): + self.login(ADMIN_ACCESS_USER) + + self.postJsonResponse('api.update_organization_team_member', + params=dict(orgname=ORGANIZATION, teamname='readers', + membername=NO_ACCESS_USER)) + + + # Verify the user was added to the team. + json = self.getJsonResponse('api.get_organization_team_members', + params=dict(orgname=ORGANIZATION, + teamname='readers')) + + assert NO_ACCESS_USER in json['members'] + + +class TestDeleteOrganizationTeamMember(ApiTestCase): + def test_deletemember(self): + self.login(ADMIN_ACCESS_USER) + + self.deleteResponse('api.delete_organization_team_member', + params=dict(orgname=ORGANIZATION, teamname='readers', + membername=READ_ACCESS_USER)) + + + # Verify the user was removed from the team. + json = self.getJsonResponse('api.get_organization_team_members', + params=dict(orgname=ORGANIZATION, + teamname='readers')) + + assert not READ_ACCESS_USER in json['members'] + + +class TestCreateRepo(ApiTestCase): + def test_duplicaterepo(self): + self.login(ADMIN_ACCESS_USER) + + json = self.postJsonResponse('api.create_repo', + data=dict(repository='simple', + visibility='public'), + expected_code=400) + + self.assertEquals('Repository already exists', json['message']) + + + def test_createrepo(self): + self.login(ADMIN_ACCESS_USER) + + json = self.postJsonResponse('api.create_repo', + data=dict(repository='newrepo', + visibility='public', + description='')) + + + self.assertEquals(ADMIN_ACCESS_USER, json['namespace']) + self.assertEquals('newrepo', json['name']) + + + def test_createrepo_underorg(self): + self.login(ADMIN_ACCESS_USER) + + json = self.postJsonResponse('api.create_repo', + data=dict(namespace=ORGANIZATION, + repository='newrepo', + visibility='private', + description='')) + + self.assertEquals(ORGANIZATION, json['namespace']) + self.assertEquals('newrepo', json['name']) + + +class TestFindRepos(ApiTestCase): + def test_findrepos_asguest(self): + json = self.getJsonResponse('api.find_repos', params=dict(query='p')) + assert len(json['repositories']) == 1 + + self.assertEquals(json['repositories'][0]['namespace'], 'public') + self.assertEquals(json['repositories'][0]['name'], 'publicrepo') + + def test_findrepos_asuser(self): + self.login(NO_ACCESS_USER) + + json = self.getJsonResponse('api.find_repos', params=dict(query='p')) + assert len(json['repositories']) == 1 + + self.assertEquals(json['repositories'][0]['namespace'], 'public') + self.assertEquals(json['repositories'][0]['name'], 'publicrepo') + + def test_findrepos_orgmember(self): + self.login(READ_ACCESS_USER) + + json = self.getJsonResponse('api.find_repos', params=dict(query='p')) + assert len(json['repositories']) > 1 + + +class TestListRepos(ApiTestCase): + def test_listrepos_asguest(self): + json = self.getJsonResponse('api.list_repos', params=dict(public=True)) + assert len(json['repositories']) == 0 + + def test_listrepos_orgmember(self): + self.login(READ_ACCESS_USER) + json = self.getJsonResponse('api.list_repos', params=dict(public=True)) + assert len(json['repositories']) > 1 + + def test_listrepos_filter(self): + self.login(READ_ACCESS_USER) + json = self.getJsonResponse('api.list_repos', + params=dict(namespace=ORGANIZATION, + public=False)) + + for repo in json['repositories']: + self.assertEquals(ORGANIZATION, repo['namespace']) + + def test_listrepos_limit(self): + self.login(READ_ACCESS_USER) + json = self.getJsonResponse('api.list_repos', params=dict(limit=2)) + + assert len(json['repositories']) == 2 + + +class TestUpdateRepo(ApiTestCase): + SIMPLE_REPO = ADMIN_ACCESS_USER + '/simple' + def test_updatedescription(self): + self.login(ADMIN_ACCESS_USER) + + self.putJsonResponse('api.update_repo', + params=dict(repository=self.SIMPLE_REPO), + data=dict(description='Some cool repo')) + + # Verify the repo description was updated. + json = self.getJsonResponse('api.get_repo', + params=dict(repository=self.SIMPLE_REPO)) + + self.assertEquals('Some cool repo', json['description']) + + +class TestChangeRepoVisibility(ApiTestCase): + SIMPLE_REPO = ADMIN_ACCESS_USER + '/simple' + def test_changevisibility(self): + self.login(ADMIN_ACCESS_USER) + + # Make public. + self.postJsonResponse('api.change_repo_visibility', + params=dict(repository=self.SIMPLE_REPO), + data=dict(visibility='public')) + + # Verify the visibility. + json = self.getJsonResponse('api.get_repo', + params=dict(repository=self.SIMPLE_REPO)) + + self.assertEquals(True, json['is_public']) + + # Make private. + self.postJsonResponse('api.change_repo_visibility', + params=dict(repository=self.SIMPLE_REPO), + data=dict(visibility='private')) + + # Verify the visibility. + json = self.getJsonResponse('api.get_repo', + params=dict(repository=self.SIMPLE_REPO)) + + self.assertEquals(False, json['is_public']) + + +class TestDeleteRepository(ApiTestCase): + SIMPLE_REPO = ADMIN_ACCESS_USER + '/simple' + def test_deleterepo(self): + self.login(ADMIN_ACCESS_USER) + + self.deleteResponse('api.delete_repository', + params=dict(repository=self.SIMPLE_REPO)) + + # Verify the repo was deleted. + self.getResponse('api.get_repo', + params=dict(repository=self.SIMPLE_REPO), + expected_code=404) + + +class TestGetRepository(ApiTestCase): + PUBLIC_REPO = PUBLIC_USER + '/publicrepo' + def test_getrepo_public_asguest(self): + json = self.getJsonResponse('api.get_repo', + params=dict(repository=self.PUBLIC_REPO)) + + self.assertEquals(PUBLIC_USER, json['namespace']) + self.assertEquals('publicrepo', json['name']) + + self.assertEquals(True, json['is_public']) + self.assertEquals(False, json['is_organization']) + self.assertEquals(False, json['is_building']) + + self.assertEquals(False, json['can_write']) + self.assertEquals(False, json['can_admin']) + + assert 'latest' in json['tags'] + + def test_getrepo_public_asowner(self): + self.login(PUBLIC_USER) + + json = self.getJsonResponse('api.get_repo', + params=dict(repository=self.PUBLIC_REPO)) + + self.assertEquals(False, json['is_organization']) + self.assertEquals(True, json['can_write']) + self.assertEquals(True, json['can_admin']) + + def test_getrepo_building(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_repo', + params=dict(repository=ADMIN_ACCESS_USER + '/building')) + + self.assertEquals(True, json['can_write']) + self.assertEquals(True, json['can_admin']) + self.assertEquals(True, json['is_building']) + self.assertEquals(False, json['is_organization']) + + def test_getrepo_org_asnonmember(self): + self.getResponse('api.get_repo', + params=dict(repository=ORGANIZATION + '/' + ORG_REPO), + expected_code=403) + + def test_getrepo_org_asreader(self): + self.login(READ_ACCESS_USER) + + json = self.getJsonResponse('api.get_repo', + params=dict(repository=ORGANIZATION + '/' + ORG_REPO)) + + self.assertEquals(ORGANIZATION, json['namespace']) + self.assertEquals(ORG_REPO, json['name']) + + self.assertEquals(False, json['can_write']) + self.assertEquals(False, json['can_admin']) + + self.assertEquals(True, json['is_organization']) + + def test_getrepo_org_asadmin(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_repo', + params=dict(repository=ORGANIZATION + '/' + ORG_REPO)) + + self.assertEquals(True, json['can_write']) + self.assertEquals(True, json['can_admin']) + + self.assertEquals(True, json['is_organization']) + + +class TestGetRepoBuilds(ApiTestCase): + def test_getrepo_nobuilds(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_repo_builds', + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + assert len(json['builds']) == 0 + + def test_getrepobuilds(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.get_repo_builds', + params=dict(repository=ADMIN_ACCESS_USER + '/building')) + + assert len(json['builds']) > 0 + build = json['builds'][0] + + assert 'id' in build + assert 'status' in build + assert 'message' in build + + +class TestRequearRepoBuild(ApiTestCase): + def test_requestrepobuild(self): + self.login(ADMIN_ACCESS_USER) + + # Ensure where not yet building. + json = self.getJsonResponse('api.get_repo_builds', + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + assert len(json['builds']) == 0 + + # Request a (fake) build. + self.postResponse('api.request_repo_build', + params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + data=dict(file_id = 'foobarbaz'), + expected_code=201) + + # Check for the build. + json = self.getJsonResponse('api.get_repo_builds', + params=dict(repository=ADMIN_ACCESS_USER + '/building')) + + assert len(json['builds']) > 0 + + +class TestWebhooks(ApiTestCase): + def test_webhooks(self): + self.login(ADMIN_ACCESS_USER) + + # Add a webhook. + json = self.postJsonResponse('api.create_webhook', + params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + data=dict(url='http://example.com')) + + self.assertEquals('http://example.com', json['parameters']['url']) + wid = json['public_id'] + + # Get the webhook. + json = self.getJsonResponse('api.get_webhook', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', public_id=wid)) + + self.assertEquals(wid, json['public_id']) + self.assertEquals('http://example.com', json['parameters']['url']) + + # Verify the webhook is listed. + json = self.getJsonResponse('api.list_webhooks', + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + ids = [w['public_id'] for w in json['webhooks']] + assert wid in ids + + # Delete the webhook. + self.deleteResponse('api.delete_webhook', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', public_id=wid), + expected_code=204) + + # Verify the webhook is gone. + self.getResponse('api.get_webhook', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', public_id=wid), + expected_code=404) + + +class TestListAndGetImage(ApiTestCase): + def test_listandgetimages(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.list_repository_images', + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + assert len(json['images']) > 0 + for image in json['images']: + assert 'id' in image + assert 'tags' in image + assert 'created' in image + assert 'comment' in image + assert 'command' in image + assert 'ancestors' in image + assert 'dbid' in image + assert 'size' in image + + ijson = self.getJsonResponse('api.get_image', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', + image_id=image['id'])) + + self.assertEquals(image['id'], ijson['id']) + + +class TestGetImageChanges(ApiTestCase): + def test_getimagechanges(self): + self.login(ADMIN_ACCESS_USER) + + # Find an image to check. + json = self.getJsonResponse('api.list_repository_images', + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + image_id = json['images'][0]['id'] + + # Lookup the image's changes. + # TODO: Fix me once we can get fake changes into the test data + #self.getJsonResponse('api.get_image_changes', + # params=dict(repository=ADMIN_ACCESS_USER + '/simple', + # image_id=image_id)) + + +class TestListAndDeleteTag(ApiTestCase): + def test_listtagimagesanddeletetag(self): + self.login(ADMIN_ACCESS_USER) + + # List the images for prod. + json = self.getJsonResponse('api.list_tag_images', + params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='prod')) + + prod_images = json['images'] + assert len(prod_images) > 0 + + # List the images for staging. + json = self.getJsonResponse('api.list_tag_images', + params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='staging')) + + staging_images = json['images'] + assert len(prod_images) == len(staging_images) + 1 + + # Delete prod. + self.deleteResponse('api.delete_full_tag', + params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='prod'), + expected_code=204) + + # Make sure the tag is gone. + self.getResponse('api.list_tag_images', + params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='prod'), + expected_code=404) + + # Make the sure the staging images are still there. + json = self.getJsonResponse('api.list_tag_images', + params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='staging')) + + self.assertEquals(staging_images, json['images']) + + + def test_deletesubtag(self): + self.login(ADMIN_ACCESS_USER) + + # List the images for prod. + json = self.getJsonResponse('api.list_tag_images', + params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='prod')) + + prod_images = json['images'] + assert len(prod_images) > 0 + + # Delete staging. + self.deleteResponse('api.delete_full_tag', + params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='staging'), + expected_code=204) + + # Make sure the prod images are still around. + json = self.getJsonResponse('api.list_tag_images', + params=dict(repository=ADMIN_ACCESS_USER + '/complex', tag='prod')) + + self.assertEquals(prod_images, json['images']) + + +class TestRepoPermissions(ApiTestCase): + def listUserPermissions(self): + return self.getJsonResponse('api.list_repo_user_permissions', + params=dict(repository=ADMIN_ACCESS_USER + '/simple'))['permissions'] + + def listTeamPermissions(self): + return self.getJsonResponse('api.list_repo_team_permissions', + params=dict(repository=ORGANIZATION + '/' + ORG_REPO))['permissions'] + + def test_userpermissions(self): + self.login(ADMIN_ACCESS_USER) + + # The repo should start with just the admin as a user perm. + permissions = self.listUserPermissions() + + self.assertEquals(1, len(permissions)) + assert ADMIN_ACCESS_USER in permissions + self.assertEquals('admin', permissions[ADMIN_ACCESS_USER]['role']) + + # Add another user. + self.putJsonResponse('api.change_user_permissions', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', username=NO_ACCESS_USER), + data=dict(role='read')) + + # Verify the user is present. + permissions = self.listUserPermissions() + + self.assertEquals(2, len(permissions)) + assert NO_ACCESS_USER in permissions + self.assertEquals('read', permissions[NO_ACCESS_USER]['role']) + + json = self.getJsonResponse('api.get_user_permissions', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', username=NO_ACCESS_USER)) + self.assertEquals('read', json['role']) + + # Change the user's permissions. + self.putJsonResponse('api.change_user_permissions', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', username=NO_ACCESS_USER), + data=dict(role='admin')) + + # Verify. + permissions = self.listUserPermissions() + + self.assertEquals(2, len(permissions)) + assert NO_ACCESS_USER in permissions + self.assertEquals('admin', permissions[NO_ACCESS_USER]['role']) + + # Delete the user's permission. + self.deleteResponse('api.delete_user_permissions', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', username=NO_ACCESS_USER)) + + # Verify. + permissions = self.listUserPermissions() + + self.assertEquals(1, len(permissions)) + assert not NO_ACCESS_USER in permissions + + + def test_teampermissions(self): + self.login(ADMIN_ACCESS_USER) + + # The repo should start with just the readers as a team perm. + permissions = self.listTeamPermissions() + + self.assertEquals(1, len(permissions)) + assert 'readers' in permissions + self.assertEquals('read', permissions['readers']['role']) + + # Add another team. + self.putJsonResponse('api.change_team_permissions', + params=dict(repository=ORGANIZATION + '/' + ORG_REPO, teamname='owners'), + data=dict(role='write')) + + # Verify the team is present. + permissions = self.listTeamPermissions() + + self.assertEquals(2, len(permissions)) + assert 'owners' in permissions + self.assertEquals('write', permissions['owners']['role']) + + json = self.getJsonResponse('api.get_team_permissions', + params=dict(repository=ORGANIZATION + '/' + ORG_REPO, teamname='owners')) + self.assertEquals('write', json['role']) + + # Change the team's permissions. + self.putJsonResponse('api.change_team_permissions', + params=dict(repository=ORGANIZATION + '/' + ORG_REPO, teamname='owners'), + data=dict(role='admin')) + + # Verify. + permissions = self.listTeamPermissions() + + self.assertEquals(2, len(permissions)) + assert 'owners' in permissions + self.assertEquals('admin', permissions['owners']['role']) + + # Delete the team's permission. + self.deleteResponse('api.delete_team_permissions', + params=dict(repository=ORGANIZATION + '/' + ORG_REPO, teamname='owners')) + + # Verify. + permissions = self.listTeamPermissions() + + self.assertEquals(1, len(permissions)) + assert not 'owners' in permissions + + +class TestApiTokens(ApiTestCase): + def listTokens(self): + return self.getJsonResponse('api.list_repo_tokens', + params=dict(repository=ADMIN_ACCESS_USER + '/simple'))['tokens'] + + def test_tokens(self): + self.login(ADMIN_ACCESS_USER) + + # Create a new token. + json = self.postJsonResponse('api.create_token', + params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + data=dict(role='read', friendlyName='mytoken'), + expected_code=201) + + self.assertEquals('mytoken', json['friendlyName']) + self.assertEquals('read', json['role']) + token_code = json['code'] + + # Verify. + tokens = self.listTokens() + assert token_code in tokens + self.assertEquals('mytoken', tokens[token_code]['friendlyName']) + + json = self.getJsonResponse('api.get_tokens', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', code=token_code)) + self.assertEquals(tokens[token_code], json) + + # Change the token's permission. + self.putJsonResponse('api.change_token', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', code=token_code), + data=dict(role='write')) + + # Verify. + json = self.getJsonResponse('api.get_tokens', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', code=token_code)) + self.assertEquals('write', json['role']) + + # Delete the token. + self.deleteResponse('api.delete_token', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', code=token_code)) + + # Verify. + self.getResponse('api.get_tokens', + params=dict(repository=ADMIN_ACCESS_USER + '/simple', code=token_code), + expected_code=404) + + +class TestUserCard(ApiTestCase): + def test_getusercard(self): + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse('api.get_user_card') + + self.assertEquals('4242', json['card']['last4']) + self.assertEquals('Visa', json['card']['type']) + + def test_setusercard_error(self): + self.login(ADMIN_ACCESS_USER) + json = self.postJsonResponse('api.set_user_card', + data=dict(token='sometoken'), + expected_code=402) + assert 'carderror' in json + + +class TestOrgCard(ApiTestCase): + def test_getorgcard(self): + self.login(ADMIN_ACCESS_USER) + json = self.getJsonResponse('api.get_org_card', + params=dict(orgname=ORGANIZATION)) + + self.assertEquals('4242', json['card']['last4']) + self.assertEquals('Visa', json['card']['type']) + + +class TestUserSubscription(ApiTestCase): + def getSubscription(self): + return self.getJsonResponse('api.get_user_subscription') + + def test_updateplan(self): + self.login(ADMIN_ACCESS_USER) + + # Change the plan. + self.putJsonResponse('api.update_user_subscription', + data=dict(plan='free')) + + # Verify + sub = self.getSubscription() + self.assertEquals('free', sub['plan']) + + # Change the plan. + self.putJsonResponse('api.update_user_subscription', + data=dict(plan='bus-large')) + + # Verify + sub = self.getSubscription() + self.assertEquals('bus-large', sub['plan']) + + +class TestOrgSubscription(ApiTestCase): + def getSubscription(self): + return self.getJsonResponse('api.get_org_subscription', params=dict(orgname=ORGANIZATION)) + + def test_updateplan(self): + self.login(ADMIN_ACCESS_USER) + + # Change the plan. + self.putJsonResponse('api.update_org_subscription', + params=dict(orgname=ORGANIZATION), + data=dict(plan='free')) + + # Verify + sub = self.getSubscription() + self.assertEquals('free', sub['plan']) + + # Change the plan. + self.putJsonResponse('api.update_org_subscription', + params=dict(orgname=ORGANIZATION), + data=dict(plan='bus-large')) + + # Verify + sub = self.getSubscription() + self.assertEquals('bus-large', sub['plan']) + + +class TestUserRobots(ApiTestCase): + def getRobotNames(self): + return [r['name'] for r in self.getJsonResponse('api.get_user_robots')['robots']] + + def test_robots(self): + self.login(NO_ACCESS_USER) + + # Create a robot. + json = self.putJsonResponse('api.create_user_robot', + params=dict(robot_shortname='bender'), + expected_code=201) + + self.assertEquals(NO_ACCESS_USER + '+bender', json['name']) + + # Verify. + robots = self.getRobotNames() + assert NO_ACCESS_USER + '+bender' in robots + + # Delete the robot. + self.deleteResponse('api.delete_user_robot', + params=dict(robot_shortname='bender')) + + # Verify. + robots = self.getRobotNames() + assert not NO_ACCESS_USER + '+bender' in robots + + +class TestOrgRobots(ApiTestCase): + def getRobotNames(self): + return [r['name'] for r in self.getJsonResponse('api.get_org_robots', + params=dict(orgname=ORGANIZATION))['robots']] + + def test_robots(self): + self.login(ADMIN_ACCESS_USER) + + # Create a robot. + json = self.putJsonResponse('api.create_org_robot', + params=dict(orgname=ORGANIZATION, robot_shortname='bender'), + expected_code=201) + + self.assertEquals(ORGANIZATION + '+bender', json['name']) + + # Verify. + robots = self.getRobotNames() + assert ORGANIZATION + '+bender' in robots + + # Delete the robot. + self.deleteResponse('api.delete_org_robot', + params=dict(orgname=ORGANIZATION, robot_shortname='bender')) + + # Verify. + robots = self.getRobotNames() + assert not ORGANIZATION + '+bender' in robots + + +class TestLogs(ApiTestCase): + def test_user_logs(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.list_user_logs') + assert 'logs' in json + assert 'start_time' in json + assert 'end_time' in json + + def test_org_logs(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.list_org_logs', params=dict(orgname=ORGANIZATION)) + assert 'logs' in json + assert 'start_time' in json + assert 'end_time' in json + + def test_performer(self): + self.login(ADMIN_ACCESS_USER) + + json = self.getJsonResponse('api.list_org_logs', params=dict(orgname=ORGANIZATION)) + all_logs = json['logs'] + + json = self.getJsonResponse('api.list_org_logs', + params=dict(performer=READ_ACCESS_USER, orgname=ORGANIZATION)) + + assert len(json['logs']) < len(all_logs) + for log in json['logs']: + self.assertEquals(READ_ACCESS_USER, log['performer']['name']) + +if __name__ == '__main__': + unittest.main() diff --git a/tools/audittagimages.py b/tools/audittagimages.py new file mode 100644 index 000000000..e521a78c3 --- /dev/null +++ b/tools/audittagimages.py @@ -0,0 +1,48 @@ +from data.database import Image, RepositoryTag, Repository + +from app import app + + +store = app.config['STORAGE'] + + +tag_query = (RepositoryTag + .select(RepositoryTag, Image, Repository) + .join(Repository) + .switch(RepositoryTag) + .join(Image)) + +for tag in tag_query: + if tag.image.repository.id != tag.repository.id: + print('Repository tag pointing to external image: %s/%s:%s' % + (tag.repository.namespace, tag.repository.name, tag.name)) + + proper_image_layer_path = store.image_layer_path(tag.repository.namespace, + tag.repository.name, + tag.image.docker_image_id) + + has_storage = False + if store.exists(proper_image_layer_path): + print('Storage already in place: %s' % proper_image_layer_path) + has_storage = True + else: + print('Storage missing: %s' % proper_image_layer_path) + + has_db_entry = False + new_image = None + try: + new_image = Image.get(Image.docker_image_id == tag.image.docker_image_id, + Image.repository == tag.repository) + has_db_entry = True + print('DB image in place: %s invalid image id: %s' % (new_image.id, + tag.image.id)) + except Image.DoesNotExist: + print('DB image missing: %s' % tag.image.docker_image_id) + + if has_storage and has_db_entry: + print('Switching tag to proper image %s/%s/%s -> %s' % + (tag.repository.namespace, tag.repository.name, tag.name, + new_image.id)) + tag.image = new_image + tag.save() + print('Done')