7df8ed4a60
Change SecScanAPI to use a uri creation func instead of test context Pass config provider through validator context Remove app config dependency for validators
414 lines
13 KiB
Python
414 lines
13 KiB
Python
""" Superuser Config API. """
|
|
|
|
import logging
|
|
import os
|
|
import signal
|
|
import subprocess
|
|
|
|
from flask import abort
|
|
|
|
from app import app, config_provider, superusers, OVERRIDE_CONFIG_DIRECTORY, ip_resolver
|
|
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,
|
|
InvalidRequest)
|
|
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
|
|
|
|
import features
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def database_is_valid():
|
|
""" Returns whether the database, as configured, is valid. """
|
|
if app.config['TESTING']:
|
|
return False
|
|
|
|
return model.is_valid()
|
|
|
|
|
|
def database_has_users():
|
|
""" Returns whether the database has any users defined. """
|
|
return model.has_users()
|
|
|
|
|
|
@resource('/v1/superuser/registrystatus')
|
|
@internal_only
|
|
@show_if(features.SUPER_USERS)
|
|
class SuperUserRegistryStatus(ApiResource):
|
|
""" Resource for determining the status of the registry, such as if config exists,
|
|
if a database is configured, and if it has any defined users.
|
|
"""
|
|
@nickname('scRegistryStatus')
|
|
@verify_not_prod
|
|
def get(self):
|
|
""" Returns the status of the registry. """
|
|
|
|
# If we have SETUP_COMPLETE, then we're ready to go!
|
|
if app.config.get('SETUP_COMPLETE', False):
|
|
return {
|
|
'provider_id': config_provider.provider_id,
|
|
'requires_restart': config_provider.requires_restart(app.config),
|
|
'status': 'ready'
|
|
}
|
|
|
|
# If there is no conf/stack volume, then report that status.
|
|
if not config_provider.volume_exists():
|
|
return {
|
|
'status': 'missing-config-dir'
|
|
}
|
|
|
|
# If there is no config file, we need to setup the database.
|
|
if not config_provider.config_exists():
|
|
return {
|
|
'status': 'config-db'
|
|
}
|
|
|
|
# If the database isn't yet valid, then we need to set it up.
|
|
if not database_is_valid():
|
|
return {
|
|
'status': 'setup-db'
|
|
}
|
|
|
|
return {
|
|
'status': 'create-superuser' if not database_has_users() else 'config'
|
|
}
|
|
|
|
|
|
class _AlembicLogHandler(logging.Handler):
|
|
def __init__(self):
|
|
super(_AlembicLogHandler, self).__init__()
|
|
self.records = []
|
|
|
|
def emit(self, record):
|
|
self.records.append({
|
|
'level': record.levelname,
|
|
'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(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.
|
|
|
|
>>> get_process_id('kthreadd')
|
|
[2]
|
|
>>> get_process_id('watchdog')
|
|
[10, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56, 61] # ymmv
|
|
>>> get_process_id('non-existent process')
|
|
[]
|
|
"""
|
|
child = subprocess.Popen(['pgrep', name], stdout=subprocess.PIPE, shell=False)
|
|
response = child.communicate()[0]
|
|
return [int(pid) for pid in response.split()]
|
|
|
|
|
|
@resource('/v1/superuser/shutdown')
|
|
@internal_only
|
|
@show_if(features.SUPER_USERS)
|
|
class SuperUserShutdown(ApiResource):
|
|
""" Resource for sending a shutdown signal to the container. """
|
|
|
|
@verify_not_prod
|
|
@nickname('scShutdownContainer')
|
|
def post(self):
|
|
""" Sends a signal to the phusion init system to shut down the container. """
|
|
# 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 app.config['TESTING'] or not database_has_users() or SuperUserPermission().can():
|
|
# Note: We skip if debugging locally.
|
|
if app.config.get('DEBUGGING') == True:
|
|
return {}
|
|
|
|
os.kill(get_process_id('my_init')[0], signal.SIGINT)
|
|
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(config, request.get_json().get('password', ''), app,
|
|
ip_resolver=ip_resolver,
|
|
config_provider=config_provider)
|
|
|
|
return validate_service_for_config(service, validator_context)
|
|
|
|
abort(403)
|