8aac3fd86e
This authentication system hits two HTTP endpoints to check and verify the existence of users: Existance endpoint: GET http://endpoint/ with Authorization: Basic (username:) => Returns 200 if the username/email exists, 4** otherwise Verification endpoint: GET http://endpoint/ with Authorization: Basic (username:password) => Returns 200 and a signed JWT with the user's username and email address if the username+password validates, 4** otherwise with the body containing an optional error message The JWT produced by the endpoint must be issued with an issuer matching that configured in the config.yaml, and the audience must be "quay.io/jwtauthn". The JWT is signed using a private key and then validated on the Quay.io side with the associated public key, found as "jwt-authn.cert" in the conf/stack directory.
363 lines
No EOL
10 KiB
Python
363 lines
No EOL
10 KiB
Python
import logging
|
|
import os
|
|
import json
|
|
import signal
|
|
|
|
from flask import abort, Response
|
|
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.provider import CannotWriteConfigException
|
|
from util.config.validator import validate_service_for_config, CONFIG_FILENAMES
|
|
from data.runmigration import run_alembic_migration
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
if 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.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'
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
@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)
|
|
|
|
abort(403) |