enforce license across registry blueprints
This commit is contained in:
parent
8fe29c5b89
commit
0c5400b7d1
8 changed files with 118 additions and 39 deletions
41
app.py
41
app.py
|
@ -1,47 +1,51 @@
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import json
|
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
from flask import Flask, request, Request, _request_ctx_stack
|
from flask import Flask, request, Request, _request_ctx_stack
|
||||||
from flask_principal import Principal
|
|
||||||
from flask_login import LoginManager, UserMixin
|
from flask_login import LoginManager, UserMixin
|
||||||
from flask_mail import Mail
|
from flask_mail import Mail
|
||||||
from werkzeug.routing import BaseConverter
|
from flask_principal import Principal
|
||||||
from jwkest.jwk import RSAKey
|
from jwkest.jwk import RSAKey
|
||||||
from Crypto.PublicKey import RSA
|
from werkzeug.routing import BaseConverter
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
from avatars.avatars import Avatar
|
from avatars.avatars import Avatar
|
||||||
from storage import Storage
|
|
||||||
from data import model
|
|
||||||
from data import database
|
from data import database
|
||||||
from data.userfiles import Userfiles
|
from data import model
|
||||||
from data.users import UserAuthentication
|
from data.archivedlogs import LogArchive
|
||||||
from data.billing import Billing
|
from data.billing import Billing
|
||||||
from data.buildlogs import BuildLogs
|
from data.buildlogs import BuildLogs
|
||||||
from data.archivedlogs import LogArchive
|
|
||||||
from data.userevent import UserEventsBuilderModule
|
|
||||||
from data.queue import WorkQueue, BuildMetricQueueReporter
|
from data.queue import WorkQueue, BuildMetricQueueReporter
|
||||||
|
from data.userevent import UserEventsBuilderModule
|
||||||
|
from data.userfiles import Userfiles
|
||||||
|
from data.users import UserAuthentication
|
||||||
|
from storage import Storage
|
||||||
from util import get_app_url
|
from util import get_app_url
|
||||||
from util.saas.analytics import Analytics
|
from util.saas.analytics import Analytics
|
||||||
from util.saas.useranalytics import UserAnalytics
|
from util.saas.useranalytics import UserAnalytics
|
||||||
from util.saas.exceptionlog import Sentry
|
from util.saas.exceptionlog import Sentry
|
||||||
from util.names import urn_generator
|
from util.names import urn_generator
|
||||||
|
from util.config.configutil import generate_secret_key
|
||||||
from util.config.oauth import (GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig,
|
from util.config.oauth import (GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig,
|
||||||
DexOAuthConfig)
|
DexOAuthConfig)
|
||||||
|
|
||||||
from util.security.signing import Signer
|
|
||||||
from util.security.instancekeys import InstanceKeys
|
|
||||||
from util.saas.cloudwatch import start_cloudwatch_sender
|
|
||||||
from util.config.provider import get_config_provider
|
from util.config.provider import get_config_provider
|
||||||
from util.config.configutil import generate_secret_key
|
|
||||||
from util.config.superusermanager import SuperUserManager
|
from util.config.superusermanager import SuperUserManager
|
||||||
from util.secscan.api import SecurityScannerAPI
|
from util.label_validator import LabelValidator
|
||||||
|
from util.license import LicenseValidator, LICENSE_FILENAME
|
||||||
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.label_validator import LabelValidator
|
from util.names import urn_generator
|
||||||
|
from util.saas.analytics import Analytics
|
||||||
|
from util.saas.cloudwatch import start_cloudwatch_sender
|
||||||
|
from util.saas.exceptionlog import Sentry
|
||||||
|
from util.secscan.api import SecurityScannerAPI
|
||||||
|
from util.security.instancekeys import InstanceKeys
|
||||||
|
from util.security.signing import Signer
|
||||||
|
|
||||||
OVERRIDE_CONFIG_DIRECTORY = 'conf/stack/'
|
OVERRIDE_CONFIG_DIRECTORY = 'conf/stack/'
|
||||||
OVERRIDE_CONFIG_YAML_FILENAME = 'conf/stack/config.yaml'
|
OVERRIDE_CONFIG_YAML_FILENAME = 'conf/stack/config.yaml'
|
||||||
|
@ -189,6 +193,9 @@ signer = Signer(app, config_provider)
|
||||||
instance_keys = InstanceKeys(app)
|
instance_keys = InstanceKeys(app)
|
||||||
label_validator = LabelValidator(app)
|
label_validator = LabelValidator(app)
|
||||||
|
|
||||||
|
license_validator = LicenseValidator(os.path.join(OVERRIDE_CONFIG_DIRECTORY, LICENSE_FILENAME))
|
||||||
|
license_validator.start()
|
||||||
|
|
||||||
start_cloudwatch_sender(metric_queue, app)
|
start_cloudwatch_sender(metric_queue, app)
|
||||||
|
|
||||||
tf = app.config['DB_TRANSACTION_FACTORY']
|
tf = app.config['DB_TRANSACTION_FACTORY']
|
||||||
|
|
|
@ -19,7 +19,7 @@ from data.database import User
|
||||||
from util.config.configutil import add_enterprise_config_defaults
|
from util.config.configutil import add_enterprise_config_defaults
|
||||||
from util.config.database import sync_database_with_config
|
from util.config.database import sync_database_with_config
|
||||||
from util.config.validator import validate_service_for_config, CONFIG_FILENAMES
|
from util.config.validator import validate_service_for_config, CONFIG_FILENAMES
|
||||||
from util.config.provider.license import decode_license, LicenseError
|
from util.license import decode_license, LicenseError
|
||||||
from data.runmigration import run_alembic_migration
|
from data.runmigration import run_alembic_migration
|
||||||
from data.users import get_federated_service_name, get_users_handler
|
from data.users import get_federated_service_name, get_users_handler
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
from flask import Blueprint, make_response
|
from flask import Blueprint, make_response
|
||||||
|
|
||||||
from app import metric_queue
|
from app import metric_queue, license_validator
|
||||||
from endpoints.decorators import anon_protect, anon_allowed
|
from endpoints.decorators import anon_protect, anon_allowed
|
||||||
|
from util.license import enforce_license_before_request
|
||||||
from util.metrics.metricqueue import time_blueprint
|
from util.metrics.metricqueue import time_blueprint
|
||||||
|
|
||||||
|
|
||||||
v1_bp = Blueprint('v1', __name__)
|
v1_bp = Blueprint('v1', __name__)
|
||||||
|
enforce_license_before_request(license_validator, v1_bp)
|
||||||
time_blueprint(v1_bp, metric_queue)
|
time_blueprint(v1_bp, metric_queue)
|
||||||
|
|
||||||
|
|
||||||
# Note: This is *not* part of the Docker index spec. This is here for our own health check,
|
# Note: This is *not* part of the Docker index spec. This is here for our own health check,
|
||||||
# since we have nginx handle the _ping below.
|
# since we have nginx handle the _ping below.
|
||||||
@v1_bp.route('/_internal_ping')
|
@v1_bp.route('/_internal_ping')
|
||||||
|
@ -29,4 +31,4 @@ def ping():
|
||||||
|
|
||||||
from endpoints.v1 import index
|
from endpoints.v1 import index
|
||||||
from endpoints.v1 import registry
|
from endpoints.v1 import registry
|
||||||
from endpoints.v1 import tag
|
from endpoints.v1 import tag
|
||||||
|
|
|
@ -9,7 +9,7 @@ from semantic_version import Spec
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
from app import app, metric_queue, get_app_url
|
from app import app, metric_queue, get_app_url, license_validator
|
||||||
from auth.auth_context import get_grant_context
|
from auth.auth_context import get_grant_context
|
||||||
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
|
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
|
||||||
AdministerRepositoryPermission)
|
AdministerRepositoryPermission)
|
||||||
|
@ -18,13 +18,17 @@ from data import model
|
||||||
from endpoints.decorators import anon_protect, anon_allowed
|
from endpoints.decorators import anon_protect, anon_allowed
|
||||||
from endpoints.v2.errors import V2RegistryException, Unauthorized
|
from endpoints.v2.errors import V2RegistryException, Unauthorized
|
||||||
from util.http import abort
|
from util.http import abort
|
||||||
from util.registry.dockerver import docker_version
|
from util.license import enforce_license_before_request
|
||||||
from util.metrics.metricqueue import time_blueprint
|
from util.metrics.metricqueue import time_blueprint
|
||||||
|
from util.registry.dockerver import docker_version
|
||||||
from util.pagination import encrypt_page_token, decrypt_page_token
|
from util.pagination import encrypt_page_token, decrypt_page_token
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
v2_bp = Blueprint('v2', __name__)
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
v2_bp = Blueprint('v2', __name__)
|
||||||
|
enforce_license_before_request(license_validator, 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
|
from app import app, signer, storage, metric_queue, license_validator
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from auth.permissions import ReadRepositoryPermission
|
from auth.permissions import ReadRepositoryPermission
|
||||||
from auth.process import process_auth
|
from auth.process import process_auth
|
||||||
|
@ -18,6 +18,7 @@ from endpoints.v2.blob import BLOB_DIGEST_ROUTE
|
||||||
from image.appc import AppCImageFormatter
|
from image.appc import AppCImageFormatter
|
||||||
from image.docker.squashed import SquashedDockerImageFormatter
|
from image.docker.squashed import SquashedDockerImageFormatter
|
||||||
from storage import Storage
|
from storage import Storage
|
||||||
|
from util.license import enforce_license_before_request
|
||||||
from util.registry.filelike import wrap_with_handler
|
from util.registry.filelike import wrap_with_handler
|
||||||
from util.registry.queuefile import QueueFile
|
from util.registry.queuefile import QueueFile
|
||||||
from util.registry.queueprocess import QueueProcess
|
from util.registry.queueprocess import QueueProcess
|
||||||
|
@ -25,9 +26,11 @@ from util.registry.torrent import (make_torrent, per_user_torrent_filename, publ
|
||||||
PieceHasher)
|
PieceHasher)
|
||||||
|
|
||||||
|
|
||||||
verbs = Blueprint('verbs', __name__)
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
verbs = Blueprint('verbs', __name__)
|
||||||
|
enforce_license_before_request(license_validator, verbs)
|
||||||
|
|
||||||
|
|
||||||
def _open_stream(formatter, namespace, repository, tag, derived_image_id, repo_image, handlers):
|
def _open_stream(formatter, namespace, repository, tag, derived_image_id, repo_image, handlers):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -9,8 +9,7 @@ from Crypto.PublicKey import RSA
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives.serialization import load_der_public_key
|
from cryptography.hazmat.primitives.serialization import load_der_public_key
|
||||||
|
|
||||||
from util.config.provider.license import (decode_license, LICENSE_PRODUCT_NAME,
|
from util.license import decode_license, LICENSE_PRODUCT_NAME, LicenseValidationError
|
||||||
LicenseValidationError)
|
|
||||||
|
|
||||||
|
|
||||||
class TestLicense(unittest.TestCase):
|
class TestLicense(unittest.TestCase):
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
import yaml
|
|
||||||
import logging
|
import logging
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from util.license import LICENSE_FILENAME, LicenseError, decode_license
|
||||||
|
|
||||||
from util.config.provider.license import LICENSE_FILENAME, LicenseError, decode_license
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CannotWriteConfigException(Exception):
|
class CannotWriteConfigException(Exception):
|
||||||
""" Exception raised when the config cannot be written. """
|
""" Exception raised when the config cannot be written. """
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SetupIncompleteException(Exception):
|
class SetupIncompleteException(Exception):
|
||||||
""" Exception raised when attempting to verify config that has not yet been setup. """
|
""" Exception raised when attempting to verify config that has not yet been setup. """
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def import_yaml(config_obj, config_file):
|
def import_yaml(config_obj, config_file):
|
||||||
with open(config_file) as f:
|
with open(config_file) as f:
|
||||||
c = yaml.safe_load(f)
|
c = yaml.safe_load(f)
|
||||||
|
@ -33,6 +37,7 @@ def import_yaml(config_obj, config_file):
|
||||||
def get_yaml(config_obj):
|
def get_yaml(config_obj):
|
||||||
return yaml.safe_dump(config_obj, encoding='utf-8', allow_unicode=True)
|
return yaml.safe_dump(config_obj, encoding='utf-8', allow_unicode=True)
|
||||||
|
|
||||||
|
|
||||||
def export_yaml(config_obj, config_file):
|
def export_yaml(config_obj, config_file):
|
||||||
try:
|
try:
|
||||||
with open(config_file, 'w') as f:
|
with open(config_file, 'w') as f:
|
||||||
|
|
|
@ -1,20 +1,29 @@
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import multiprocessing
|
||||||
|
import time
|
||||||
|
|
||||||
from dateutil import parser
|
from ctypes import c_bool
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
||||||
|
from dateutil import parser
|
||||||
|
from flask import abort
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
import json
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
TRIAL_GRACE_PERIOD = timedelta(7, 0) # 1 week
|
TRIAL_GRACE_PERIOD = timedelta(7, 0) # 1 week
|
||||||
MONTHLY_GRACE_PERIOD = timedelta(30, 0) # 1 month
|
MONTHLY_GRACE_PERIOD = timedelta(30, 0) # 1 month
|
||||||
YEARLY_GRACE_PERIOD = timedelta(90, 0) # 3 months
|
YEARLY_GRACE_PERIOD = timedelta(90, 0) # 3 months
|
||||||
|
|
||||||
LICENSE_PRODUCT_NAME = "quay-enterprise"
|
LICENSE_PRODUCT_NAME = "quay-enterprise"
|
||||||
|
LICENSE_FILENAME = 'license'
|
||||||
|
|
||||||
|
|
||||||
class LicenseError(Exception):
|
class LicenseError(Exception):
|
||||||
""" Exception raised if the license could not be read, decoded or has expired. """
|
""" Exception raised if the license could not be read, decoded or has expired. """
|
||||||
|
@ -115,9 +124,6 @@ class License(object):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
LICENSE_FILENAME = 'license'
|
|
||||||
|
|
||||||
|
|
||||||
_PROD_LICENSE_PUBLIC_KEY_DATA = """
|
_PROD_LICENSE_PUBLIC_KEY_DATA = """
|
||||||
-----BEGIN PUBLIC KEY-----
|
-----BEGIN PUBLIC KEY-----
|
||||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuCkRnkuqox3A0djgRnHR
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuCkRnkuqox3A0djgRnHR
|
||||||
|
@ -129,8 +135,6 @@ ScjObTKaSUOGen6aYFF5Bd6V/ucxHmcmJlycwNZOKGFpbhLU173/oBJ+okvDbJpN
|
||||||
qwIDAQAB
|
qwIDAQAB
|
||||||
-----END PUBLIC KEY-----
|
-----END PUBLIC KEY-----
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
_PROD_LICENSE_PUBLIC_KEY = load_pem_public_key(_PROD_LICENSE_PUBLIC_KEY_DATA,
|
_PROD_LICENSE_PUBLIC_KEY = load_pem_public_key(_PROD_LICENSE_PUBLIC_KEY_DATA,
|
||||||
backend=default_backend())
|
backend=default_backend())
|
||||||
|
|
||||||
|
@ -150,3 +154,58 @@ def decode_license(license_contents, public_key_instance=None):
|
||||||
raise LicenseDecodeError('Could not decode license found: %s' % ve.message)
|
raise LicenseDecodeError('Could not decode license found: %s' % ve.message)
|
||||||
|
|
||||||
return License(decoded)
|
return License(decoded)
|
||||||
|
|
||||||
|
|
||||||
|
LICENSE_VALIDATION_INTERVAL = 3600 # seconds
|
||||||
|
LICENSE_VALIDATION_EXPIRED_INTERVAL = 60 # seconds
|
||||||
|
|
||||||
|
|
||||||
|
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, license_path):
|
||||||
|
self._license_path = license_path
|
||||||
|
|
||||||
|
# multiprocessing.Value does not ensure consistent write-after-reads, but we don't need that.
|
||||||
|
self._license_is_expired = multiprocessing.Value(c_bool, True)
|
||||||
|
|
||||||
|
super(LicenseValidator, self).__init__()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expired(self):
|
||||||
|
return self._license_is_expired.value
|
||||||
|
|
||||||
|
def _check_expiration(self):
|
||||||
|
try:
|
||||||
|
with open(self._license_path) as f:
|
||||||
|
current_license = decode_license(f.read())
|
||||||
|
logger.debug('updating license expiration to %s', current_license.is_expired)
|
||||||
|
self._license_is_expired.value = current_license.is_expired
|
||||||
|
return current_license.is_expired
|
||||||
|
except (IOError, LicenseError):
|
||||||
|
logger.exception('failed to validate license')
|
||||||
|
self._license_is_expired.value = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
logger.debug('Starting license validation thread')
|
||||||
|
while True:
|
||||||
|
expired = self._check_expiration()
|
||||||
|
sleep_time = LICENSE_VALIDATION_EXPIRED_INTERVAL if expired 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(license_validator, blueprint):
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
def _enforce_license():
|
||||||
|
if license_validator.expired:
|
||||||
|
abort(402)
|
||||||
|
blueprint.before_request(_enforce_license)
|
Reference in a new issue