Merge pull request #3212 from quay/delete-setup-page

Delete setup page
This commit is contained in:
Sam Chow 2018-08-27 12:47:15 -04:00 committed by GitHub
commit fb9c4fecce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 647 additions and 5135 deletions

View file

@ -150,11 +150,11 @@ def kubernetes_only(f):
nickname = partial(add_method_metadata, 'nickname')
import config_endpoints.api
import config_endpoints.api.discovery
import config_endpoints.api.kubeconfig
import config_endpoints.api.suconfig
import config_endpoints.api.superuser
import config_endpoints.api.tar_config_loader
import config_endpoints.api.user
import config_app.config_endpoints.api
import config_app.config_endpoints.api.discovery
import config_app.config_endpoints.api.kubeconfig
import config_app.config_endpoints.api.suconfig
import config_app.config_endpoints.api.superuser
import config_app.config_endpoints.api.tar_config_loader
import config_app.config_endpoints.api.user

View file

@ -74,6 +74,11 @@ class SuperUserConfig(ApiResource):
# Write the configuration changes to the config override file.
config_provider.save_config(config_object)
# now try to connect to the db provided in their config to validate it works
combined = dict(**app.config)
combined.update(config_provider.get_config())
configure(combined, testing=app.config['TESTING'])
return {
'exists': True,
'config': config_object

View file

@ -54,9 +54,10 @@ class SuperUserCustomCertificate(ApiResource):
return '', 204
# Call the update script with config dir location to install the certificate immediately.
cert_dir = os.path.join(config_provider.get_config_dir_path(), EXTRA_CA_DIRECTORY)
if subprocess.call([os.path.join(INIT_SCRIPTS_LOCATION, 'certs_install.sh')], env={ 'CERTDIR': cert_dir }) != 0:
raise Exception('Could not install certificates')
if not app.config['TESTING']:
cert_dir = os.path.join(config_provider.get_config_dir_path(), EXTRA_CA_DIRECTORY)
if subprocess.call([os.path.join(INIT_SCRIPTS_LOCATION, 'certs_install.sh')], env={ 'CERTDIR': cert_dir }) != 0:
raise Exception('Could not install certificates')
return '', 204

View file

@ -0,0 +1,149 @@
import json as py_json
import unittest
from contextlib import contextmanager
from urllib import urlencode
from urlparse import urlparse, parse_qs, urlunparse
from config_app.c_app import app, config_provider
from config_app.config_endpoints.api import api
from initdb import setup_database_for_testing, finished_database_for_testing
CSRF_TOKEN_KEY = '_csrf_token'
CSRF_TOKEN = '123csrfforme'
READ_ACCESS_USER = 'reader'
ADMIN_ACCESS_USER = 'devtable'
ADMIN_ACCESS_EMAIL = 'jschorr@devtable.com'
# OVERRIDES FROM PORTING FROM OLD APP:
all_queues = [] # the config app doesn't have any queues
class ApiTestCase(unittest.TestCase):
maxDiff = None
@staticmethod
def _add_csrf(without_csrf):
parts = urlparse(without_csrf)
query = parse_qs(parts[4])
query[CSRF_TOKEN_KEY] = CSRF_TOKEN
return urlunparse(list(parts[0:4]) + [urlencode(query)] + list(parts[5:]))
def url_for(self, resource_name, params=None, skip_csrf=False):
params = params or {}
url = api.url_for(resource_name, **params)
if not skip_csrf:
url = ApiTestCase._add_csrf(url)
return url
def setUp(self):
setup_database_for_testing(self)
self.app = app.test_client()
self.ctx = app.test_request_context()
self.ctx.__enter__()
self.setCsrfToken(CSRF_TOKEN)
def tearDown(self):
finished_database_for_testing(self)
config_provider.clear()
self.ctx.__exit__(True, None, None)
def setCsrfToken(self, token):
with self.app.session_transaction() as sess:
sess[CSRF_TOKEN_KEY] = token
@contextmanager
def toggleFeature(self, name, enabled):
import features
previous_value = getattr(features, name)
setattr(features, name, enabled)
yield
setattr(features, name, previous_value)
def getJsonResponse(self, resource_name, params={}, expected_code=200):
rv = self.app.get(api.url_for(resource_name, **params))
self.assertEquals(expected_code, rv.status_code)
data = rv.data
parsed = py_json.loads(data)
return parsed
def postResponse(self, resource_name, params={}, data={}, file=None, headers=None,
expected_code=200):
data = py_json.dumps(data)
headers = headers or {}
headers.update({"Content-Type": "application/json"})
if file is not None:
data = {'file': file}
headers = None
rv = self.app.post(self.url_for(resource_name, params), data=data, headers=headers)
self.assertEquals(rv.status_code, expected_code)
return rv.data
def getResponse(self, resource_name, params={}, expected_code=200):
rv = self.app.get(api.url_for(resource_name, **params))
self.assertEquals(rv.status_code, expected_code)
return rv.data
def putResponse(self, resource_name, params={}, data={}, expected_code=200):
rv = self.app.put(
self.url_for(resource_name, params), data=py_json.dumps(data),
headers={"Content-Type": "application/json"})
self.assertEquals(rv.status_code, expected_code)
return rv.data
def deleteResponse(self, resource_name, params={}, expected_code=204):
rv = self.app.delete(self.url_for(resource_name, params))
if rv.status_code != expected_code:
print 'Mismatch data for resource DELETE %s: %s' % (resource_name, rv.data)
self.assertEquals(rv.status_code, expected_code)
return rv.data
def deleteEmptyResponse(self, resource_name, params={}, expected_code=204):
rv = self.app.delete(self.url_for(resource_name, params))
self.assertEquals(rv.status_code, expected_code)
self.assertEquals(rv.data, '') # ensure response body empty
return
def postJsonResponse(self, resource_name, params={}, data={}, expected_code=200):
rv = self.app.post(
self.url_for(resource_name, params), data=py_json.dumps(data),
headers={"Content-Type": "application/json"})
if rv.status_code != expected_code:
print 'Mismatch data for resource POST %s: %s' % (resource_name, rv.data)
self.assertEquals(rv.status_code, expected_code)
data = rv.data
parsed = py_json.loads(data)
return parsed
def putJsonResponse(self, resource_name, params={}, data={}, expected_code=200, skip_csrf=False):
rv = self.app.put(
self.url_for(resource_name, params, skip_csrf), data=py_json.dumps(data),
headers={"Content-Type": "application/json"})
if rv.status_code != expected_code:
print 'Mismatch data for resource PUT %s: %s' % (resource_name, rv.data)
self.assertEquals(rv.status_code, expected_code)
data = rv.data
parsed = py_json.loads(data)
return parsed
def assertNotInTeam(self, data, membername):
for memberData in data['members']:
if memberData['name'] == membername:
self.fail(membername + ' found in team: ' + data['name'])
def assertInTeam(self, data, membername):
for member_data in data['members']:
if member_data['name'] == membername:
return
self.fail(membername + ' not found in team: ' + data['name'])

View file

@ -0,0 +1,208 @@
from StringIO import StringIO
from mockldap import MockLdap
from data import database, model
from util.security.test.test_ssl_util import generate_test_cert
from config_app.c_app import app
from config_app.config_test import ApiTestCase, all_queues, ADMIN_ACCESS_USER, ADMIN_ACCESS_EMAIL
from config_app.config_endpoints.api import api_bp
from config_app.config_endpoints.api.superuser import SuperUserCustomCertificate, SuperUserCustomCertificates
from config_app.config_endpoints.api.suconfig import SuperUserConfig, SuperUserCreateInitialSuperUser, \
SuperUserConfigFile, SuperUserRegistryStatus
try:
app.register_blueprint(api_bp, url_prefix='/api')
except ValueError:
# This blueprint was already registered
pass
class TestSuperUserCreateInitialSuperUser(ApiTestCase):
def test_create_superuser(self):
data = {
'username': 'newsuper',
'password': 'password',
'email': 'jschorr+fake@devtable.com',
}
# Add some fake config.
fake_config = {
'AUTHENTICATION_TYPE': 'Database',
'SECRET_KEY': 'fakekey',
}
self.putJsonResponse(SuperUserConfig, data=dict(config=fake_config, hostname='fakehost'))
# Try to write with config. Should 403 since there are users in the DB.
self.postResponse(SuperUserCreateInitialSuperUser, data=data, expected_code=403)
# Delete all users in the DB.
for user in list(database.User.select()):
model.user.delete_user(user, all_queues)
# Create the superuser.
self.postJsonResponse(SuperUserCreateInitialSuperUser, data=data)
# Ensure the user exists in the DB.
self.assertIsNotNone(model.user.get_user('newsuper'))
# Ensure that the current user is a superuser in the config.
json = self.getJsonResponse(SuperUserConfig)
self.assertEquals(['newsuper'], json['config']['SUPER_USERS'])
# Ensure that the current user is a superuser in memory by trying to call an API
# that will fail otherwise.
self.getResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'))
class TestSuperUserConfig(ApiTestCase):
def test_get_status_update_config(self):
# With no config the status should be 'config-db'.
json = self.getJsonResponse(SuperUserRegistryStatus)
self.assertEquals('config-db', json['status'])
# Add some fake config.
fake_config = {
'AUTHENTICATION_TYPE': 'Database',
'SECRET_KEY': 'fakekey',
}
json = self.putJsonResponse(SuperUserConfig, data=dict(config=fake_config,
hostname='fakehost'))
self.assertEquals('fakekey', json['config']['SECRET_KEY'])
self.assertEquals('fakehost', json['config']['SERVER_HOSTNAME'])
self.assertEquals('Database', json['config']['AUTHENTICATION_TYPE'])
# With config the status should be 'setup-db'.
# TODO(sam): fix this test
# json = self.getJsonResponse(SuperUserRegistryStatus)
# self.assertEquals('setup-db', json['status'])
def test_config_file(self):
# Try for an invalid file. Should 404.
self.getResponse(SuperUserConfigFile, params=dict(filename='foobar'), expected_code=404)
# Try for a valid filename. Should not exist.
json = self.getJsonResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'))
self.assertFalse(json['exists'])
# Add the file.
self.postResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'),
file=(StringIO('my file contents'), 'ssl.cert'))
# Should now exist.
json = self.getJsonResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'))
self.assertTrue(json['exists'])
def test_update_with_external_auth(self):
# Run a mock LDAP.
mockldap = MockLdap({
'dc=quay,dc=io': {
'dc': ['quay', 'io']
},
'ou=employees,dc=quay,dc=io': {
'dc': ['quay', 'io'],
'ou': 'employees'
},
'uid=' + ADMIN_ACCESS_USER + ',ou=employees,dc=quay,dc=io': {
'dc': ['quay', 'io'],
'ou': 'employees',
'uid': [ADMIN_ACCESS_USER],
'userPassword': ['password'],
'mail': [ADMIN_ACCESS_EMAIL],
},
})
config = {
'AUTHENTICATION_TYPE': 'LDAP',
'LDAP_BASE_DN': ['dc=quay', 'dc=io'],
'LDAP_ADMIN_DN': 'uid=devtable,ou=employees,dc=quay,dc=io',
'LDAP_ADMIN_PASSWD': 'password',
'LDAP_USER_RDN': ['ou=employees'],
'LDAP_UID_ATTR': 'uid',
'LDAP_EMAIL_ATTR': 'mail',
}
mockldap.start()
try:
# Write the config with the valid password.
self.putResponse(SuperUserConfig,
data={'config': config,
'password': 'password',
'hostname': 'foo'}, expected_code=200)
# Ensure that the user row has been linked.
# TODO(sam): fix this test
# self.assertEquals(ADMIN_ACCESS_USER,
# model.user.verify_federated_login('ldap', ADMIN_ACCESS_USER).username)
finally:
mockldap.stop()
class TestSuperUserCustomCertificates(ApiTestCase):
def test_custom_certificates(self):
# Upload a certificate.
cert_contents, _ = generate_test_cert(hostname='somecoolhost', san_list=['DNS:bar', 'DNS:baz'])
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert.crt'),
file=(StringIO(cert_contents), 'testcert.crt'), expected_code=204)
# Make sure it is present.
json = self.getJsonResponse(SuperUserCustomCertificates)
self.assertEquals(1, len(json['certs']))
cert_info = json['certs'][0]
self.assertEquals('testcert.crt', cert_info['path'])
self.assertEquals(set(['somecoolhost', 'bar', 'baz']), set(cert_info['names']))
self.assertFalse(cert_info['expired'])
# Remove the certificate.
self.deleteResponse(SuperUserCustomCertificate, params=dict(certpath='testcert.crt'))
# Make sure it is gone.
json = self.getJsonResponse(SuperUserCustomCertificates)
self.assertEquals(0, len(json['certs']))
def test_expired_custom_certificate(self):
# Upload a certificate.
cert_contents, _ = generate_test_cert(hostname='somecoolhost', expires=-10)
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert.crt'),
file=(StringIO(cert_contents), 'testcert.crt'), expected_code=204)
# Make sure it is present.
json = self.getJsonResponse(SuperUserCustomCertificates)
self.assertEquals(1, len(json['certs']))
cert_info = json['certs'][0]
self.assertEquals('testcert.crt', cert_info['path'])
self.assertEquals(set(['somecoolhost']), set(cert_info['names']))
self.assertTrue(cert_info['expired'])
def test_invalid_custom_certificate(self):
# Upload an invalid certificate.
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert.crt'),
file=(StringIO('some contents'), 'testcert.crt'), expected_code=204)
# Make sure it is present but invalid.
json = self.getJsonResponse(SuperUserCustomCertificates)
self.assertEquals(1, len(json['certs']))
cert_info = json['certs'][0]
self.assertEquals('testcert.crt', cert_info['path'])
self.assertEquals('no start line', cert_info['error'])
def test_path_sanitization(self):
# Upload a certificate.
cert_contents, _ = generate_test_cert(hostname='somecoolhost', expires=-10)
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert/../foobar.crt'),
file=(StringIO(cert_contents), 'testcert/../foobar.crt'), expected_code=204)
# Make sure it is present.
json = self.getJsonResponse(SuperUserCustomCertificates)
self.assertEquals(1, len(json['certs']))
cert_info = json['certs'][0]
self.assertEquals('foobar.crt', cert_info['path'])

View file

@ -0,0 +1,149 @@
import unittest
from data.database import User
from data import model
from config_app.config_endpoints.api.suconfig import SuperUserConfig, SuperUserConfigValidate, SuperUserConfigFile, \
SuperUserRegistryStatus, SuperUserCreateInitialSuperUser
from config_app.config_endpoints.api import api_bp
from config_app.config_test import ApiTestCase, READ_ACCESS_USER, ADMIN_ACCESS_USER
from config_app.c_app import app, config_provider
try:
app.register_blueprint(api_bp, url_prefix='/api')
except ValueError:
# This blueprint was already registered
pass
# OVERRIDES FROM PORTING FROM OLD APP:
all_queues = [] # the config app doesn't have any queues
class FreshConfigProvider(object):
def __enter__(self):
config_provider.reset_for_test()
return config_provider
def __exit__(self, type, value, traceback):
config_provider.reset_for_test()
class TestSuperUserRegistryStatus(ApiTestCase):
def test_registry_status(self):
with FreshConfigProvider():
json = self.getJsonResponse(SuperUserRegistryStatus)
self.assertEquals('config-db', json['status'])
class TestSuperUserConfigFile(ApiTestCase):
def test_get_superuser_invalid_filename(self):
with FreshConfigProvider():
self.getResponse(SuperUserConfigFile, params=dict(filename='somefile'), expected_code=404)
def test_get_superuser(self):
with FreshConfigProvider():
result = self.getJsonResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'))
self.assertFalse(result['exists'])
def test_post_no_file(self):
with FreshConfigProvider():
# No file
self.postResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'), expected_code=400)
def test_post_superuser_invalid_filename(self):
with FreshConfigProvider():
self.postResponse(SuperUserConfigFile, params=dict(filename='somefile'), expected_code=404)
def test_post_superuser(self):
with FreshConfigProvider():
self.postResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'), expected_code=400)
class TestSuperUserCreateInitialSuperUser(ApiTestCase):
def test_no_config_file(self):
with FreshConfigProvider():
# If there is no config.yaml, then this method should security fail.
data = dict(username='cooluser', password='password', email='fake@example.com')
self.postResponse(SuperUserCreateInitialSuperUser, data=data, expected_code=403)
def test_config_file_with_db_users(self):
with FreshConfigProvider():
# Write some config.
self.putJsonResponse(SuperUserConfig, data=dict(config={}, hostname='foobar'))
# If there is a config.yaml, but existing DB users exist, then this method should security
# fail.
data = dict(username='cooluser', password='password', email='fake@example.com')
self.postResponse(SuperUserCreateInitialSuperUser, data=data, expected_code=403)
def test_config_file_with_no_db_users(self):
with FreshConfigProvider():
# Write some config.
self.putJsonResponse(SuperUserConfig, data=dict(config={}, hostname='foobar'))
# Delete all the users in the DB.
for user in list(User.select()):
model.user.delete_user(user, all_queues)
# This method should now succeed.
data = dict(username='cooluser', password='password', email='fake@example.com')
result = self.postJsonResponse(SuperUserCreateInitialSuperUser, data=data)
self.assertTrue(result['status'])
# Verify the superuser was created.
User.get(User.username == 'cooluser')
# Verify the superuser was placed into the config.
result = self.getJsonResponse(SuperUserConfig)
self.assertEquals(['cooluser'], result['config']['SUPER_USERS'])
class TestSuperUserConfigValidate(ApiTestCase):
def test_nonsuperuser_noconfig(self):
with FreshConfigProvider():
result = self.postJsonResponse(SuperUserConfigValidate, params=dict(service='someservice'),
data=dict(config={}))
self.assertFalse(result['status'])
def test_nonsuperuser_config(self):
with FreshConfigProvider():
# The validate config call works if there is no config.yaml OR the user is a superuser.
# Add a config, and verify it breaks when unauthenticated.
json = self.putJsonResponse(SuperUserConfig, data=dict(config={}, hostname='foobar'))
self.assertTrue(json['exists'])
result = self.postJsonResponse(SuperUserConfigValidate, params=dict(service='someservice'),
data=dict(config={}))
self.assertFalse(result['status'])
class TestSuperUserConfig(ApiTestCase):
def test_get_superuser(self):
with FreshConfigProvider():
json = self.getJsonResponse(SuperUserConfig)
# Note: We expect the config to be none because a config.yaml should never be checked into
# the directory.
self.assertIsNone(json['config'])
def test_put(self):
with FreshConfigProvider() as config:
json = self.putJsonResponse(SuperUserConfig, data=dict(config={}, hostname='foobar'))
self.assertTrue(json['exists'])
# Verify the config file exists.
self.assertTrue(config.config_exists())
# This should succeed.
json = self.putJsonResponse(SuperUserConfig, data=dict(config={}, hostname='barbaz'))
self.assertTrue(json['exists'])
json = self.getJsonResponse(SuperUserConfig)
self.assertIsNotNone(json['config'])
if __name__ == '__main__':
unittest.main()

View file

@ -78,3 +78,6 @@ class TestConfigProvider(BaseProvider):
def get_volume_path(self, directory, filename):
return os.path.join(directory, filename)
def get_config_dir_path(self):
return ''

View file

