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:
parent
041a7fcd36
commit
3586955669
23 changed files with 19 additions and 1471 deletions
4
app.py
4
app.py
|
@ -44,7 +44,6 @@ from util.config.configutil import generate_secret_key
|
||||||
from util.config.provider import get_config_provider
|
from util.config.provider import get_config_provider
|
||||||
from util.config.superusermanager import SuperUserManager
|
from util.config.superusermanager import SuperUserManager
|
||||||
from util.label_validator import LabelValidator
|
from util.label_validator import LabelValidator
|
||||||
from util.license import LicenseValidator
|
|
||||||
from util.metrics.metricqueue import MetricQueue
|
from util.metrics.metricqueue import MetricQueue
|
||||||
from util.metrics.prometheus import PrometheusPlugin
|
from util.metrics.prometheus import PrometheusPlugin
|
||||||
from util.saas.cloudwatch import start_cloudwatch_sender
|
from util.saas.cloudwatch import start_cloudwatch_sender
|
||||||
|
@ -203,9 +202,6 @@ instance_keys = InstanceKeys(app)
|
||||||
label_validator = LabelValidator(app)
|
label_validator = LabelValidator(app)
|
||||||
build_canceller = BuildCanceller(app)
|
build_canceller = BuildCanceller(app)
|
||||||
|
|
||||||
license_validator = LicenseValidator(config_provider)
|
|
||||||
license_validator.start()
|
|
||||||
|
|
||||||
start_cloudwatch_sender(metric_queue, app)
|
start_cloudwatch_sender(metric_queue, app)
|
||||||
|
|
||||||
github_trigger = GithubOAuthService(app.config, 'GITHUB_TRIGGER_CONFIG')
|
github_trigger = GithubOAuthService(app.config, 'GITHUB_TRIGGER_CONFIG')
|
||||||
|
|
|
@ -21,7 +21,6 @@ from endpoints.common import common_login
|
||||||
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, is_valid_config_upload_filename
|
from util.config.validator import validate_service_for_config, is_valid_config_upload_filename
|
||||||
from util.license import decode_license, LicenseDecodeError
|
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
|
@ -68,12 +67,6 @@ 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 {
|
||||||
|
@ -265,51 +258,6 @@ 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 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>')
|
@resource('/v1/superuser/config/file/<filename>')
|
||||||
@internal_only
|
@internal_only
|
||||||
@show_if(features.SUPER_USERS)
|
@show_if(features.SUPER_USERS)
|
||||||
|
|
|
@ -13,7 +13,7 @@ from flask import request, make_response, jsonify
|
||||||
|
|
||||||
import features
|
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 import scopes
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from auth.permissions import SuperUserPermission
|
from auth.permissions import SuperUserPermission
|
||||||
|
@ -28,7 +28,6 @@ from endpoints.api.superuser_models_pre_oci import (pre_oci_model, ServiceKeyDoe
|
||||||
ServiceKeyAlreadyApproved,
|
ServiceKeyAlreadyApproved,
|
||||||
InvalidRepositoryBuildException)
|
InvalidRepositoryBuildException)
|
||||||
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, LicenseDecodeError
|
|
||||||
from util.security.ssl import load_certificate, CertInvalidException
|
from util.security.ssl import load_certificate, CertInvalidException
|
||||||
from util.config.validator import EXTRA_CA_DIRECTORY
|
from util.config.validator import EXTRA_CA_DIRECTORY
|
||||||
from _init import ROOT_DIR
|
from _init import ROOT_DIR
|
||||||
|
@ -968,77 +967,6 @@ class SuperUserCustomCertificate(ApiResource):
|
||||||
raise Unauthorized()
|
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')
|
@resource('/v1/superuser/<build_uuid>/logs')
|
||||||
@path_param('build_uuid', 'The UUID of the build')
|
@path_param('build_uuid', 'The UUID of the build')
|
||||||
@show_if(features.SUPER_USERS)
|
@show_if(features.SUPER_USERS)
|
||||||
|
|
|
@ -9,7 +9,7 @@ from flask_principal import identity_changed
|
||||||
import endpoints.decorated # Register the various exceptions via decorators.
|
import endpoints.decorated # Register the various exceptions via decorators.
|
||||||
import features
|
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 import scopes
|
||||||
from auth.permissions import QuayDeferredPermissionUser
|
from auth.permissions import QuayDeferredPermissionUser
|
||||||
from config import frontend_visible_config
|
from config import frontend_visible_config
|
||||||
|
@ -143,8 +143,6 @@ def render_page_template(name, route_data=None, **kwargs):
|
||||||
hostname=app.config['SERVER_HOSTNAME'],
|
hostname=app.config['SERVER_HOSTNAME'],
|
||||||
preferred_scheme=app.config['PREFERRED_URL_SCHEME'],
|
preferred_scheme=app.config['PREFERRED_URL_SCHEME'],
|
||||||
version_number=version_number,
|
version_number=version_number,
|
||||||
license_insufficient=license_validator.insufficient,
|
|
||||||
license_expiring=license_validator.expiring_soon,
|
|
||||||
current_year=datetime.datetime.now().year,
|
current_year=datetime.datetime.now().year,
|
||||||
**kwargs)
|
**kwargs)
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
from flask import Blueprint, make_response
|
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 endpoints.decorators import anon_protect, anon_allowed
|
||||||
from util.metrics.metricqueue import time_blueprint
|
from util.metrics.metricqueue import time_blueprint
|
||||||
|
|
||||||
v1_bp = Blueprint('v1', __name__)
|
v1_bp = Blueprint('v1', __name__)
|
||||||
license_validator.enforce_license_before_request(v1_bp)
|
|
||||||
time_blueprint(v1_bp, metric_queue)
|
time_blueprint(v1_bp, metric_queue)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ from semantic_version import Spec
|
||||||
|
|
||||||
import features
|
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.auth_context import get_authenticated_context
|
||||||
from auth.permissions import (
|
from auth.permissions import (
|
||||||
ReadRepositoryPermission, ModifyRepositoryPermission, AdministerRepositoryPermission)
|
ReadRepositoryPermission, ModifyRepositoryPermission, AdministerRepositoryPermission)
|
||||||
|
@ -26,7 +26,6 @@ from util.pagination import encrypt_page_token, decrypt_page_token
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
v2_bp = Blueprint('v2', __name__)
|
v2_bp = Blueprint('v2', __name__)
|
||||||
license_validator.enforce_license_before_request(v2_bp)
|
|
||||||
time_blueprint(v2_bp, metric_queue)
|
time_blueprint(v2_bp, metric_queue)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from flask import redirect, Blueprint, abort, send_file, make_response, request
|
||||||
|
|
||||||
import features
|
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.auth_context import get_authenticated_user
|
||||||
from auth.decorators import process_auth
|
from auth.decorators import process_auth
|
||||||
from auth.permissions import ReadRepositoryPermission
|
from auth.permissions import ReadRepositoryPermission
|
||||||
|
@ -27,7 +27,6 @@ from util.registry.torrent import (
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
verbs = Blueprint('verbs', __name__)
|
verbs = Blueprint('verbs', __name__)
|
||||||
license_validator.enforce_license_before_request(verbs)
|
|
||||||
|
|
||||||
LAYER_MIMETYPE = 'binary/octet-stream'
|
LAYER_MIMETYPE = 'binary/octet-stream'
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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>
|
|
|
@ -3,16 +3,6 @@
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Custom SSL certificates -->
|
<!-- Custom SSL certificates -->
|
||||||
<div class="co-panel" id="custom-ssl">
|
<div class="co-panel" id="custom-ssl">
|
||||||
<div class="co-panel-heading">
|
<div class="co-panel-heading">
|
||||||
|
|
|
@ -39,9 +39,6 @@ import * as URI from 'urijs';
|
||||||
// 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',
|
|
||||||
|
|
||||||
// DB is being configured.
|
// DB is being configured.
|
||||||
'CONFIG_DB': 'config-db',
|
'CONFIG_DB': 'config-db',
|
||||||
|
|
||||||
|
@ -100,8 +97,7 @@ import * as URI from 'urijs';
|
||||||
$scope.currentConfig = null;
|
$scope.currentConfig = null;
|
||||||
|
|
||||||
$scope.currentState = {
|
$scope.currentState = {
|
||||||
'hasDatabaseSSLCert': false,
|
'hasDatabaseSSLCert': false
|
||||||
'licenseValid': false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.$watch('currentStep', function(currentStep) {
|
$scope.$watch('currentStep', function(currentStep) {
|
||||||
|
@ -127,7 +123,6 @@ import * as URI from 'urijs';
|
||||||
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({
|
||||||
|
@ -173,7 +168,6 @@ import * as URI from 'urijs';
|
||||||
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),
|
||||||
|
|
|
@ -9,13 +9,12 @@
|
||||||
<div class="co-main-content-panel" style="padding: 20px;">
|
<div class="co-main-content-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="Upload License" text="1"></span>
|
<span class="cor-step" title="Configure Database" 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="3"></span>
|
<span class="cor-step" title="Create Superuser" text="2"></span>
|
||||||
<span class="cor-step" title="Configure Registry" text="4"></span>
|
<span class="cor-step" title="Configure Registry" text="3"></span>
|
||||||
<span class="cor-step" title="Validate Configuration" text="5"></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="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>
|
||||||
|
@ -37,13 +36,12 @@
|
||||||
<!-- 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="Upload License" text="1"></span>
|
<span class="cor-step" title="Configure Database" 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="3"></span>
|
<span class="cor-step" title="Create Superuser" text="2"></span>
|
||||||
<span class="cor-step" title="Configure Registry" text="4"></span>
|
<span class="cor-step" title="Configure Registry" text="3"></span>
|
||||||
<span class="cor-step" title="Validate Configuration" text="5"></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="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>
|
||||||
|
@ -130,20 +128,6 @@
|
||||||
The container must be restarted to apply the configuration changes.
|
The container must be restarted to apply the configuration changes.
|
||||||
</div>
|
</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 -->
|
<!-- 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)">
|
||||||
|
@ -242,15 +226,6 @@
|
||||||
Database Validation Issue: {{ errors.DatabaseValidationError }}
|
Database Validation Issue: {{ errors.DatabaseValidationError }}
|
||||||
</div>
|
</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 -->
|
<!-- 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)">
|
||||||
|
|
|
@ -128,19 +128,6 @@ mixpanel.init("{{ mixpanel_key }}", { track_pageview : false, debug: {{ is_debug
|
||||||
<div id="co-l-footer-wrapper">
|
<div id="co-l-footer-wrapper">
|
||||||
<nav class="navbar navbar-default header-bar co-m-navbar co-fx-box-shadow" role="navigation"></nav>
|
<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 class="quay-message-bar"></div>
|
||||||
<div quay-require="['BILLING']">
|
<div quay-require="['BILLING']">
|
||||||
<div class="quay-service-status-bar"></div>
|
<div class="quay-service-status-bar"></div>
|
||||||
|
|
|
@ -51,7 +51,7 @@ from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserMana
|
||||||
SuperUserOrganizationManagement, SuperUserOrganizationList,
|
SuperUserOrganizationManagement, SuperUserOrganizationList,
|
||||||
SuperUserAggregateLogs, SuperUserServiceKeyManagement,
|
SuperUserAggregateLogs, SuperUserServiceKeyManagement,
|
||||||
SuperUserServiceKey, SuperUserServiceKeyApproval,
|
SuperUserServiceKey, SuperUserServiceKeyApproval,
|
||||||
SuperUserTakeOwnership, SuperUserLicense,
|
SuperUserTakeOwnership,
|
||||||
SuperUserCustomCertificates,
|
SuperUserCustomCertificates,
|
||||||
SuperUserCustomCertificate, SuperUserRepositoryBuildLogs,
|
SuperUserCustomCertificate, SuperUserRepositoryBuildLogs,
|
||||||
SuperUserRepositoryBuildResource, SuperUserRepositoryBuildStatus)
|
SuperUserRepositoryBuildResource, SuperUserRepositoryBuildStatus)
|
||||||
|
@ -4187,37 +4187,6 @@ class TestSuperUserCustomCertificate(ApiTestCase):
|
||||||
self._run_test('DELETE', 204, 'devtable', None)
|
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):
|
class TestSuperUserManagement(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
|
|
|
@ -4036,20 +4036,13 @@ 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 'upload-license'.
|
# With no config the status should be 'config-db'.
|
||||||
json = self.getJsonResponse(SuperUserRegistryStatus)
|
json = self.getJsonResponse(SuperUserRegistryStatus)
|
||||||
self.assertEquals('upload-license', json['status'])
|
self.assertEquals('config-db', 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',
|
||||||
|
|
|
@ -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()
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ class TestSuperUserRegistryStatus(ApiTestCase):
|
||||||
def test_registry_status(self):
|
def test_registry_status(self):
|
||||||
with FreshConfigProvider():
|
with FreshConfigProvider():
|
||||||
json = self.getJsonResponse(SuperUserRegistryStatus)
|
json = self.getJsonResponse(SuperUserRegistryStatus)
|
||||||
self.assertEquals('upload-license', json['status'])
|
self.assertEquals('config-db', json['status'])
|
||||||
|
|
||||||
|
|
||||||
class TestSuperUserConfigFile(ApiTestCase):
|
class TestSuperUserConfigFile(ApiTestCase):
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
from jsonschema import validate, ValidationError
|
from jsonschema import validate, ValidationError
|
||||||
|
|
||||||
|
|
||||||
from util.config.schema import CONFIG_SCHEMA
|
from util.config.schema import CONFIG_SCHEMA
|
||||||
from util.license import LICENSE_FILENAME, LicenseDecodeError, decode_license
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -63,8 +59,6 @@ def export_yaml(config_obj, config_file):
|
||||||
class BaseProvider(object):
|
class BaseProvider(object):
|
||||||
""" A configuration provider helps to load, save, and handle config override in the application.
|
""" A configuration provider helps to load, save, and handle config override in the application.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
|
||||||
self.license = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def provider_id(self):
|
def provider_id(self):
|
||||||
|
@ -128,30 +122,3 @@ class BaseProvider(object):
|
||||||
""" Helper for constructing file paths, which may differ between providers. For example,
|
""" Helper for constructing file paths, which may differ between providers. For example,
|
||||||
kubernetes can't have subfolders in configmaps """
|
kubernetes can't have subfolders in configmaps """
|
||||||
raise NotImplementedError
|
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)
|
|
||||||
|
|
|
@ -4,21 +4,9 @@ import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from util.config.provider.baseprovider import BaseProvider
|
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']
|
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):
|
class TestConfigProvider(BaseProvider):
|
||||||
""" Implementation of the config provider for testing. Everything is kept in-memory instead on
|
""" Implementation of the config provider for testing. Everything is kept in-memory instead on
|
||||||
the real file system. """
|
the real file system. """
|
||||||
|
@ -83,9 +71,6 @@ class TestConfigProvider(BaseProvider):
|
||||||
def requires_restart(self, app_config):
|
def requires_restart(self, app_config):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_license(self):
|
|
||||||
return TestLicense()
|
|
||||||
|
|
||||||
def reset_for_test(self):
|
def reset_for_test(self):
|
||||||
self._config['SUPER_USERS'] = ['devtable']
|
self._config['SUPER_USERS'] = ['devtable']
|
||||||
self.files = {}
|
self.files = {}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import logging
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from data.users import LDAP_CERT_FILENAME
|
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_database import DatabaseValidator
|
||||||
from util.config.validators.validate_redis import RedisValidator
|
from util.config.validators.validate_redis import RedisValidator
|
||||||
from util.config.validators.validate_storage import StorageValidator
|
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'
|
EXTRA_CA_DIRECTORY = 'extra_ca_certs'
|
||||||
|
|
||||||
VALIDATORS = {
|
VALIDATORS = {
|
||||||
LicenseValidator.name: LicenseValidator.validate,
|
|
||||||
DatabaseValidator.name: DatabaseValidator.validate,
|
DatabaseValidator.name: DatabaseValidator.validate,
|
||||||
RedisValidator.name: RedisValidator.validate,
|
RedisValidator.name: RedisValidator.validate,
|
||||||
StorageValidator.name: StorageValidator.validate,
|
StorageValidator.name: StorageValidator.validate,
|
||||||
|
|
|
@ -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)
|
|
|
@ -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)
|
|
416
util/license.py
416
util/license.py
|
@ -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)
|
|
Reference in a new issue