diff --git a/doc/reference/hub-configuration.md b/doc/reference/hub-configuration.md index 216b43c..b5d9b07 100644 --- a/doc/reference/hub-configuration.md +++ b/doc/reference/hub-configuration.md @@ -36,15 +36,31 @@ configuration at `/.well-known/openid-configuration`. The client ID of `not-my-board`. Get this value from the OpenID provider. -### `auth.show_claims` +### `auth.issuers` + +**Type:** Table \ +**Required:** No + +Contains extra configuration per OpenID provider. + +### `auth.issuers.` + +**Type:** Table \ +**Required:** No + +Contains configuration for the OpenID provider with the URL matching +``. + +### `auth.issuers..show_claims` **Type:** Array of strings \ **Required:** No -Allows the administrator to filter the shown claims when users log in. Specify -the claims an administrator might need to give the user permissions. If the -option is not set, then all claims are shown. If it's set to an empty array, -then no claims are shown. +Allows the administrator to filter the shown claims of the OpenID Connect ID +token. The filtered claims are logged by the *Hub* and are shown to the users, +when they log in. Specify the claims an administrator might need to give the +user permissions. If the option is not set, then all claims are shown. If it's +set to an empty array, then no claims are shown. ### `auth.permissions` @@ -100,6 +116,8 @@ log_level = "info" [auth] issuer = "http://keycloak.example.com/realms/master" client_id = "not-my-board" + +[auth.issuers."http://keycloak.example.com/realms/master"] show_claims = ["sub", "preferred_username"] [[auth.permissions]] @@ -119,7 +137,12 @@ log_level = "info" [auth] issuer = "https://login.microsoftonline.com/common/v2.0" client_id = "11111111-2222-1111-2222-000000000000" -show_claims = ["oid", "iss", "preferred_username"] + +[auth.issuers."https://login.microsoftonline.com/common/v2.0"] +show_claims = ["preferred_username", "oid", "iss"] + +[auth.issuers."https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0"] +show_claims = ["preferred_username", "oid", "iss"] [[auth.permissions]] claims.oid = "11111111-2222-1111-2222-333333333333" diff --git a/not_my_board/_hub.py b/not_my_board/_hub.py index 9669506..87c8eee 100644 --- a/not_my_board/_hub.py +++ b/not_my_board/_hub.py @@ -12,7 +12,7 @@ import pathlib import random from dataclasses import dataclass -from typing import Dict, Union +from typing import Dict, List, Optional, Union import asgineer @@ -165,17 +165,26 @@ def __init__(self, config=None, http_client=None): auth_config = config.get("auth") if auth_config: - required_keys = {"issuer", "client_id"} - optional_keys = {"show_claims"} - keys = required_keys | (optional_keys & auth_config.keys()) - self._auth_info = {k: auth_config[k] for k in keys} - trusted_issuers = {auth_config["issuer"]} for permission in auth_config["permissions"]: issuer = permission["claims"].get("iss") if issuer: trusted_issuers.add(issuer) + def make_issuer_config(issuer): + issuer_config = auth_config.get("issuers", {}).get(issuer, {}) + return IssuerConfig(**issuer_config) + + self._issuer_configs = { + issuer: make_issuer_config(issuer) for issuer in trusted_issuers + } + + keys = {"issuer", "client_id"} + self._auth_info = {k: auth_config[k] for k in keys} + issuer_config = self._issuer_configs[auth_config["issuer"]] + if issuer_config.show_claims is not None: + self._auth_info["show_claims"] = issuer_config.show_claims + def make_permission(d): d["claims"].setdefault("iss", auth_config["issuer"]) return Permission.from_dict(d) @@ -189,6 +198,7 @@ def make_permission(d): self._auth_info = {} self._permissions = [] self._validator = None + self._issuer_configs = {} self._id_generator = itertools.count(start=1) @@ -216,7 +226,10 @@ async def _connection_context(self, channel): id_ = next(self._id_generator) connection_id_var.set(id_) self._reservations[id_] = set() - authenticator = Authenticator(self._permissions, self._validator, channel) + authenticator = Authenticator( + self._permissions, self._validator, channel, self._issuer_configs + ) + authenticator_var.set(authenticator) try: @@ -343,15 +356,17 @@ class Authenticator(util.ContextStack): _leeway = datetime.timedelta(seconds=30) _timeout = datetime.timedelta(seconds=30) - def __init__(self, permissions, validator, channel): + def __init__(self, permissions, validator, channel, issuer_configs): self._permissions = permissions self._validator = validator self._channel = channel + self._issuer_configs = issuer_configs self._required_roles = set() self._refresh_start_event = asyncio.Event() self._roles = None self._expires = None self._roles_lock = asyncio.Lock() + self._previous_claims = None async def _context_stack(self, stack): if self._validator: @@ -386,6 +401,22 @@ async def _request_roles(self): ) roles = set() + if logger.isEnabledFor(logging.INFO): + show_claims = self._issuer_configs[token_claims["iss"]].show_claims + if show_claims is not None: + filtered_claims = [ + (c, token_claims[c]) for c in show_claims if c in token_claims + ] + else: + filtered_claims = list(token_claims.items()) + + if filtered_claims and filtered_claims != self._previous_claims: + claims_str = ", ".join( + [f"{k!r}: {v!r}" for k, v in filtered_claims] + ) + logger.info("Token claims: %s", claims_str) + self._previous_claims = filtered_claims + for permission in self._permissions: if permission.roles <= roles: # permission rule has no new roles, skip @@ -452,6 +483,11 @@ def from_dict(cls, d): return cls(claims, set(d["roles"])) +@dataclass +class IssuerConfig: + show_claims: Optional[List[str]] = None + + def _unmap_ip(ip_str): """Resolve IPv4-mapped-on-IPv6 to an IPv4 address""" ip = ipaddress.ip_address(ip_str)