diff --git a/app.py b/app.py index b1281b760..6ce4fbf90 100644 --- a/app.py +++ b/app.py @@ -29,6 +29,7 @@ from util.names import urn_generator from util.oauth import GoogleOAuthConfig, GithubOAuthConfig from util.queuemetrics import QueueMetrics from util.config.configutil import generate_secret_key +from util.config.superusermanager import SuperUserManager app = Flask(__name__) @@ -105,6 +106,7 @@ build_logs = BuildLogs(app) queue_metrics = QueueMetrics(app) authentication = UserAuthentication(app) userevents = UserEventsBuilderModule(app) +superusers = SuperUserManager(app) github_login = GithubOAuthConfig(app.config, 'GITHUB_LOGIN_CONFIG') github_trigger = GithubOAuthConfig(app.config, 'GITHUB_TRIGGER_CONFIG') diff --git a/auth/permissions.py b/auth/permissions.py index efa135155..47a41c257 100644 --- a/auth/permissions.py +++ b/auth/permissions.py @@ -7,7 +7,7 @@ from functools import partial import scopes from data import model -from app import app +from app import app, superusers logger = logging.getLogger(__name__) @@ -94,8 +94,7 @@ class QuayDeferredPermissionUser(Identity): return super(QuayDeferredPermissionUser, self).can(permission) # Add the superuser need, if applicable. - if (user_object.username is not None and - user_object.username in app.config.get('SUPER_USERS', [])): + if superusers.is_superuser(user_object.username): self.provides.add(_SuperUserNeed()) # Add the user specific permissions, only for non-oauth permission diff --git a/endpoints/api/suconfig.py b/endpoints/api/suconfig.py index 50b3635cf..05efb4cd7 100644 --- a/endpoints/api/suconfig.py +++ b/endpoints/api/suconfig.py @@ -7,7 +7,7 @@ from endpoints.api import (ApiResource, nickname, resource, internal_only, show_ require_fresh_login, request, validate_json_request, verify_not_prod) from endpoints.common import common_login -from app import app, CONFIG_PROVIDER +from app import app, CONFIG_PROVIDER, superusers from data import model from auth.permissions import SuperUserPermission from auth.auth_context import get_authenticated_user @@ -49,7 +49,7 @@ class SuperUserRegistryStatus(ApiResource): 'file_exists': file_exists, 'is_testing': app.config['TESTING'], 'valid_db': database_is_valid(), - 'ready': not app.config['TESTING'] and file_exists and bool(app.config['SUPER_USERS']) + 'ready': not app.config['TESTING'] and file_exists and superusers.has_superusers() } @@ -215,7 +215,7 @@ class SuperUserCreateInitialSuperUser(ApiResource): CONFIG_PROVIDER.save_yaml(config_object) # Update the in-memory config for the new superuser. - app.config['SUPER_USERS'] = [username] + superusers.register_superuser(username) # Conduct login with that user. common_login(superuser) diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py index c8717bd7b..a391b3130 100644 --- a/endpoints/api/superuser.py +++ b/endpoints/api/superuser.py @@ -4,7 +4,7 @@ import json import os from random import SystemRandom -from app import app, avatar +from app import app, avatar, superusers from flask import request from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, @@ -109,7 +109,7 @@ def user_view(user): 'email': user.email, 'verified': user.verified, 'avatar': avatar.compute_hash(user.email, name=user.username), - 'super_user': user.username in app.config['SUPER_USERS'] + 'super_user': superusers.is_superuser(user.username) } @resource('/v1/superuser/usage/') @@ -217,7 +217,7 @@ class SuperUserSendRecoveryEmail(ApiResource): if not user or user.organization or user.robot: abort(404) - if username in app.config['SUPER_USERS']: + if superusers.is_superuser(username): abort(403) code = model.create_reset_password_email_code(user.email) @@ -277,7 +277,7 @@ class SuperUserManagement(ApiResource): if not user or user.organization or user.robot: abort(404) - if username in app.config['SUPER_USERS']: + if superusers.is_superuser(username): abort(403) model.delete_user(user) @@ -296,7 +296,7 @@ class SuperUserManagement(ApiResource): if not user or user.organization or user.robot: abort(404) - if username in app.config['SUPER_USERS']: + if superusers.is_superuser(username): abort(403) user_data = request.get_json() diff --git a/static/partials/super-user.html b/static/partials/super-user.html index 847a0ed23..7800c9b1e 100644 --- a/static/partials/super-user.html +++ b/static/partials/super-user.html @@ -376,7 +376,7 @@ Database Server: + placeholder="dbserverhost">
The server (and optionally, custom port) where the database lives
diff --git a/util/config/superusermanager.py b/util/config/superusermanager.py new file mode 100644 index 000000000..5930da9cf --- /dev/null +++ b/util/config/superusermanager.py @@ -0,0 +1,38 @@ +from multiprocessing.sharedctypes import Value, Array +from util.validation import MAX_LENGTH + +class SuperUserManager(object): + """ In-memory helper class for quickly accessing (and updating) the valid + set of super users. This class communicates across processes to ensure + that the shared set is always the same. + """ + + def __init__(self, app): + usernames = app.config.get('SUPER_USERS', []) + usernames_str = ','.join(usernames) + + self._max_length = len(usernames_str) + MAX_LENGTH + 1 + self._array = Array('c', self._max_length, lock=True) + self._array.value = usernames_str + + def is_superuser(self, username): + """ Returns if the given username represents a super user. """ + usernames = self._array.value.split(',') + return username in usernames + + def register_superuser(self, username): + """ Registers a new username as a super user for the duration of the container. + Note that this does *not* change any underlying config files. + """ + usernames = self._array.value.split(',') + usernames.append(username) + new_string = ','.join(usernames) + + if len(new_string) <= self._max_length: + self._array.value = new_string + else: + raise Exception('Maximum superuser count reached. Please report this to support.') + + def has_superusers(self): + """ Returns whether there are any superusers defined. """ + return bool(self._array.value)