Add configurable group names for roles (#17)

This commit is contained in:
Christiaan Goossens
2025-01-05 22:24:48 +01:00
committed by GitHub
parent 2131fe5d36
commit 00da053f50
8 changed files with 59 additions and 12 deletions

View File

@@ -18,6 +18,7 @@ from .config import (
ID_TOKEN_SIGNING_ALGORITHM,
FEATURES,
CLAIMS,
ROLES,
)
# pylint: enable=useless-import-alias
@@ -61,6 +62,7 @@ async def async_setup(hass: HomeAssistant, config):
id_token_signing_alg=my_config.get(ID_TOKEN_SIGNING_ALGORITHM),
features=my_config.get(FEATURES, {}),
claims=my_config.get(CLAIMS, {}),
roles=my_config.get(ROLES, {}),
)
# Register the views

View File

@@ -15,6 +15,9 @@ CLAIMS = "claims"
CLAIMS_DISPLAY_NAME = "display_name"
CLAIMS_USERNAME = "username"
CLAIMS_GROUPS = "groups"
ROLES = "roles"
ROLE_ADMINS = "admin"
ROLE_USERS = "user"
DEFAULT_TITLE = "OpenID Connect (SSO)"
@@ -63,6 +66,18 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CLAIMS_GROUPS): vol.Coerce(str),
}
),
# Determine which specific group values will be mapped to which roles
# Optional, defaults user = null, admin = 'admins'
# If user role is set, users that do not have either will be rejected!
vol.Optional(ROLES): vol.Schema(
{
# Which group name should we use to assign the user role?
vol.Optional(ROLE_USERS): vol.Coerce(str),
# What group name should we use to assign the admin role?
# Defaults to admins
vol.Optional(ROLE_ADMINS): vol.Coerce(str),
}
),
}
)
},

View File

@@ -52,5 +52,15 @@ class OIDCCallbackView(HomeAssistantView):
)
return web.Response(text=view_html, content_type="text/html")
if user_details.get("role") == "invalid":
view_html = await get_view(
"error",
{
"error": "User is not in the correct group to access Home Assistant, "
+ "contact your administrator!",
},
)
return web.Response(text=view_html, content_type="text/html")
code = await self.oidc_provider.async_save_user_info(user_details)
return web.HTTPFound(get_url("/auth/oidc/finish?code=" + code))

View File

@@ -46,9 +46,9 @@ class OIDCFinishView(HomeAssistantView):
# Set a cookie to enable autologin on only the specific path used
# for the POST request, with all strict parameters set
# This cookie should not be read by any Javascript or any other paths.
# It can be really short lifetime as we redirect immediately (15 seconds)
# It can be really short lifetime as we redirect immediately (5 seconds)
"set-cookie": "auth_oidc_code="
+ code
+ "; Path=/auth/login_flow; SameSite=Strict; HttpOnly; Max-Age=15",
+ "; Path=/auth/login_flow; SameSite=Strict; HttpOnly; Max-Age=5",
},
)

View File

@@ -15,6 +15,8 @@ from .config import (
CLAIMS_DISPLAY_NAME,
CLAIMS_USERNAME,
CLAIMS_GROUPS,
ROLE_ADMINS,
ROLE_USERS,
)
_LOGGER = logging.getLogger(__name__)
@@ -67,11 +69,14 @@ class OIDCClient:
features = kwargs.get("features")
claims = kwargs.get("claims")
roles = kwargs.get("roles")
self.disable_pkce: bool = features.get(FEATURES_DISABLE_PKCE)
self.display_name_claim = claims.get(CLAIMS_DISPLAY_NAME, "name")
self.username_claim = claims.get(CLAIMS_USERNAME, "preferred_username")
self.groups_claim = claims.get(CLAIMS_GROUPS, "groups")
self.user_role = roles.get(ROLE_USERS, None)
self.admin_role = roles.get(ROLE_ADMINS, "admins")
def _base64url_encode(self, value: str) -> str:
"""Uses base64url encoding on a given string"""
@@ -356,6 +361,20 @@ class OIDCClient:
# TODO: If the configured claims are not present in id_token, we should fetch userinfo
# Get and parse groups (to check if it's an array)
groups = id_token.get(self.groups_claim, [])
if not isinstance(groups, list):
_LOGGER.warning("Groups claim is not a list, using empty list instead.")
groups = []
# Assign role if user has the required groups
role = "invalid"
if self.user_role in groups or self.user_role is None:
role = "system-users"
if self.admin_role in groups:
role = "system-admin"
# Create a user details dict based on the contents of the id_token & userinfo
data: UserDetails = {
# Subject Identifier. A locally unique and never reassigned identifier within the
@@ -371,8 +390,8 @@ class OIDCClient:
"display_name": id_token.get(self.display_name_claim),
# Username, configurable
"username": id_token.get(self.username_claim),
# Groups, configurable
"groups": id_token.get(self.groups_claim),
# Role
"role": role,
}
# Log which details were obtained for debugging

View File

@@ -259,14 +259,11 @@ class OpenIDAuthProvider(AuthProvider):
sub = credentials.data["sub"]
meta = self._user_meta.get(sub, {})
groups = meta.get("groups") or []
# TODO: Allow setting which group is for admins
group = "system-admin" if "admins" in groups else "system-users"
role = meta.get("role")
return UserMeta(
name=meta.get("display_name"),
is_active=True,
group=group,
group=role,
local_only=False,
)

View File

@@ -1,7 +1,9 @@
"""Generic data types"""
# Dict class to give a type to the user details
from typing import Literal
class UserDetails(dict):
"""User details representation"""
@@ -12,5 +14,5 @@ class UserDetails(dict):
# Preferred username for the user, will be used when first generating the account
# or to link the account on first login
username: str
# Groups that the user has, if any are sent from the OIDC provider
groups: list[str]
# Home Assistant role to assign to this user
role: Literal["system-admin", "system-users", "invalid"]