mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-15 05:53:00 +00:00
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:
@@ -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()
|
||||
|
@@ -300,6 +300,14 @@ class Authenticator(LoggingConfigurable):
|
||||
If empty, does not perform any additional restriction.
|
||||
|
||||
.. 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`
|
||||
|
@@ -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):
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user