From 1940fd993928d3070af6420cda81914ff6366175 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 16 Feb 2016 15:31:23 -0500 Subject: [PATCH] Add UI to the setup tool for enabling ACI conversion Fixes #1211 --- config.py | 3 ++ endpoints/common.py | 1 + endpoints/verbs.py | 2 + endpoints/web.py | 1 + .../directives/config/config-setup-tool.html | 46 +++++++++++++++++++ static/js/core-config-setup.js | 4 ++ static/js/directives/ui/fetch-tag-dialog.js | 14 +++--- templates/index.html | 4 +- util/config/configutil.py | 8 ++++ util/config/validator.py | 18 +++++++- util/security/signing.py | 23 ++++++---- 11 files changed, 106 insertions(+), 18 deletions(-) diff --git a/config.py b/config.py index 46c295b8e..b3d230138 100644 --- a/config.py +++ b/config.py @@ -199,6 +199,9 @@ class DefaultConfig(object): # Feature Flag: Whether or not to rotate old action logs to storage. FEATURE_ACTION_LOG_ROTATION = False + # Feature Flag: Whether to enable conversion to ACIs. + FEATURE_ACI_CONVERSION = False + # Feature Flag: Whether to allow for "namespace-less" repositories when pulling and pushing from # Docker. FEATURE_LIBRARY_SUPPORT = True diff --git a/endpoints/common.py b/endpoints/common.py index 5d5a8258e..b4c897dad 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -207,6 +207,7 @@ def render_page_template(name, route_data=None, **kwargs): sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''), is_debug=str(app.config.get('DEBUGGING', False)).lower(), show_chat=features.OLARK_CHAT, + aci_conversion=features.ACI_CONVERSION, has_billing=features.BILLING, contact_href=contact_href, hostname=app.config['SERVER_HOSTNAME'], diff --git a/endpoints/verbs.py b/endpoints/verbs.py index 907006feb..c89febc2b 100644 --- a/endpoints/verbs.py +++ b/endpoints/verbs.py @@ -351,6 +351,7 @@ def os_arch_checker(os, arch): return checker +@route_show_if(features.ACI_CONVERSION) @anon_protect @verbs.route('/aci/////sig///', methods=['GET']) @verbs.route('/aci/////aci.asc///', methods=['GET']) @@ -360,6 +361,7 @@ def get_aci_signature(server, namespace, repository, tag, os, arch): os=os, arch=arch) +@route_show_if(features.ACI_CONVERSION) @anon_protect @verbs.route('/aci/////aci///', methods=['GET', 'HEAD']) @process_auth diff --git a/endpoints/web.py b/endpoints/web.py index 9e3a69e52..e05c4eb97 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -99,6 +99,7 @@ def snapshot(path = ''): abort(404) +@route_show_if(features.ACI_CONVERSION) @web.route('/aci-signing-key') @no_cache @anon_protect diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html index 7e67107cd..4f5a9a190 100644 --- a/static/directives/config/config-setup-tool.html +++ b/static/directives/config/config-setup-tool.html @@ -286,6 +286,52 @@ + +
+
+ rkt Conversion +
+
+
+

If enabled, all images in the registry can be fetched via rkt fetch or any other AppC discovery-compliant implementation.