@ -321,7 +321,7 @@ def _db_from_url(url, db_kwargs, connect_timeout=DEFAULT_DB_CONNECT_TIMEOUT,
return driver(parsed_url.database, **db_kwargs)
def configure(config_object):
def configure(config_object, testing=False):
logger.debug('Configuring database')
db_kwargs = dict(config_object['DB_CONNECTION_ARGS'])
write_db_uri = config_object['DB_URI']
@ -344,7 +344,7 @@ def configure(config_object):
@contextmanager
def _ensure_under_transaction():
if not config_object['TESTING']:
if not testing and not config_object['TESTING']:
if db.transaction_depth() == 0:
raise Exception('Expected to be under a transaction')

View file

@ -7,21 +7,10 @@ import subprocess
from flask import abort
from app import (app, config_provider, superusers, OVERRIDE_CONFIG_DIRECTORY, ip_resolver,
instance_keys, INIT_SCRIPTS_LOCATION)
from app import app, config_provider
from auth.permissions import SuperUserPermission
from auth.auth_context import get_authenticated_user
from data.database import configure
from data.runmigration import run_alembic_migration
from data.users import get_federated_service_name, get_users_handler
from endpoints.api.suconfig_models_pre_oci import pre_oci_model as model
from endpoints.api import (ApiResource, nickname, resource, internal_only, show_if,
require_fresh_login, request, validate_json_request, verify_not_prod)
from endpoints.common import common_login
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, is_valid_config_upload_filename,
ValidatorContext)
from endpoints.api import (ApiResource, nickname, resource, internal_only, show_if, verify_not_prod)
import features
@ -96,41 +85,6 @@ class _AlembicLogHandler(logging.Handler):
'message': record.getMessage()
})
@resource('/v1/superuser/setupdb')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserSetupDatabase(ApiResource):
""" Resource for invoking alembic to setup the database. """
@verify_not_prod
@nickname('scSetupDatabase')
def get(self):
""" Invokes the alembic upgrade process. """
# Note: This method is called after the database configured is saved, but before the
# database has any tables. Therefore, we only allow it to be run in that unique case.
if config_provider.config_exists() and not database_is_valid():
# Note: We need to reconfigure the database here as the config has changed.
combined = dict(**app.config)
combined.update(config_provider.get_config())
configure(combined)
app.config['DB_URI'] = combined['DB_URI']
log_handler = _AlembicLogHandler()
try:
run_alembic_migration(app.config['DB_URI'], log_handler)
except Exception as ex:
return {
'error': str(ex)
}
return {
'logs': log_handler.records
}
abort(403)
# From: https://stackoverflow.com/a/44712205
def get_process_id(name):
"""Return process ids found by (partial) name or regex.
@ -168,251 +122,3 @@ class SuperUserShutdown(ApiResource):
return {}
abort(403)
@resource('/v1/superuser/config')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserConfig(ApiResource):
""" Resource for fetching and updating the current configuration, if any. """
schemas = {
'UpdateConfig': {
'type': 'object',
'description': 'Updates the YAML config file',
'required': [
'config',
'hostname'
],
'properties': {
'config': {
'type': 'object'
},
'hostname': {
'type': 'string'
},
'password': {
'type': 'string'
},
},
},
}
@require_fresh_login
@verify_not_prod
@nickname('scGetConfig')
def get(self):
""" Returns the currently defined configuration, if any. """
if SuperUserPermission().can():
config_object = config_provider.get_config()
return {
'config': config_object
}
abort(403)
@nickname('scUpdateConfig')
@verify_not_prod
@validate_json_request('UpdateConfig')
def put(self):
""" Updates the config override file. """
# Note: This method is called to set the database configuration before super users exists,
# so we also allow it to be called if there is no valid registry configuration setup.
if not config_provider.config_exists() or SuperUserPermission().can():
config_object = request.get_json()['config']
hostname = request.get_json()['hostname']
# Add any enterprise defaults missing from the config.
add_enterprise_config_defaults(config_object, app.config['SECRET_KEY'], hostname)
# Write the configuration changes to the config override file.
config_provider.save_config(config_object)
# If the authentication system is federated, link the superuser account to the
# the authentication system chosen.
service_name = get_federated_service_name(config_object['AUTHENTICATION_TYPE'])
if service_name is not None:
current_user = get_authenticated_user()
if current_user is None:
abort(401)
service_name = get_federated_service_name(config_object['AUTHENTICATION_TYPE'])
if not model.has_federated_login(current_user.username, service_name):
# Verify the user's credentials and retrieve the user's external username+email.
handler = get_users_handler(config_object, config_provider, OVERRIDE_CONFIG_DIRECTORY)
(result, err_msg) = handler.verify_credentials(current_user.username,
request.get_json().get('password', ''))
if not result:
logger.error('Could not save configuration due to external auth failure: %s', err_msg)
abort(400)
# Link the existing user to the external user.
model.attach_federated_login(current_user.username, service_name, result.username)
# Ensure database is up-to-date with config
sync_database_with_config(config_object)
return {
'exists': True,
'config': config_object
}
abort(403)
@resource('/v1/superuser/config/file/<filename>')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserConfigFile(ApiResource):
""" Resource for fetching the status of config files and overriding them. """
@nickname('scConfigFileExists')
@verify_not_prod
def get(self, filename):
""" Returns whether the configuration file with the given name exists. """
if not is_valid_config_upload_filename(filename):
abort(404)
if SuperUserPermission().can():
return {
'exists': config_provider.volume_file_exists(filename)
}
abort(403)
@nickname('scUpdateConfigFile')
@verify_not_prod
def post(self, filename):
""" Updates the configuration file with the given name. """
if not is_valid_config_upload_filename(filename):
abort(404)
# Note: This method can be called before the configuration exists
# to upload the database SSL cert.
if not config_provider.config_exists() or SuperUserPermission().can():
uploaded_file = request.files['file']
if not uploaded_file:
abort(400)
config_provider.save_volume_file(uploaded_file, filename)
return {
'status': True
}
abort(403)
@resource('/v1/superuser/config/createsuperuser')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserCreateInitialSuperUser(ApiResource):
""" Resource for creating the initial super user. """
schemas = {
'CreateSuperUser': {
'type': 'object',
'description': 'Information for creating the initial super user',
'required': [
'username',
'password',
'email'
],
'properties': {
'username': {
'type': 'string',
'description': 'The username for the superuser'
},
'password': {
'type': 'string',
'description': 'The password for the superuser'
},
'email': {
'type': 'string',
'description': 'The e-mail address for the superuser'
},
},
},
}
@nickname('scCreateInitialSuperuser')
@verify_not_prod
@validate_json_request('CreateSuperUser')
def post(self):
""" Creates the initial super user, updates the underlying configuration and
sets the current session to have that super user. """
# Special security check: This method is only accessible when:
# - There is a valid config YAML file.
# - There are currently no users in the database (clean install)
#
# We do this special security check because at the point this method is called, the database
# is clean but does not (yet) have any super users for our permissions code to check against.
if config_provider.config_exists() and not database_has_users():
data = request.get_json()
username = data['username']
password = data['password']
email = data['email']
# Create the user in the database.
superuser_uuid = model.create_superuser(username, password, email)
# Add the user to the config.
config_object = config_provider.get_config()
config_object['SUPER_USERS'] = [username]
config_provider.save_config(config_object)
# Update the in-memory config for the new superuser.
superusers.register_superuser(username)
# Conduct login with that user.
common_login(superuser_uuid)
return {
'status': True
}
abort(403)
@resource('/v1/superuser/config/validate/<service>')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserConfigValidate(ApiResource):
""" Resource for validating a block of configuration against an external service. """
schemas = {
'ValidateConfig': {
'type': 'object',
'description': 'Validates configuration',
'required': [
'config'
],
'properties': {
'config': {
'type': 'object'
},
'password': {
'type': 'string',
'description': 'The users password, used for auth validation'
}
},
},
}
@nickname('scValidateConfig')
@verify_not_prod
@validate_json_request('ValidateConfig')
def post(self, service):
""" Validates the given config for the given service. """
# Note: This method is called to validate the database configuration before super users exists,
# so we also allow it to be called if there is no valid registry configuration setup. Note that
# this is also safe since this method does not access any information not given in the request.
if not config_provider.config_exists() or SuperUserPermission().can():
config = request.get_json()['config']
validator_context = ValidatorContext.from_app(app, config,
request.get_json().get('password', ''),
instance_keys=instance_keys,
ip_resolver=ip_resolver,
config_provider=config_provider,
init_scripts_location=INIT_SCRIPTS_LOCATION)
return validate_service_for_config(service, validator_context)
abort(403)

View file

@ -7,13 +7,11 @@ import socket
from datetime import datetime, timedelta
from random import SystemRandom
import pathvalidate
from flask import request, make_response, jsonify
import features
from app import app, avatar, superusers, authentication, config_provider, INIT_SCRIPTS_LOCATION
from app import app, avatar, superusers, authentication, config_provider
from auth import scopes
from auth.auth_context import get_authenticated_user
from auth.permissions import SuperUserPermission
@ -29,8 +27,6 @@ from endpoints.api.superuser_models_pre_oci import (pre_oci_model, ServiceKeyDoe
InvalidRepositoryBuildException)
from endpoints.api.logs_models_pre_oci import pre_oci_model as log_model
from util.useremails import send_confirmation_email, send_recovery_email
from util.security.ssl import load_certificate, CertInvalidException
from util.config.validator import EXTRA_CA_DIRECTORY
from _init import ROOT_DIR
logger = logging.getLogger(__name__)
@ -860,118 +856,6 @@ class SuperUserServiceKeyApproval(ApiResource):
raise Unauthorized()
@resource('/v1/superuser/customcerts')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserCustomCertificates(ApiResource):
""" Resource for managing custom certificates. """
@nickname('getCustomCertificates')
@require_fresh_login
@require_scope(scopes.SUPERUSER)
@verify_not_prod
def get(self):
if SuperUserPermission().can():
has_extra_certs_path = config_provider.volume_file_exists(EXTRA_CA_DIRECTORY)
extra_certs_found = config_provider.list_volume_directory(EXTRA_CA_DIRECTORY)
if extra_certs_found is None:
return {
'status': 'file' if has_extra_certs_path else 'none',
}
cert_views = []
for extra_cert_path in extra_certs_found:
try:
cert_full_path = config_provider.get_volume_path(EXTRA_CA_DIRECTORY, extra_cert_path)
with config_provider.get_volume_file(cert_full_path) as f:
certificate = load_certificate(f.read())
cert_views.append({
'path': extra_cert_path,
'names': list(certificate.names),
'expired': certificate.expired,
})
except CertInvalidException as cie:
cert_views.append({
'path': extra_cert_path,
'error': cie.message,
})
except IOError as ioe:
cert_views.append({
'path': extra_cert_path,
'error': ioe.message,
})
return {
'status': 'directory',
'certs': cert_views,
}
raise Unauthorized()
@resource('/v1/superuser/customcerts/<certpath>')
@internal_only
@show_if(features.SUPER_USERS)
class SuperUserCustomCertificate(ApiResource):
""" Resource for managing a custom certificate. """
@nickname('uploadCustomCertificate')
@require_fresh_login
@require_scope(scopes.SUPERUSER)
@verify_not_prod
def post(self, certpath):
if SuperUserPermission().can():
uploaded_file = request.files['file']
if not uploaded_file:
raise InvalidRequest('Missing certificate file')
# Save the certificate.
certpath = pathvalidate.sanitize_filename(certpath)
if not certpath.endswith('.crt'):
raise InvalidRequest('Invalid certificate file: must have suffix `.crt`')
logger.debug('Saving custom certificate %s', certpath)
cert_full_path = config_provider.get_volume_path(EXTRA_CA_DIRECTORY, certpath)
config_provider.save_volume_file(uploaded_file, cert_full_path)
logger.debug('Saved custom certificate %s', certpath)
# Validate the certificate.
try:
logger.debug('Loading custom certificate %s', certpath)
with config_provider.get_volume_file(cert_full_path) as f:
load_certificate(f.read())
except CertInvalidException:
logger.exception('Got certificate invalid error for cert %s', certpath)
return '', 204
except IOError:
logger.exception('Got IO error for cert %s', certpath)
return '', 204
# Call the update script to install the certificate immediately.
if not app.config['TESTING']:
logger.debug('Calling certs_install.sh')
if os.system(os.path.join(INIT_SCRIPTS_LOCATION, 'certs_install.sh')) != 0:
raise Exception('Could not install certificates')
logger.debug('certs_install.sh completed')
return '', 204
raise Unauthorized()
@nickname('deleteCustomCertificate')
@require_fresh_login
@require_scope(scopes.SUPERUSER)
@verify_not_prod
def delete(self, certpath):
if SuperUserPermission().can():
cert_full_path = config_provider.get_volume_path(EXTRA_CA_DIRECTORY, certpath)
config_provider.remove_volume_file(cert_full_path)
return '', 204
raise Unauthorized()
@resource('/v1/superuser/<build_uuid>/logs')
@path_param('build_uuid', 'The UUID of the build')
@show_if(features.SUPER_USERS)

View file

@ -1267,11 +1267,6 @@ SECURITY_TESTS = [
(SuperUserList, 'POST', None, {'username': 'foo'}, 'freshuser', 403),
(SuperUserList, 'POST', None, {'username': 'foo'}, 'reader', 403),
(SuperUserCustomCertificates, 'GET', None, None, None, 401),
(SuperUserCustomCertificates, 'GET', None, None, 'devtable', 200),
(SuperUserCustomCertificates, 'GET', None, None, 'freshuser', 403),
(SuperUserCustomCertificates, 'GET', None, None, 'reader', 403),
(SuperUserSystemLogServices, 'GET', None, None, None, 401),
(SuperUserSystemLogServices, 'GET', None, None, 'devtable', 200),
(SuperUserSystemLogServices, 'GET', None, None, 'freshuser', 403),
@ -1282,15 +1277,6 @@ SECURITY_TESTS = [
(SuperUserGetLogsForService, 'GET', {'service': 'foo'}, None, 'freshuser', 403),
(SuperUserGetLogsForService, 'GET', {'service': 'foo'}, None, 'reader', 403),
(SuperUserCustomCertificate, 'DELETE', {'certpath': 'somecert.crt'}, None, None, 401),
(SuperUserCustomCertificate, 'DELETE', {'certpath': 'somecert.crt'}, None, 'devtable', 204),
(SuperUserCustomCertificate, 'DELETE', {'certpath': 'somecert.crt'}, None, 'freshuser', 403),
(SuperUserCustomCertificate, 'DELETE', {'certpath': 'somecert.crt'}, None, 'reader', 403),
(SuperUserCustomCertificate, 'POST', {'certpath': 'somecert.crt'}, None, None, 401),
(SuperUserCustomCertificate, 'POST', {'certpath': 'somecert.crt'}, None, 'devtable', 400),
(SuperUserCustomCertificate, 'POST', {'certpath': 'somecert.crt'}, None, 'freshuser', 403),
(SuperUserCustomCertificate, 'POST', {'certpath': 'somecert.crt'}, None, 'reader', 403),
(SuperUserManagement, 'DELETE', {'username': 'freshuser'}, None, None, 401),
(SuperUserManagement, 'DELETE', {'username': 'freshuser'}, None, 'devtable', 204),
(SuperUserManagement, 'DELETE', {'username': 'freshuser'}, None, 'freshuser', 403),

View file

@ -9,12 +9,13 @@ from flask_principal import identity_changed
import endpoints.decorated # Register the various exceptions via decorators.
import features
from app import app, oauth_apps, oauth_login, LoginWrappedDBUser, user_analytics
from app import app, oauth_apps, oauth_login, LoginWrappedDBUser, user_analytics, IS_KUBERNETES
from auth import scopes
from auth.permissions import QuayDeferredPermissionUser
from config import frontend_visible_config
from external_libraries import get_external_javascript, get_external_css
from endpoints.common_models_pre_oci import pre_oci_model as model
from util.config.provider.k8sprovider import QE_NAMESPACE
from util.secscan import PRIORITY_LEVELS
from util.saas.useranalytics import build_error_callback
from util.timedeltastring import convert_to_timedelta
@ -143,6 +144,7 @@ def render_page_template(name, route_data=None, **kwargs):
preferred_scheme=app.config['PREFERRED_URL_SCHEME'],
version_number=version_number,
current_year=datetime.datetime.now().year,
kubernetes_namespace=IS_KUBERNETES and QE_NAMESPACE,
**kwargs)
resp = make_response(contents)

View file

@ -1,8 +0,0 @@
<div class="config-bool-field-element">
<form name="fieldform" novalidate>
<label>
<input type="checkbox" ng-model="binding">
<span ng-transclude/>
</label>
</form>
</div>

View file

@ -1,76 +0,0 @@
<div class="config-certificates-field-element">
<div class="resource-view" resource="certificatesResource" error-message="'Could not load certificates list'">
<!-- File -->
<div class="co-alert co-alert-warning" ng-if="certInfo.status == 'file'">
<code>extra_ca_certs</code> is a single file and cannot be processed by this tool. If a valid and appended list of certificates, they will be installed on container startup.
</div>
<div ng-if="certInfo.status != 'file'">
<div class="description">
<p>This section lists any custom or self-signed SSL certificates that are installed in the <span class="registry-name"></span> container on startup after being read from the <code>extra_ca_certs</code> directory in the configuration volume.
</p>
<p>
Custom certificates are typically used in place of publicly signed certificates for corporate-internal services.
</p>
<p>Please <strong>make sure</strong> that all custom names used for downstream services (such as Clair) are listed in the certificates below.</p>
</div>
<table class="config-table" style="margin-bottom: 20px;">
<tr>
<td>Upload certificates:</td>
<td>
<div class="file-upload-box"
select-message="Select custom certificate to add to configuration. Must be in PEM format and end extension '.crt'"
files-selected="handleCertsSelected(files, callback)"
reset="resetUpload"
extensions="['.crt']"></div>
</td>
</tr>
</table>
<table class="co-table">
<thead>
<td>Certificate Filename</td>
<td>Status</td>
<td>Names Handled</td>
<td class="options-col"></td>
</thead>
<tr ng-repeat="certificate in certInfo.certs" ng-if="!certsUploading">
<td>{{ certificate.path }}</td>
<td class="cert-status">
<div ng-if="certificate.error" class="red">
<i class="fa fa-exclamation-circle"></i>
Error: {{ certificate.error }}
</div>
<div ng-if="certificate.expired" class="orange">
<i class="fa fa-exclamation-triangle"></i>
Certificate is expired
</div>
<div ng-if="!certificate.error && !certificate.expired" class="green">
<i class="fa fa-check-circle"></i>
Certificate is valid
</div>
</td>
<td>
<div class="empty" ng-if="!certificate.names">(None)</div>
<a class="dns-name" ng-href="http://{{ name }}" ng-repeat="name in certificate.names" ng-safenewtab>{{ name }}</a>
</td>
<td class="options-col">
<span class="cor-options-menu">
<span class="cor-option" option-click="deleteCert(certificate.path)">
<i class="fa fa-times"></i> Delete Certificate
</span>
</span>
</td>
</tr>
</table>
<div ng-if="certsUploading" style="margin-top: 20px; text-align: center;">
<div class="cor-loader-inline"></div>
Uploading, validating and updating certificate(s)
</div>
<div class="empty" ng-if="!certInfo.certs.length && !certsUploading" style="margin-top: 20px;">
<div class="empty-primary-msg">No custom certificates found.</div>
</div>
</div>
</div>
</div>

View file

@ -1,46 +0,0 @@
<div class="config-contact-field-element">
<table>
<tr>
<td>
<div class="dropdown">
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
<span ng-switch="kind">
<span ng-switch-when="mailto"><i class="fa fa-envelope"></i>E-mail</span>
<span ng-switch-when="irc"><i class="fa fa-comment"></i>IRC</span>
<span ng-switch-when="tel"><i class="fa fa-phone"></i>Phone</span>
<span ng-switch-default><i class="fa fa-ticket"></i>URL</span>
</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li role="presentation">
<a role="menuitem" tabindex="-1" ng-click="kind = 'mailto'">
<i class="fa fa-envelope"></i> E-mail
</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="-1" ng-click="kind = 'irc'">
<i class="fa fa-comment"></i> IRC
</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="-1" ng-click="kind = 'tel'">
<i class="fa fa-phone"></i> Telephone
</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="-1" ng-click="kind = 'http'">
<i class="fa fa-ticket"></i> URL
</a>
</li>
</ul>
</div>
</td>
<td>
<form>
<input class="form-control" placeholder="{{ getPlaceholder(kind) }}" ng-model="value">
</form>
</td>
</tr>
</table>
</div>

View file

@ -1,4 +0,0 @@
<div class="config-contacts-field-element">
<div class="config-contact-field" binding="item.value" ng-repeat="item in items">
</div>
</div>

View file

@ -1,13 +0,0 @@
<div class="config-file-field-element">
<span ng-show="uploadProgress == null">
<span ng-if="hasFile">
<code>/conf/stack/{{ filename }}</code>
<span style="margin-left: 20px; display: inline-block;">Select a replacement file:</span>
</span>
<span class="nofile" ng-if="!hasFile && skipCheckFile != 'true'">Please select a file to upload as <b>{{ filename }}</b>: </span>
<input type="file" ng-file-select="onFileSelect($files)">
</span>
<span ng-show="uploadProgress != null">
Uploading file as <strong>{{ filename }}</strong>... {{ uploadProgress }}%
</span>
</div>

View file

@ -1,17 +0,0 @@
<div class="config-list-field-element">
<ul ng-show="binding && binding.length">
<li class="item" ng-repeat="item in binding">
<span class="item-title">{{ item }}</span>
<span class="item-delete">
<a ng-click="removeItem(item)">Remove</a>
</span>
</li>
</ul>
<span class="empty" ng-if="!binding || binding.length == 0">No {{ itemTitle }}s defined</span>
<form class="form-control-container" ng-submit="addItem()">
<input type="text" class="form-control" placeholder="{{ placeholder }}"
ng-pattern="getRegexp(itemPattern)"
ng-model="newItemName" style="display: inline-block">
<button class="btn btn-default" style="display: inline-block">Add</button>
</form>
</div>

View file

@ -1,20 +0,0 @@
<div class="config-map-field-element">
<table class="table" ng-show="hasValues(binding)">
<tr class="item" ng-repeat="(key, value) in binding">
<td class="item-title">{{ key }}</td>
<td class="item-value">{{ value }}</td>
<td class="item-delete">
<a ng-click="removeKey(key)">Remove</a>
</td>
</tr>
</table>
<span class="empty" ng-if="!hasValues(binding)">No entries defined</span>
<form class="form-control-container" ng-submit="addEntry()">
Add Key-Value:
<select ng-model="newKey">
<option ng-repeat="key in keys" value="{{ key }}">{{ key }}</option>
</select>
<input type="text" class="form-control" placeholder="Value" ng-model="newValue">
<button class="btn btn-default" style="display: inline-block">Add Entry</button>
</form>
</div>

View file

@ -1,6 +0,0 @@
<div class="config-numeric-field-element">
<form name="fieldform" novalidate>
<input type="number" class="form-control" placeholder="{{ placeholder || '' }}"
ng-model="bindinginternal" ng-trim="false" ng-minlength="1" required>
</form>
</div>

View file

@ -1 +0,0 @@
<div class="config-parsed-field-element"></div>

View file

@ -1,29 +0,0 @@
<div class="config-service-key-field-element">
<!-- Loading -->
<div class="cor-loader" ng-if="loading"></div>
<!-- Loading error -->
<div class="co-alert co-alert-warning" ng-if="loadError">
Could not load service keys
</div>
<!-- Key config -->
<div ng-show="!loading && !loadError">
<div ng-show="hasValidKey">
<i class="fa fa-check"></i>
Valid key for service <code>{{ serviceName }}</code> exists
</div>
<div ng-show="!hasValidKey">
No valid key found for service <code>{{ serviceName }}</code>
<a class="co-modify-link" ng-click="showRequestServiceKey()">Create Key</a>
</div>
</div>
<!-- Note: This field is a hidden text field that binds to a model that is set to non-empty
when there is a valid key. This allows us to use the existing Angular form validation
code.
-->
<input type="text" ng-model="hasValidKeyStr" ng-required="true" style="position: absolute; top: 0px; left: 0px; visibility: hidden; width: 0px; height: 0px;">
<div class="request-service-key-dialog" request-key-info="requestKeyInfo" key-created="handleKeyCreated(key)"></div>
</div>

File diff suppressed because it is too large Load diff

View file

@ -1,10 +0,0 @@
<div class="config-string-field-element">
<form name="fieldform" novalidate>
<input type="text" class="form-control" placeholder="{{ placeholder || '' }}"
ng-model="binding" ng-trim="false" ng-minlength="1"
ng-pattern="getRegexp(pattern)" ng-required="!isOptional">
<div class="alert alert-danger" ng-show="errorMessage">
{{ errorMessage }}
</div>
</form>
</div>

View file

@ -1,6 +0,0 @@
<div class="config-string-list-field-element">
<form name="fieldform" novalidate>
<input type="text" class="form-control" placeholder="{{ placeholder || '' }}"
ng-model="internalBinding" ng-trim="true" ng-minlength="1" ng-required="!isOptional">
</form>
</div>

View file

@ -1,10 +0,0 @@
<div class="config-variable-field-element">
<div class="btn-group">
<button type="button" class="btn btn-default"
ng-repeat="section in sections"
ng-click="setSection(section)"
ng-class="section == currentSection ? 'active' : ''">{{ section.title }}</button>
</div>
<span ng-transclude></span>
</div>

View file

@ -1,137 +0,0 @@
<div class="request-service-key-dialog-element">
<!-- Modal message dialog -->
<div class="co-dialog modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true" ng-show="!working">&times;</button>
<h4 class="modal-title">Create key for service {{ requestKeyInfo.service }}</h4>
</div>
<div class="modal-body" ng-show="working">
<div class="cor-loader"></div>
</div>
<div class="modal-body" ng-show="!working">
<!-- Step 0 -->
<div ng-show="step == 0">
<table class="co-option-table">
<tr>
<td><input type="radio" id="automaticKey" ng-model="requestKind" value="automatic"></td>
<td>
<label for="automaticKey">Have the service provide a key</label>
<div class="help-text">Recommended for <code>{{ requestKeyInfo.service }}</code> installations where the single instance is setup now.</div>
</td>
</tr>
<tr>
<td><input type="radio" id="presharedKey" ng-model="requestKind" value="preshared"></td>
<td>
<label for="presharedKey">Generate shared key</label>
<div class="help-text">Recommended for <code>{{ requestKeyInfo.service }}</code> installations where the instances are dynamically started.</div>
</td>
</tr>
</table>
</div>
<!-- Step 1 (automatic) -->
<div ng-show="step == 1 && requestKind == 'automatic'" style="text-align: center">
<div style="margin-top: 20px;">
Please start the <code>{{ requestKeyInfo.service }}</code> service now, configured for <a href="https://github.com/coreos/jwtproxy#autogenerated-private-key" ng-safenewtab>autogenerated private key</a>. The key approval process will continue automatically once the service connects to Quay.
</div>
<div style="margin-top: 20px;">
Waiting for service to connect
</div>
<div style="margin-top: 10px; margin-bottom: 20px;">
<div class="cor-loader-inline"></div>
</div>
</div>
<!-- Step 2 (automatic) -->
<div ng-show="step == 2 && requestKind == 'automatic'" style="text-align: center">
A key for service <code>{{ requestKeyInfo.service }}</code> has been automatically generated, approved and saved in the service's keystore.
</div>
<!-- Step 1 (generate) -->
<div ng-show="step == 1 && requestKind == 'preshared'">
<form name="createForm" ng-submit="createPresharedKey()">
<table class="co-form-table">
<tr>
<td><label for="create-key-name">Key Name:</label></td>
<td>
<input class="form-control" name="create-key-name" type="text" ng-model="preshared.name" placeholder="Friendly Key Name">
<span class="co-help-text">
A friendly name for the key for later reference.
</span>
</td>
</tr>
<tr>
<td><label for="create-key-expiration">Expiration date (optional):</label></td>
<td>
<span class="datetime-picker" datetime="preshared.expiration"></span>
<span class="co-help-text">
The date and time that the key expires. If left blank, the key will never expire.
</span>
</td>
</tr>
<tr>
<td><label for="create-key-notes">Approval Notes (optional):</label></td>
<td>
<markdown-input content="preshared.notes"
can-write="true"
(content-changed)="updateNotes($event.content)"
field-title="notes"></markdown-input>
<span class="co-help-text">
Optional notes for additional human-readable information about why the key was created.
</span>
</td>
</tr>
</table>
</form>
</div>
<!-- Step 2 (generate) -->
<div ng-show="step == 2 && requestKind == 'preshared'">
<div class="co-alert co-alert-warning">
The following key has been generated for service <code>{{ requestKeyInfo.service }}</code>.
<br><br>
Please copy the key's ID and copy/download the key's private contents and place it in the directory with the service's configuration.
<br><br>
<strong>Once this dialog is closed this private key will not be accessible anywhere else!</strong>
</div>
<label>Key ID:</label>
<div class="copy-box" value="createdKey.kid"></div>
<label>Private Key (PEM):</label>
<textarea class="key-display form-control" onclick="this.focus();this.select()" readonly>{{ createdKey.private_key }}</textarea>
</div>
</div>
<div class="modal-footer" ng-show="!working">
<button type="button" class="btn btn-primary" ng-show="step == 1 && requestKind == 'preshared'"
ng-disabled="createForm.$invalid"
ng-click="createPresharedKey()">
Generate Key
</button>
<button type="button" class="btn btn-primary" ng-show="step == 0 && requestKind == 'preshared'"
ng-click="showGenerate()">
Continue
</button>
<button type="button" class="btn btn-primary" ng-show="step == 0 && requestKind == 'automatic'"
ng-click="startApproval()">
Start Approval
</button>
<button type="button" class="btn btn-primary" ng-click="downloadPrivateKey(createdKey)" ng-if="createdKey && isDownloadSupported()">
<i class="fa fa-download"></i> Download Private Key
</button>
<button type="button" class="btn btn-default" data-dismiss="modal" ng-show="step == 2">Close</button>
<button type="button" class="btn btn-default" data-dismiss="modal" ng-show="step != 2">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>
</div>

File diff suppressed because it is too large Load diff

View file

@ -318,39 +318,6 @@ angular.module("core-ui", [])
return directiveDefinitionObject;
})
.directive('corStepBar', function() {
var directiveDefinitionObject = {
priority: 4,
templateUrl: '/static/directives/cor-step-bar.html',
replace: true,
transclude: true,
restrict: 'C',
scope: {
'progress': '=progress'
},
controller: function($rootScope, $scope, $element) {
$scope.$watch('progress', function(progress) {
if (!progress) { return; }
var index = 0;
for (var i = 0; i < progress.length; ++i) {
if (progress[i]) {
index = i;
}
}
$element.find('.transclude').children('.co-step-element').each(function(i, elem) {
$(elem).removeClass('active');
if (i <= index) {
$(elem).addClass('active');
}
});
});
}
};
return directiveDefinitionObject;
})
.directive('corCheckableMenu', function() {
var directiveDefinitionObject = {
priority: 1,

View file

@ -1,124 +0,0 @@
/**
* An element which displays a dialog for requesting or creating a service key.
*/
angular.module('quay').directive('requestServiceKeyDialog', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/request-service-key-dialog.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'requestKeyInfo': '=requestKeyInfo',
'keyCreated': '&keyCreated'
},
controller: function($scope, $element, $timeout, ApiService) {
var handleNewKey = function(key) {
var data = {
'notes': 'Approved during setup of service ' + key.service
};
var params = {
'kid': key.kid
};
ApiService.approveServiceKey(data, params).then(function(resp) {
$scope.keyCreated({'key': key});
$scope.step = 2;
}, ApiService.errorDisplay('Could not approve service key'));
};
var checkKeys = function() {
var isShown = ($element.find('.modal').data('bs.modal') || {}).isShown;
if (!isShown) {
return;
}
// TODO: filter by service.
ApiService.listServiceKeys().then(function(resp) {
var keys = resp['keys'];
for (var i = 0; i < keys.length; ++i) {
var key = keys[i];
if (key.service == $scope.requestKeyInfo.service && !key.approval && key.rotation_duration) {
handleNewKey(key);
return;
}
}
$timeout(checkKeys, 1000);
}, ApiService.errorDisplay('Could not list service keys'));
};
$scope.show = function() {
$scope.working = false;
$scope.step = 0;
$scope.requestKind = null;
$scope.preshared = {
'name': $scope.requestKeyInfo.service + ' Service Key',
'notes': 'Created during setup for service `' + $scope.requestKeyInfo.service + '`'
};
$element.find('.modal').modal({});
};
$scope.hide = function() {
$scope.loading = false;
$element.find('.modal').modal('hide');
};
$scope.showGenerate = function() {
$scope.step = 1;
};
$scope.startApproval = function() {
$scope.step = 1;
checkKeys();
};
$scope.isDownloadSupported = function() {
var isSafari = /^((?!chrome).)*safari/i.test(navigator.userAgent);
if (isSafari) {
// Doesn't work properly in Safari, sadly.
return false;
}
try { return !!new Blob(); } catch(e) {}
return false;
};
$scope.downloadPrivateKey = function(key) {
var blob = new Blob([key.private_key]);
FileSaver.saveAs(blob, key.service + '.pem');
};
$scope.createPresharedKey = function() {
$scope.working = true;
var data = {
'name': $scope.preshared.name,
'service': $scope.requestKeyInfo.service,
'expiration': $scope.preshared.expiration || null,
'notes': $scope.preshared.notes
};
ApiService.createServiceKey(data).then(function(resp) {
$scope.working = false;
$scope.step = 2;
$scope.createdKey = resp;
$scope.keyCreated({'key': resp});
}, ApiService.errorDisplay('Could not create service key'));
};
$scope.updateNotes = function(content) {
$scope.preshared.notes = content;
};
$scope.$watch('requestKeyInfo', function(info) {
if (info && info.service) {
$scope.show();
}
});
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,82 @@
(function() {
/**
* The Incomplete Setup page provides information to the user about what's wrong with the current configuration
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('incomplete-setup', 'incomplete-setup.html', IncompleteSetupCtrl,
{
'newLayout': true,
'title': 'Quay Enterprise Setup Incomplete'
})
}]);
function IncompleteSetupCtrl($scope, $timeout, ApiService, Features, UserService, ContainerService, CoreDialog) {
if (!Features.SUPER_USERS) {
return;
}
$scope.States = {
// Loading the state of the product.
'LOADING': 'loading',
// The configuration directory is missing.
'MISSING_CONFIG_DIR': 'missing-config-dir',
// The config.yaml exists but it is invalid.
'INVALID_CONFIG': 'config-invalid',
};
$scope.currentStep = $scope.States.LOADING;
$scope.$watch('currentStep', function(currentStep) {
switch (currentStep) {
case $scope.States.MISSING_CONFIG_DIR:
$scope.showMissingConfigDialog();
break;
case $scope.States.INVALID_CONFIG:
$scope.showInvalidConfigDialog();
break;
}
});
$scope.showInvalidConfigDialog = function() {
var message = "The <code>config.yaml</code> file found in <code>conf/stack</code> could not be parsed."
var title = "Invalid configuration file";
CoreDialog.fatal(title, message);
};
$scope.showMissingConfigDialog = function() {
var title = "Missing configuration volume";
var message = "It looks like Quay was not mounted with a configuration volume. The volume should be " +
"mounted into the container at <code>/conf/stack</code>. " +
"<br>If you have a tarball, please ensure you untar it into a directory and re-run this container with: " +
"<br><br><pre>docker run -v /path/to/config:/conf/stack</pre>" +
"<br>If you haven't configured your Quay instance, please run the container with: " +
"<br><br><pre>docker run &lt;name-of-image&gt; config </pre>" +
"For more information, " +
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
"Read the Setup Guide</a>";
if (window.__kubernetes_namespace) {
title = "Configuration Secret Missing";
message = `It looks like the Quay Enterprise secret is not present in the namespace <code>${window.__kubernetes_namespace}.</code>` +
"<br>Please double-check that the secret exists, or " +
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
"refer to the Setup Guide</a>";
}
CoreDialog.fatal(title, message);
};
$scope.checkStatus = function() {
ContainerService.checkStatus(function(resp) {
$scope.currentStep = resp['status'];
}, $scope.currentConfig);
};
// Load the initial status.
$scope.checkStatus();
};
})();

View file

@ -15,7 +15,7 @@
$scope.userRegistered = false;
if (!Config['SETUP_COMPLETE'] && !Features.BILLING) {
$location.path('/setup');
$location.path('/incomplete-setup');
return;
}

View file

@ -1,325 +0,0 @@
import * as URI from 'urijs';
(function() {
/**
* The Setup page provides a nice GUI walkthrough experience for setting up Quay Enterprise.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('setup', 'setup.html', SetupCtrl,
{
'newLayout': true,
'title': 'Quay Enterprise Setup'
})
}]);
function SetupCtrl($scope, $timeout, ApiService, Features, UserService, ContainerService, CoreDialog) {
if (!Features.SUPER_USERS) {
return;
}
$scope.HOSTNAME_REGEX = '^[a-zA-Z-0-9_\.\-]+(:[0-9]+)?$';
$scope.validateHostname = function(hostname) {
if (hostname.indexOf('127.0.0.1') == 0 || hostname.indexOf('localhost') == 0) {
return 'Please specify a non-localhost hostname. "localhost" will refer to the container, not your machine.'
}
return null;
};
// Note: The values of the enumeration are important for isStepFamily. For example,
// *all* states under the "configuring db" family must start with "config-db".
$scope.States = {
// Loading the state of the product.
'LOADING': 'loading',
// The configuration directory is missing.
'MISSING_CONFIG_DIR': 'missing-config-dir',
// The config.yaml exists but it is invalid.
'INVALID_CONFIG': 'config-invalid',
// DB is being configured.
'CONFIG_DB': 'config-db',
// DB information is being validated.
'VALIDATING_DB': 'config-db-validating',
// DB information is being saved to the config.
'SAVING_DB': 'config-db-saving',
// A validation error occurred with the database.
'DB_ERROR': 'config-db-error',
// Database is being setup.
'DB_SETUP': 'setup-db',
// Database setup has succeeded.
'DB_SETUP_SUCCESS': 'setup-db-success',
// An error occurred when setting up the database.
'DB_SETUP_ERROR': 'setup-db-error',
// The container is being restarted for the database changes.
'DB_RESTARTING': 'setup-db-restarting',
// A superuser is being configured.
'CREATE_SUPERUSER': 'create-superuser',
// The superuser is being created.
'CREATING_SUPERUSER': 'create-superuser-creating',
// An error occurred when setting up the superuser.
'SUPERUSER_ERROR': 'create-superuser-error',
// The superuser was created successfully.
'SUPERUSER_CREATED': 'create-superuser-created',
// General configuration is being setup.
'CONFIG': 'config',
// The configuration is fully valid.
'VALID_CONFIG': 'valid-config',
// The container is being restarted for the configuration changes.
'CONFIG_RESTARTING': 'config-restarting',
// The product is ready for use.
'READY': 'ready'
}
$scope.csrf_token = window.__token;
$scope.currentStep = $scope.States.LOADING;
$scope.errors = {};
$scope.stepProgress = [];
$scope.hasSSL = false;
$scope.hostname = null;
$scope.currentConfig = null;
$scope.currentState = {
'hasDatabaseSSLCert': false
};
$scope.$watch('currentStep', function(currentStep) {
$scope.stepProgress = $scope.getProgress(currentStep);
switch (currentStep) {
case $scope.States.CONFIG:
$('#setupModal').modal('hide');
break;
case $scope.States.MISSING_CONFIG_DIR:
$scope.showMissingConfigDialog();
break;
case $scope.States.INVALID_CONFIG:
$scope.showInvalidConfigDialog();
break;
case $scope.States.DB_SETUP:
$scope.performDatabaseSetup();
// Fall-through.
case $scope.States.CREATE_SUPERUSER:
case $scope.States.DB_RESTARTING:
case $scope.States.CONFIG_DB:
case $scope.States.VALID_CONFIG:
case $scope.States.READY:
$('#setupModal').modal({
keyboard: false,
backdrop: 'static'
});
break;
}
});
$scope.restartContainer = function(state) {
$scope.currentStep = state;
ContainerService.restartContainer(function() {
$scope.checkStatus()
});
};
$scope.showSuperuserPanel = function() {
$('#setupModal').modal('hide');
var prefix = $scope.hasSSL ? 'https' : 'http';
var hostname = $scope.hostname;
if (!hostname) {
hostname = document.location.hostname;
if (document.location.port) {
hostname = hostname + ':' + document.location.port;
}
}
window.location = prefix + '://' + hostname + '/superuser';
};
$scope.configurationSaved = function(config) {
$scope.hasSSL = config['PREFERRED_URL_SCHEME'] == 'https';
$scope.hostname = config['SERVER_HOSTNAME'];
$scope.currentConfig = config;
$scope.currentStep = $scope.States.VALID_CONFIG;
};
$scope.getProgress = function(step) {
var isStep = $scope.isStep;
var isStepFamily = $scope.isStepFamily;
var States = $scope.States;
return [
isStepFamily(step, States.CONFIG_DB),
isStepFamily(step, States.DB_SETUP),
isStep(step, States.DB_RESTARTING),
isStepFamily(step, States.CREATE_SUPERUSER),
isStep(step, States.CONFIG),
isStep(step, States.VALID_CONFIG),
isStep(step, States.CONFIG_RESTARTING),
isStep(step, States.READY)
];
};
$scope.isStepFamily = function(step, family) {
if (!step) { return false; }
return step.indexOf(family) == 0;
};
$scope.isStep = function(step) {
for (var i = 1; i < arguments.length; ++i) {
if (arguments[i] == step) {
return true;
}
}
return false;
};
$scope.beginSetup = function() {
$scope.currentStep = $scope.States.CONFIG_DB;
};
$scope.showInvalidConfigDialog = function() {
var message = "The <code>config.yaml</code> file found in <code>conf/stack</code> could not be parsed."
var title = "Invalid configuration file";
CoreDialog.fatal(title, message);
};
$scope.showMissingConfigDialog = function() {
var message = "It looks like Quay was not mounted with a configuration volume. The volume should be " +
"mounted into the container at <code>/conf/stack</code>. " +
"<br>If you have a tarball, please ensure you untar it into a directory and re-run this container with: " +
"<br><br><pre>docker run -v /path/to/config:/conf/stack</pre>" +
"<br>If you haven't configured your Quay instance, please run the container with: " +
"<br><br><pre>docker run &lt;name-of-image&gt; config </pre>" +
"For more information, " +
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
"Read the Setup Guide</a>";
var title = "Missing configuration volume";
CoreDialog.fatal(title, message);
};
$scope.parseDbUri = function(value) {
if (!value) { return null; }
// Format: mysql+pymysql://<username>:<url escaped password>@<hostname>/<database_name>
var uri = URI(value);
return {
'kind': uri.protocol(),
'username': uri.username(),
'password': uri.password(),
'server': uri.host(),
'database': uri.path() ? uri.path().substr(1) : ''
};
};
$scope.serializeDbUri = function(fields) {
if (!fields['server']) { return ''; }
if (!fields['database']) { return ''; }
var uri = URI();
try {
uri = uri && uri.host(fields['server']);
uri = uri && uri.protocol(fields['kind']);
uri = uri && uri.username(fields['username']);
uri = uri && uri.password(fields['password']);
uri = uri && uri.path('/' + (fields['database'] || ''));
uri = uri && uri.toString();
} catch (ex) {
return '';
}
return uri;
};
$scope.createSuperUser = function() {
$scope.currentStep = $scope.States.CREATING_SUPERUSER;
ApiService.scCreateInitialSuperuser($scope.superUser, null).then(function(resp) {
UserService.load();
$scope.checkStatus();
}, function(resp) {
$scope.currentStep = $scope.States.SUPERUSER_ERROR;
$scope.errors.SuperuserCreationError = ApiService.getErrorMessage(resp, 'Could not create superuser');
});
};
$scope.performDatabaseSetup = function() {
$scope.currentStep = $scope.States.DB_SETUP;
ApiService.scSetupDatabase(null, null).then(function(resp) {
if (resp['error']) {
$scope.currentStep = $scope.States.DB_SETUP_ERROR;
$scope.errors.DatabaseSetupError = resp['error'];
} else {
$scope.currentStep = $scope.States.DB_SETUP_SUCCESS;
}
}, ApiService.errorDisplay('Could not setup database. Please report this to support.'))
};
$scope.validateDatabase = function() {
$scope.currentStep = $scope.States.VALIDATING_DB;
$scope.databaseInvalid = null;
var data = {
'config': {
'DB_URI': $scope.databaseUri
},
'hostname': window.location.host
};
if ($scope.currentState.hasDatabaseSSLCert) {
data['config']['DB_CONNECTION_ARGS'] = {
'ssl': {
'ca': 'conf/stack/database.pem'
}
};
}
var params = {
'service': 'database'
};
ApiService.scValidateConfig(data, params).then(function(resp) {
var status = resp.status;
if (status) {
$scope.currentStep = $scope.States.SAVING_DB;
ApiService.scUpdateConfig(data, null).then(function(resp) {
$scope.checkStatus();
}, ApiService.errorDisplay('Cannot update config. Please report this to support'));
} else {
$scope.currentStep = $scope.States.DB_ERROR;
$scope.errors.DatabaseValidationError = resp.reason;
}
}, ApiService.errorDisplay('Cannot validate database. Please report this to support'));
};
$scope.checkStatus = function() {
ContainerService.checkStatus(function(resp) {
$scope.currentStep = resp['status'];
}, $scope.currentConfig);
};
// Load the initial status.
$scope.checkStatus();
};
})();

View file

@ -18,7 +18,6 @@ var quayDependencies: string[] = [
'pasvaz.bindonce',
'ansiToHtml',
'core-ui',
'core-config-setup',
'infinite-scroll',
'ngTagsInput',
];

View file

@ -49,8 +49,7 @@ function provideRoutes($routeProvider: ng.route.IRouteProvider,
if (INJECTED_FEATURES.SUPER_USERS) {
// QE Management
routeBuilder.route('/superuser/', 'superuser')
// QE Setup
.route('/setup/', 'setup');
.route('/incomplete-setup/', 'incomplete-setup');
}
routeBuilder

View file

@ -0,0 +1,21 @@
<div>
<div class="cor-loader" ng-show="currentStep == States.LOADING"></div>
<div class="page-content" quay-show="Features.SUPER_USERS">
<div class="cor-title">
<span class="cor-title-link"></span>
<span class="cor-title-content">Quay Enterprise Setup Incomplete</span>
</div>
<div class="co-main-content-panel" style="padding: 20px;">
<div class="co-alert co-alert-warning">
<div><strong>Your configuration is not setup yet</strong></div>
<div>Please refer to
<a target="_blank" href="https://coreos.com/quay-enterprise/docs/latest/initial-setup.html">
the docs to set up your instance.
</a>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,307 +0,0 @@
<div>
<div class="cor-loader" ng-show="currentStep == States.LOADING"></div>
<div class="page-content" quay-show="Features.SUPER_USERS && currentStep == States.CONFIG">
<div class="cor-title">
<span class="cor-title-link"></span>
<span class="cor-title-content">Quay Enterprise Setup</span>
</div>
<div class="co-main-content-panel" style="padding: 20px;">
<div class="co-alert alert alert-info">
<span class="cor-step-bar" progress="stepProgress">
<span class="cor-step" title="Configure Database" text="1"></span>
<span class="cor-step" title="Setup Database" icon="database"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Create Superuser" text="2"></span>
<span class="cor-step" title="Configure Registry" text="3"></span>
<span class="cor-step" title="Validate Configuration" text="4"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Setup Complete" icon="check"></span>
</span>
<div><strong>Almost done!</strong></div>
<div>Configure your Redis database and other settings below</div>
</div>
<div class="config-setup-tool" is-active="isStep(currentStep, States.CONFIG)"
configuration-saved="configurationSaved(config)"></div>
</div>
</div>
</div>
<!-- Modal message dialog -->
<div class="co-dialog modal fade initial-setup-modal" id="setupModal">
<div class="modal-dialog">
<div class="modal-content">
<!-- Header -->
<div class="modal-header">
<span class="cor-step-bar" progress="stepProgress">
<span class="cor-step" title="Configure Database" text="1"></span>
<span class="cor-step" title="Setup Database" icon="database"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Create Superuser" text="2"></span>
<span class="cor-step" title="Configure Registry" text="3"></span>
<span class="cor-step" title="Validate Configuration" text="4"></span>
<span class="cor-step" title="Container Restart" icon="refresh"></span>
<span class="cor-step" title="Setup Complete" icon="check"></span>
</span>
<h4 class="modal-title"><span><span class="registry-name" is-short="true"></span> Setup</h4>
</div>
<form id="superuserForm" name="superuserForm" ng-submit="createSuperUser()">
<!-- Content: CREATE_SUPERUSER or SUPERUSER_ERROR or CREATING_SUPERUSER -->
<div class="modal-body config-setup-tool-element" style="padding: 20px"
ng-show="isStep(currentStep, States.CREATE_SUPERUSER, States.SUPERUSER_ERROR, States.CREATING_SUPERUSER)">
<p>A superuser is the main administrator of your <span class="registry-name" is-short="true"></span>. Only superusers can edit configuration settings.</p>
<div class="form-group">
<label>Username</label>
<input class="form-control" type="text" ng-model="superUser.username"
ng-pattern="/^[a-z0-9_]{4,30}$/" required>
<div class="help-text">Minimum 4 characters in length</div>
</div>
<div class="form-group">
<label>Email address</label>
<input class="form-control" type="email" ng-model="superUser.email" required>
</div>
<div class="form-group">
<label>Password</label>
<input class="form-control" type="password" ng-model="superUser.password"
ng-pattern="/^[^\s]+$/"
ng-minlength="8" required>
<div class="help-text">Minimum 8 characters in length</div>
</div>
<div class="form-group">
<label>Repeat Password</label>
<input class="form-control" type="password" ng-model="superUser.repeatPassword"
match="superUser.password" required>
</div>
</div>
<!-- Footer: CREATE_SUPERUSER or SUPERUSER_ERROR -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.CREATE_SUPERUSER, States.SUPERUSER_ERROR)">
<button type="submit" class="btn btn-primary" ng-disabled="!superuserForm.$valid">
Create Super User
</button>
</div>
</form>
<!-- Content: DB_RESTARTING or CONFIG_RESTARTING -->
<div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.DB_RESTARTING, States.CONFIG_RESTARTING)">
<h4 style="margin-bottom: 20px;">
<i class="fa fa-lg fa-refresh" style="margin-right: 10px;"></i>
<span class="registry-name"></span> is currently being restarted
</h4>
This can take several minutes. If the container does not restart on its own,
please re-execute the <code>docker run</code> command.
</div>
<!-- Content: READY -->
<div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.READY)">
<h4>Installation and setup of <span class="registry-name"></span> is complete</h4>
You can now invite users to join, create organizations and start pushing and pulling
repositories.
<strong ng-if="hasSSL" style="margin-top: 20px;">
Note: SSL is enabled. Please make sure to visit with
an <u>https</u> prefix
</strong>
</div>
<!-- Content: VALID_CONFIG -->
<div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.VALID_CONFIG)">
<h4>All configuration has been validated and saved</h4>
The container must be restarted to apply the configuration changes.
</div>
<!-- Content: DB_SETUP_SUCCESS -->
<div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.DB_SETUP_SUCCESS)">
<h4>The database has been setup and is ready</h4>
The container must be restarted to apply the configuration changes.
</div>
<!-- Content: DB_SETUP or DB_SETUP_ERROR -->
<div class="modal-body" style="padding: 20px;"
ng-show="isStep(currentStep, States.DB_SETUP, States.DB_SETUP_ERROR)">
<h4>
<i class="fa fa-lg fa-database" style="margin-right: 10px;"></i>
<span class="registry-name"></span> is currently setting up its database
schema
</h4>
This can take several minutes.
</div>
<!-- Content: CONFIG_DB or DB_ERROR or VALIDATING_DB or SAVING_DB -->
<div class="modal-body validate-database config-setup-tool-element"
ng-show="isStep(currentStep, States.CONFIG_DB, States.DB_ERROR, States.VALIDATING_DB, States.SAVING_DB)">
<p>
Please enter the connection details for your <strong>empty</strong> database. The schema will be created in the following step.</p>
</p>
<div class="config-parsed-field" binding="databaseUri"
parser="parseDbUri(value)"
serializer="serializeDbUri(fields)">
<table class="config-table">
<tr>
<td class="non-input">Database Type:</td>
<td>
<select ng-model="fields.kind">
<option value="mysql+pymysql">MySQL</option>
<option value="postgresql">Postgres</option>
</select>
</td>
</tr>
<tr ng-show="fields.kind">
<td>Database Server:</td>
<td>
<span class="config-string-field" binding="fields.server"
placeholder="dbserverhost"
pattern="{{ HOSTNAME_REGEX }}"
validator="validateHostname(value)">></span>
<div class="help-text">
The server (and optionally, custom port) where the database lives
</div>
</td>
</tr>
<tr ng-show="fields.kind">
<td>Username:</td>
<td>
<span class="config-string-field" binding="fields.username"
placeholder="someuser"></span>
<div class="help-text">This user must have <strong>full access</strong> to the database</div>
</td>
</tr>
<tr ng-show="fields.kind">
<td>Password:</td>
<td>
<input class="form-control" type="password" ng-model="fields.password"></span>
</td>
</tr>
<tr ng-show="fields.kind">
<td>Database Name:</td>
<td>
<span class="config-string-field" binding="fields.database"
placeholder="registry-database"></span>
</td>
</tr>
<tr ng-show="fields.kind">
<td>SSL Certificate:</td>
<td>
<span class="config-file-field" filename="database.pem"
skip-check-file="true" has-file="currentState.hasDatabaseSSLCert"></span>
<div class="help-text">Optional SSL certicate (in PEM format) to use to connect to the database</div>
</td>
</tr>
</table>
</div>
</div>
<!-- Footer: CREATING_SUPERUSER -->
<div class="modal-footer working" ng-show="isStep(currentStep, States.CREATING_SUPERUSER)">
<span class="cor-loader-inline"></span> Creating superuser...
</div>
<!-- Footer: SUPERUSER_ERROR -->
<div class="modal-footer alert alert-warning"
ng-show="isStep(currentStep, States.SUPERUSER_ERROR)">
{{ errors.SuperuserCreationError }}
</div>
<!-- Footer: DB_SETUP_ERROR -->
<div class="modal-footer alert alert-warning"
ng-show="isStep(currentStep, States.DB_SETUP_ERROR)">
Database Setup Failed. Please report this to support: {{ errors.DatabaseSetupError }}
</div>
<!-- Footer: DB_ERROR -->
<div class="modal-footer alert alert-warning" ng-show="isStep(currentStep, States.DB_ERROR)">
Database Validation Issue: {{ errors.DatabaseValidationError }}
</div>
<!-- Footer: CONFIG_DB or DB_ERROR -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.CONFIG_DB, States.DB_ERROR)">
<span class="left-align" ng-show="isStep(currentStep, States.DB_ERROR)">
<i class="fa fa-warning"></i>
Problem Detected
</span>
<button type="submit" class="btn btn-primary"
ng-disabled="!databaseUri"
ng-click="validateDatabase()">
Validate Database Settings
</button>
</div>
<!-- Footer: READY -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.READY)">
<span class="left-align">
<i class="fa fa-check"></i>
Installation Complete!
</span>
<a ng-click="showSuperuserPanel()" class="btn btn-primary">
View Superuser Panel
</a>
</div>
<!-- Footer: VALID_CONFIG -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.VALID_CONFIG)">
<span class="left-align">
<i class="fa fa-check"></i>
Configuration Validated and Saved
</span>
<button type="submit" class="btn btn-primary"
ng-click="restartContainer(States.CONFIG_RESTARTING)">
Restart Container
</button>
</div>
<!-- Footer: DB_SETUP_SUCCESS -->
<div class="modal-footer"
ng-show="isStep(currentStep, States.DB_SETUP_SUCCESS)">
<span class="left-align">
<i class="fa fa-check"></i>
Database Setup and Ready
</span>
<button type="submit" class="btn btn-primary"
ng-click="restartContainer(States.DB_RESTARTING)">
Restart Container
</button>
</div>
<!-- Footer: DB_SETUP -->
<div class="modal-footer working" ng-show="isStep(currentStep, States.DB_SETUP)">
<span class="cor-loader-inline"></span> Setting up database...
</div>
<!-- Footer: SAVING_DB -->
<div class="modal-footer working" ng-show="isStep(currentStep, States.SAVING_DB)">
<span class="cor-loader-inline"></span> Saving database configuration...
</div>
<!-- Footer: VALIDATING_DB -->
<div class="modal-footer working" ng-show="isStep(currentStep, States.VALIDATING_DB)">
<span class="cor-loader-inline"></span> Testing database settings...
</div>
<!-- Footer: DB_RESTARTING or CONFIG_RESTARTING-->
<div class="modal-footer working"
ng-show="isStep(currentStep, States.DB_RESTARTING, States.CONFIG_RESTARTING)">
<span class="cor-loader-inline"></span> Waiting for container to restart...
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

View file

@ -46,10 +46,6 @@
tab-init="loadDebugServices()">
<i class="fa fa-bug"></i>
</cor-tab>
<cor-tab tab-title="Registry Settings" tab-id="setup"
tab-init="loadConfig()">
<i class="fa fa-cog"></i>
</cor-tab>
<cor-tab tab-title="Globally visible user messages" tab-id="message-of-the-day"
tab-init="loadMessageOfTheDay()">
<i class="fa fa-newspaper-o"></i>
@ -62,15 +58,6 @@
</cor-tabs>
<cor-tab-content>
<!-- Setup tab -->
<cor-tab-pane id="setup">
<div class="co-alert co-alert-warning" ng-if="configProviderId != 'k8s'">
<strong>Warning:</strong> If you are running Quay Enterprise under multiple containers, additional actions are necessary in order to apply any configuration changes made here to the entire cluster. It is recommended to make (and validate) configuration changes here, then copy your configuration to all instances and restart them.
</div>
<div class="config-setup-tool" is-active="configStatus == 'ready'"
configuration-saved="configurationSaved(config)"></div>
</cor-tab-pane>
<!-- Super user build logs tab-->
<cor-tab-pane id="super-user-build-logs">
<div class="super-user-build-logs" is-enabled="superUserBuildLogsActive"></div>

View file

@ -35,6 +35,7 @@
window.__auth_scopes = {{ scope_set|tojson|safe }};
window.__vuln_priority = {{ vuln_priority_set|tojson|safe }}
window.__token = '{{ csrf_token() }}';
window.__kubernetes_namespace = {{ kubernetes_namespace|tojson|safe }};
{% if error_code %}
window.__error_code = {{ error_code }};

View file

@ -8,7 +8,6 @@ import re
import json as py_json
from mock import patch
from StringIO import StringIO
from calendar import timegm
from contextlib import contextmanager
from httmock import urlmatch, HTTMock, all_requests
@ -18,7 +17,6 @@ from urlparse import urlparse, urlunparse, parse_qs
from playhouse.test_utils import assert_query_count, _QueryLogHandler
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
from mockldap import MockLdap
from endpoints.api import api_bp, api
from endpoints.building import PreparedBuild
@ -71,16 +69,12 @@ from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPe
RepositoryTeamPermissionList, RepositoryUserPermissionList)
from endpoints.api.superuser import (
SuperUserLogs, SuperUserManagement, SuperUserServiceKeyManagement,
SuperUserServiceKey, SuperUserServiceKeyApproval, SuperUserTakeOwnership,
SuperUserCustomCertificates, SuperUserCustomCertificate)
SuperUserServiceKey, SuperUserServiceKeyApproval, SuperUserTakeOwnership)
from endpoints.api.globalmessages import (
GlobalUserMessage,
GlobalUserMessages,)
from endpoints.api.secscan import RepositoryImageSecurity, RepositoryManifestSecurity
from endpoints.api.suconfig import (
SuperUserRegistryStatus, SuperUserConfig, SuperUserConfigFile, SuperUserCreateInitialSuperUser)
from endpoints.api.manifest import RepositoryManifestLabels, ManageRepositoryManifestLabel
from util.security.test.test_ssl_util import generate_test_cert
from util.morecollections import AttrDict
try:
@ -3985,227 +3979,6 @@ class TestSuperUserLogs(ApiTestCase):
assert len(json['logs']) > 0
class TestSuperUserCreateInitialSuperUser(ApiTestCase):
def test_create_superuser(self):
data = {
'username': 'newsuper',
'password': 'password',
'email': 'jschorr+fake@devtable.com',
}
# Try to write before some config. Should 403.
self.postResponse(SuperUserCreateInitialSuperUser, data=data, expected_code=403)
# Add some fake config.
fake_config = {
'AUTHENTICATION_TYPE': 'Database',
'SECRET_KEY': 'fakekey',
}
self.putJsonResponse(SuperUserConfig, data=dict(config=fake_config, hostname='fakehost'))
# Try to write with config. Should 403 since there are users in the DB.
self.postResponse(SuperUserCreateInitialSuperUser, data=data, expected_code=403)
# Delete all users in the DB.
for user in list(database.User.select()):
model.user.delete_user(user, all_queues)
# Create the superuser.
self.postJsonResponse(SuperUserCreateInitialSuperUser, data=data)
# Ensure the user exists in the DB.
self.assertIsNotNone(model.user.get_user('newsuper'))
# Ensure that the current user is newsuper.
json = self.getJsonResponse(User)
self.assertEquals('newsuper', json['username'])
# Ensure that the current user is a superuser in the config.
json = self.getJsonResponse(SuperUserConfig)
self.assertEquals(['newsuper'], json['config']['SUPER_USERS'])
# Ensure that the current user is a superuser in memory by trying to call an API
# that will fail otherwise.
self.getResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'))
class TestSuperUserConfig(ApiTestCase):
def test_get_status_update_config(self):
# With no config the status should be 'config-db'.
json = self.getJsonResponse(SuperUserRegistryStatus)
self.assertEquals('config-db', json['status'])
# And the config should 401.
self.getResponse(SuperUserConfig, expected_code=401)
# Add some fake config.
fake_config = {
'AUTHENTICATION_TYPE': 'Database',
'SECRET_KEY': 'fakekey',
}
json = self.putJsonResponse(SuperUserConfig, data=dict(config=fake_config,
hostname='fakehost'))
self.assertEquals('fakekey', json['config']['SECRET_KEY'])
self.assertEquals('fakehost', json['config']['SERVER_HOSTNAME'])
self.assertEquals('Database', json['config']['AUTHENTICATION_TYPE'])
# With config the status should be 'setup-db'.
json = self.getJsonResponse(SuperUserRegistryStatus)
self.assertEquals('setup-db', json['status'])
def test_config_file(self):
# Try without an account. Should 403.
self.getResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'), expected_code=403)
# Login to a superuser.
self.login(ADMIN_ACCESS_USER)
# Try for an invalid file. Should 404.
self.getResponse(SuperUserConfigFile, params=dict(filename='foobar'), expected_code=404)
# Try for a valid filename. Should not exist.
json = self.getJsonResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'))
self.assertFalse(json['exists'])
# Add the file.
self.postResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'),
file=(StringIO('my file contents'), 'ssl.cert'))
# Should now exist.
json = self.getJsonResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'))
self.assertTrue(json['exists'])
def test_update_with_external_auth(self):
self.login(ADMIN_ACCESS_USER)
# Run a mock LDAP.
mockldap = MockLdap({
'dc=quay,dc=io': {
'dc': ['quay', 'io']
},
'ou=employees,dc=quay,dc=io': {
'dc': ['quay', 'io'],
'ou': 'employees'
},
'uid=' + ADMIN_ACCESS_USER + ',ou=employees,dc=quay,dc=io': {
'dc': ['quay', 'io'],
'ou': 'employees',
'uid': [ADMIN_ACCESS_USER],
'userPassword': ['password'],
'mail': [ADMIN_ACCESS_EMAIL],
},
})
config = {
'AUTHENTICATION_TYPE': 'LDAP',
'LDAP_BASE_DN': ['dc=quay', 'dc=io'],
'LDAP_ADMIN_DN': 'uid=devtable,ou=employees,dc=quay,dc=io',
'LDAP_ADMIN_PASSWD': 'password',
'LDAP_USER_RDN': ['ou=employees'],
'LDAP_UID_ATTR': 'uid',
'LDAP_EMAIL_ATTR': 'mail',
}
mockldap.start()
try:
# Try writing some config with an invalid password.
self.putResponse(SuperUserConfig, data={'config': config,
'hostname': 'foo'}, expected_code=400)
self.putResponse(SuperUserConfig,
data={'config': config,
'password': 'invalid',
'hostname': 'foo'}, expected_code=400)
# Write the config with the valid password.
self.putResponse(SuperUserConfig,
data={'config': config,
'password': 'password',
'hostname': 'foo'}, expected_code=200)
# Ensure that the user row has been linked.
self.assertEquals(ADMIN_ACCESS_USER,
model.user.verify_federated_login('ldap', ADMIN_ACCESS_USER).username)
finally:
mockldap.stop()
class TestSuperUserCustomCertificates(ApiTestCase):
def test_custom_certificates(self):
self.login(ADMIN_ACCESS_USER)
# Upload a certificate.
cert_contents, _ = generate_test_cert(hostname='somecoolhost', san_list=['DNS:bar', 'DNS:baz'])
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert.crt'),
file=(StringIO(cert_contents), 'testcert.crt'), expected_code=204)
# Make sure it is present.
json = self.getJsonResponse(SuperUserCustomCertificates)
self.assertEquals(1, len(json['certs']))
cert_info = json['certs'][0]
self.assertEquals('testcert.crt', cert_info['path'])
self.assertEquals(set(['somecoolhost', 'bar', 'baz']), set(cert_info['names']))
self.assertFalse(cert_info['expired'])
# Remove the certificate.
self.deleteResponse(SuperUserCustomCertificate, params=dict(certpath='testcert.crt'))
# Make sure it is gone.
json = self.getJsonResponse(SuperUserCustomCertificates)
self.assertEquals(0, len(json['certs']))
def test_expired_custom_certificate(self):
self.login(ADMIN_ACCESS_USER)
# Upload a certificate.
cert_contents, _ = generate_test_cert(hostname='somecoolhost', expires=-10)
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert.crt'),
file=(StringIO(cert_contents), 'testcert.crt'), expected_code=204)
# Make sure it is present.
json = self.getJsonResponse(SuperUserCustomCertificates)
self.assertEquals(1, len(json['certs']))
cert_info = json['certs'][0]
self.assertEquals('testcert.crt', cert_info['path'])
self.assertEquals(set(['somecoolhost']), set(cert_info['names']))
self.assertTrue(cert_info['expired'])
def test_invalid_custom_certificate(self):
self.login(ADMIN_ACCESS_USER)
# Upload an invalid certificate.
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert.crt'),
file=(StringIO('some contents'), 'testcert.crt'), expected_code=204)
# Make sure it is present but invalid.
json = self.getJsonResponse(SuperUserCustomCertificates)
self.assertEquals(1, len(json['certs']))
cert_info = json['certs'][0]
self.assertEquals('testcert.crt', cert_info['path'])
self.assertEquals('no start line', cert_info['error'])
def test_path_sanitization(self):
self.login(ADMIN_ACCESS_USER)
# Upload a certificate.
cert_contents, _ = generate_test_cert(hostname='somecoolhost', expires=-10)
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert/../foobar.crt'),
file=(StringIO(cert_contents), 'testcert/../foobar.crt'), expected_code=204)
# Make sure it is present.
json = self.getJsonResponse(SuperUserCustomCertificates)
self.assertEquals(1, len(json['certs']))
cert_info = json['certs'][0]
self.assertEquals('foobar.crt', cert_info['path'])
class TestSuperUserTakeOwnership(ApiTestCase):
def test_take_ownership_superuser(self):
self.login(ADMIN_ACCESS_USER)

View file

@ -1,9 +1,6 @@
from test.test_api_usage import ApiTestCase, READ_ACCESS_USER, ADMIN_ACCESS_USER
from endpoints.api.suconfig import (SuperUserRegistryStatus, SuperUserConfig, SuperUserConfigFile,
SuperUserCreateInitialSuperUser, SuperUserConfigValidate)
from app import config_provider, all_queues
from data.database import User
from data import model
from test.test_api_usage import ApiTestCase
from endpoints.api.suconfig import SuperUserRegistryStatus
from app import config_provider
import unittest
@ -24,169 +21,5 @@ class TestSuperUserRegistryStatus(ApiTestCase):
self.assertEquals('config-db', json['status'])
class TestSuperUserConfigFile(ApiTestCase):
def test_get_non_superuser(self):
with FreshConfigProvider():
# No user.
self.getResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'), expected_code=403)
# Non-superuser.
self.login(READ_ACCESS_USER)
self.getResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'), expected_code=403)
def test_get_superuser_invalid_filename(self):
with FreshConfigProvider():
self.login(ADMIN_ACCESS_USER)
self.getResponse(SuperUserConfigFile, params=dict(filename='somefile'), expected_code=404)
def test_get_superuser(self):
with FreshConfigProvider():
self.login(ADMIN_ACCESS_USER)
result = self.getJsonResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'))
self.assertFalse(result['exists'])
def test_post_non_superuser(self):
with FreshConfigProvider():
# No user, before config.yaml exists.
self.postResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'), expected_code=400)
# Write some config.
self.putJsonResponse(SuperUserConfig, data=dict(config={}, hostname='foobar'))
# No user, with config.yaml.
self.postResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'), expected_code=403)
# Non-superuser.
self.login(READ_ACCESS_USER)
self.postResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'), expected_code=403)
def test_post_superuser_invalid_filename(self):
with FreshConfigProvider():
self.login(ADMIN_ACCESS_USER)
self.postResponse(SuperUserConfigFile, params=dict(filename='somefile'), expected_code=404)
def test_post_superuser(self):
with FreshConfigProvider():
self.login(ADMIN_ACCESS_USER)
self.postResponse(SuperUserConfigFile, params=dict(filename='ssl.cert'), expected_code=400)
class TestSuperUserCreateInitialSuperUser(ApiTestCase):
def test_no_config_file(self):
with FreshConfigProvider():
# If there is no config.yaml, then this method should security fail.
data = dict(username='cooluser', password='password', email='fake@example.com')
self.postResponse(SuperUserCreateInitialSuperUser, data=data, expected_code=403)
def test_config_file_with_db_users(self):
with FreshConfigProvider():
# Write some config.
self.putJsonResponse(SuperUserConfig, data=dict(config={}, hostname='foobar'))
# If there is a config.yaml, but existing DB users exist, then this method should security
# fail.
data = dict(username='cooluser', password='password', email='fake@example.com')
self.postResponse(SuperUserCreateInitialSuperUser, data=data, expected_code=403)
def test_config_file_with_no_db_users(self):
with FreshConfigProvider():
# Write some config.
self.putJsonResponse(SuperUserConfig, data=dict(config={}, hostname='foobar'))
# Delete all the users in the DB.
for user in list(User.select()):
model.user.delete_user(user, all_queues)
# This method should now succeed.
data = dict(username='cooluser', password='password', email='fake@example.com')
result = self.postJsonResponse(SuperUserCreateInitialSuperUser, data=data)
self.assertTrue(result['status'])
# Verify the superuser was created.
User.get(User.username == 'cooluser')
# Verify the superuser was placed into the config.
result = self.getJsonResponse(SuperUserConfig)
self.assertEquals(['cooluser'], result['config']['SUPER_USERS'])
class TestSuperUserConfigValidate(ApiTestCase):
def test_nonsuperuser_noconfig(self):
with FreshConfigProvider():
self.login(ADMIN_ACCESS_USER)
result = self.postJsonResponse(SuperUserConfigValidate, params=dict(service='someservice'),
data=dict(config={}))
self.assertFalse(result['status'])
def test_nonsuperuser_config(self):
with FreshConfigProvider():
# The validate config call works if there is no config.yaml OR the user is a superuser.
# Add a config, and verify it breaks when unauthenticated.
json = self.putJsonResponse(SuperUserConfig, data=dict(config={}, hostname='foobar'))
self.assertTrue(json['exists'])
self.postResponse(SuperUserConfigValidate, params=dict(service='someservice'),
data=dict(config={}),
expected_code=403)
# Now login as a superuser.
self.login(ADMIN_ACCESS_USER)
result = self.postJsonResponse(SuperUserConfigValidate, params=dict(service='someservice'),
data=dict(config={}))
self.assertFalse(result['status'])
class TestSuperUserConfig(ApiTestCase):
def test_get_non_superuser(self):
with FreshConfigProvider():
# No user.
self.getResponse(SuperUserConfig, expected_code=401)
# Non-superuser.
self.login(READ_ACCESS_USER)
self.getResponse(SuperUserConfig, expected_code=403)
def test_get_superuser(self):
with FreshConfigProvider():
self.login(ADMIN_ACCESS_USER)
json = self.getJsonResponse(SuperUserConfig)
# Note: We expect the config to be none because a config.yaml should never be checked into
# the directory.
self.assertIsNone(json['config'])
def test_put(self):
with FreshConfigProvider() as config:
# The update config call works if there is no config.yaml OR the user is a superuser. First
# try writing it without a superuser present.
json = self.putJsonResponse(SuperUserConfig, data=dict(config={}, hostname='foobar'))
self.assertTrue(json['exists'])
# Verify the config file exists.
self.assertTrue(config.config_exists())
# Try writing it again. This should now fail, since the config.yaml exists.
self.putResponse(SuperUserConfig, data=dict(config={}, hostname='barbaz'), expected_code=403)
# Login as a non-superuser.
self.login(READ_ACCESS_USER)
# Try writing it again. This should fail.
self.putResponse(SuperUserConfig, data=dict(config={}, hostname='barbaz'), expected_code=403)
# Login as a superuser.
self.login(ADMIN_ACCESS_USER)
# This should succeed.
json = self.putJsonResponse(SuperUserConfig, data=dict(config={}, hostname='barbaz'))
self.assertTrue(json['exists'])
json = self.getJsonResponse(SuperUserConfig)
self.assertIsNotNone(json['config'])
if __name__ == '__main__':
unittest.main()

View file

@ -50,6 +50,10 @@ class KubernetesConfigProvider(BaseFileProvider):
# in Kubernetes secrets.
return "_".join([directory.rstrip('/'), filename])
def volume_exists(self):
secret = self._lookup_secret()
return secret is not None
def volume_file_exists(self, relative_file_path):
if '/' in relative_file_path:
raise Exception('Expected path from get_volume_path, but found slashes')