Merge pull request #2009 from coreos-inc/qe2-license
Add license support for QE
This commit is contained in:
commit
2a7dbd3348
23 changed files with 902 additions and 219 deletions
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
Reference in a new issue