parent
ded0a27901
commit
1940fd9939
11 changed files with 106 additions and 18 deletions
|
@ -199,6 +199,9 @@ class DefaultConfig(object):
|
||||||
# Feature Flag: Whether or not to rotate old action logs to storage.
|
# Feature Flag: Whether or not to rotate old action logs to storage.
|
||||||
FEATURE_ACTION_LOG_ROTATION = False
|
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
|
# Feature Flag: Whether to allow for "namespace-less" repositories when pulling and pushing from
|
||||||
# Docker.
|
# Docker.
|
||||||
FEATURE_LIBRARY_SUPPORT = True
|
FEATURE_LIBRARY_SUPPORT = True
|
||||||
|
|
|
@ -207,6 +207,7 @@ def render_page_template(name, route_data=None, **kwargs):
|
||||||
sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''),
|
sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''),
|
||||||
is_debug=str(app.config.get('DEBUGGING', False)).lower(),
|
is_debug=str(app.config.get('DEBUGGING', False)).lower(),
|
||||||
show_chat=features.OLARK_CHAT,
|
show_chat=features.OLARK_CHAT,
|
||||||
|
aci_conversion=features.ACI_CONVERSION,
|
||||||
has_billing=features.BILLING,
|
has_billing=features.BILLING,
|
||||||
contact_href=contact_href,
|
contact_href=contact_href,
|
||||||
hostname=app.config['SERVER_HOSTNAME'],
|
hostname=app.config['SERVER_HOSTNAME'],
|
||||||
|
|
|
@ -351,6 +351,7 @@ def os_arch_checker(os, arch):
|
||||||
return checker
|
return checker
|
||||||
|
|
||||||
|
|
||||||
|
@route_show_if(features.ACI_CONVERSION)
|
||||||
@anon_protect
|
@anon_protect
|
||||||
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/sig/<os>/<arch>/', methods=['GET'])
|
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/sig/<os>/<arch>/', methods=['GET'])
|
||||||
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/aci.asc/<os>/<arch>/', methods=['GET'])
|
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/aci.asc/<os>/<arch>/', methods=['GET'])
|
||||||
|
@ -360,6 +361,7 @@ def get_aci_signature(server, namespace, repository, tag, os, arch):
|
||||||
os=os, arch=arch)
|
os=os, arch=arch)
|
||||||
|
|
||||||
|
|
||||||
|
@route_show_if(features.ACI_CONVERSION)
|
||||||
@anon_protect
|
@anon_protect
|
||||||
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/aci/<os>/<arch>/', methods=['GET', 'HEAD'])
|
@verbs.route('/aci/<server>/<namespace>/<repository>/<tag>/aci/<os>/<arch>/', methods=['GET', 'HEAD'])
|
||||||
@process_auth
|
@process_auth
|
||||||
|
|
|
@ -99,6 +99,7 @@ def snapshot(path = ''):
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@route_show_if(features.ACI_CONVERSION)
|
||||||
@web.route('/aci-signing-key')
|
@web.route('/aci-signing-key')
|
||||||
@no_cache
|
@no_cache
|
||||||
@anon_protect
|
@anon_protect
|
||||||
|
|
|
@ -286,6 +286,52 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ACI Conversion -->
|
||||||
|
<div class="co-panel">
|
||||||
|
<div class="co-panel-heading">
|
||||||
|
<i class="fa rocket-icon" style="width: 20px; height: 20px; background-size: cover; vertical-align: middle;"></i> <a href="http://github.com/coreos/rkt" target="_blank">rkt</a> Conversion
|
||||||
|
</div>
|
||||||
|
<div class="co-panel-body">
|
||||||
|
<div class="description">
|
||||||
|
<p>If enabled, all images in the registry can be fetched via <code>rkt fetch</code> or any other <a href="https://github.com/appc/spec/blob/master/spec/discovery.md" target="_blank">AppC discovery</a>-compliant implementation.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="co-checkbox">
|
||||||
|
<input id="ftmail" type="checkbox" ng-model="config.FEATURE_ACI_CONVERSION">
|
||||||
|
<label for="ftmail">Enable ACI Conversion</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="co-alert co-alert-info" ng-if="config.FEATURE_ACI_CONVERSION" style="margin-top: 20px;">
|
||||||
|
Documentation on generating these keys can be found at <a href="https://tectonic.com/quay-enterprise/docs/latest/aci-signing-keys.html" target="_blank">Generating ACI Signing Keys</a>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="config-table" ng-if="config.FEATURE_ACI_CONVERSION">
|
||||||
|
<tr>
|
||||||
|
<td class="non-input">GPG2 Public Key File:</td>
|
||||||
|
<td>
|
||||||
|
<span class="config-file-field" filename="signing-public.gpg" has-file="hasfile.gpgSigningPublic"></span>
|
||||||
|
<div class="help-text">
|
||||||
|
The certificate must be in PEM format.
|
||||||
|
</div
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="non-input">GPG2 Private Key File:</td>
|
||||||
|
<td>
|
||||||
|
<span class="config-file-field" filename="signing-private.gpg" has-file="hasfile.gpgSigningPrivate"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="non-input">GPG2 Private Key Name:</td>
|
||||||
|
<td>
|
||||||
|
<span class="config-string-field" binding="config.GPG2_PRIVATE_KEY_NAME"
|
||||||
|
placeholder="Name of the private key in the private key file (Example: EAB32227)"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- E-mail -->
|
<!-- E-mail -->
|
||||||
<div class="co-panel">
|
<div class="co-panel">
|
||||||
<div class="co-panel-heading">
|
<div class="co-panel-heading">
|
||||||
|
|
|
@ -35,6 +35,10 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
||||||
return config.AUTHENTICATION_TYPE == 'Keystone';
|
return config.AUTHENTICATION_TYPE == 'Keystone';
|
||||||
}, 'password': true},
|
}, 'password': true},
|
||||||
|
|
||||||
|
{'id': 'signer', 'title': 'ACI Signing', 'condition': function(config) {
|
||||||
|
return config.FEATURE_ACI_CONVERSION;
|
||||||
|
}},
|
||||||
|
|
||||||
{'id': 'mail', 'title': 'E-mail Support', 'condition': function(config) {
|
{'id': 'mail', 'title': 'E-mail Support', 'condition': function(config) {
|
||||||
return config.FEATURE_MAILING;
|
return config.FEATURE_MAILING;
|
||||||
}},
|
}},
|
||||||
|
|
|
@ -12,7 +12,7 @@ angular.module('quay').directive('fetchTagDialog', function () {
|
||||||
'repository': '=repository',
|
'repository': '=repository',
|
||||||
'actionHandler': '=actionHandler'
|
'actionHandler': '=actionHandler'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, $timeout, ApiService, UserService, Config) {
|
controller: function($scope, $element, $timeout, ApiService, UserService, Config, Features) {
|
||||||
$scope.clearCounter = 0;
|
$scope.clearCounter = 0;
|
||||||
$scope.currentFormat = null;
|
$scope.currentFormat = null;
|
||||||
$scope.currentEntity = null;
|
$scope.currentEntity = null;
|
||||||
|
@ -35,11 +35,13 @@ angular.module('quay').directive('fetchTagDialog', function () {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Features.ACI_CONVERSION) {
|
||||||
$scope.formats.push({
|
$scope.formats.push({
|
||||||
'title': 'Rocket Fetch',
|
'title': 'Rocket Fetch',
|
||||||
'icon': 'rocket-icon',
|
'icon': 'rocket-icon',
|
||||||
'command': 'rkt fetch {hostname}/{namespace}/{name}:{tag}'
|
'command': 'rkt fetch {hostname}/{namespace}/{name}:{tag}'
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$scope.formats.push({
|
$scope.formats.push({
|
||||||
'title': 'Basic Docker Pull',
|
'title': 'Basic Docker Pull',
|
||||||
|
|
|
@ -10,9 +10,11 @@
|
||||||
<meta id="descriptionTag" name="description" content="Quay is the best place to build, store, and distribute your containers. Public repositories are always free."></meta>
|
<meta id="descriptionTag" name="description" content="Quay is the best place to build, store, and distribute your containers. Public repositories are always free."></meta>
|
||||||
<meta name="google-site-verification" content="GalDznToijTsHYmLjJvE4QaB9uk_IP16aaGDz5D75T4" />
|
<meta name="google-site-verification" content="GalDznToijTsHYmLjJvE4QaB9uk_IP16aaGDz5D75T4" />
|
||||||
<meta name="fragment" content="!" />
|
<meta name="fragment" content="!" />
|
||||||
|
|
||||||
|
{% if aci_conversion %}
|
||||||
<meta name="ac-discovery" content="{{ hostname }} {{ preferred_scheme }}://{{ hostname }}/c1/aci/{name}/{version}/{ext}/{os}/{arch}/">
|
<meta name="ac-discovery" content="{{ hostname }} {{ preferred_scheme }}://{{ hostname }}/c1/aci/{name}/{version}/{ext}/{os}/{arch}/">
|
||||||
<meta name="ac-discovery-pubkeys" content="{{ hostname }} {{ preferred_scheme }}://{{ hostname }}/aci-signing-key">
|
<meta name="ac-discovery-pubkeys" content="{{ hostname }} {{ preferred_scheme }}://{{ hostname }}/aci-signing-key">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,14 @@ def add_enterprise_config_defaults(config_obj, current_secret_key, hostname):
|
||||||
# Default features that are off.
|
# Default features that are off.
|
||||||
config_obj['FEATURE_MAILING'] = config_obj.get('FEATURE_MAILING', False)
|
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_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.
|
# Default auth type.
|
||||||
if not 'AUTHENTICATION_TYPE' in config_obj:
|
if not 'AUTHENTICATION_TYPE' in config_obj:
|
||||||
|
|
|
@ -6,6 +6,7 @@ import peewee
|
||||||
import OpenSSL
|
import OpenSSL
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from StringIO import StringIO
|
||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
from data.users.keystone import KeystoneUsers
|
from data.users.keystone import KeystoneUsers
|
||||||
from data.users.externaljwt import ExternalJWTAuthN
|
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 auth.auth_context import get_authenticated_user
|
||||||
from util.config.oauth import GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig
|
from util.config.oauth import GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig
|
||||||
from bitbucket import BitBucket
|
from bitbucket import BitBucket
|
||||||
|
from util.security.signing import SIGNING_ENGINES
|
||||||
|
|
||||||
from app import app, config_provider, get_app_url, OVERRIDE_CONFIG_DIRECTORY
|
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']
|
SSL_FILENAMES = ['ssl.cert', 'ssl.key']
|
||||||
DB_SSL_FILENAMES = ['database.pem']
|
DB_SSL_FILENAMES = ['database.pem']
|
||||||
JWT_FILENAMES = ['jwt-authn.cert']
|
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):
|
def get_storage_providers(config):
|
||||||
storage_config = config.get('DISTRIBUTED_STORAGE_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))
|
'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 = {
|
_VALIDATORS = {
|
||||||
'database': _validate_database,
|
'database': _validate_database,
|
||||||
'redis': _validate_redis,
|
'redis': _validate_redis,
|
||||||
|
@ -423,4 +438,5 @@ _VALIDATORS = {
|
||||||
'ldap': _validate_ldap,
|
'ldap': _validate_ldap,
|
||||||
'jwt': _validate_jwt,
|
'jwt': _validate_jwt,
|
||||||
'keystone': _validate_keystone,
|
'keystone': _validate_keystone,
|
||||||
|
'signer': _validate_signer,
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,22 +4,22 @@ from StringIO import StringIO
|
||||||
|
|
||||||
class GPG2Signer(object):
|
class GPG2Signer(object):
|
||||||
""" Helper class for signing data using GPG2. """
|
""" Helper class for signing data using GPG2. """
|
||||||
def __init__(self, app, key_directory):
|
def __init__(self, config, key_directory):
|
||||||
if not app.config.get('GPG2_PRIVATE_KEY_NAME'):
|
if not config.get('GPG2_PRIVATE_KEY_NAME'):
|
||||||
raise Exception('Missing configuration key 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')
|
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')
|
raise Exception('Missing configuration key GPG2_PUBLIC_KEY_FILENAME')
|
||||||
|
|
||||||
self._ctx = gpgme.Context()
|
self._ctx = gpgme.Context()
|
||||||
self._ctx.armor = True
|
self._ctx.armor = True
|
||||||
self._private_key_name = app.config['GPG2_PRIVATE_KEY_NAME']
|
self._private_key_name = config['GPG2_PRIVATE_KEY_NAME']
|
||||||
self._public_key_path = os.path.join(key_directory, app.config['GPG2_PUBLIC_KEY_FILENAME'])
|
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):
|
if not os.path.exists(key_file):
|
||||||
raise Exception('Missing key file %s' % key_file)
|
raise Exception('Missing key file %s' % key_file)
|
||||||
|
|
||||||
|
@ -37,10 +37,13 @@ class GPG2Signer(object):
|
||||||
def detached_sign(self, stream):
|
def detached_sign(self, stream):
|
||||||
""" Signs the given stream, returning the signature. """
|
""" Signs the given stream, returning the signature. """
|
||||||
ctx = self._ctx
|
ctx = self._ctx
|
||||||
|
try:
|
||||||
ctx.signers = [ctx.get_key(self._private_key_name)]
|
ctx.signers = [ctx.get_key(self._private_key_name)]
|
||||||
|
except:
|
||||||
|
raise Exception('Invalid private key name')
|
||||||
|
|
||||||
signature = StringIO()
|
signature = StringIO()
|
||||||
new_sigs = ctx.sign(stream, signature, gpgme.SIG_MODE_DETACH)
|
new_sigs = ctx.sign(stream, signature, gpgme.SIG_MODE_DETACH)
|
||||||
|
|
||||||
signature.seek(0)
|
signature.seek(0)
|
||||||
return signature.getvalue()
|
return signature.getvalue()
|
||||||
|
|
||||||
|
@ -58,7 +61,7 @@ class Signer(object):
|
||||||
if preference is None:
|
if preference is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return SIGNING_ENGINES[preference](app, key_directory)
|
return SIGNING_ENGINES[preference](app.config, key_directory)
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
return getattr(self.state, name, None)
|
return getattr(self.state, name, None)
|
||||||
|
|
Reference in a new issue