Add license upload step to the setup flow

Fixes #853
This commit is contained in:
Joseph Schorr 2015-12-08 15:00:50 -05:00
parent 5211c407ff
commit 8fe29c5b89
12 changed files with 320 additions and 60 deletions

View file

@ -6,7 +6,8 @@ import signal
from flask import abort from flask import abort
from endpoints.api import (ApiResource, nickname, resource, internal_only, show_if, from endpoints.api import (ApiResource, nickname, resource, internal_only, show_if,
require_fresh_login, request, validate_json_request, verify_not_prod) require_fresh_login, request, validate_json_request, verify_not_prod,
InvalidRequest)
from endpoints.common import common_login from endpoints.common import common_login
from app import app, config_provider, superusers, OVERRIDE_CONFIG_DIRECTORY from app import app, config_provider, superusers, OVERRIDE_CONFIG_DIRECTORY
@ -18,6 +19,7 @@ from data.database import User
from util.config.configutil import add_enterprise_config_defaults from util.config.configutil import add_enterprise_config_defaults
from util.config.database import sync_database_with_config from util.config.database import sync_database_with_config
from util.config.validator import validate_service_for_config, CONFIG_FILENAMES from util.config.validator import validate_service_for_config, CONFIG_FILENAMES
from util.config.provider.license import decode_license, LicenseError
from data.runmigration import run_alembic_migration from data.runmigration import run_alembic_migration
from data.users import get_federated_service_name, get_users_handler from data.users import get_federated_service_name, get_users_handler
@ -62,6 +64,12 @@ class SuperUserRegistryStatus(ApiResource):
'status': 'missing-config-dir' 'status': 'missing-config-dir'
} }
# If there is no license file, we need to ask the user to upload it.
if not config_provider.has_license_file():
return {
'status': 'upload-license'
}
# If there is no config file, we need to setup the database. # If there is no config file, we need to setup the database.
if not config_provider.config_exists(): if not config_provider.config_exists():
return { return {
@ -244,6 +252,50 @@ class SuperUserConfig(ApiResource):
abort(403) abort(403)
@resource('/v1/superuser/config/license')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserSetAndValidateLicense(ApiResource):
""" Resource for setting and validating a license. """
schemas = {
'ValidateLicense': {
'type': 'object',
'description': 'Validates and sets a license',
'required': [
'license',
],
'properties': {
'license': {
'type': 'string'
},
},
},
}
@nickname('suSetAndValidateLicense')
@verify_not_prod
@validate_json_request('ValidateLicense')
def post(self):
""" Validates the given license contents and then saves it to the config volume. """
if config_provider.has_license_file():
abort(403)
license_contents = request.get_json()['license']
try:
decoded_license = decode_license(license_contents)
except LicenseError as le:
raise InvalidRequest(le.message)
if decoded_license.is_expired:
raise InvalidRequest('License has expired')
config_provider.save_license(license_contents)
return {
'decoded': decoded_license.subscription,
'success': True
}
@resource('/v1/superuser/config/file/<filename>') @resource('/v1/superuser/config/file/<filename>')
@internal_only @internal_only
@show_if(features.SUPER_USERS) @show_if(features.SUPER_USERS)

View file

@ -0,0 +1,56 @@
.initial-setup-modal .upload-license textarea {
border: 1px solid #eee !important;
transition: all ease-in-out 200ms;
resize: none;
}
.initial-setup-modal .upload-license textarea {
padding: 10px;
margin-top: 20px;
margin-bottom: 10px;
}
.initial-setup-modal .upload-license .validate-message {
display: inline-block;
margin-left: 10px;
margin-top: 10px;
}
.initial-setup-modal .upload-license .license-invalid h5 {
font-size: 18px;
color: red;
}
.initial-setup-modal .upload-license .license-invalid h6 {
margin-bottom: 10px;
font-size: 16px;
}
.initial-setup-modal .upload-license .license-invalid .fa {
margin-right: 6px;
}
.initial-setup-modal .license-valid h5 {
color: #2FC98E;
font-size: 16px;
margin-bottom: 16px;
}
.initial-setup-modal .license-valid .fa {
margin-right: 6px;
}
.initial-setup-modal .license-valid table {
margin-top: 40px;
}
.initial-setup-modal .license-valid table td {
border: 0px;
padding: 4px;
}
.initial-setup-modal .license-valid table td:first-child {
font-weight: bold;
max-width: 100px;
padding-right: 20px;
}

View file

@ -37,6 +37,15 @@
// The config.yaml exists but it is invalid. // The config.yaml exists but it is invalid.
'INVALID_CONFIG': 'config-invalid', 'INVALID_CONFIG': 'config-invalid',
// License is being uploaded.
'UPLOAD_LICENSE': 'upload-license',
// License is being validated.
'VALIDATING_LICENSE': 'upload-license-validating',
// License is validated.
'VALIDATED_LICENSE': 'upload-license-validated',
// DB is being configured. // DB is being configured.
'CONFIG_DB': 'config-db', 'CONFIG_DB': 'config-db',
@ -95,7 +104,10 @@
$scope.currentConfig = null; $scope.currentConfig = null;
$scope.currentState = { $scope.currentState = {
'hasDatabaseSSLCert': false 'hasDatabaseSSLCert': false,
'licenseContents': '',
'licenseError': null,
'licenseDecoded': null,
}; };
$scope.$watch('currentStep', function(currentStep) { $scope.$watch('currentStep', function(currentStep) {
@ -121,6 +133,7 @@
case $scope.States.CREATE_SUPERUSER: case $scope.States.CREATE_SUPERUSER:
case $scope.States.DB_RESTARTING: case $scope.States.DB_RESTARTING:
case $scope.States.CONFIG_DB: case $scope.States.CONFIG_DB:
case $scope.States.UPLOAD_LICENSE:
case $scope.States.VALID_CONFIG: case $scope.States.VALID_CONFIG:
case $scope.States.READY: case $scope.States.READY:
$('#setupModal').modal({ $('#setupModal').modal({
@ -131,6 +144,27 @@
} }
}); });
$scope.validateLicense = function() {
$scope.currentStep = $scope.States.VALIDATING_LICENSE;
var data = {
'license': $scope.currentState.licenseContents
};
ApiService.suSetAndValidateLicense(data).then(function(resp) {
$scope.currentStep = $scope.States.VALIDATED_LICENSE;
$scope.currentState.licenseError = null;
$scope.currentState.licenseDecoded = resp['decoded'];
}, function(resp) {
$scope.currentStep = $scope.States.UPLOAD_LICENSE;
$scope.currentState.licenseError = ApiService.getErrorMessage(resp);
$scope.currentState.licenseContents = '';
$scope.currentState.licenseDecoded = null;
});
};
$scope.restartContainer = function(state) { $scope.restartContainer = function(state) {
$scope.currentStep = state; $scope.currentStep = state;
ContainerService.restartContainer(function() { ContainerService.restartContainer(function() {
@ -166,6 +200,7 @@
var States = $scope.States; var States = $scope.States;
return [ return [
isStepFamily(step, States.UPLOAD_LICENSE),
isStepFamily(step, States.CONFIG_DB), isStepFamily(step, States.CONFIG_DB),
isStepFamily(step, States.DB_SETUP), isStepFamily(step, States.DB_SETUP),
isStep(step, States.DB_RESTARTING), isStep(step, States.DB_RESTARTING),
@ -191,6 +226,10 @@
return false; return false;
}; };
$scope.beginSetup = function() {
$scope.currentStep = $scope.States.CONFIG_DB;
};
$scope.showInvalidConfigDialog = function() { $scope.showInvalidConfigDialog = function() {
var message = "The <code>config.yaml</code> file found in <code>conf/stack</code> could not be parsed." var message = "The <code>config.yaml</code> file found in <code>conf/stack</code> could not be parsed."
var title = "Invalid configuration file"; var title = "Invalid configuration file";

View file

@ -9,12 +9,13 @@
<div class="cor-tab-panel" style="padding: 20px;"> <div class="cor-tab-panel" style="padding: 20px;">
<div class="co-alert alert alert-info"> <div class="co-alert alert alert-info">
<span class="cor-step-bar" progress="stepProgress"> <span class="cor-step-bar" progress="stepProgress">
<span class="cor-step" title="Configure Database" text="1"></span> <span class="cor-step" title="Upload License" text="1"></span>
<span class="cor-step" title="Configure Database" text="2"></span>
<span class="cor-step" title="Setup Database" icon="database"></span> <span class="cor-step" title="Setup Database" icon="database"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span> <span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Create Superuser" text="2"></span> <span class="cor-step" title="Create Superuser" text="3"></span>
<span class="cor-step" title="Configure Registry" text="3"></span> <span class="cor-step" title="Configure Registry" text="4"></span>
<span class="cor-step" title="Validate Configuration" text="4"></span> <span class="cor-step" title="Validate Configuration" text="5"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span> <span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Setup Complete" icon="check"></span> <span class="cor-step" title="Setup Complete" icon="check"></span>
</span> </span>
@ -36,12 +37,13 @@
<!-- Header --> <!-- Header -->
<div class="modal-header"> <div class="modal-header">
<span class="cor-step-bar" progress="stepProgress"> <span class="cor-step-bar" progress="stepProgress">
<span class="cor-step" title="Configure Database" text="1"></span> <span class="cor-step" title="Upload License" text="1"></span>
<span class="cor-step" title="Configure Database" text="2"></span>
<span class="cor-step" title="Setup Database" icon="database"></span> <span class="cor-step" title="Setup Database" icon="database"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span> <span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Create Superuser" text="2"></span> <span class="cor-step" title="Create Superuser" text="3"></span>
<span class="cor-step" title="Configure Registry" text="3"></span> <span class="cor-step" title="Configure Registry" text="4"></span>
<span class="cor-step" title="Validate Configuration" text="4"></span> <span class="cor-step" title="Validate Configuration" text="5"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span> <span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Setup Complete" icon="check"></span> <span class="cor-step" title="Setup Complete" icon="check"></span>
</span> </span>
@ -128,6 +130,37 @@
The container must be restarted to apply the configuration changes. The container must be restarted to apply the configuration changes.
</div> </div>
<!-- Content: VALIDATED_LICENSE -->
<div class="modal-body license-valid" style="padding: 20px;"
ng-show="isStep(currentStep, States.VALIDATED_LICENSE)">
<h5><i class="fa fa-check"></i> License Validated</h5>
Your license has been validated and saved. Please press "Next" to continue setup of your Quay Enterprise installation.
<table class="co-table">
<tr><td>Product:</td><td>{{ currentState.licenseDecoded.publicProductName || currentState.licenseDecoded.productName }}</td></tr>
<tr><td>Plan:</td><td>{{ currentState.licenseDecoded.publicPlanName || currentState.licenseDecoded.planName }}</td></tr>
</table>
</div>
<!-- Content: UPLOAD_LICENSE or VALIDATING_LICENSE -->
<div class="modal-body upload-license" style="padding: 20px;"
ng-show="isStep(currentStep, States.UPLOAD_LICENSE, States.VALIDATING_LICENSE)"
ng-class="isStep(currentStep, States.VALIDATING_LICENSE) ? 'validating' : 'entering'">
<h4>
Quay Enterprise License
</h4>
<div>
Please provide your Quay Enterprise License. It can be found under the "Raw Format" tab
of your Quay Enterprise subscription in the <a href="https://account.tectonic.com" target="_blank">Tectonic Account</a>.
</div>
<textarea id="enterLicenseBox" ng-model="currentState.licenseContents" placeholder="Paste your raw license here, which should already be in base64 format: GtqMjMwNDgyM3Vq..."
ng-readonly="isStep(currentStep, States.VALIDATING_LICENSE)"></textarea>
<div class="license-invalid" ng-visible="isStep(currentStep, States.UPLOAD_LICENSE) && currentState.licenseError">
<h5><i class="fa fa-times-circle"></i> Validation Failed</h5>
<h6>{{ currentState.licenseError }}</h6>
Please try copying your license from the Tectonic Account again.
</div>
</div>
<!-- Content: DB_SETUP or DB_SETUP_ERROR --> <!-- Content: DB_SETUP or DB_SETUP_ERROR -->
<div class="modal-body" style="padding: 20px;" <div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.DB_SETUP, States.DB_SETUP_ERROR)"> ng-show="isStep(currentStep, States.DB_SETUP, States.DB_SETUP_ERROR)">
@ -226,6 +259,29 @@
Database Validation Issue: {{ errors.DatabaseValidationError }} Database Validation Issue: {{ errors.DatabaseValidationError }}
</div> </div>
<!-- Footer: UPLOAD_LICENSE or VALIDATING_LICENSE -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.UPLOAD_LICENSE, States.VALIDATING_LICENSE)">
<div ng-show="isStep(currentStep, States.VALIDATING_LICENSE)">
<span class="cor-loader-inline"></span>
Validating License...
</div>
<button type="submit" class="btn btn-primary" ng-click="validateLicense()"
ng-disabled="!currentState.licenseContents"
ng-show="isStep(currentStep, States.UPLOAD_LICENSE)">
Validate License
</button>
</div>
<!-- Footer: VALIDATED_LICENSE -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.VALIDATED_LICENSE)">
<button type="submit" class="btn btn-primary" ng-click="beginSetup()">
Next
</button>
</div>
<!-- Footer: CONFIG_DB or DB_ERROR --> <!-- Footer: CONFIG_DB or DB_ERROR -->
<div class="modal-footer" <div class="modal-footer"
ng-show="isStep(currentStep, States.CONFIG_DB, States.DB_ERROR)"> ng-show="isStep(currentStep, States.CONFIG_DB, States.DB_ERROR)">

View file

@ -3740,13 +3740,20 @@ class TestSuperUserCreateInitialSuperUser(ApiTestCase):
class TestSuperUserConfig(ApiTestCase): class TestSuperUserConfig(ApiTestCase):
def test_get_status_update_config(self): def test_get_status_update_config(self):
# With no config the status should be 'config-db'. # With no config the status should be 'upload-license'.
json = self.getJsonResponse(SuperUserRegistryStatus) json = self.getJsonResponse(SuperUserRegistryStatus)
self.assertEquals('config-db', json['status']) self.assertEquals('upload-license', json['status'])
# And the config should 401. # And the config should 401.
self.getResponse(SuperUserConfig, expected_code=401) self.getResponse(SuperUserConfig, expected_code=401)
# Add a fake license file.
config_provider.save_license('something')
# With no config but a license the status should be 'config-db'.
json = self.getJsonResponse(SuperUserRegistryStatus)
self.assertEquals('config-db', json['status'])
# Add some fake config. # Add some fake config.
fake_config = { fake_config = {
'AUTHENTICATION_TYPE': 'Database', 'AUTHENTICATION_TYPE': 'Database',

View file

@ -3,6 +3,7 @@ import unittest
from datetime import datetime, timedelta from datetime import datetime, timedelta
import jwt import jwt
import json
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
@ -22,10 +23,14 @@ class TestLicense(unittest.TestCase):
return (public_key, private_key) return (public_key, private_key)
def create_license(self, license_data): def create_license(self, license_data):
jwt_data = {
'license': json.dumps(license_data),
}
(public_key, private_key) = self.keys() (public_key, private_key) = self.keys()
# Encode the license with the JWT key. # Encode the license with the JWT key.
encoded = jwt.encode(license_data, private_key, algorithm='RS256') encoded = jwt.encode(jwt_data, private_key, algorithm='RS256')
# Decode it into a license object. # Decode it into a license object.
return decode_license(encoded, public_key_instance=public_key) return decode_license(encoded, public_key_instance=public_key)
@ -53,7 +58,7 @@ class TestLicense(unittest.TestCase):
if 'duration' in kwargs: if 'duration' in kwargs:
sub['durationPeriod'] = kwargs['duration'] sub['durationPeriod'] = kwargs['duration']
license_data['subscriptions'] = [sub] license_data['subscriptions'] = {'somesub': sub}
decoded_license = self.create_license(license_data) decoded_license = self.create_license(license_data)
return decoded_license return decoded_license
@ -83,15 +88,15 @@ class TestLicense(unittest.TestCase):
self.assertTrue(license.is_expired) self.assertTrue(license.is_expired)
def test_monthly_license_valid(self): def test_monthly_license_valid(self):
license = self.get_license(timedelta(days=30), service_end=timedelta(days=10), duration='monthly') license = self.get_license(timedelta(days=30), service_end=timedelta(days=10), duration='months')
self.assertFalse(license.is_expired) self.assertFalse(license.is_expired)
def test_monthly_license_withingrace(self): def test_monthly_license_withingrace(self):
license = self.get_license(timedelta(days=30), service_end=timedelta(days=-10), duration='monthly') license = self.get_license(timedelta(days=30), service_end=timedelta(days=-10), duration='months')
self.assertFalse(license.is_expired) self.assertFalse(license.is_expired)
def test_monthly_license_outsidegrace(self): def test_monthly_license_outsidegrace(self):
license = self.get_license(timedelta(days=30), service_end=timedelta(days=-40), duration='monthly') license = self.get_license(timedelta(days=30), service_end=timedelta(days=-40), duration='months')
self.assertTrue(license.is_expired) self.assertTrue(license.is_expired)
def test_yearly_license_withingrace(self): def test_yearly_license_withingrace(self):

View file

@ -21,7 +21,7 @@ class TestSuperUserRegistryStatus(ApiTestCase):
def test_registry_status(self): def test_registry_status(self):
with ConfigForTesting(): with ConfigForTesting():
json = self.getJsonResponse(SuperUserRegistryStatus) json = self.getJsonResponse(SuperUserRegistryStatus)
self.assertEquals('config-db', json['status']) self.assertEquals('upload-license', json['status'])
class TestSuperUserConfigFile(ApiTestCase): class TestSuperUserConfigFile(ApiTestCase):

View file

@ -75,8 +75,12 @@ class BaseProvider(object):
""" Returns whether the file with the given name exists under the config override volume. """ """ Returns whether the file with the given name exists under the config override volume. """
raise NotImplementedError raise NotImplementedError
def get_volume_file(self, filename, mode='r'): def get_volume_file(self, filename):
""" Returns a Python file referring to the given name under the config override volumne. """ """ Returns a Python file referring to the given name under the config override volume. """
raise NotImplementedError
def write_volume_file(self, filename, contents):
""" Writes the given contents to the config override volumne, with the given filename. """
raise NotImplementedError raise NotImplementedError
def save_volume_file(self, filename, flask_file): def save_volume_file(self, filename, flask_file):
@ -91,6 +95,14 @@ class BaseProvider(object):
""" """
raise NotImplementedError raise NotImplementedError
def _get_license_file(self):
""" Returns the contents of the license file. """
try:
return self.get_volume_file(LICENSE_FILENAME)
except IOError:
msg = 'Could not open license file. Please make sure it is in your config volume.'
raise LicenseError(msg)
def validate_license(self, config): def validate_license(self, config):
""" Validates that the configuration matches the license file (if any). """ """ Validates that the configuration matches the license file (if any). """
if not config.get('SETUP_COMPLETE', False): if not config.get('SETUP_COMPLETE', False):
@ -102,11 +114,10 @@ class BaseProvider(object):
self.license = decode_license(license_file_contents) self.license = decode_license(license_file_contents)
self.license.validate(config) self.license.validate(config)
def _get_license_file(self): def save_license(self, license_file_contents):
""" Returns the contents of the license file. """ """ Saves the given contents as the license file. """
try: self.write_volume_file(LICENSE_FILENAME, license_file_contents)
return self.get_volume_file(LICENSE_FILENAME)
except IOError:
msg = 'Could not open license file. Please make sure it is in your config volume.'
raise LicenseError(msg)
def has_license_file(self):
""" Returns true if a license file was found in the config directory. """
return self.volume_file_exists(LICENSE_FILENAME)

View file

@ -49,8 +49,15 @@ class FileConfigProvider(BaseProvider):
def volume_file_exists(self, filename): def volume_file_exists(self, filename):
return os.path.exists(os.path.join(self.config_volume, filename)) return os.path.exists(os.path.join(self.config_volume, filename))
def get_volume_file(self, filename, mode='r'): def get_volume_file(self, filename):
return open(os.path.join(self.config_volume, filename), mode) return open(os.path.join(self.config_volume, filename))
def write_volume_file(self, filename, contents):
filepath = os.path.join(self.config_volume, filename)
with open(filepath, mode='w') as f:
f.write(contents)
return filepath
def save_volume_file(self, filename, flask_file): def save_volume_file(self, filename, flask_file):
filepath = os.path.join(self.config_volume, filename) filepath = os.path.join(self.config_volume, filename)

View file

@ -47,6 +47,14 @@ class KubernetesConfigProvider(FileConfigProvider):
self._update_secret_file(self.yaml_filename, get_yaml(config_obj)) self._update_secret_file(self.yaml_filename, get_yaml(config_obj))
super(KubernetesConfigProvider, self).save_config(config_obj) super(KubernetesConfigProvider, self).save_config(config_obj)
def write_volume_file(self, filename, contents):
super(KubernetesConfigProvider, self).write_volume_file(filename, contents)
try:
self._update_secret_file(filename, contents)
except IOError as ioe:
raise CannotWriteConfigException(str(ioe))
def save_volume_file(self, filename, flask_file): def save_volume_file(self, filename, flask_file):
filepath = super(KubernetesConfigProvider, self).save_volume_file(filename, flask_file) filepath = super(KubernetesConfigProvider, self).save_volume_file(filename, flask_file)

View file

@ -6,6 +6,7 @@ from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_public_key from cryptography.hazmat.primitives.serialization import load_pem_public_key
import jwt import jwt
import json
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -44,6 +45,19 @@ class License(object):
def __init__(self, decoded): def __init__(self, decoded):
self.decoded = decoded self.decoded = decoded
@property
def subscription(self):
""" Returns the Quay Enterprise subscription, if any. """
for sub in self.decoded.get('subscriptions', {}).values():
if sub.get('productName') == LICENSE_PRODUCT_NAME:
return sub
return None
@property
def is_expired(self):
return self._get_expired(datetime.now())
def validate(self, config): def validate(self, config):
""" Validates the license and all its entitlements against the given config. """ """ Validates the license and all its entitlements against the given config. """
# Check that the license has not expired. # Check that the license has not expired.
@ -58,10 +72,6 @@ class License(object):
max_regions) max_regions)
raise LicenseValidationError(msg) raise LicenseValidationError(msg)
@property
def is_expired(self):
return self._get_expired(datetime.now())
def _get_expired(self, compare_date): def _get_expired(self, compare_date):
# Check if the license overall has expired. # Check if the license overall has expired.
expiration_date = _get_date(self.decoded, 'expirationDate') expiration_date = _get_date(self.decoded, 'expirationDate')
@ -70,37 +80,37 @@ class License(object):
return True return True
# Check for any QE subscriptions. # Check for any QE subscriptions.
for sub in self.decoded.get('subscriptions', []): sub = self.subscription
if sub.get('productName') != LICENSE_PRODUCT_NAME: if sub is None:
continue return True
# Check for a trial-only license. # Check for a trial-only license.
if sub.get('trialOnly', False): if sub.get('trialOnly', False):
trial_end_date = _get_date(sub, 'trialEnd') trial_end_date = _get_date(sub, 'trialEnd')
logger.debug('Trial-only license expires on %s', trial_end_date) logger.debug('Trial-only license expires on %s', trial_end_date)
return trial_end_date <= (compare_date - TRIAL_GRACE_PERIOD) return trial_end_date <= (compare_date - TRIAL_GRACE_PERIOD)
# Check for a normal license that is in trial. # Check for a normal license that is in trial.
service_end_date = _get_date(sub, 'serviceEnd') service_end_date = _get_date(sub, 'serviceEnd')
if sub.get('inTrial', False): if sub.get('inTrial', False):
# If the subscription is in a trial, but not a trial only # If the subscription is in a trial, but not a trial only
# subscription, give 7 days after trial end to update license # subscription, give 7 days after trial end to update license
# to one which has been paid (they've put in a credit card and it # to one which has been paid (they've put in a credit card and it
# might auto convert, so we could assume it will auto-renew) # might auto convert, so we could assume it will auto-renew)
logger.debug('In-trial license expires on %s', service_end_date) logger.debug('In-trial license expires on %s', service_end_date)
return service_end_date <= (compare_date - TRIAL_GRACE_PERIOD) return service_end_date <= (compare_date - TRIAL_GRACE_PERIOD)
# Otherwise, check the service expiration. # Otherwise, check the service expiration.
duration_period = sub.get('durationPeriod', 'monthly') duration_period = sub.get('durationPeriod', 'months')
# If the subscription is monthly, give 3 months grace period # If the subscription is monthly, give 3 months grace period
if duration_period == "monthly": if duration_period == "months":
logger.debug('Monthly license expires on %s', service_end_date) logger.debug('Monthly license expires on %s', service_end_date)
return service_end_date <= (compare_date - MONTHLY_GRACE_PERIOD) return service_end_date <= (compare_date - MONTHLY_GRACE_PERIOD)
if duration_period == "years": if duration_period == "years":
logger.debug('Yearly license expires on %s', service_end_date) logger.debug('Yearly license expires on %s', service_end_date)
return service_end_date <= (compare_date - YEARLY_GRACE_PERIOD) return service_end_date <= (compare_date - YEARLY_GRACE_PERIOD)
return True return True
@ -128,9 +138,15 @@ def decode_license(license_contents, public_key_instance=None):
""" Decodes the specified license contents, returning the decoded license. """ """ Decodes the specified license contents, returning the decoded license. """
license_public_key = public_key_instance or _PROD_LICENSE_PUBLIC_KEY license_public_key = public_key_instance or _PROD_LICENSE_PUBLIC_KEY
try: try:
decoded = jwt.decode(license_contents, key=license_public_key) jwt_data = jwt.decode(license_contents, key=license_public_key)
except jwt.exceptions.DecodeError as de: except jwt.exceptions.DecodeError as de:
logger.exception('Could not decode license file') logger.exception('Could not decode license file')
raise LicenseDecodeError('Could not decode license found: %s' % de.message) raise LicenseDecodeError('Could not decode license found: %s' % de.message)
try:
decoded = json.loads(jwt_data.get('license', '{}'))
except ValueError as ve:
logger.exception('Could not decode license file')
raise LicenseDecodeError('Could not decode license found: %s' % ve.message)
return License(decoded) return License(decoded)

View file

@ -46,6 +46,9 @@ class TestConfigProvider(BaseProvider):
def save_volume_file(self, filename, flask_file): def save_volume_file(self, filename, flask_file):
self.files[filename] = '' self.files[filename] = ''
def write_volume_file(self, filename, contents):
self.files[filename] = contents
def get_volume_file(self, filename, mode='r'): def get_volume_file(self, filename, mode='r'):
if filename in REAL_FILES: if filename in REAL_FILES:
return open(filename, mode=mode) return open(filename, mode=mode)