Add superuser config section for updating license
This commit is contained in:
parent
5fee4d6d19
commit
ee96693252
11 changed files with 370 additions and 34 deletions
|
@ -18,11 +18,12 @@ from auth.permissions import SuperUserPermission
|
||||||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request,
|
from endpoints.api import (ApiResource, nickname, resource, validate_json_request,
|
||||||
internal_only, require_scope, show_if, parse_args,
|
internal_only, require_scope, show_if, parse_args,
|
||||||
query_param, abort, require_fresh_login, path_param, verify_not_prod,
|
query_param, abort, require_fresh_login, path_param, verify_not_prod,
|
||||||
page_support, log_action)
|
page_support, log_action, InvalidRequest)
|
||||||
from endpoints.api.logs import get_logs, get_aggregate_logs
|
from endpoints.api.logs import get_logs, get_aggregate_logs
|
||||||
from data import model
|
from data import model
|
||||||
from data.database import ServiceKeyApprovalType
|
from data.database import ServiceKeyApprovalType
|
||||||
from util.useremails import send_confirmation_email, send_recovery_email
|
from util.useremails import send_confirmation_email, send_recovery_email
|
||||||
|
from util.license import decode_license, LicenseError
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -819,3 +820,143 @@ class SuperUserServiceKeyApproval(ApiResource):
|
||||||
return make_response('', 201)
|
return make_response('', 201)
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/superuser/license')
|
||||||
|
@internal_only
|
||||||
|
@show_if(features.SUPER_USERS)
|
||||||
|
class SuperUserLicense(ApiResource):
|
||||||
|
""" Resource for getting and setting a license. """
|
||||||
|
schemas = {
|
||||||
|
'UpdateLicense': {
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Updates a license',
|
||||||
|
'required': [
|
||||||
|
'license',
|
||||||
|
],
|
||||||
|
'properties': {
|
||||||
|
'license': {
|
||||||
|
'type': 'string'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@nickname('getLicense')
|
||||||
|
@require_fresh_login
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
|
@verify_not_prod
|
||||||
|
def get(self):
|
||||||
|
""" Returns the current decoded license. """
|
||||||
|
if SuperUserPermission().can():
|
||||||
|
try:
|
||||||
|
decoded_license = config_provider.get_license()
|
||||||
|
except LicenseError as le:
|
||||||
|
raise InvalidRequest(le.message)
|
||||||
|
|
||||||
|
if decoded_license.is_expired:
|
||||||
|
raise InvalidRequest('License has expired')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'decoded': decoded_license.subscription,
|
||||||
|
'success': True
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
@nickname('updateLicense')
|
||||||
|
@require_fresh_login
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
|
@verify_not_prod
|
||||||
|
@validate_json_request('UpdateLicense')
|
||||||
|
def put(self):
|
||||||
|
""" Validates the given license contents and then saves it to the config volume. """
|
||||||
|
if SuperUserPermission().can():
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/messages')
|
||||||
|
@show_if(features.SUPER_USERS)
|
||||||
|
class SuperUserMessages(ApiResource):
|
||||||
|
""" Resource for getting a list of super user messages """
|
||||||
|
|
||||||
|
schemas = {
|
||||||
|
'GetMessage': {
|
||||||
|
'id': 'GetMessage',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Messages that a super user has saved in the past',
|
||||||
|
'properties': {
|
||||||
|
'message': {
|
||||||
|
'type': 'array',
|
||||||
|
'description': 'A list of messages',
|
||||||
|
'itemType': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'id': {
|
||||||
|
'type': 'integer',
|
||||||
|
'description': 'The message id',
|
||||||
|
},
|
||||||
|
'content': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The actual message',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'CreateMessage': {
|
||||||
|
'id': 'CreateMessage',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Create a new message',
|
||||||
|
'properties': {
|
||||||
|
'message': {
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'A single message',
|
||||||
|
'properties': {
|
||||||
|
'content': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The actual message',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@nickname('getMessages')
|
||||||
|
def get(self):
|
||||||
|
""" Return a super users messages """
|
||||||
|
return {
|
||||||
|
'messages': [message_view(m) for m in model.message.get_messages()],
|
||||||
|
}
|
||||||
|
|
||||||
|
@verify_not_prod
|
||||||
|
@nickname('createMessages')
|
||||||
|
@validate_json_request('CreateMessage')
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
|
def post(self):
|
||||||
|
""" Create a message """
|
||||||
|
if SuperUserPermission().can():
|
||||||
|
model.message.create([request.get_json()['message']])
|
||||||
|
return make_response('', 201)
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
def message_view(message):
|
||||||
|
return {'id': message.id, 'content': message.content}
|
||||||
|
|
|
@ -567,6 +567,33 @@ a:focus {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-license-field-element textarea {
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-license-field-element .license-status {
|
||||||
|
margin-bottom: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-license-field-element table td:first-child {
|
||||||
|
width: 150px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-license-field-element .fa {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-license-field-element .license-valid h4 {
|
||||||
|
color: #2FC98E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-license-field-element .license-invalid h4 {
|
||||||
|
color: #D64456;
|
||||||
|
}
|
||||||
|
|
||||||
.co-checkbox {
|
.co-checkbox {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
40
static/directives/config/config-license-field.html
Normal file
40
static/directives/config/config-license-field.html
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<div class="config-license-field-element">
|
||||||
|
<!-- Note: This hidden input will only have a value if there is a valid license, ensuring that the user cannot save
|
||||||
|
config if the license is invalid (since this box will be empty and therefore "required") -->
|
||||||
|
<input type="text" name="licenseRequiredBox" ng-model="requiredBox" style="visibility: hidden; height: 1px; position: absolute;" required>
|
||||||
|
|
||||||
|
<div class="cor-loader-inline" ng-show="state == 'loading-license'"></div>
|
||||||
|
|
||||||
|
<div class="license-valid license-status" ng-show="state == 'license-valid'">
|
||||||
|
<h4><i class="fa fa-check-circle"></i>License Valid</h4>
|
||||||
|
<table class="co-table">
|
||||||
|
<tr><td>Product:</td><td>{{ licenseDecoded.publicProductName || licenseDecoded.productName }}</td></tr>
|
||||||
|
<tr><td>Plan:</td><td>{{ licenseDecoded.publicPlanName || licenseDecoded.planName }}</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="license-invalid license-status" ng-show="state == 'license-error'">
|
||||||
|
<h4><i class="fa fa-times-circle"></i> Validation Failed</h4>
|
||||||
|
<h5>{{ licenseError }}</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-default" ng-show="!showingEditor" ng-click="showEditor($event)"><i class="fa fa-pencil"></i> Update License</button>
|
||||||
|
|
||||||
|
<div class="license-editor" ng-show="showingEditor">
|
||||||
|
<p>
|
||||||
|
Your license 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>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<textarea id="enterLicenseBox" ng-model="licenseContents" class="form-control"
|
||||||
|
placeholder="Paste your raw license here, which should already be in base64 format: GtqMjMwNDgyM3Vq..."
|
||||||
|
ng-readonly="state == 'validating-license'"></textarea>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" ng-show="state != 'validating-license'"
|
||||||
|
ng-click="validateAndUpdate($event)" ng-disabled="!licenseContents">Update License</button>
|
||||||
|
|
||||||
|
<div class="license-validating" ng-show="state == 'validating-license'">
|
||||||
|
<span class="cor-loader-inline"></span> Validating License
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -3,6 +3,16 @@
|
||||||
<div ng-show="config && config['SUPER_USERS']">
|
<div ng-show="config && config['SUPER_USERS']">
|
||||||
<form id="configform" name="configform">
|
<form id="configform" name="configform">
|
||||||
|
|
||||||
|
<!-- License -->
|
||||||
|
<div class="co-panel">
|
||||||
|
<div class="co-panel-heading">
|
||||||
|
<i class="fa fa-credit-card-alt"></i> License
|
||||||
|
</div>
|
||||||
|
<div class="co-panel-body">
|
||||||
|
<div class="config-license-field"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Basic Configuration -->
|
<!-- Basic Configuration -->
|
||||||
<div class="co-panel">
|
<div class="co-panel">
|
||||||
<div class="co-panel-heading">
|
<div class="co-panel-heading">
|
||||||
|
|
|
@ -1246,5 +1246,69 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return directiveDefinitionObject;
|
return directiveDefinitionObject;
|
||||||
|
})
|
||||||
|
|
||||||
|
.directive('configLicenseField', function () {
|
||||||
|
var directiveDefinitionObject = {
|
||||||
|
priority: 0,
|
||||||
|
templateUrl: '/static/directives/config/config-license-field.html',
|
||||||
|
replace: false,
|
||||||
|
transclude: false,
|
||||||
|
restrict: 'C',
|
||||||
|
scope: {
|
||||||
|
},
|
||||||
|
controller: function($scope, $element, ApiService) {
|
||||||
|
$scope.state = 'loading-license';
|
||||||
|
$scope.showingEditor = false;
|
||||||
|
$scope.requiredBox = '';
|
||||||
|
|
||||||
|
var loadLicense = function() {
|
||||||
|
ApiService.getLicense().then(function(resp) {
|
||||||
|
$scope.state = 'license-valid';
|
||||||
|
$scope.showingEditor = false;
|
||||||
|
$scope.licenseDecoded = resp['decoded'];
|
||||||
|
$scope.requiredBox = 'filled';
|
||||||
|
}, function(resp) {
|
||||||
|
$scope.licenseError = ApiService.getErrorMessage(resp);
|
||||||
|
$scope.state = 'license-error';
|
||||||
|
$scope.showingEditor = true;
|
||||||
|
$scope.requiredBox = '';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
loadLicense();
|
||||||
|
|
||||||
|
$scope.showEditor = function($event) {
|
||||||
|
$event.preventDefault();
|
||||||
|
$event.stopPropagation();
|
||||||
|
|
||||||
|
$scope.showingEditor = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.validateAndUpdate = function($event) {
|
||||||
|
$event.preventDefault();
|
||||||
|
$event.stopPropagation();
|
||||||
|
|
||||||
|
$scope.state = 'validating-license';
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
'license': $scope.licenseContents
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.updateLicense(data).then(function(resp) {
|
||||||
|
$scope.state = 'license-valid';
|
||||||
|
$scope.showingEditor = false;
|
||||||
|
$scope.licenseDecoded = resp['decoded'];
|
||||||
|
$scope.requiredBox = 'filled';
|
||||||
|
}, function(resp) {
|
||||||
|
$scope.licenseError = ApiService.getErrorMessage(resp);
|
||||||
|
$scope.state = 'license-error';
|
||||||
|
$scope.showingEditor = true;
|
||||||
|
$scope.requiredBox = '';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return directiveDefinitionObject;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -107,7 +107,7 @@
|
||||||
'hasDatabaseSSLCert': false,
|
'hasDatabaseSSLCert': false,
|
||||||
'licenseContents': '',
|
'licenseContents': '',
|
||||||
'licenseError': null,
|
'licenseError': null,
|
||||||
'licenseDecoded': null,
|
'licenseDecoded': null
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.$watch('currentStep', function(currentStep) {
|
$scope.$watch('currentStep', function(currentStep) {
|
||||||
|
|
|
@ -51,8 +51,7 @@ from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserMana
|
||||||
SuperUserOrganizationManagement, SuperUserOrganizationList,
|
SuperUserOrganizationManagement, SuperUserOrganizationList,
|
||||||
SuperUserAggregateLogs, SuperUserServiceKeyManagement,
|
SuperUserAggregateLogs, SuperUserServiceKeyManagement,
|
||||||
SuperUserServiceKey, SuperUserServiceKeyApproval,
|
SuperUserServiceKey, SuperUserServiceKeyApproval,
|
||||||
SuperUserTakeOwnership,)
|
SuperUserTakeOwnership, SuperUserMessages, SuperUserLicense)
|
||||||
from endpoints.api.globalmessages import (GlobalUserMessage, GlobalUserMessages,)
|
|
||||||
from endpoints.api.secscan import RepositoryImageSecurity
|
from endpoints.api.secscan import RepositoryImageSecurity
|
||||||
from endpoints.api.manifest import RepositoryManifestLabels, ManageRepositoryManifestLabel
|
from endpoints.api.manifest import RepositoryManifestLabels, ManageRepositoryManifestLabel
|
||||||
|
|
||||||
|
@ -4158,6 +4157,37 @@ class TestSuperUserList(ApiTestCase):
|
||||||
self._run_test('GET', 200, 'devtable', None)
|
self._run_test('GET', 200, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSuperUserLicense(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(SuperUserLicense)
|
||||||
|
|
||||||
|
def test_get_anonymous(self):
|
||||||
|
self._run_test('GET', 401, None, None)
|
||||||
|
|
||||||
|
def test_get_freshuser(self):
|
||||||
|
self._run_test('GET', 403, 'freshuser', None)
|
||||||
|
|
||||||
|
def test_get_reader(self):
|
||||||
|
self._run_test('GET', 403, 'reader', None)
|
||||||
|
|
||||||
|
def test_get_devtable(self):
|
||||||
|
self._run_test('GET', 400, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_put_anonymous(self):
|
||||||
|
self._run_test('PUT', 401, None, {})
|
||||||
|
|
||||||
|
def test_put_freshuser(self):
|
||||||
|
self._run_test('PUT', 403, 'freshuser', {'license': ''})
|
||||||
|
|
||||||
|
def test_put_reader(self):
|
||||||
|
self._run_test('PUT', 403, 'reader', {'license': ''})
|
||||||
|
|
||||||
|
def test_put_devtable(self):
|
||||||
|
self._run_test('PUT', 400, 'devtable', {'license': ''})
|
||||||
|
|
||||||
|
|
||||||
class TestSuperUserManagement(ApiTestCase):
|
class TestSuperUserManagement(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
|
|
|
@ -111,18 +111,19 @@ class TestLicense(unittest.TestCase):
|
||||||
self.assertFalse(license.is_expired)
|
self.assertFalse(license.is_expired)
|
||||||
|
|
||||||
def test_validate_basic_license(self):
|
def test_validate_basic_license(self):
|
||||||
decoded = self.get_license(timedelta(days=30), entitlements={})
|
decoded = self.get_license(timedelta(days=30), service_end=timedelta(days=40),
|
||||||
|
duration='months', entitlements={})
|
||||||
decoded.validate({'DISTRIBUTED_STORAGE_CONFIG': [{}]})
|
decoded.validate({'DISTRIBUTED_STORAGE_CONFIG': [{}]})
|
||||||
|
|
||||||
def test_validate_storage_entitlement_valid(self):
|
def test_validate_storage_entitlement_valid(self):
|
||||||
decoded = self.get_license(timedelta(days=30), entitlements={
|
decoded = self.get_license(timedelta(days=30), service_end=timedelta(days=40), entitlements={
|
||||||
'software.quay.regions': 2,
|
'software.quay.regions': 2,
|
||||||
})
|
})
|
||||||
|
|
||||||
decoded.validate({'DISTRIBUTED_STORAGE_CONFIG': [{}]})
|
decoded.validate({'DISTRIBUTED_STORAGE_CONFIG': [{}]})
|
||||||
|
|
||||||
def test_validate_storage_entitlement_invalid(self):
|
def test_validate_storage_entitlement_invalid(self):
|
||||||
decoded = self.get_license(timedelta(days=30), entitlements={
|
decoded = self.get_license(timedelta(days=30), service_end=timedelta(days=40), entitlements={
|
||||||
'software.quay.regions': 1,
|
'software.quay.regions': 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -102,22 +102,22 @@ class BaseProvider(object):
|
||||||
|
|
||||||
def _get_license_file(self):
|
def _get_license_file(self):
|
||||||
""" Returns the contents of the license file. """
|
""" Returns the contents of the license file. """
|
||||||
|
if not self.has_license_file():
|
||||||
|
msg = 'Could not find license file. Please make sure it is in your config volume.'
|
||||||
|
raise LicenseError(msg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.get_volume_file(LICENSE_FILENAME)
|
return self.get_volume_file(LICENSE_FILENAME)
|
||||||
except IOError:
|
except IOError:
|
||||||
msg = 'Could not open license file. Please make sure it is in your config volume.'
|
msg = 'Could not open license file. Please make sure it is in your config volume.'
|
||||||
raise LicenseError(msg)
|
raise LicenseError(msg)
|
||||||
|
|
||||||
def validate_license(self, config):
|
def get_license(self):
|
||||||
""" Validates that the configuration matches the license file (if any). """
|
""" Returns the decoded license, if any. """
|
||||||
if not config.get('SETUP_COMPLETE', False):
|
|
||||||
raise SetupIncompleteException()
|
|
||||||
|
|
||||||
with self._get_license_file() as f:
|
with self._get_license_file() as f:
|
||||||
license_file_contents = f.read()
|
license_file_contents = f.read()
|
||||||
|
|
||||||
self.license = decode_license(license_file_contents)
|
return decode_license(license_file_contents)
|
||||||
self.license.validate(config)
|
|
||||||
|
|
||||||
def save_license(self, license_file_contents):
|
def save_license(self, license_file_contents):
|
||||||
""" Saves the given contents as the license file. """
|
""" Saves the given contents as the license file. """
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import json
|
import json
|
||||||
from StringIO import StringIO
|
import io
|
||||||
|
|
||||||
from util.config.provider.baseprovider import BaseProvider
|
from util.config.provider.baseprovider import BaseProvider
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ class TestConfigProvider(BaseProvider):
|
||||||
if filename in REAL_FILES:
|
if filename in REAL_FILES:
|
||||||
return open(filename, mode=mode)
|
return open(filename, mode=mode)
|
||||||
|
|
||||||
return StringIO(self.files[filename])
|
return io.BytesIO(self.files[filename])
|
||||||
|
|
||||||
def requires_restart(self, app_config):
|
def requires_restart(self, app_config):
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -43,11 +43,26 @@ class LicenseValidationError(LicenseError):
|
||||||
def _get_date(decoded, field):
|
def _get_date(decoded, field):
|
||||||
""" Retrieves the encoded date found at the given field under the decoded license block. """
|
""" Retrieves the encoded date found at the given field under the decoded license block. """
|
||||||
date_str = decoded.get(field)
|
date_str = decoded.get(field)
|
||||||
if date_str:
|
return parser.parse(date_str).replace(tzinfo=None) if date_str else None
|
||||||
return parser.parse(date_str).replace(tzinfo=None)
|
|
||||||
|
|
||||||
return datetime.now() - timedelta(days=2)
|
|
||||||
|
|
||||||
|
class LicenseExpirationDate(object):
|
||||||
|
def __init__(self, title, expiration_date, grace_period=None):
|
||||||
|
self.title = title
|
||||||
|
self.expiration_date = expiration_date
|
||||||
|
self.grace_period = grace_period or timedelta(seconds=0)
|
||||||
|
|
||||||
|
def check_expired(self, cutoff_date=None):
|
||||||
|
return self.expiration_and_grace <= (cutoff_date or datetime.now())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expiration_and_grace(self):
|
||||||
|
return self.expiration_date + self.grace_period
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'License expiration "%s" date %s with grace %s: %s' % (self.title, self.expiration_date,
|
||||||
|
self.grace_period,
|
||||||
|
self.check_expired())
|
||||||
|
|
||||||
class License(object):
|
class License(object):
|
||||||
""" License represents a fully decoded and validated (but potentially expired) license. """
|
""" License represents a fully decoded and validated (but potentially expired) license. """
|
||||||
|
@ -65,7 +80,8 @@ class License(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_expired(self):
|
def is_expired(self):
|
||||||
return self._get_expired(datetime.now())
|
cutoff_date = datetime.now()
|
||||||
|
return bool([dt for dt in self._get_expiration_dates() if dt.check_expired(cutoff_date)])
|
||||||
|
|
||||||
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. """
|
||||||
|
@ -81,47 +97,54 @@ class License(object):
|
||||||
max_regions)
|
max_regions)
|
||||||
raise LicenseValidationError(msg)
|
raise LicenseValidationError(msg)
|
||||||
|
|
||||||
def _get_expired(self, compare_date):
|
def _get_expiration_dates(self):
|
||||||
# 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')
|
||||||
if expiration_date <= compare_date:
|
if expiration_date is None:
|
||||||
logger.debug('License expired on %s', expiration_date)
|
yield LicenseExpirationDate('No valid Tectonic Account License', datetime.min)
|
||||||
return True
|
return
|
||||||
|
|
||||||
|
yield LicenseExpirationDate('Tectonic Account License', expiration_date)
|
||||||
|
|
||||||
# Check for any QE subscriptions.
|
# Check for any QE subscriptions.
|
||||||
sub = self.subscription
|
sub = self.subscription
|
||||||
if sub is None:
|
if sub is None:
|
||||||
return True
|
yield LicenseExpirationDate('No Quay Enterprise Subscription', datetime.min)
|
||||||
|
return
|
||||||
|
|
||||||
# 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)
|
if trial_end_date is None:
|
||||||
return trial_end_date <= (compare_date - TRIAL_GRACE_PERIOD)
|
yield LicenseExpirationDate('Invalid trial subscription', datetime.min)
|
||||||
|
else:
|
||||||
|
yield LicenseExpirationDate('Trial subscription', trial_end_date, TRIAL_GRACE_PERIOD)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
# 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 service_end_date is None:
|
||||||
|
yield LicenseExpirationDate('No valid Quay Enterprise Subscription', datetime.min)
|
||||||
|
return
|
||||||
|
|
||||||
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)
|
yield LicenseExpirationDate('In-trial subscription', service_end_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', 'months')
|
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 == "months":
|
if duration_period == "months":
|
||||||
logger.debug('Monthly license expires on %s', service_end_date)
|
yield LicenseExpirationDate('Monthly subscription', service_end_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)
|
yield LicenseExpirationDate('Yearly subscription', service_end_date, YEARLY_GRACE_PERIOD)
|
||||||
return service_end_date <= (compare_date - YEARLY_GRACE_PERIOD)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
_PROD_LICENSE_PUBLIC_KEY_DATA = """
|
_PROD_LICENSE_PUBLIC_KEY_DATA = """
|
||||||
|
|
Reference in a new issue