From 5211c407ffac0cee358f7c82ca9bec5cfba4df58 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 30 Oct 2015 15:48:29 -0400 Subject: [PATCH 1/8] Add license checking to Quay Based off of mjibson's changes Fixes #499 --- util/config/provider/baseprovider.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/util/config/provider/baseprovider.py b/util/config/provider/baseprovider.py index 18dd13add..5b8f56310 100644 --- a/util/config/provider/baseprovider.py +++ b/util/config/provider/baseprovider.py @@ -110,5 +110,3 @@ class BaseProvider(object): msg = 'Could not open license file. Please make sure it is in your config volume.' raise LicenseError(msg) - - From 8fe29c5b897eaa853349d31086ce8989f0f70b9e Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 8 Dec 2015 15:00:50 -0500 Subject: [PATCH 2/8] Add license upload step to the setup flow Fixes #853 --- endpoints/api/suconfig.py | 54 ++++++++++++++++++- static/css/pages/setup.css | 56 ++++++++++++++++++++ static/js/pages/setup.js | 41 ++++++++++++++- static/partials/setup.html | 72 ++++++++++++++++++++++--- test/test_api_usage.py | 11 +++- test/test_license.py | 15 ++++-- test/test_suconfig_api.py | 2 +- util/config/provider/baseprovider.py | 29 +++++++---- util/config/provider/fileprovider.py | 11 +++- util/config/provider/k8sprovider.py | 8 +++ util/config/provider/license.py | 78 +++++++++++++++++----------- util/config/provider/testprovider.py | 3 ++ 12 files changed, 320 insertions(+), 60 deletions(-) create mode 100644 static/css/pages/setup.css diff --git a/endpoints/api/suconfig.py b/endpoints/api/suconfig.py index 502c048d4..d8c22cbf2 100644 --- a/endpoints/api/suconfig.py +++ b/endpoints/api/suconfig.py @@ -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.config.provider.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/') @internal_only @show_if(features.SUPER_USERS) diff --git a/static/css/pages/setup.css b/static/css/pages/setup.css new file mode 100644 index 000000000..2ca87dbdc --- /dev/null +++ b/static/css/pages/setup.css @@ -0,0 +1,56 @@ +.initial-setup-modal .upload-license textarea { + border: 1px solid #eee !important; + transition: all ease-in-out 200ms; + resize: none; +} + +.initial-setup-modal .upload-license textarea { + padding: 10px; + margin-top: 20px; + margin-bottom: 10px; +} + +.initial-setup-modal .upload-license .validate-message { + display: inline-block; + margin-left: 10px; + margin-top: 10px; +} + +.initial-setup-modal .upload-license .license-invalid h5 { + font-size: 18px; + color: red; +} + +.initial-setup-modal .upload-license .license-invalid h6 { + margin-bottom: 10px; + font-size: 16px; +} + +.initial-setup-modal .upload-license .license-invalid .fa { + margin-right: 6px; +} + +.initial-setup-modal .license-valid h5 { + color: #2FC98E; + font-size: 16px; + margin-bottom: 16px; +} + +.initial-setup-modal .license-valid .fa { + margin-right: 6px; +} + +.initial-setup-modal .license-valid table { + margin-top: 40px; +} + +.initial-setup-modal .license-valid table td { + border: 0px; + padding: 4px; +} + +.initial-setup-modal .license-valid table td:first-child { + font-weight: bold; + max-width: 100px; + padding-right: 20px; +} diff --git a/static/js/pages/setup.js b/static/js/pages/setup.js index cbd539c16..0b5eecad7 100644 --- a/static/js/pages/setup.js +++ b/static/js/pages/setup.js @@ -37,6 +37,15 @@ // The config.yaml exists but it is invalid. 'INVALID_CONFIG': 'config-invalid', + // License is being uploaded. + 'UPLOAD_LICENSE': 'upload-license', + + // License is being validated. + 'VALIDATING_LICENSE': 'upload-license-validating', + + // License is validated. + 'VALIDATED_LICENSE': 'upload-license-validated', + // DB is being configured. 'CONFIG_DB': 'config-db', @@ -95,7 +104,10 @@ $scope.currentConfig = null; $scope.currentState = { - 'hasDatabaseSSLCert': false + 'hasDatabaseSSLCert': false, + 'licenseContents': '', + 'licenseError': null, + 'licenseDecoded': null, }; $scope.$watch('currentStep', function(currentStep) { @@ -121,6 +133,7 @@ case $scope.States.CREATE_SUPERUSER: case $scope.States.DB_RESTARTING: case $scope.States.CONFIG_DB: + case $scope.States.UPLOAD_LICENSE: case $scope.States.VALID_CONFIG: case $scope.States.READY: $('#setupModal').modal({ @@ -131,6 +144,27 @@ } }); + $scope.validateLicense = function() { + $scope.currentStep = $scope.States.VALIDATING_LICENSE; + + var data = { + 'license': $scope.currentState.licenseContents + }; + + ApiService.suSetAndValidateLicense(data).then(function(resp) { + $scope.currentStep = $scope.States.VALIDATED_LICENSE; + + $scope.currentState.licenseError = null; + $scope.currentState.licenseDecoded = resp['decoded']; + }, function(resp) { + $scope.currentStep = $scope.States.UPLOAD_LICENSE; + + $scope.currentState.licenseError = ApiService.getErrorMessage(resp); + $scope.currentState.licenseContents = ''; + $scope.currentState.licenseDecoded = null; + }); + }; + $scope.restartContainer = function(state) { $scope.currentStep = state; ContainerService.restartContainer(function() { @@ -166,6 +200,7 @@ var States = $scope.States; return [ + isStepFamily(step, States.UPLOAD_LICENSE), isStepFamily(step, States.CONFIG_DB), isStepFamily(step, States.DB_SETUP), isStep(step, States.DB_RESTARTING), @@ -191,6 +226,10 @@ return false; }; + $scope.beginSetup = function() { + $scope.currentStep = $scope.States.CONFIG_DB; + }; + $scope.showInvalidConfigDialog = function() { var message = "The config.yaml file found in conf/stack could not be parsed." var title = "Invalid configuration file"; diff --git a/static/partials/setup.html b/static/partials/setup.html index e28837b6e..e238ab846 100644 --- a/static/partials/setup.html +++ b/static/partials/setup.html @@ -9,12 +9,13 @@
- + + - - - + + + @@ -36,12 +37,13 @@ + + + + + + + + + + + +