Remove license code in Quay

No longer needed under Red Hat rules \o/

Fixes https://jira.coreos.com/browse/QUAY-883
This commit is contained in:
Joseph Schorr 2018-03-20 17:03:35 -04:00
parent 041a7fcd36
commit 3586955669
23 changed files with 19 additions and 1471 deletions

4
app.py
View file

@ -44,7 +44,6 @@ from util.config.configutil import generate_secret_key
from util.config.provider import get_config_provider
from util.config.superusermanager import SuperUserManager
from util.label_validator import LabelValidator
from util.license import LicenseValidator
from util.metrics.metricqueue import MetricQueue
from util.metrics.prometheus import PrometheusPlugin
from util.saas.cloudwatch import start_cloudwatch_sender
@ -203,9 +202,6 @@ instance_keys = InstanceKeys(app)
label_validator = LabelValidator(app)
build_canceller = BuildCanceller(app)
license_validator = LicenseValidator(config_provider)
license_validator.start()
start_cloudwatch_sender(metric_queue, app)
github_trigger = GithubOAuthService(app.config, 'GITHUB_TRIGGER_CONFIG')

View file

@ -21,7 +21,6 @@ from endpoints.common import common_login
from util.config.configutil import add_enterprise_config_defaults
from util.config.database import sync_database_with_config
from util.config.validator import validate_service_for_config, is_valid_config_upload_filename
from util.license import decode_license, LicenseDecodeError
import features
@ -68,12 +67,6 @@ class SuperUserRegistryStatus(ApiResource):
'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 not config_provider.config_exists():
return {
@ -265,51 +258,6 @@ class SuperUserConfig(ApiResource):
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 LicenseDecodeError as le:
raise InvalidRequest(le.message)
statuses = decoded_license.validate({})
all_met = all(status.is_met() for status in statuses)
if all_met:
config_provider.save_license(license_contents)
return {
'status': [status.as_dict(for_private=True) for status in statuses],
'success': all_met,
}
@resource('/v1/superuser/config/file/<filename>')
@internal_only
@show_if(features.SUPER_USERS)

View file

@ -13,7 +13,7 @@ from flask import request, make_response, jsonify
import features
from app import app, avatar, superusers, authentication, config_provider, license_validator
from app import app, avatar, superusers, authentication, config_provider
from auth import scopes
from auth.auth_context import get_authenticated_user
from auth.permissions import SuperUserPermission
@ -28,7 +28,6 @@ from endpoints.api.superuser_models_pre_oci import (pre_oci_model, ServiceKeyDoe
ServiceKeyAlreadyApproved,
InvalidRepositoryBuildException)
from util.useremails import send_confirmation_email, send_recovery_email
from util.license import decode_license, LicenseDecodeError
from util.security.ssl import load_certificate, CertInvalidException
from util.config.validator import EXTRA_CA_DIRECTORY
from _init import ROOT_DIR
@ -968,77 +967,6 @@ class SuperUserCustomCertificate(ApiResource):
raise Unauthorized()
@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 LicenseDecodeError as le:
raise InvalidRequest(le.message)
statuses = decoded_license.validate(app.config)
all_met = all(status.is_met() for status in statuses)
return {
'status': [status.as_dict(for_private=True) for status in statuses],
'success': all_met,
}
raise Unauthorized()
@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 LicenseDecodeError as le:
raise InvalidRequest(le.message)
statuses = decoded_license.validate(app.config)
all_met = all(status.is_met() for status in statuses)
if all_met:
# Save the license and update the license check thread.
config_provider.save_license(license_contents)
license_validator.compute_license_sufficiency()
return {
'status': [status.as_dict(for_private=True) for status in statuses],
'success': all_met,
}
raise Unauthorized()
@resource('/v1/superuser/<build_uuid>/logs')
@path_param('build_uuid', 'The UUID of the build')
@show_if(features.SUPER_USERS)

View file

@ -9,7 +9,7 @@ from flask_principal import identity_changed
import endpoints.decorated # Register the various exceptions via decorators.
import features
from app import app, oauth_apps, oauth_login, LoginWrappedDBUser, user_analytics, license_validator
from app import app, oauth_apps, oauth_login, LoginWrappedDBUser, user_analytics
from auth import scopes
from auth.permissions import QuayDeferredPermissionUser
from config import frontend_visible_config
@ -143,8 +143,6 @@ def render_page_template(name, route_data=None, **kwargs):
hostname=app.config['SERVER_HOSTNAME'],
preferred_scheme=app.config['PREFERRED_URL_SCHEME'],
version_number=version_number,
license_insufficient=license_validator.insufficient,
license_expiring=license_validator.expiring_soon,
current_year=datetime.datetime.now().year,
**kwargs)

View file

@ -1,11 +1,10 @@
from flask import Blueprint, make_response
from app import metric_queue, license_validator
from app import metric_queue
from endpoints.decorators import anon_protect, anon_allowed
from util.metrics.metricqueue import time_blueprint
v1_bp = Blueprint('v1', __name__)
license_validator.enforce_license_before_request(v1_bp)
time_blueprint(v1_bp, metric_queue)

View file

@ -10,7 +10,7 @@ from semantic_version import Spec
import features
from app import app, metric_queue, get_app_url, license_validator
from app import app, metric_queue, get_app_url
from auth.auth_context import get_authenticated_context
from auth.permissions import (
ReadRepositoryPermission, ModifyRepositoryPermission, AdministerRepositoryPermission)
@ -26,7 +26,6 @@ from util.pagination import encrypt_page_token, decrypt_page_token
logger = logging.getLogger(__name__)
v2_bp = Blueprint('v2', __name__)
license_validator.enforce_license_before_request(v2_bp)
time_blueprint(v2_bp, metric_queue)

