38a6b3621c
When the user commits the configuration, if they have chosen a non-DB auth system, we now auto-link the superuser account to that auth system, to ensure they can login again after restart.
379 lines
No EOL
11 KiB
Python
379 lines
No EOL
11 KiB
Python
""" Superuser Config API. """
|
|
|
|
import logging
|
|
import os
|
|
import signal
|
|
|
|
from flask import abort
|
|
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 app import app, CONFIG_PROVIDER, superusers
|
|
from data import model
|
|
from data.database import configure
|
|
from auth.permissions import SuperUserPermission
|
|
from auth.auth_context import get_authenticated_user
|
|
from data.database import User
|
|
from util.config.configutil import add_enterprise_config_defaults
|
|
from util.config.validator import validate_service_for_config, CONFIG_FILENAMES
|
|
from data.runmigration import run_alembic_migration
|
|
from data.users import get_federated_service_name
|
|
|
|
import features
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def database_is_valid():
|
|
""" Returns whether the database, as configured, is valid. """
|
|
if app.config['TESTING']:
|
|
return False
|
|
|
|
try:
|
|
list(User.select().limit(1))
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
|
|
def database_has_users():
|
|
""" Returns whether the database has any users defined. """
|
|
return bool(list(User.select().limit(1)))
|
|
|
|
|
|
@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 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.yaml_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'
|
|
}
|
|
|
|
# If we have SETUP_COMPLETE, then we're ready to go!
|
|
if app.config.get('SETUP_COMPLETE', False):
|
|
return {
|
|
'requires_restart': CONFIG_PROVIDER.requires_restart(app.config),
|
|
'status': 'ready'
|
|
}
|
|
|
|
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.yaml_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_yaml())
|
|
|
|
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)
|
|
|
|
|
|
|
|
@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(1, 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': {
|
|
'id': 'UpdateConfig',
|
|
'type': 'object',
|
|
'description': 'Updates the YAML config file',
|
|
'required': [
|
|
'config',
|
|
'hostname'
|
|
],
|
|
'properties': {
|
|
'config': {
|
|
'type': 'object'
|
|
},
|
|
'hostname': {
|
|
'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_yaml()
|
|
return {
|
|
'config': config_object
|
|
}
|
|
|
|
abort(403)
|
|
|
|
@nickname('scUpdateConfig')
|
|
@verify_not_prod
|
|
@validate_json_request('UpdateConfig')
|
|
def put(self):
|
|
""" Updates the config.yaml 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.yaml_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 YAML file.
|
|
CONFIG_PROVIDER.save_yaml(config_object)
|
|
|
|
# If the authentication system is not the database, link the superuser account to the
|
|
# the authentication system chosen.
|
|
if config_object.get('AUTHENTICATION_TYPE', 'Database') != 'Database':
|
|
service_name = get_federated_service_name(config_object['AUTHENTICATION_TYPE'])
|
|
current_user = get_authenticated_user()
|
|
model.user.confirm_attached_federated_login(current_user, service_name)
|
|
|
|
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 filename in CONFIG_FILENAMES:
|
|
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 filename in CONFIG_FILENAMES:
|
|
abort(404)
|
|
|
|
# Note: This method can be called before the configuration exists
|
|
# to upload the database SSL cert.
|
|
if not CONFIG_PROVIDER.yaml_exists() or SuperUserPermission().can():
|
|
uploaded_file = request.files['file']
|
|
if not uploaded_file:
|
|
abort(400)
|
|
|
|
CONFIG_PROVIDER.save_volume_file(filename, uploaded_file)
|
|
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': {
|
|
'id': '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.yaml_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 = model.user.create_user(username, password, email, auto_verify=True)
|
|
|
|
# Add the user to the config.
|
|
config_object = CONFIG_PROVIDER.get_yaml()
|
|
config_object['SUPER_USERS'] = [username]
|
|
CONFIG_PROVIDER.save_yaml(config_object)
|
|
|
|
# Update the in-memory config for the new superuser.
|
|
superusers.register_superuser(username)
|
|
|
|
# Conduct login with that user.
|
|
common_login(superuser)
|
|
|
|
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': {
|
|
'id': '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.yaml_exists() or SuperUserPermission().can():
|
|
config = request.get_json()['config']
|
|
return validate_service_for_config(service, config, request.get_json().get('password', ''))
|
|
|
|
abort(403) |