+
+ +
+ + +
+ +
+ Documentation on generating these keys can be found at Generating ACI Signing Keys. +
+ + + + + + + + + + + + + +
GPG2 Public Key File: + +
+ The certificate must be in PEM format. +
+
GPG2 Private Key File: + +
GPG2 Private Key Name: + +
+
+
+
diff --git a/static/js/core-config-setup.js b/static/js/core-config-setup.js index 721d7d05d..4ac8c3e5e 100644 --- a/static/js/core-config-setup.js +++ b/static/js/core-config-setup.js @@ -35,6 +35,10 @@ angular.module("core-config-setup", ['angularFileUpload']) return config.AUTHENTICATION_TYPE == 'Keystone'; }, 'password': true}, + {'id': 'signer', 'title': 'ACI Signing', 'condition': function(config) { + return config.FEATURE_ACI_CONVERSION; + }}, + {'id': 'mail', 'title': 'E-mail Support', 'condition': function(config) { return config.FEATURE_MAILING; }}, diff --git a/static/js/directives/ui/fetch-tag-dialog.js b/static/js/directives/ui/fetch-tag-dialog.js index 7f902f53a..0ae6505ef 100644 --- a/static/js/directives/ui/fetch-tag-dialog.js +++ b/static/js/directives/ui/fetch-tag-dialog.js @@ -12,7 +12,7 @@ angular.module('quay').directive('fetchTagDialog', function () { 'repository': '=repository', 'actionHandler': '=actionHandler' }, - controller: function($scope, $element, $timeout, ApiService, UserService, Config) { + controller: function($scope, $element, $timeout, ApiService, UserService, Config, Features) { $scope.clearCounter = 0; $scope.currentFormat = null; $scope.currentEntity = null; @@ -35,11 +35,13 @@ angular.module('quay').directive('fetchTagDialog', function () { }); } - $scope.formats.push({ - 'title': 'Rocket Fetch', - 'icon': 'rocket-icon', - 'command': 'rkt fetch {hostname}/{namespace}/{name}:{tag}' - }); + if (Features.ACI_CONVERSION) { + $scope.formats.push({ + 'title': 'Rocket Fetch', + 'icon': 'rocket-icon', + 'command': 'rkt fetch {hostname}/{namespace}/{name}:{tag}' + }); + } $scope.formats.push({ 'title': 'Basic Docker Pull', diff --git a/templates/index.html b/templates/index.html index 6e5151196..dada2efe3 100644 --- a/templates/index.html +++ b/templates/index.html @@ -10,9 +10,11 @@ + +{% if aci_conversion %} - +{% endif %} {% endblock %} diff --git a/util/config/configutil.py b/util/config/configutil.py index 5b9bc1dcb..461e93a70 100644 --- a/util/config/configutil.py +++ b/util/config/configutil.py @@ -21,6 +21,14 @@ def add_enterprise_config_defaults(config_obj, current_secret_key, hostname): # Default features that are off. config_obj['FEATURE_MAILING'] = config_obj.get('FEATURE_MAILING', False) config_obj['FEATURE_BUILD_SUPPORT'] = config_obj.get('FEATURE_BUILD_SUPPORT', False) + config_obj['FEATURE_ACI_CONVERSION'] = config_obj.get('FEATURE_ACI_CONVERSION', True) + + # Default the signer config. + config_obj['GPG2_PRIVATE_KEY_FILENAME'] = config_obj.get('GPG2_PRIVATE_KEY_FILENAME', + 'signing-private.gpg') + config_obj['GPG2_PUBLIC_KEY_FILENAME'] = config_obj.get('GPG2_PUBLIC_KEY_FILENAME', + 'signing-public.gpg') + config_obj['SIGNING_ENGINE'] = config_obj.get('SIGNING_ENGINE', 'gpg2') # Default auth type. if not 'AUTHENTICATION_TYPE' in config_obj: diff --git a/util/config/validator.py b/util/config/validator.py index 412f3ebb2..4f8853cb5 100644 --- a/util/config/validator.py +++ b/util/config/validator.py @@ -6,6 +6,7 @@ import peewee import OpenSSL import logging +from StringIO import StringIO from fnmatch import fnmatch from data.users.keystone import KeystoneUsers from data.users.externaljwt import ExternalJWTAuthN @@ -18,6 +19,7 @@ from storage import get_storage_driver from auth.auth_context import get_authenticated_user from util.config.oauth import GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig from bitbucket import BitBucket +from util.security.signing import SIGNING_ENGINES from app import app, config_provider, get_app_url, OVERRIDE_CONFIG_DIRECTORY @@ -27,8 +29,9 @@ logger = logging.getLogger(__name__) SSL_FILENAMES = ['ssl.cert', 'ssl.key'] DB_SSL_FILENAMES = ['database.pem'] JWT_FILENAMES = ['jwt-authn.cert'] +ACI_CERT_FILENAMES = ['signing-public.gpg', 'signing-private.gpg'] -CONFIG_FILENAMES = SSL_FILENAMES + DB_SSL_FILENAMES + JWT_FILENAMES +CONFIG_FILENAMES = SSL_FILENAMES + DB_SSL_FILENAMES + JWT_FILENAMES + ACI_CERT_FILENAMES def get_storage_providers(config): storage_config = config.get('DISTRIBUTED_STORAGE_CONFIG', {}) @@ -409,6 +412,18 @@ def _validate_keystone(config, password): 'OR Keystone auth is misconfigured.') % (username, err_msg)) +def _validate_signer(config, _): + """ Validates the GPG public+private key pair used for signing converted ACIs. """ + if config.get('SIGNING_ENGINE') is None: + return + + if config['SIGNING_ENGINE'] not in SIGNING_ENGINES: + raise Exception('Unknown signing engine: %s' % config['SIGNING_ENGINE']) + + engine = SIGNING_ENGINES[config['SIGNING_ENGINE']](config, OVERRIDE_CONFIG_DIRECTORY) + engine.detached_sign(StringIO('test string')) + + _VALIDATORS = { 'database': _validate_database, 'redis': _validate_redis, @@ -423,4 +438,5 @@ _VALIDATORS = { 'ldap': _validate_ldap, 'jwt': _validate_jwt, 'keystone': _validate_keystone, + 'signer': _validate_signer, } diff --git a/util/security/signing.py b/util/security/signing.py index a57e4ebd7..5b5810aeb 100644 --- a/util/security/signing.py +++ b/util/security/signing.py @@ -4,22 +4,22 @@ from StringIO import StringIO class GPG2Signer(object): """ Helper class for signing data using GPG2. """ - def __init__(self, app, key_directory): - if not app.config.get('GPG2_PRIVATE_KEY_NAME'): + def __init__(self, config, key_directory): + if not config.get('GPG2_PRIVATE_KEY_NAME'): raise Exception('Missing configuration key GPG2_PRIVATE_KEY_NAME') - if not app.config.get('GPG2_PRIVATE_KEY_FILENAME'): + if not config.get('GPG2_PRIVATE_KEY_FILENAME'): raise Exception('Missing configuration key GPG2_PRIVATE_KEY_FILENAME') - if not app.config.get('GPG2_PUBLIC_KEY_FILENAME'): + if not config.get('GPG2_PUBLIC_KEY_FILENAME'): raise Exception('Missing configuration key GPG2_PUBLIC_KEY_FILENAME') self._ctx = gpgme.Context() self._ctx.armor = True - self._private_key_name = app.config['GPG2_PRIVATE_KEY_NAME'] - self._public_key_path = os.path.join(key_directory, app.config['GPG2_PUBLIC_KEY_FILENAME']) + self._private_key_name = config['GPG2_PRIVATE_KEY_NAME'] + self._public_key_path = os.path.join(key_directory, config['GPG2_PUBLIC_KEY_FILENAME']) - key_file = os.path.join(key_directory, app.config['GPG2_PRIVATE_KEY_FILENAME']) + key_file = os.path.join(key_directory, config['GPG2_PRIVATE_KEY_FILENAME']) if not os.path.exists(key_file): raise Exception('Missing key file %s' % key_file) @@ -37,10 +37,13 @@ class GPG2Signer(object): def detached_sign(self, stream): """ Signs the given stream, returning the signature. """ ctx = self._ctx - ctx.signers = [ctx.get_key(self._private_key_name)] + try: + ctx.signers = [ctx.get_key(self._private_key_name)] + except: + raise Exception('Invalid private key name') + signature = StringIO() new_sigs = ctx.sign(stream, signature, gpgme.SIG_MODE_DETACH) - signature.seek(0) return signature.getvalue() @@ -58,7 +61,7 @@ class Signer(object): if preference is None: return None - return SIGNING_ENGINES[preference](app, key_directory) + return SIGNING_ENGINES[preference](app.config, key_directory) def __getattr__(self, name): return getattr(self.state, name, None)