Merge pull request #2009 from coreos-inc/qe2-license

Add license support for QE
This commit is contained in:
josephschorr 2016-10-17 23:11:43 -04:00 committed by GitHub
commit 2a7dbd3348
23 changed files with 902 additions and 219 deletions

View file

@ -6,7 +6,8 @@ import signal
from flask import abort
from endpoints.api import (ApiResource, nickname, resource, internal_only, show_if,
require_fresh_login, request, validate_json_request, verify_not_prod)
require_fresh_login, request, validate_json_request, verify_not_prod,
InvalidRequest)
from endpoints.common import common_login
from app import app, config_provider, superusers, OVERRIDE_CONFIG_DIRECTORY
@ -18,6 +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.license import decode_license, LicenseError
from data.runmigration import run_alembic_migration
from data.users import get_federated_service_name, get_users_handler
@ -62,6 +64,12 @@ class SuperUserRegistryStatus(ApiResource):
'status': 'missing-config-dir'
}
# If there is no license file, we need to ask the user to upload it.
if not config_provider.has_license_file():
return {
'status': 'upload-license'
}
# If there is no config file, we need to setup the database.
if not config_provider.config_exists():
return {
@ -244,6 +252,50 @@ class SuperUserConfig(ApiResource):
abort(403)
@resource('/v1/superuser/config/license')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserSetAndValidateLicense(ApiResource):
""" Resource for setting and validating a license. """
schemas = {
'ValidateLicense': {
'type': 'object',
'description': 'Validates and sets a license',
'required': [
'license',
],
'properties': {
'license': {
'type': 'string'
},
},
},
}
@nickname('suSetAndValidateLicense')
@verify_not_prod
@validate_json_request('ValidateLicense')
def post(self):
""" Validates the given license contents and then saves it to the config volume. """
if config_provider.has_license_file():
abort(403)
license_contents = request.get_json()['license']
try:
decoded_license = decode_license(license_contents)
except LicenseError as le:
raise InvalidRequest(le.message)
if decoded_license.is_expired:
raise InvalidRequest('License has expired')
config_provider.save_license(license_contents)
return {
'decoded': decoded_license.subscription,
'success': True
}
@resource('/v1/superuser/config/file/<filename>')
@internal_only
@show_if(features.SUPER_USERS)

View file

@ -18,11 +18,12 @@ from auth.permissions import SuperUserPermission
from endpoints.api import (ApiResource, nickname, resource, validate_json_request,
internal_only, require_scope, show_if, parse_args,
query_param, abort, require_fresh_login, path_param, verify_not_prod,
page_support, log_action)
page_support, log_action, InvalidRequest)
from endpoints.api.logs import get_logs, get_aggregate_logs
from data import model
from data.database import ServiceKeyApprovalType
from util.useremails import send_confirmation_email, send_recovery_email
from util.license import decode_license, LicenseError
logger = logging.getLogger(__name__)
@ -819,3 +820,143 @@ class SuperUserServiceKeyApproval(ApiResource):
return make_response('', 201)
abort(403)
@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 LicenseError as le:
raise InvalidRequest(le.message)
if decoded_license.is_expired:
raise InvalidRequest('License has expired')
return {
'decoded': decoded_license.subscription,
'success': True
}
abort(403)
@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 LicenseError as le:
raise InvalidRequest(le.message)
if decoded_license.is_expired:
raise InvalidRequest('License has expired')
config_provider.save_license(license_contents)
return {
'decoded': decoded_license.subscription,
'success': True
}
abort(403)
@resource('/v1/messages')
@show_if(features.SUPER_USERS)
class SuperUserMessages(ApiResource):
""" Resource for getting a list of super user messages """
schemas = {
'GetMessage': {
'id': 'GetMessage',
'type': 'object',
'description': 'Messages that a super user has saved in the past',
'properties': {
'message': {
'type': 'array',
'description': 'A list of messages',
'itemType': {
'type': 'object',
'properties': {
'id': {
'type': 'integer',
'description': 'The message id',
},
'content': {
'type': 'string',
'description': 'The actual message',
},
},
},
},
},
},
'CreateMessage': {
'id': 'CreateMessage',
'type': 'object',
'description': 'Create a new message',
'properties': {
'message': {
'type': 'object',
'description': 'A single message',
'properties': {
'content': {
'type': 'string',
'description': 'The actual message',
},
},
},
},
}
}
@nickname('getMessages')
def get(self):
""" Return a super users messages """
return {
'messages': [message_view(m) for m in model.message.get_messages()],
}
@verify_not_prod
@nickname('createMessages')
@validate_json_request('CreateMessage')
@require_scope(scopes.SUPERUSER)
def post(self):
""" Create a message """
if SuperUserPermission().can():
model.message.create([request.get_json()['message']])
return make_response('', 201)
abort(403)
def message_view(message):
return {'id': message.id, 'content': message.content}

View file

@ -1,14 +1,15 @@
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.metrics.metricqueue import time_blueprint
v1_bp = Blueprint('v1', __name__)
license_validator.enforce_license_before_request(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')
@ -29,4 +30,4 @@ def ping():
from endpoints.v1 import index
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
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,16 +18,32 @@ 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.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__)
license_validator.enforce_license_before_request(v2_bp)
time_blueprint(v2_bp, metric_queue)
@v2_bp.app_errorhandler(V2RegistryException)
def handle_registry_v2_exception(error):
response = jsonify({
'errors': [error.as_dict()]
})
response.status_code = error.http_status_code
if response.status_code == 401:
response.headers.extend(get_auth_headers(repository=error.repository, scopes=error.scopes))
logger.debug('sending response: %s', response.get_data())
return response
_MAX_RESULTS_PER_PAGE = 50
@ -72,19 +88,6 @@ def paginate(limit_kwarg_name='limit', offset_kwarg_name='offset',
return wrapper
@v2_bp.app_errorhandler(V2RegistryException)
def handle_registry_v2_exception(error):
response = jsonify({
'errors': [error.as_dict()]
})
response.status_code = error.http_status_code
if response.status_code == 401:
response.headers.extend(get_auth_headers(repository=error.repository, scopes=error.scopes))
logger.debug('sending response: %s', response.get_data())
return response
def _require_repo_permission(permission_class, scopes=None, allow_public=False):
def wrapper(func):
@wraps(func)
@ -117,7 +120,6 @@ def get_input_stream(flask_request):
return flask_request.stream
# TODO remove when v2 is deployed everywhere
def route_show_if(value):
def decorator(f):
@wraps(f)
@ -129,6 +131,7 @@ def route_show_if(value):
return decorated_function
return decorator
@v2_bp.route('/')
@route_show_if(features.ADVERTISE_V2)
@process_registry_jwt_auth()

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
@ -25,9 +25,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__)
license_validator.enforce_license_before_request(verbs)
def _open_stream(formatter, namespace, repository, tag, derived_image_id, repo_image, handlers):
"""