View file

@ -5,7 +5,7 @@ from flask import redirect, Blueprint, abort, send_file, make_response, request
import features
from app import app, signer, storage, metric_queue, license_validator, config_provider, ip_resolver
from app import app, signer, storage, metric_queue, config_provider, ip_resolver
from auth.auth_context import get_authenticated_user
from auth.decorators import process_auth
from auth.permissions import ReadRepositoryPermission
@ -27,7 +27,6 @@ from util.registry.torrent import (
logger = logging.getLogger(__name__)
verbs = Blueprint('verbs', __name__)
license_validator.enforce_license_before_request(verbs)
LAYER_MIMETYPE = 'binary/octet-stream'

View file

@ -1,49 +0,0 @@
.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 .config-license-field {
margin-top: 30px;
}
.initial-setup-modal .license-valid .fa {
margin-right: 6px;
}
.initial-setup-modal .license-valid table {
margin-top: 40px;
}

View file

@ -1,75 +0,0 @@
<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 == LicenseStates.validating"></div>
<div class="license-valid license-status" ng-show="state == LicenseStates.valid">
<h4><i class="fa fa-check-circle"></i>License Valid</h4>
<table class="co-table">
<thead>
<td>Requirement</td>
<td>Required Count</td>
<td>Subscription</td>
<td>Subscription Count</td>
<td>Expiration Date</td>
</thead>
<tr ng-repeat="status in licenseStatus">
<td>{{ requirementTitles[status.requirement.name] }}</td>
<td>{{ status.requirement.count }}</td>
<td>{{ status.entitlement.product_name }}</td>
<td>{{ status.entitlement.count }}</td>
<td><time-ago datetime="status.entitlement.expiration.expiration_date"></time-ago></td>
</tr>
</table>
</div>
<div class="license-invalid license-status" ng-show="state == LicenseStates.invalid">
<h4><i class="fa fa-times-circle"></i> Validation Failed</h4>
<h5 ng-if="licenseError">{{ licenseError }}</h5>
<h5 ng-if="!licenseError && licenseStatus">
<p>The following errors were found:</p>
<ul>
<li ng-repeat="status in licenseStatus" ng-if="status.status != 'EntitlementStatus.met'">
<div ng-switch on="status.status">
<!-- insufficient_count -->
<div ng-switch-when="EntitlementStatus.insufficient_count">
<strong>{{ requirementTitles[status.requirement.name] }}</strong>: <code class="required">{{ status.requirement.count }}</code> <span ng-if="status.requirement.count != 1">are</span><span ng-if="status.requirement.count == 1">is</span> required: License provides <code>{{ status.entitlement.count }}</code>
</div>
<!-- no_matching -->
<div ng-switch-when="EntitlementStatus.no_matching">
<strong>{{ requirementTitles[status.requirement.name] }}</strong>: License is missing requirement
</div>
<!-- expired -->
<div ng-switch-when="EntitlementStatus.expired">
<strong>{{ requirementTitles[status.requirement.name] }}</strong>: Requirement expired on <code>{{ status.entitlement.expiration.expiration_date }}</code>
</div>
</div>
</li>
</ul>
</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 == LicenseStates.validating"></textarea>
<button class="btn btn-primary" ng-show="state != LicenseStates.validating"
ng-click="validateAndUpdate($event)" ng-disabled="!licenseContents">Update License</button>
<div class="license-validating" ng-show="state == LicenseStates.validating">
<span class="cor-loader-inline"></span> Validating License
</div>
</div>
</div>

View file

@ -3,16 +3,6 @@
<div ng-show="config && config['SUPER_USERS']">
<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>
<!-- Custom SSL certificates -->
<div class="co-panel" id="custom-ssl">
<div class="co-panel-heading">

View file

@ -39,9 +39,6 @@ import * as URI from 'urijs';
// The config.yaml exists but it is invalid.
'INVALID_CONFIG': 'config-invalid',
// License is being uploaded.
'UPLOAD_LICENSE': 'upload-license',
// DB is being configured.
'CONFIG_DB': 'config-db',
@ -100,8 +97,7 @@ import * as URI from 'urijs';
$scope.currentConfig = null;
$scope.currentState = {
'hasDatabaseSSLCert': false,
'licenseValid': false
'hasDatabaseSSLCert': false
};
$scope.$watch('currentStep', function(currentStep) {
@ -127,7 +123,6 @@ import * as URI from 'urijs';
case $scope.States.CREATE_SUPERUSER:
case $scope.States.DB_RESTARTING:
case $scope.States.CONFIG_DB:
case $scope.States.UPLOAD_LICENSE:
case $scope.States.VALID_CONFIG:
case $scope.States.READY:
$('#setupModal').modal({
@ -173,7 +168,6 @@ import * as URI from 'urijs';
var States = $scope.States;
return [
isStepFamily(step, States.UPLOAD_LICENSE),
isStepFamily(step, States.CONFIG_DB),
isStepFamily(step, States.DB_SETUP),
isStep(step, States.DB_RESTARTING),

View file

@ -9,13 +9,12 @@
<div class="co-main-content-panel" style="padding: 20px;">
<div class="co-alert alert alert-info">
<span class="cor-step-bar" progress="stepProgress">
<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="Configure Database" text="1"></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="Create Superuser" text="3"></span>
<span class="cor-step" title="Configure Registry" text="4"></span>
<span class="cor-step" title="Validate Configuration" text="5"></span>
<span class="cor-step" title="Create Superuser" text="2"></span>
<span class="cor-step" title="Configure Registry" text="3"></span>
<span class="cor-step" title="Validate Configuration" text="4"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Setup Complete" icon="check"></span>
</span>
@ -37,13 +36,12 @@
<!-- Header -->
<div class="modal-header">
<span class="cor-step-bar" progress="stepProgress">
<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="Configure Database" text="1"></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="Create Superuser" text="3"></span>
<span class="cor-step" title="Configure Registry" text="4"></span>
<span class="cor-step" title="Validate Configuration" text="5"></span>
<span class="cor-step" title="Create Superuser" text="2"></span>
<span class="cor-step" title="Configure Registry" text="3"></span>
<span class="cor-step" title="Validate Configuration" text="4"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Setup Complete" icon="check"></span>
</span>
@ -130,20 +128,6 @@
The container must be restarted to apply the configuration changes.
</div>
<!-- Content: UPLOAD_LICENSE -->
<div class="modal-body upload-license entering" style="padding: 20px;"
ng-show="isStep(currentStep, States.UPLOAD_LICENSE)">
<h4>
Quay Enterprise License
</h4>
<div>
Please provide your Quay Enterprise License. It can be
found by clicking "copy and paste" link under "CoreOS License" tab
of your Account in the <a href="https://account.coreos.com" target="_blank">Tectonic Account</a>.
</div>
<div class="config-license-field" for-setup="true" is-valid="currentState.licenseValid"></div>
</div>
<!-- Content: DB_SETUP or DB_SETUP_ERROR -->
<div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.DB_SETUP, States.DB_SETUP_ERROR)">
@ -242,15 +226,6 @@
Database Validation Issue: {{ errors.DatabaseValidationError }}
</div>
<!-- Footer: UPLOAD_LICENSE -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.UPLOAD_LICENSE)">
<button type="submit" class="btn btn-primary" ng-click="beginSetup()"
ng-disabled="!currentState.licenseValid">
Continue
</button>
</div>
<!-- Footer: CONFIG_DB or DB_ERROR -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.CONFIG_DB, States.DB_ERROR)">

View file

@ -128,19 +128,6 @@ mixpanel.init("{{ mixpanel_key }}", { track_pageview : false, debug: {{ is_debug
<div id="co-l-footer-wrapper">
<nav class="navbar navbar-default header-bar co-m-navbar co-fx-box-shadow" role="navigation"></nav>
{% if not has_billing and license_expiring %}
<div class="co-alert co-alert-warning" style="margin-bottom: 0px;">
The Quay Enterprise license will expire shortly. Please contact your administrator to avoid
service disruption.
</div>
{% endif %}
{% if license_insufficient %}
<div class="co-alert co-alert-danger" style="margin-bottom: 0px;">
The Quay Enterprise license has expired or is insufficient for this installation. Please contact your administrator.
</div>
{% endif %}
<div class="quay-message-bar"></div>
<div quay-require="['BILLING']">
<div class="quay-service-status-bar"></div>

View file

@ -51,7 +51,7 @@ from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserMana
SuperUserOrganizationManagement, SuperUserOrganizationList,
SuperUserAggregateLogs, SuperUserServiceKeyManagement,
SuperUserServiceKey, SuperUserServiceKeyApproval,
SuperUserTakeOwnership, SuperUserLicense,
SuperUserTakeOwnership,
SuperUserCustomCertificates,
SuperUserCustomCertificate, SuperUserRepositoryBuildLogs,
SuperUserRepositoryBuildResource, SuperUserRepositoryBuildStatus)
@ -4187,37 +4187,6 @@ class TestSuperUserCustomCertificate(ApiTestCase):
self._run_test('DELETE', 204, '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', 200, '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):
def setUp(self):
ApiTestCase.setUp(self)

View file

@ -4036,20 +4036,13 @@ class TestSuperUserCreateInitialSuperUser(ApiTestCase):
class TestSuperUserConfig(ApiTestCase):
def test_get_status_update_config(self):
# With no config the status should be 'upload-license'.
# With no config the status should be 'config-db'.
json = self.getJsonResponse(SuperUserRegistryStatus)
self.assertEquals('upload-license', json['status'])
self.assertEquals('config-db', json['status'])
# And the config should 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.
fake_config = {
'AUTHENTICATION_TYPE': 'Database',

View file

@ -1,570 +0,0 @@
import unittest
from datetime import datetime, timedelta
import jwt
import json
from Crypto.PublicKey import RSA
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_der_public_key
from util.license import (decode_license, LicenseDecodeError, ExpirationType,
MONTHLY_GRACE_PERIOD, YEARLY_GRACE_PERIOD, TRIAL_GRACE_PERIOD,
QUAY_DEPLOYMENTS_ENTITLEMENT, QUAY_ENTITLEMENT)
def get_date(delta):
return str(datetime.now() + delta)
class TestLicense(unittest.TestCase):
def keys(self):
with open('test/data/test.pem') as f:
private_key = f.read()
public_key = load_der_public_key(RSA.importKey(private_key).publickey().exportKey('DER'),
backend=default_backend())
return (public_key, private_key)
def create_license(self, license_data, keys=None):
jwt_data = {
'license': json.dumps(license_data),
}
(public_key, private_key) = keys or self.keys()
# Encode the license with the JWT key.
encoded = jwt.encode(jwt_data, private_key, algorithm='RS256')
# Decode it into a license object.
return decode_license(encoded, public_key_instance=public_key)
def test_license_decodeerror_invalid(self):
with self.assertRaises(LicenseDecodeError):
decode_license('some random stuff')
def test_license_decodeerror_badkey(self):
(_, private_key) = self.keys()
jwt_data = {
'license': json.dumps({}),
}
encoded_stuff = jwt.encode(jwt_data, private_key, algorithm='RS256')
with self.assertRaises(LicenseDecodeError):
# Note that since we don't give a key here, the prod one will be used, and it should fail.
decode_license(encoded_stuff)
def assertValid(self, license, config=None):
results = license.validate(config or {})
is_met = all([r.is_met() for r in results])
self.assertTrue(is_met, [r for r in results if not r.is_met()])
def assertNotValid(self, license, config=None, requirement=None, expired=None):
results = license.validate(config or {})
is_met = all([r.is_met() for r in results])
self.assertFalse(is_met)
invalid_results = [r for r in results if not r.is_met()]
if requirement is not None:
self.assertEquals(invalid_results[0].requirement.name, requirement)
if expired is not None:
self.assertEquals(invalid_results[0].entitlement.expiration.expiration_type, expired)
def test_missing_subscriptions(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
})
self.assertNotValid(license, requirement=QUAY_ENTITLEMENT)
def test_empty_subscriptions(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {},
})
self.assertNotValid(license, requirement=QUAY_ENTITLEMENT)
def test_missing_quay_entitlement(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {
"somesub": {
"serviceEnd": get_date(timedelta(days=10)),
"entitlements": {
QUAY_DEPLOYMENTS_ENTITLEMENT: 0,
},
},
},
})
self.assertNotValid(license, requirement=QUAY_ENTITLEMENT)
def test_valid_quay_entitlement(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {
"somesub": {
"serviceEnd": get_date(timedelta(days=10)),
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 1,
},
},
},
})
self.assertValid(license)
def test_missing_expiration(self):
license = self.create_license({
"subscriptions": {
"somesub": {
"serviceEnd": get_date(timedelta(days=10)),
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 1,
},
},
},
})
self.assertNotValid(license, expired=ExpirationType.license_wide)
def test_expired_license(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=-10)),
"subscriptions": {
"somesub": {
"serviceEnd": get_date(timedelta(days=10)),
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 1,
},
},
},
})
self.assertNotValid(license, expired=ExpirationType.license_wide)
def test_expired_sub_implicit_monthly_withingrace(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {
"somesub": {
"serviceEnd": get_date(MONTHLY_GRACE_PERIOD * -1 + timedelta(days=1)),
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 1,
},
},
},
})
self.assertValid(license)
def test_expired_sub_monthly_withingrace(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {
"somesub": {
"serviceEnd": get_date(MONTHLY_GRACE_PERIOD * -1 + timedelta(days=1)),
"durationPeriod": "monthly",
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 1,
},
},
},
})
self.assertValid(license)
def test_expired_sub_monthly_outsidegrace(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {
"somesub": {
"serviceEnd": get_date(MONTHLY_GRACE_PERIOD * -1 + timedelta(days=-1)),
"durationPeriod": "monthly",
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 1,
},
},
},
})
self.assertNotValid(license, expired=ExpirationType.monthly)
def test_expired_sub_yearly_withingrace(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {
"somesub": {
"serviceEnd": get_date(YEARLY_GRACE_PERIOD * -1 + timedelta(days=1)),
"durationPeriod": "yearly",
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 1,
},
},
},
})
self.assertValid(license)
def test_expired_sub_yearly_outsidegrace(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {
"somesub": {
"serviceEnd": get_date(YEARLY_GRACE_PERIOD * -1 + timedelta(days=-1)),
"durationPeriod": "yearly",
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 1,
},
},
},
})
self.assertNotValid(license, expired=ExpirationType.yearly)
def test_expired_sub_intrial_withingrace(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {
"somesub": {
"serviceEnd": get_date(TRIAL_GRACE_PERIOD * -1 + timedelta(days=1)),
"inTrial": True,
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 1,
},
},
},
})
self.assertValid(license)
def test_expired_sub_intrial_outsidegrace(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {
"somesub": {
"serviceEnd": get_date(TRIAL_GRACE_PERIOD * -1 + timedelta(days=-1)),
"inTrial": True,
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 1,
},
},
},
})
self.assertNotValid(license, expired=ExpirationType.in_trial)
def test_expired_sub_trialonly_withingrace(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {
"somesub": {
"trialEnd": get_date(TRIAL_GRACE_PERIOD * -1 + timedelta(days=1)),
"trialOnly": True,
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 1,
},
},
},
})
self.assertValid(license)
def test_expired_sub_trialonly_outsidegrace(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {
"somesub": {
"trialEnd": get_date(TRIAL_GRACE_PERIOD * -1 + timedelta(days=-1)),
"trialOnly": True,
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 1,
},
},
},
})
self.assertNotValid(license, expired=ExpirationType.trial_only)
def test_valid_quay_entitlement_regions(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {
"somesub": {
"serviceEnd": get_date(timedelta(days=10)),
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 1,
},
},
},
})
config = {
'DISTRIBUTED_STORAGE_CONFIG': [
{'name': 'first'},
],
}
self.assertValid(license, config=config)
def test_invalid_quay_entitlement_regions(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {
"somesub": {
"serviceEnd": get_date(timedelta(days=10)),
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 1,
},
},
},
})
config = {
'DISTRIBUTED_STORAGE_CONFIG': [
{'name': 'first'},
{'name': 'second'},
],
}
self.assertNotValid(license, config=config, requirement=QUAY_DEPLOYMENTS_ENTITLEMENT)
def test_valid_regions_across_multiple_sub(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {
"somesub": {
"serviceEnd": get_date(timedelta(days=10)),
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 1,
},
},
"anothersub": {
"serviceEnd": get_date(timedelta(days=20)),
"entitlements": {
QUAY_DEPLOYMENTS_ENTITLEMENT: 5,
},
},
},
})
config = {
'DISTRIBUTED_STORAGE_CONFIG': [
{'name': 'first'},
{'name': 'second'},
],
}
self.assertValid(license, config=config)
def test_valid_regions_across_multiple_sub_one_expired(self):
# Setup a license with one sub having too few regions, and another having enough, but it is
# expired.
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {
"somesub": {
"serviceEnd": get_date(timedelta(days=10)),
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 1,
},
},
"anothersub": {
"trialEnd": get_date(TRIAL_GRACE_PERIOD * -1 + timedelta(days=-1)),
"trialOnly": True,
"entitlements": {
QUAY_DEPLOYMENTS_ENTITLEMENT: 5,
},
},
},
})
config = {
'DISTRIBUTED_STORAGE_CONFIG': [
{'name': 'first'},
{'name': 'second'},
],
}
self.assertNotValid(license, config=config, requirement=QUAY_DEPLOYMENTS_ENTITLEMENT,
expired=ExpirationType.trial_only)
def test_valid_regions_across_multiple_sub_one_expired(self):
service_end = get_date(timedelta(days=20))
expiration_date = get_date(timedelta(days=10))
license = self.create_license({
"expirationDate": expiration_date,
"subscriptions": {
"somesub": {
"trialEnd": get_date(TRIAL_GRACE_PERIOD * -1 + timedelta(days=-1)),
"trialOnly": True,
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 3,
},
},
"anothersub": {
"serviceEnd": service_end,
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 5,
},
},
},
})
config = {
'DISTRIBUTED_STORAGE_CONFIG': [
{'name': 'first'},
{'name': 'second'},
],
}
self.assertValid(license, config=config)
entitlements = license.validate(config)
self.assertEquals(2, len(entitlements))
self.assertEntitlement(entitlements[0], QUAY_ENTITLEMENT, expiration_date)
self.assertEntitlement(entitlements[1], QUAY_DEPLOYMENTS_ENTITLEMENT, expiration_date)
def test_quay_is_under_expired_sub(self):
license = self.create_license({
"expirationDate": get_date(timedelta(days=10)),
"subscriptions": {
"somesub": {
"trialEnd": get_date(TRIAL_GRACE_PERIOD * -1 + timedelta(days=-1)),
"trialOnly": True,
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 3,
},
},
"anothersub": {
"serviceEnd": get_date(timedelta(days=20)),
"entitlements": {
QUAY_DEPLOYMENTS_ENTITLEMENT: 5,
},
},
},
})
config = {
'DISTRIBUTED_STORAGE_CONFIG': [
{'name': 'first'},
{'name': 'second'},
],
}
self.assertNotValid(license, config=config, expired=ExpirationType.trial_only,
requirement=QUAY_ENTITLEMENT)
def assertEntitlement(self, entitlement, expected_name, expected_date):
self.assertEquals(expected_name, entitlement.requirement.name)
self.assertEquals(expected_date, str(entitlement.entitlement.expiration.expiration_date))
def test_license_with_multiple_subscriptions(self):
service_end = get_date(timedelta(days=20))
expiration_date = get_date(timedelta(days=10))
trial_end = get_date(timedelta(days=2))
license = self.create_license({
"expirationDate": expiration_date,
"subscriptions": {
"realsub": {
"serviceEnd": service_end,
"entitlements": {
QUAY_ENTITLEMENT: 1,
},
},
"trialsub": {
"trialEnd": trial_end,
"trialOnly": True,
"inTrial": True,
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 3,
},
},
},
})
config = {
'DISTRIBUTED_STORAGE_CONFIG': [
{'name': 'first'},
{'name': 'second'},
],
}
self.assertValid(license, config=config)
entitlements = license.validate(config)
self.assertEquals(2, len(entitlements))
self.assertEntitlement(entitlements[0], QUAY_ENTITLEMENT, expiration_date)
self.assertEntitlement(entitlements[1], QUAY_DEPLOYMENTS_ENTITLEMENT, trial_end)
def test_license_with_multiple_subscriptions_one_expired(self):
service_end = get_date(timedelta(days=20))
expiration_date = get_date(timedelta(days=10))
trial_end = get_date(timedelta(days=-2))
license = self.create_license({
"expirationDate": expiration_date,
"subscriptions": {
"realsub": {
"serviceEnd": service_end,
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 3,
},
},
"trialsub": {
"trialEnd": trial_end,
"trialOnly": True,
"inTrial": True,
"entitlements": {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: 3,
},
},
},
})
config = {
'DISTRIBUTED_STORAGE_CONFIG': [
{'name': 'first'},
{'name': 'second'},
],
}
self.assertValid(license, config=config)
entitlements = license.validate(config)
self.assertEquals(2, len(entitlements))
self.assertEntitlement(entitlements[0], QUAY_ENTITLEMENT, expiration_date)
self.assertEntitlement(entitlements[1], QUAY_DEPLOYMENTS_ENTITLEMENT, expiration_date)
if __name__ == '__main__':
unittest.main()

