From fbfe7fdb541441017d78ccd0b7aa161698c71953 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 15 Sep 2015 14:33:35 -0400 Subject: [PATCH 1/2] Make change repo visibility and create repo raise a 402 when applicable We now check the user or org's subscription plan and raise a 402 if the user attempts to create/make a repo private over their limit --- endpoints/api/billing.py | 32 ++++++++++++++++++++++++++++- endpoints/api/repository.py | 24 ++++++++++++++++++++-- test/test_api_usage.py | 40 +++++++++++++++++++++++++++++++++++++ test/testconfig.py | 1 + 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/endpoints/api/billing.py b/endpoints/api/billing.py index d889c7c82..aae577908 100644 --- a/endpoints/api/billing.py +++ b/endpoints/api/billing.py @@ -12,12 +12,42 @@ from auth.permissions import AdministerOrganizationPermission from auth.auth_context import get_authenticated_user from auth import scopes from data import model -from data.billing import PLANS +from data.billing import PLANS, get_plan import features import uuid import json +def lookup_allowed_private_repos(namespace): + """ Returns false if the given namespace has used its allotment of private repositories. """ + # Lookup the namespace and verify it has a subscription. + namespace_user = model.user.get_namespace_user(namespace) + if namespace_user is None: + return False + + if not namespace_user.stripe_id: + return False + + # Ask Stripe for the subscribed plan. + # TODO: Can we cache this or make it faster somehow? + try: + cus = billing.Customer.retrieve(namespace_user.stripe_id) + except stripe.APIConnectionError: + abort(503, message='Cannot contact Stripe') + + if not cus.subscription: + return False + + # Find the number of private repositories used by the namespace and compare it to the + # plan subscribed. + private_repos = model.user.get_private_repo_count(namespace) + current_plan = get_plan(cus.subscription.plan.id) + if current_plan is None: + return False + + return private_repos < current_plan['privateRepos'] + + def carderror_response(e): return {'carderror': e.message}, 402 diff --git a/endpoints/api/repository.py b/endpoints/api/repository.py index 215931785..b241a70a0 100644 --- a/endpoints/api/repository.py +++ b/endpoints/api/repository.py @@ -2,6 +2,7 @@ import logging import datetime +import features from datetime import timedelta @@ -15,7 +16,8 @@ from endpoints.api import (truthy_bool, format_date, nickname, log_action, valid require_repo_read, require_repo_write, require_repo_admin, RepositoryParamResource, resource, query_param, parse_args, ApiResource, request_error, require_scope, Unauthorized, NotFound, InvalidRequest, - path_param) + path_param, ExceedsLicenseException) +from endpoints.api.billing import lookup_allowed_private_repos from auth.permissions import (ModifyRepositoryPermission, AdministerRepositoryPermission, CreateRepositoryPermission) @@ -26,6 +28,18 @@ from auth import scopes logger = logging.getLogger(__name__) +def check_allowed_private_repos(namespace): + """ Checks to see if the given namespace has reached its private repository limit. If so, + raises a ExceedsLicenseException. + """ + # Not enabled if billing is disabled. + if not features.BILLING: + return + + if not lookup_allowed_private_repos(namespace): + raise ExceedsLicenseException() + + @resource('/v1/repository') class RepositoryList(ApiResource): """Operations for creating and listing repositories.""" @@ -87,6 +101,8 @@ class RepositoryList(ApiResource): raise request_error(message='Repository already exists') visibility = req['visibility'] + if visibility == 'private': + check_allowed_private_repos(namespace_name) repo = model.repository.create_repository(namespace_name, repository_name, owner, visibility) repo.description = req['description'] @@ -339,7 +355,11 @@ class RepositoryVisibility(RepositoryParamResource): repo = model.repository.get_repository(namespace, repository) if repo: values = request.get_json() - model.repository.set_repository_visibility(repo, values['visibility']) + visibility = values['visibility'] + if visibility == 'private': + check_allowed_private_repos(namespace) + + model.repository.set_repository_visibility(repo, visibility) log_action('change_repo_visibility', namespace, {'repo': repository, 'visibility': values['visibility']}, repo=repo) diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 163a10977..20a8a8fec 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -315,8 +315,18 @@ class TestGetUserPrivateAllowed(ApiTestCase): def test_allowed(self): self.login(ADMIN_ACCESS_USER) + + # Change the subscription of the namespace. + self.putJsonResponse(UserPlan, data=dict(plan='personal-30')) + json = self.getJsonResponse(PrivateRepositories) assert json['privateCount'] >= 6 + assert not json['privateAllowed'] + + # Change the subscription of the namespace. + self.putJsonResponse(UserPlan, data=dict(plan='bus-large-30')) + + json = self.getJsonResponse(PrivateRepositories) assert json['privateAllowed'] @@ -1435,6 +1445,36 @@ class TestUpdateRepo(ApiTestCase): class TestChangeRepoVisibility(ApiTestCase): SIMPLE_REPO = ADMIN_ACCESS_USER + '/simple' + + def test_trychangevisibility(self): + self.login(ADMIN_ACCESS_USER) + + # Make public. + self.postJsonResponse(RepositoryVisibility, + params=dict(repository=self.SIMPLE_REPO), + data=dict(visibility='public')) + + # Verify the visibility. + json = self.getJsonResponse(Repository, + params=dict(repository=self.SIMPLE_REPO)) + + self.assertEquals(True, json['is_public']) + + # Change the subscription of the namespace. + self.putJsonResponse(UserPlan, data=dict(plan='personal-30')) + + # Try to make private. + self.postJsonResponse(RepositoryVisibility, + params=dict(repository=self.SIMPLE_REPO), + data=dict(visibility='private'), + expected_code=402) + + # Verify the visibility. + json = self.getJsonResponse(Repository, + params=dict(repository=self.SIMPLE_REPO)) + + self.assertEquals(True, json['is_public']) + def test_changevisibility(self): self.login(ADMIN_ACCESS_USER) diff --git a/test/testconfig.py b/test/testconfig.py index 2ee3e89bb..34ae8da48 100644 --- a/test/testconfig.py +++ b/test/testconfig.py @@ -20,6 +20,7 @@ TEST_DB_FILE = NamedTemporaryFile(delete=True) class TestConfig(DefaultConfig): TESTING = True SECRET_KEY = 'a36c9d7d-25a9-4d3f-a586-3d2f8dc40a83' + BILLING_TYPE = 'FakeStripe' TEST_DB_FILE = TEST_DB_FILE DB_URI = os.environ.get('TEST_DATABASE_URI', 'sqlite:///{0}'.format(TEST_DB_FILE.name)) From 2739cf47ba82656b71c44dc24d6ec8531a22830f Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 16 Sep 2015 14:00:06 -0400 Subject: [PATCH 2/2] Prevent change visibility of a repo in the UI when disallowed by billing plan Fixes #486 - Extracts out the check plan logic and UI from the new repo page into its own directive (repo-count-checker) - Adds the new directive to the repo settings panel - Some additional UI improvements for the repo settings panel --- .../repo-view/repo-panel-settings.css | 12 +++ .../css/directives/ui/repo-count-checker.css | 26 ++++++ static/directives/repo-count-checker.html | 24 ++++++ .../repo-view/repo-panel-settings.html | 33 +++++--- static/js/directives/ui/repo-count-checker.js | 81 +++++++++++++++++++ static/js/pages/new-repo.js | 76 ----------------- static/js/services/user-service.js | 4 + static/partials/new-repo.html | 26 +----- 8 files changed, 173 insertions(+), 109 deletions(-) create mode 100644 static/css/directives/ui/repo-count-checker.css create mode 100644 static/directives/repo-count-checker.html create mode 100644 static/js/directives/ui/repo-count-checker.js diff --git a/static/css/directives/repo-view/repo-panel-settings.css b/static/css/directives/repo-view/repo-panel-settings.css index ff626b9ba..8925365eb 100644 --- a/static/css/directives/repo-view/repo-panel-settings.css +++ b/static/css/directives/repo-view/repo-panel-settings.css @@ -29,6 +29,18 @@ margin-top: -7px !important; } +.repo-panel-settings-element .repo-count-checker { + margin-top: 20px; +} + +.repo-panel-settings-element .co-alert { + margin-bottom: 0px; +} + +.repo-panel-settings-element .panel-body { + border-bottom: 0px; +} + @media (max-width: 767px) { .repo-panel-settings-element .delete-btn { float: none; diff --git a/static/css/directives/ui/repo-count-checker.css b/static/css/directives/ui/repo-count-checker.css new file mode 100644 index 000000000..d357afdba --- /dev/null +++ b/static/css/directives/ui/repo-count-checker.css @@ -0,0 +1,26 @@ +.repo-count-checker .btn { + margin-top: 0px !important; +} + +.repo-count-checker .co-alert { + margin-bottom: 6px !important; + padding-right: 120px; +} + +.repo-count-checker .co-alert .btn { + position: absolute; + top: 10px; + right: 10px; +} + +@media (max-width: 767px) { + .repo-count-checker .co-alert { + padding-right: 10px; + } + + .repo-count-checker .co-alert .btn { + position: relative; + margin-top: 20px; + margin-bottom: 10px; + } +} \ No newline at end of file diff --git a/static/directives/repo-count-checker.html b/static/directives/repo-count-checker.html new file mode 100644 index 000000000..4b89b1204 --- /dev/null +++ b/static/directives/repo-count-checker.html @@ -0,0 +1,24 @@ +
+
+
+ In order to make this repository private under + your personal namespace + organization {{ repo.namespace }}, you will need to upgrade your plan to + + {{ planRequired.title }} + . + This will cost ${{ planRequired.price / 100 }}/month. + Upgrade now +
+ or did you mean to have this repository under the {{ user.organizations[0].name }} namespace? +
+
+
+
+
+ This organization has reached its private repository limit. Please contact your administrator. +
+
+
+ \ No newline at end of file diff --git a/static/directives/repo-view/repo-panel-settings.html b/static/directives/repo-view/repo-panel-settings.html index b49d835b2..d6c6d7996 100644 --- a/static/directives/repo-view/repo-panel-settings.html +++ b/static/directives/repo-view/repo-panel-settings.html @@ -22,12 +22,10 @@
- +
-
Repository Settings
- +
Repository Visibility
-
@@ -44,12 +42,23 @@
This repository is currently public and is visible to all users, and may be pulled by all users.
- -
- + +
+
+
+
+ + + +
+
Delete Repository
+
+
+
+
- -