Add an in-memory superusermanager, which stores the current list of superusers in a process-shared Value. We do this because in the ER, when we add a new superuser, we need to ensure that ALL workers have their lists updated (otherwise we get the behavior that some workers validate the new permission and others do not).
This commit is contained in:
parent
da4bcbbee0
commit
28d319ad26
6 changed files with 51 additions and 12 deletions
2
app.py
2
app.py
|
@ -29,6 +29,7 @@ from util.names import urn_generator
|
||||||
from util.oauth import GoogleOAuthConfig, GithubOAuthConfig
|
from util.oauth import GoogleOAuthConfig, GithubOAuthConfig
|
||||||
from util.queuemetrics import QueueMetrics
|
from util.queuemetrics import QueueMetrics
|
||||||
from util.config.configutil import generate_secret_key
|
from util.config.configutil import generate_secret_key
|
||||||
|
from util.config.superusermanager import SuperUserManager
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
@ -105,6 +106,7 @@ build_logs = BuildLogs(app)
|
||||||
queue_metrics = QueueMetrics(app)
|
queue_metrics = QueueMetrics(app)
|
||||||
authentication = UserAuthentication(app)
|
authentication = UserAuthentication(app)
|
||||||
userevents = UserEventsBuilderModule(app)
|
userevents = UserEventsBuilderModule(app)
|
||||||
|
superusers = SuperUserManager(app)
|
||||||
|
|
||||||
github_login = GithubOAuthConfig(app.config, 'GITHUB_LOGIN_CONFIG')
|
github_login = GithubOAuthConfig(app.config, 'GITHUB_LOGIN_CONFIG')
|
||||||
github_trigger = GithubOAuthConfig(app.config, 'GITHUB_TRIGGER_CONFIG')
|
github_trigger = GithubOAuthConfig(app.config, 'GITHUB_TRIGGER_CONFIG')
|
||||||
|
|
|
@ -7,7 +7,7 @@ from functools import partial
|
||||||
import scopes
|
import scopes
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from app import app
|
from app import app, superusers
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -94,8 +94,7 @@ class QuayDeferredPermissionUser(Identity):
|
||||||
return super(QuayDeferredPermissionUser, self).can(permission)
|
return super(QuayDeferredPermissionUser, self).can(permission)
|
||||||
|
|
||||||
# Add the superuser need, if applicable.
|
# Add the superuser need, if applicable.
|
||||||
if (user_object.username is not None and
|
if superusers.is_superuser(user_object.username):
|
||||||
user_object.username in app.config.get('SUPER_USERS', [])):
|
|
||||||
self.provides.add(_SuperUserNeed())
|
self.provides.add(_SuperUserNeed())
|
||||||
|
|
||||||
# Add the user specific permissions, only for non-oauth permission
|
# Add the user specific permissions, only for non-oauth permission
|
||||||
|
|
|
@ -7,7 +7,7 @@ from endpoints.api import (ApiResource, nickname, resource, internal_only, show_
|
||||||
require_fresh_login, request, validate_json_request, verify_not_prod)
|
require_fresh_login, request, validate_json_request, verify_not_prod)
|
||||||
|
|
||||||
from endpoints.common import common_login
|
from endpoints.common import common_login
|
||||||
from app import app, CONFIG_PROVIDER
|
from app import app, CONFIG_PROVIDER, superusers
|
||||||
from data import model
|
from data import model
|
||||||
from auth.permissions import SuperUserPermission
|
from auth.permissions import SuperUserPermission
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
|
@ -49,7 +49,7 @@ class SuperUserRegistryStatus(ApiResource):
|
||||||
'file_exists': file_exists,
|
'file_exists': file_exists,
|
||||||
'is_testing': app.config['TESTING'],
|
'is_testing': app.config['TESTING'],
|
||||||
'valid_db': database_is_valid(),
|
'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)
|
CONFIG_PROVIDER.save_yaml(config_object)
|
||||||
|
|
||||||
# Update the in-memory config for the new superuser.
|
# Update the in-memory config for the new superuser.
|
||||||
app.config['SUPER_USERS'] = [username]
|
superusers.register_superuser(username)
|
||||||
|
|
||||||
# Conduct login with that user.
|
# Conduct login with that user.
|
||||||
common_login(superuser)
|
common_login(superuser)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
from app import app, avatar
|
from app import app, avatar, superusers
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
|
||||||
|
@ -109,7 +109,7 @@ def user_view(user):
|
||||||
'email': user.email,
|
'email': user.email,
|
||||||
'verified': user.verified,
|
'verified': user.verified,
|
||||||
'avatar': avatar.compute_hash(user.email, name=user.username),
|
'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/')
|
@resource('/v1/superuser/usage/')
|
||||||
|
@ -217,7 +217,7 @@ class SuperUserSendRecoveryEmail(ApiResource):
|
||||||
if not user or user.organization or user.robot:
|
if not user or user.organization or user.robot:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
if username in app.config['SUPER_USERS']:
|
if superusers.is_superuser(username):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
code = model.create_reset_password_email_code(user.email)
|
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:
|
if not user or user.organization or user.robot:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
if username in app.config['SUPER_USERS']:
|
if superusers.is_superuser(username):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
model.delete_user(user)
|
model.delete_user(user)
|
||||||
|
@ -296,7 +296,7 @@ class SuperUserManagement(ApiResource):
|
||||||
if not user or user.organization or user.robot:
|
if not user or user.organization or user.robot:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
if username in app.config['SUPER_USERS']:
|
if superusers.is_superuser(username):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
user_data = request.get_json()
|
user_data = request.get_json()
|
||||||
|
|
|
@ -376,7 +376,7 @@
|
||||||
<td>Database Server:</td>
|
<td>Database Server:</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="config-string-field" binding="fields.server"
|
<span class="config-string-field" binding="fields.server"
|
||||||
placeholder="localhost"></span>
|
placeholder="dbserverhost"></span>
|
||||||
<div class="help-text">
|
<div class="help-text">
|
||||||
The server (and optionally, custom port) where the database lives
|
The server (and optionally, custom port) where the database lives
|
||||||
</div>
|
</div>
|
||||||
|
|
38
util/config/superusermanager.py
Normal file
38
util/config/superusermanager.py
Normal file
|
@ -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)
|
Reference in a new issue