View file

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

View file

@ -1,13 +1,9 @@
import logging
import yaml
from jsonschema import validate, ValidationError
from util.config.schema import CONFIG_SCHEMA
from util.license import LICENSE_FILENAME, LicenseDecodeError, decode_license
logger = logging.getLogger(__name__)
@ -63,8 +59,6 @@ def export_yaml(config_obj, config_file):
class BaseProvider(object):
""" A configuration provider helps to load, save, and handle config override in the application.
"""
def __init__(self):
self.license = None
@property
def provider_id(self):
@ -128,30 +122,3 @@ class BaseProvider(object):
""" Helper for constructing file paths, which may differ between providers. For example,
kubernetes can't have subfolders in configmaps """
raise NotImplementedError
def _get_license_file(self):
""" 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 LicenseDecodeError(msg)
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 LicenseDecodeError(msg)
def get_license(self):
""" Returns the decoded license, if any. """
with self._get_license_file() as f:
license_file_contents = f.read()
return decode_license(license_file_contents)
def save_license(self, license_file_contents):
""" Saves the given contents as the license file. """
self.write_volume_file(LICENSE_FILENAME, license_file_contents)
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

@ -4,21 +4,9 @@ import os
from datetime import datetime, timedelta
from util.config.provider.baseprovider import BaseProvider
from util.license import (EntitlementValidationResult, Entitlement, Expiration, ExpirationType,
EntitlementRequirement)
REAL_FILES = ['test/data/signing-private.gpg', 'test/data/signing-public.gpg', 'test/data/test.pem']
class TestLicense(object):
def validate_entitlement_requirement(self, entitlement_req, check_time):
expiration = Expiration(ExpirationType.license_wide, datetime.now() + timedelta(days=31))
entitlement = Entitlement('fake', 0, 'someprod', expiration)
fakereq = EntitlementRequirement('fake', 0)
return EntitlementValidationResult(fakereq, datetime.now(), entitlement)
def validate(self, config):
return [self.validate_entitlement_requirement(None, None)]
class TestConfigProvider(BaseProvider):
""" Implementation of the config provider for testing. Everything is kept in-memory instead on
the real file system. """
@ -82,10 +70,7 @@ class TestConfigProvider(BaseProvider):
def requires_restart(self, app_config):
return False
def get_license(self):
return TestLicense()
def reset_for_test(self):
self._config['SUPER_USERS'] = ['devtable']
self.files = {}

View file

@ -3,7 +3,6 @@ import logging
from auth.auth_context import get_authenticated_user
from data.users import LDAP_CERT_FILENAME
from util.config.validators.validate_license import LicenseValidator
from util.config.validators.validate_database import DatabaseValidator
from util.config.validators.validate_redis import RedisValidator
from util.config.validators.validate_storage import StorageValidator
@ -42,7 +41,6 @@ CONFIG_FILE_SUFFIXES = ['-cloudfront-signing-key.pem']
EXTRA_CA_DIRECTORY = 'extra_ca_certs'
VALIDATORS = {
LicenseValidator.name: LicenseValidator.validate,
DatabaseValidator.name: DatabaseValidator.validate,
RedisValidator.name: RedisValidator.validate,
StorageValidator.name: StorageValidator.validate,

View file

@ -1,48 +0,0 @@
import pytest
from mock import patch
from util.config.validators import ConfigValidationException
from util.config.validators.validate_license import LicenseValidator
from util.morecollections import AttrDict
from util.license import License, QUAY_DEPLOYMENTS_ENTITLEMENT, QUAY_ENTITLEMENT
from test.fixtures import *
@pytest.mark.parametrize('deployments, allowed_deployments', [
(1, 1),
(3, 3),
(3, 2),
(3, 1),
])
def test_too_many_storage_engines(deployments, allowed_deployments, app):
def get_license():
decoded = {
'expirationDate': '2157-12-1',
'subscriptions': {
'someSubscription': {
'serviceEnd': '2157-12-1',
'durationPeriod': 'yearly',
'entitlements': {
QUAY_ENTITLEMENT: 1,
QUAY_DEPLOYMENTS_ENTITLEMENT: allowed_deployments,
},
},
},
}
return License(decoded)
storage_configs = [(str(i), {}) for i in range(0, deployments)]
with patch('app.config_provider.get_license', get_license):
validator = LicenseValidator()
if allowed_deployments < deployments:
with pytest.raises(ConfigValidationException):
validator.validate({
'DISTRIBUTED_STORAGE_CONFIG': storage_configs,
}, None, None)
else:
validator.validate({
'DISTRIBUTED_STORAGE_CONFIG': storage_configs,
}, None, None)

View file

@ -1,19 +0,0 @@
from app import config_provider
from util.config.validators import BaseValidator, ConfigValidationException
from util.license import LicenseDecodeError, EntitlementStatus
class LicenseValidator(BaseValidator):
name = "license"
@classmethod
def validate(cls, config, user, user_password):
try:
decoded_license = config_provider.get_license()
except LicenseDecodeError as le:
raise ConfigValidationException('Could not decode license: %s' % le.message)
results = decoded_license.validate(config)
all_met = all(result.is_met() for result in results)
if not all_met:
reason = [result.description() for result in results if not result.is_met()]
raise ConfigValidationException('License does not match configuration: %s' % reason)

View file

@ -1,416 +0,0 @@
import json
import logging
import multiprocessing
import time
from ctypes import c_bool
from datetime import datetime, timedelta
from threading import Thread
from functools import total_ordering
from enum import Enum, IntEnum
from collections import namedtuple
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from dateutil import parser
from flask import make_response
import jwt
logger = logging.getLogger(__name__)
TRIAL_GRACE_PERIOD = timedelta(days=7) # 1 week
MONTHLY_GRACE_PERIOD = timedelta(days=335) # 11 months
YEARLY_GRACE_PERIOD = timedelta(days=90) # 3 months
LICENSE_SOON_DELTA = timedelta(days=7) # 1 week
LICENSE_FILENAME = 'license'
QUAY_ENTITLEMENT = 'software.quay'
QUAY_DEPLOYMENTS_ENTITLEMENT = 'software.quay.deployments'
class LicenseDecodeError(Exception):
""" Exception raised if the license could not be read, decoded or has expired. """
pass
def _get_date(decoded, field, default_date=datetime.min):
""" Retrieves the encoded date found at the given field under the decoded license block. """
date_str = decoded.get(field)
return parser.parse(date_str).replace(tzinfo=None) if date_str else default_date
@total_ordering
class Entitlement(object):
""" An entitlement is a specific piece of software or functionality granted
by a license. It has an expiration date, as well as the count of the
things being granted. Entitlements are orderable by their counts.
"""
def __init__(self, entitlement_name, count, product_name, expiration):
self.name = entitlement_name
self.count = count
self.product_name = product_name
self.expiration = expiration
def __lt__(self, rhs):
return self.count < rhs.count
def __repr__(self):
return str(dict(
name=self.name,
count=self.count,
product_name=self.product_name,
expiration=repr(self.expiration),
))
def as_dict(self, for_private=False):
data = {
'name': self.name,
}
if for_private:
data.update({
'count': self.count,
'product_name': self.product_name,
'expiration': self.expiration.as_dict(for_private=True),
})
return data
class ExpirationType(Enum):
""" An enum which represents the different possible types of expirations. If
you posess an expired enum, you can use this to figure out at what level
the expiration was most restrictive.
"""
license_wide = 'License Wide Expiration'
trial_only = 'Trial Only Expiration'
in_trial = 'In-Trial Expiration'
monthly = 'Monthly Subscription Expiration'
yearly = 'Yearly Subscription Expiration'
@total_ordering
class Expiration(object):
""" An Expiration is an orderable representation of an expiration date and a
grace period. If you sort Expiration objects, they will be sorted by the
actual cutoff date, which is the combination of the expiration date and
the grace period.
"""
def __init__(self, expiration_type, exp_date, grace_period=timedelta(seconds=0)):
self.expiration_type = expiration_type
self.expiration_date = exp_date
self.grace_period = grace_period
@property
def expires_at(self):
return self.expiration_date + self.grace_period
def is_expired(self, now):
""" Check if the current object should already be considered expired when
compared with the passed in datetime object.
"""
return self.expires_at < now
def __lt__(self, rhs):
return self.expires_at < rhs.expires_at
def __repr__(self):
return str(dict(
expiration_type=repr(self.expiration_type),
expiration_date=repr(self.expiration_date),
grace_period=repr(self.grace_period),
))
def as_dict(self, for_private=False):
data = {
'expiration_type': str(self.expiration_type),
}
if for_private:
data.update({
'expiration_date': str(self.expiration_date),
'grace_period': str(self.grace_period),
})
return data
class EntitlementStatus(IntEnum):
""" An EntitlementStatus represent the current effectiveness of an
Entitlement when compared with its corresponding requirement. As an
example, if the software requires 9 items, and the Entitlement only
provides for 7, you would use an insufficient_count status.
"""
met = 0
expired = 1
insufficient_count = 2
no_matching = 3
@total_ordering
class EntitlementValidationResult(object):
""" An EntitlementValidationResult encodes the combination of a specific
entitlement and the software requirement which caused it to be examined.
They are orderable by the value of the EntitlementStatus enum, and will
in general be sorted by most to least satisfiable status type.
"""
def __init__(self, requirement, created_at, entitlement=None):
self.requirement = requirement
self._created_at = created_at
self.entitlement = entitlement
def get_status(self):
""" Returns the EntitlementStatus when comparing the specified Entitlement
with the corresponding requirement.
"""
if self.entitlement is not None:
if self.entitlement.expiration.is_expired(self._created_at):
return EntitlementStatus.expired
if self.entitlement.count < self.requirement.count:
return EntitlementStatus.insufficient_count
return EntitlementStatus.met
return EntitlementStatus.no_matching
def is_met(self):
""" Returns whether this specific EntitlementValidationResult meets all
of the criteria for being sufficient, including unexpired (or in the
grace period), and with a sufficient count.
"""
return self.get_status() == EntitlementStatus.met
def __lt__(self, rhs):
# If this result has the same status as another, return the result with an expiration date
# further in the future, as it will be more relevant. The results may expire, but so long as
# this result is valid, so will the entitlement.
if self.get_status() == rhs.get_status():
return (self.entitlement.expiration.expiration_date >
rhs.entitlement.expiration.expiration_date)
# Otherwise, sort lexically by status.
return self.get_status() < rhs.get_status()
def __repr__(self):
return str(dict(
requirement=repr(self.requirement),
created_at=repr(self._created_at),
entitlement=repr(self.entitlement),
))
def description(self):
msg = '%s requires %s: has status %s'
return msg % (self.requirement.name, self.requirement.count, self.get_status())
def as_dict(self, for_private=False):
def req_view():
return {
'name': self.requirement.name,
'count': self.requirement.count,
}
data = {
'requirement': req_view(),
'status': str(self.get_status()),
}
if self.entitlement is not None:
data['entitlement'] = self.entitlement.as_dict(for_private=for_private)
return data
class License(object):
""" License represents a fully decoded and validated (but potentially expired) license. """
def __init__(self, decoded):
self.decoded = decoded
def validate_entitlement_requirement(self, entitlement_req, check_time):
all_active_entitlements = list(self._find_entitlements(entitlement_req.name))
if len(all_active_entitlements) == 0:
return EntitlementValidationResult(entitlement_req, check_time)
entitlement_results = [EntitlementValidationResult(entitlement_req, check_time, ent)
for ent in all_active_entitlements]
entitlement_results.sort()
return entitlement_results[0]
def _find_entitlements(self, entitlement_name):
license_expiration = Expiration(
ExpirationType.license_wide,
_get_date(self.decoded, 'expirationDate'),
)
for sub in self.decoded.get('subscriptions', {}).values():
entitlement_count = sub.get('entitlements', {}).get(entitlement_name)
if entitlement_count is not None:
entitlement_expiration = min(self._sub_expiration(sub), license_expiration)
yield Entitlement(
entitlement_name,
entitlement_count,
sub.get('productName', 'unknown'),
entitlement_expiration,
)
@staticmethod
def _sub_expiration(subscription):
# A trial license has its own end logic, and uses the trialEnd property
if subscription.get('trialOnly', False):
trial_expiration = Expiration(
ExpirationType.trial_only,
_get_date(subscription, 'trialEnd'),
TRIAL_GRACE_PERIOD,
)
return trial_expiration
# From here we always use the serviceEnd
service_end = _get_date(subscription, 'serviceEnd')
if subscription.get('inTrial', False):
return Expiration(ExpirationType.in_trial, service_end, TRIAL_GRACE_PERIOD)
if subscription.get('durationPeriod') == 'yearly':
return Expiration(ExpirationType.yearly, service_end, YEARLY_GRACE_PERIOD)
# We assume monthly license unless specified otherwise
return Expiration(ExpirationType.monthly, service_end, MONTHLY_GRACE_PERIOD)
def validate(self, config):
""" Returns a list of EntitlementValidationResult objects, one per requirement.
"""
requirements = _gen_entitlement_requirements(config)
now = datetime.now()
return [self.validate_entitlement_requirement(req, now) for req in requirements]
_PROD_LICENSE_PUBLIC_KEY_DATA = """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuCkRnkuqox3A0djgRnHR
e3U3jHrcbd5iUqdbfO/8E2TMbiByIy3NzUyJrMIzrTjdxTVIZF/ueaHLEtgaofUA
1X73OZlsaGyNVDFA2eGZRgyNrmfLFoxnN2KB+gEJ88nPkHZXY+4ncZBjVMKfHQEv
busC7xpnF7Diy2GxZKDZRnvjL4ZNrocdoeE0GuroWwebtck5Ea7LqzRxCJ5T3UWt
EozttOBQAqCmKxSDdtdw+CsK/uTfl6Yh9xCZUrCeh5taSOHOvU0ne/p3gM+AsjU4
ScjObTKaSUOGen6aYFF5Bd6V/ucxHmcmJlycwNZOKGFpbhLU173/oBJ+okvDbJpN
qwIDAQAB
-----END PUBLIC KEY-----
"""
_PROD_LICENSE_PUBLIC_KEY = load_pem_public_key(_PROD_LICENSE_PUBLIC_KEY_DATA,
backend=default_backend())
def decode_license(license_contents, public_key_instance=None):
""" Decodes the specified license contents, returning the decoded license. """
license_public_key = public_key_instance or _PROD_LICENSE_PUBLIC_KEY
try:
jwt_data = jwt.decode(license_contents, key=license_public_key)
except jwt.exceptions.DecodeError as de:
logger.exception('Could not decode license file')
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)
LICENSE_VALIDATION_INTERVAL = 3600 # seconds
LICENSE_VALIDATION_EXPIRED_INTERVAL = 60 # seconds
EntitlementRequirement = namedtuple('EntitlementRequirements', ['name', 'count'])
def _gen_entitlement_requirements(config_obj):
config_regions = len(config_obj.get('DISTRIBUTED_STORAGE_CONFIG', []))
return [
EntitlementRequirement(QUAY_ENTITLEMENT, 1),
EntitlementRequirement(QUAY_DEPLOYMENTS_ENTITLEMENT, config_regions),
]
class LicenseValidator(Thread):
"""
LicenseValidator is a thread that asynchronously reloads and validates license files.
This thread is meant to be run before registry gunicorn workers fork and uses shared memory as a
synchronization primitive.
"""
def __init__(self, config_provider, *args, **kwargs):
config = config_provider.get_config() or {}
self._config_provider = config_provider
self._entitlement_requirements = _gen_entitlement_requirements(config)
# multiprocessing.Value does not ensure consistent write-after-reads, but we don't need that.
self._license_is_insufficient = multiprocessing.Value(c_bool, True)
self._license_expiring_soon = multiprocessing.Value(c_bool, True)
super(LicenseValidator, self).__init__(*args, **kwargs)
self.daemon = True
@property
def expiring_soon(self):
""" Returns whether the license will be expiring soon (a week from now). """
return self._license_expiring_soon.value
@property
def insufficient(self):
return self._license_is_insufficient.value
def compute_license_sufficiency(self):
""" Check whether all of our requirements are met, and set the status of
the result of the check, which will be used to disable the software.
Returns True if any requirements are not met, and False if all are met.
"""
try:
current_license = self._config_provider.get_license()
now = datetime.now()
soon = now + LICENSE_SOON_DELTA
any_invalid = not all(current_license.validate_entitlement_requirement(req, now).is_met()
for req in self._entitlement_requirements)
soon_invalid = not all(current_license.validate_entitlement_requirement(req, soon).is_met()
for req in self._entitlement_requirements)
logger.debug('updating license license_is_insufficient to %s', any_invalid)
logger.debug('updating license license_expiring_soon to %s', soon_invalid)
except (IOError, LicenseDecodeError):
logger.exception('failed to validate license')
any_invalid = True
soon_invalid = False
self._license_is_insufficient.value = any_invalid
self._license_expiring_soon.value = soon_invalid
return any_invalid
def run(self):
logger.debug('Starting license validation thread')
while True:
invalid = self.compute_license_sufficiency()
sleep_time = LICENSE_VALIDATION_EXPIRED_INTERVAL if invalid else LICENSE_VALIDATION_INTERVAL
logger.debug('waiting %d seconds before retrying to validate license', sleep_time)
time.sleep(sleep_time)
def enforce_license_before_request(self, blueprint, response_func=None):
"""
Adds a pre-check to a Flask blueprint such that if the provided license_validator determines the
license has become invalid, the client will receive a HTTP 402 response.
"""
if response_func is None:
def _response_func():
return make_response('License is insufficient.', 402)
response_func = _response_func
def _enforce_license():
if self.insufficient:
logger.debug('blocked interaction due to insufficient license')
return response_func()
blueprint.before_request(_enforce_license)