import logging import os import json from flask import abort from endpoints.api import (ApiResource, nickname, resource, internal_only, show_if, hide_if, require_fresh_login, request, validate_json_request) from endpoints.common import common_login from app import app, OVERRIDE_CONFIG_YAML_FILENAME, OVERRIDE_CONFIG_DIRECTORY from data import model from data.database import User, validate_database_url from auth.permissions import SuperUserPermission from auth.auth_context import get_authenticated_user from util.configutil import (import_yaml, export_yaml, add_enterprise_config_defaults, set_config_value) import features logger = logging.getLogger(__name__) CONFIG_FILE_WHITELIST = ['ssl.key', 'ssl.cert'] def database_is_valid(): try: User.select().limit(1) return True except: return False def database_has_users(): return bool(list(User.select().limit(1))) @resource('/v1/superuser/registrystatus') @internal_only @show_if(features.SUPER_USERS) @hide_if(features.BILLING) # Make sure it is never allowed in prod. 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') def get(self): """ Returns whether a valid configuration, database and users exist. """ current_user = get_authenticated_user() return { 'dir_exists': os.path.exists(OVERRIDE_CONFIG_DIRECTORY), 'file_exists': os.path.exists(OVERRIDE_CONFIG_YAML_FILENAME), 'is_testing': app.config['TESTING'], 'valid_db': database_is_valid(), 'ready': current_user and current_user.username in app.config['SUPER_USERS'] } @resource('/v1/superuser/config') @internal_only @show_if(features.SUPER_USERS) @hide_if(features.BILLING) # Make sure it is never allowed in prod. class SuperUserGetConfig(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 @nickname('scGetConfig') def get(self): """ Returns the currently defined configuration, if any. """ if SuperUserPermission().can(): config_object = {} try: import_yaml(config_object, OVERRIDE_CONFIG_YAML_FILENAME) except Exception: config_object = None return { 'config': config_object } abort(403) @nickname('scUpdateConfig') @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 os.path.exists(OVERRIDE_CONFIG_YAML_FILENAME) 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. export_yaml(config_object, OVERRIDE_CONFIG_YAML_FILENAME) return { 'exists': True, 'config': config_object } abort(403) @resource('/v1/superuser/config/file/') @internal_only @show_if(features.SUPER_USERS) @hide_if(features.BILLING) # Make sure it is never allowed in prod. class SuperUserConfigFile(ApiResource): """ Resource for fetching the status of config files and overriding them. """ @nickname('scConfigFileExists') def get(self, filename): """ Returns whether the configuration file with the given name exists. """ if not filename in CONFIG_FILE_WHITELIST: abort(404) if SuperUserPermission().can(): return { 'exists': os.path.exists(os.path.join(OVERRIDE_CONFIG_DIRECTORY, filename)) } abort(403) @nickname('scUpdateConfigFile') def post(self, filename): """ Updates the configuration file with the given name. """ if not filename in CONFIG_FILE_WHITELIST: abort(404) if SuperUserPermission().can(): uploaded_file = request.files['file'] if not uploaded_file: abort(404) uploaded_file.save(os.path.join(OVERRIDE_CONFIG_DIRECTORY, filename)) return { 'status': True } abort(403) @resource('/v1/superuser/config/createsuperuser') @internal_only @show_if(features.SUPER_USERS) @hide_if(features.BILLING) # Make sure it is never allowed in prod. 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') @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 os.path.exists(OVERRIDE_CONFIG_YAML_FILENAME) 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.create_user(username, password, email, auto_verify=True) # Add the user to the config. set_config_value(OVERRIDE_CONFIG_YAML_FILENAME, 'SUPER_USERS', [username]) app.config['SUPER_USERS'] = [username] # Conduct login with that user. common_login(superuser) return { 'status': True } abort(403) @resource('/v1/superuser/config/validate/') @internal_only @show_if(features.SUPER_USERS) @hide_if(features.BILLING) # Make sure it is never allowed in prod. 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' } }, }, } @nickname('scValidateConfig') @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 os.path.exists(OVERRIDE_CONFIG_YAML_FILENAME) or SuperUserPermission().can(): config = request.get_json()['config'] if service == 'database': try: validate_database_url(config['DB_URI']) return { 'status': True } except Exception as ex: logger.exception('Could not validate database') return { 'status': False, 'reason': str(ex) } return {} abort(403)