enforce license across registry blueprints

This commit is contained in:
Jimmy Zelinskie 2016-10-10 16:23:42 -04:00 committed by Joseph Schorr
parent 8fe29c5b89
commit 0c5400b7d1
8 changed files with 118 additions and 39 deletions

41
app.py
View file

@ -1,47 +1,51 @@
import json
import logging
import os
import json
from functools import partial
from Crypto.PublicKey import RSA
from flask import Flask, request, Request, _request_ctx_stack
from flask_principal import Principal
from flask_login import LoginManager, UserMixin
from flask_mail import Mail
from werkzeug.routing import BaseConverter
from flask_principal import Principal
from jwkest.jwk import RSAKey
from Crypto.PublicKey import RSA
from werkzeug.routing import BaseConverter
import features
from avatars.avatars import Avatar
from storage import Storage
from data import model
from data import database
from data.userfiles import Userfiles
from data.users import UserAuthentication
from data import model
from data.archivedlogs import LogArchive
from data.billing import Billing
from data.buildlogs import BuildLogs
from data.archivedlogs import LogArchive
from data.userevent import UserEventsBuilderModule
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.saas.analytics import Analytics
from util.saas.useranalytics import UserAnalytics
from util.saas.exceptionlog import Sentry
from util.names import urn_generator
from util.config.configutil import generate_secret_key
from util.config.oauth import (GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig,
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.configutil import generate_secret_key
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.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_YAML_FILENAME = 'conf/stack/config.yaml'
@ -189,6 +193,9 @@ signer = Signer(app, config_provider)
instance_keys = InstanceKeys(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)
tf = app.config['DB_TRANSACTION_FACTORY']

View file

@ -19,7 +19,7 @@ from data.database import User
from util.config.configutil import add_enterprise_config_defaults
from util.config.database import sync_database_with_config
from util.config.validator import validate_service_for_config, 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.users import get_federated_service_name, get_users_handler

View file

@ -1,14 +1,16 @@
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 util.license import enforce_license_before_request
from util.metrics.metricqueue import time_blueprint
v1_bp = Blueprint('v1', __name__)
enforce_license_before_request(license_validator, v1_bp)
time_blueprint(v1_bp, metric_queue)
# 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.
@v1_bp.route('/_internal_ping')

View file

@ -9,7 +9,7 @@ from semantic_version import Spec
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.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
AdministerRepositoryPermission)
@ -18,13 +18,17 @@ from data import model
from endpoints.decorators import anon_protect, anon_allowed
from endpoints.v2.errors import V2RegistryException, Unauthorized
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.registry.dockerver import docker_version
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)

View file

@ -5,7 +5,7 @@ from flask import redirect, Blueprint, abort, send_file, make_response, request
import features
from app import app, signer, storage, metric_queue
from app import app, signer, storage, metric_queue, license_validator
from auth.auth_context import get_authenticated_user
from auth.permissions import ReadRepositoryPermission
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.docker.squashed import SquashedDockerImageFormatter
from storage import Storage
from util.license import enforce_license_before_request
from util.registry.filelike import wrap_with_handler
from util.registry.queuefile import QueueFile
from util.registry.queueprocess import QueueProcess
@ -25,9 +26,11 @@ from util.registry.torrent import (make_torrent, per_user_torrent_filename, publ
PieceHasher)
verbs = Blueprint('verbs', __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):
"""

View file

@ -9,8 +9,7 @@ from Crypto.PublicKey import RSA
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_der_public_key
from util.config.provider.license import (decode_license, LICENSE_PRODUCT_NAME,
LicenseValidationError)
from util.license import decode_license, LICENSE_PRODUCT_NAME, LicenseValidationError
class TestLicense(unittest.TestCase):

View file

@ -1,18 +1,22 @@
import yaml
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__)
class CannotWriteConfigException(Exception):
""" Exception raised when the config cannot be written. """
pass
class SetupIncompleteException(Exception):
""" Exception raised when attempting to verify config that has not yet been setup. """
pass
def import_yaml(config_obj, config_file):
with open(config_file) as f:
c = yaml.safe_load(f)
@ -33,6 +37,7 @@ def import_yaml(config_obj, config_file):
def get_yaml(config_obj):
return yaml.safe_dump(config_obj, encoding='utf-8', allow_unicode=True)
def export_yaml(config_obj, config_file):
try:
with open(config_file, 'w') as f:

View file

@ -1,20 +1,29 @@
import json
import logging
import multiprocessing
import time
from dateutil import parser
from ctypes import c_bool
from datetime import datetime, timedelta
from threading import Thread
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from dateutil import parser
from flask import abort
import jwt
import json
logger = logging.getLogger(__name__)
TRIAL_GRACE_PERIOD = timedelta(7, 0) # 1 week
MONTHLY_GRACE_PERIOD = timedelta(30, 0) # 1 month
YEARLY_GRACE_PERIOD = timedelta(90, 0) # 3 months
LICENSE_PRODUCT_NAME = "quay-enterprise"
LICENSE_FILENAME = 'license'
class LicenseError(Exception):
""" Exception raised if the license could not be read, decoded or has expired. """
@ -115,9 +124,6 @@ class License(object):
return True
LICENSE_FILENAME = 'license'
_PROD_LICENSE_PUBLIC_KEY_DATA = """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuCkRnkuqox3A0djgRnHR
@ -129,8 +135,6 @@ ScjObTKaSUOGen6aYFF5Bd6V/ucxHmcmJlycwNZOKGFpbhLU173/oBJ+okvDbJpN
qwIDAQAB
-----END PUBLIC KEY-----
"""
_PROD_LICENSE_PUBLIC_KEY = load_pem_public_key(_PROD_LICENSE_PUBLIC_KEY_DATA,
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)
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)