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 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']

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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):
""" """

View file

@ -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):

View file

@ -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:

View file

@ -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)