Add configurable group names for roles (#17)
This commit is contained in:
committed by
GitHub
parent
2131fe5d36
commit
00da053f50
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user