add Spawner.server_token_scopes config

consistent behavior with oauth_client_allowed_scopes,
where the _intersection_ of requested and owner-held permissions is granted,
instead of failing

Enables different users to have different permissions in $JUPTYERHUB_API_TOKEN,
either via callables or via requesting as much as you may want and only granting the subset.

Additionally, the !server filter can now be correctly applied to the server token

default behavior is unchanged
This commit is contained in:
Min RK
2023-03-22 12:03:26 +01:00
parent 64a253dbef
commit 73b1922c17
3 changed files with 114 additions and 3 deletions

View File

@@ -13,7 +13,7 @@ from tornado import gen, web
from tornado.httputil import urlencode
from tornado.log import app_log
from . import orm
from . import orm, roles, scopes
from ._version import __version__, _check_version
from .crypto import CryptKeeper, EncryptionUnavailable, InvalidToken, decrypt, encrypt
from .metrics import RUNNING_SERVERS, TOTAL_USERS
@@ -673,13 +673,53 @@ class User:
orm_server = orm.Server(base_url=base_url)
db.add(orm_server)
note = "Server at %s" % base_url
api_token = self.new_api_token(note=note, roles=['server'])
db.commit()
spawner = self.get_spawner(server_name, replace_failed=True)
spawner.server = server = Server(orm_server=orm_server)
assert spawner.orm_spawner.server is orm_server
requested_scopes = spawner.server_token_scopes
if callable(requested_scopes):
requested_scopes = await maybe_future(requested_scopes(spawner))
if not requested_scopes:
# nothing requested, default to 'server' role
requested_scopes = orm.Role.find(db, "server").scopes
requested_scopes = set(requested_scopes)
# resolve !server filter, which won't resolve elsewhere,
# because this token is not owned by the server's own oauth client
server_filter = f"={self.name}/{server_name}"
requested_scopes = {
scope + server_filter if scope.endswith("!server") else scope
for scope in requested_scopes
}
have_scopes = roles.roles_to_scopes(roles.get_roles_for(self.orm_user))
have_scopes |= {"inherit"}
jupyterhub_client = (
db.query(orm.OAuthClient)
.filter_by(
identifier="jupyterhub",
)
.one()
)
resolved_scopes, excluded_scopes = scopes._resolve_requested_scopes(
requested_scopes, have_scopes, self.orm_user, jupyterhub_client, db
)
if excluded_scopes:
# what level should this be?
# for admins-get-more use case, this is going to happen for most users
# but for misconfiguration, folks will want to know!
self.log.debug(
"Not assigning requested scopes for %s: requested=%s, assigned=%s, excluded=%s",
spawner._log_name,
requested_scopes,
resolved_scopes,
excluded_scopes,
)
api_token = self.new_api_token(note=note, scopes=resolved_scopes)
# pass requesting handler to the spawner
# e.g. for processing GET params
spawner.handler = handler
@@ -808,6 +848,7 @@ class User:
spawner.api_token,
generated=False,
note="retrieved from spawner %s" % server_name,
scopes=resolved_scopes,
)
# update OAuth client secret with updated API token
if oauth_provider: