commit
fb9c4fecce
42 changed files with 647 additions and 5135 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
149
config_app/config_test/__init__.py
Normal file
149
config_app/config_test/__init__.py
Normal 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'])
|
||||
|
208
config_app/config_test/test_api_usage.py
Normal file
208
config_app/config_test/test_api_usage.py
Normal 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'])
|
||||
|
149
config_app/config_test/test_suconfig_api.py
Normal file
149
config_app/config_test/test_suconfig_api.py
Normal 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()
|
|
@ -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 ''
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1 +0,0 @@
|
|||
<div class="config-parsed-field-element"></div>
|
|
@ -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
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">×</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
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
});
|
82
static/js/pages/incomplete-setup.js
Normal file
82
static/js/pages/incomplete-setup.js
Normal 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 <name-of-image> 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();
|
||||
};
|
||||
})();
|
|
@ -15,7 +15,7 @@
|
|||
$scope.userRegistered = false;
|
||||
|
||||
if (!Config['SETUP_COMPLETE'] && !Features.BILLING) {
|
||||
$location.path('/setup');
|
||||
$location.path('/incomplete-setup');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 <name-of-image> 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();
|
||||
};
|
||||
})();
|
|
@ -18,7 +18,6 @@ var quayDependencies: string[] = [
|
|||
'pasvaz.bindonce',
|
||||
'ansiToHtml',
|
||||
'core-ui',
|
||||
'core-config-setup',
|
||||
'infinite-scroll',
|
||||
'ngTagsInput',
|
||||
];
|
||||
|
|
|
@ -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
|
||||
|
|
21
static/partials/incomplete-setup.html
Normal file
21
static/partials/incomplete-setup.html
Normal 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>
|
|
@ -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 -->
|
|
@ -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>
|
||||
|
|
|
@ -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 }};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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')
|
||||
|
|
Reference in a new issue