Revoke all permissions from Authenticator.blocked_users

rather than only disabling login, fully block the user from Hub operations
by removing all group membership and role assignments
This commit is contained in:
Min RK
2024-08-12 13:48:05 +02:00
parent a377f8bc7f
commit 6be699c333
4 changed files with 123 additions and 2 deletions

View File

@@ -2182,7 +2182,11 @@ class JupyterHub(Application):
# but changes to the allowed_users set can occur in the database,
# and persist across sessions.
total_users = 0
blocked_users = self.authenticator.blocked_users
for user in db.query(orm.User):
if user.name in blocked_users:
# don't call add_user with blocked users
continue
try:
f = self.authenticator.add_user(user)
if f:
@@ -2238,6 +2242,35 @@ class JupyterHub(Application):
await maybe_future(f)
return user
async def init_blocked_users(self):
"""Revoke all permissions for users in Authenticator.blocked_users"""
blocked_users = self.authenticator.blocked_users
if not blocked_users:
# nothing to check
return
db = self.db
for user in db.query(orm.User).filter(orm.User.name.in_(blocked_users)):
# revoke permissions from blocked users
# so already-issued credentials have no access to the API
self.log.debug(f"Found blocked user in database: {user.name}")
if user.admin:
self.log.warning(
f"Removing admin permissions from blocked user {user.name}"
)
user.admin = False
if user.roles:
self.log.warning(
f"Removing blocked user {user.name} from roles: {', '.join(role.name for role in user.roles)}"
)
user.roles = []
if user.groups:
self.log.warning(
f"Removing blocked user {user.name} from groups: {', '.join(group.name for group in user.groups)}"
)
user.groups = []
db.commit()
async def init_groups(self):
"""Load predefined groups into the database"""
db = self.db
@@ -2965,6 +2998,18 @@ class JupyterHub(Application):
async def check_spawner(user, name, spawner):
status = 0
if spawner.server:
if user.name in self.authenticator.blocked_users:
self.log.warning(
f"Stopping spawner for blocked user: {spawner._log_name}"
)
try:
await user.stop(name)
except Exception:
self.log.exception(
f"Failed to stop {spawner._log_name}",
exc_info=True,
)
return
try:
status = await spawner.poll()
except Exception:
@@ -3356,6 +3401,7 @@ class JupyterHub(Application):
self.init_services()
await self.init_api_tokens()
await self.init_role_assignment()
await self.init_blocked_users()
self.init_tornado_settings()
self.init_handlers()
self.init_tornado_application()

View File

@@ -301,6 +301,14 @@ class Authenticator(LoggingConfigurable):
.. versionadded: 0.9
.. versionchanged:: 5.2
Users blocked via `blocked_users` that may have logged in in the past
have all permissions and group membership revoked
and all servers stopped at JupyterHub startup.
Previously, User permissions (e.g. API tokens)
and servers were unaffected and required additional
administrator operations to block after a user is added to `blocked_users`.
.. versionchanged:: 1.2
`Authenticator.blacklist` renamed to `blocked_users`
"""

View File

@@ -1234,6 +1234,8 @@ class APIToken(Hashed, Base):
orm_token.expires_at = cls.now() + timedelta(seconds=expires_in)
db.commit()
if return_orm:
return orm_token
return token
def update_scopes(self, new_scopes):

View File

@@ -8,6 +8,7 @@ import os
import re
import sys
import time
from concurrent.futures import ThreadPoolExecutor
from subprocess import PIPE, Popen, check_output
from tempfile import NamedTemporaryFile, TemporaryDirectory
from unittest.mock import patch
@@ -16,6 +17,8 @@ import pytest
import traitlets
from traitlets.config import Config
from jupyterhub.scopes import get_scopes_for
from .. import orm
from ..app import COOKIE_SECRET_BYTES, JupyterHub
from .mocking import MockHub
@@ -289,8 +292,7 @@ def persist_db(tmpdir):
def new_hub(request, tmpdir, persist_db):
"""Fixture to launch a new hub for testing"""
async def new_hub():
kwargs = {}
async def new_hub(**kwargs):
ssl_enabled = getattr(request.module, "ssl_enabled", False)
if ssl_enabled:
kwargs['internal_certs_location'] = str(tmpdir)
@@ -537,3 +539,66 @@ async def test_recreate_service_from_database(
# start one more, service should be gone
app = await new_hub()
assert service_name not in app._service_map
async def test_revoke_blocked_users(app, username, groupname, new_hub):
config = Config()
config.Authenticator.admin_users = {username}
config.JupyterHub.load_groups = {
groupname: {
"users": [username],
},
}
config.JupyterHub.load_roles = [
{
"name": "testrole",
"scopes": ["access:services"],
"groups": [groupname],
}
]
app = await new_hub(config=config)
user = app.users[username]
# load some credentials, start server
await user.spawn()
# await app.proxy.add_user(user)
spawner = user.spawners['']
token = user.new_api_token(return_orm=True)
app.cleanup_servers = False
app.stop()
# before state
assert await spawner.poll() is None
assert sorted(role.name for role in user.roles) == ['admin', 'user']
assert [g.name for g in user.groups] == [groupname]
assert user.admin
user_scopes = get_scopes_for(user)
assert "access:servers" in user_scopes
token_scopes = get_scopes_for(token)
assert "access:servers" in token_scopes
# start a new hub, now with blocked users
config = Config()
name_doesnt_exist = user.name + "-doesntexist"
config.Authenticator.blocked_users = {user.name, name_doesnt_exist}
config.JupyterHub.init_spawners_timeout = 60
# background spawner.proc.wait to avoid waiting for zombie process here
with ThreadPoolExecutor(1) as pool:
pool.submit(spawner.proc.wait)
app2 = await new_hub(config=config)
assert app2.db_url == app.db_url
# check that blocked user has no permissions
user2 = app2.users[user.name]
assert user2.roles == []
assert user2.groups == []
assert user2.admin is False
user_scopes = get_scopes_for(user2)
assert user_scopes == set()
token = orm.APIToken.find(app2.db, token.token)
token_scopes = get_scopes_for(token)
assert token_scopes == set()
# spawner stopped
assert user2.spawners == {}
assert await spawner.poll() is not None