diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index ec541f5a336e..b02e26a0632c 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -2549,6 +2549,20 @@ class BannerModel(BaseModel): ENABLE_OAUTH_GROUP_CREATION = os.getenv('ENABLE_OAUTH_GROUP_CREATION', 'False').lower() == 'true' +# PATCH OIDC +AAK_OAUTH_ENABLE_ROLE_GROUPS_MAPPING = ( + os.environ.get("AAK_OAUTH_ENABLE_ROLE_GROUPS_MAPPING", "False").lower() == "true" +) +# AAK Group claim configuration +AAK_OAUTH_GROUP_CLAIMS = os.environ.get( + "AAK_OAUTH_GROUP_CLAIMS", + '["companyname", "division", "department", "extensionAttribute12", "Office"]' +) +AAK_OAUTH_GROUP_ID_CLAIM = os.environ.get("AAK_OAUTH_GROUP_ID_CLAIM", "extensionAttribute7") +AAK_OAUTH_GROUP_ID_SEPARATOR = os.environ.get("AAK_OAUTH_GROUP_ID_SEPARATOR", ";") +# Debug: Override OAuth roles for testing +AAK_OAUTH_DEBUG_FORCE_ROLE = os.environ.get("AAK_OAUTH_DEBUG_FORCE_ROLE", "") +# //PATCH OIDC oauth_group_default_share = os.getenv('OAUTH_GROUP_DEFAULT_SHARE', 'true').strip().lower() OAUTH_GROUP_DEFAULT_SHARE = 'members' if oauth_group_default_share == 'members' else oauth_group_default_share == 'true' diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 0cbfcb2ebf76..05af72dec31c 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -42,6 +42,11 @@ ENABLE_OAUTH_SIGNUP, JWT_EXPIRES_IN, OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID, + AAK_OAUTH_ENABLE_ROLE_GROUPS_MAPPING, # PATCH OIDC + AAK_OAUTH_GROUP_CLAIMS, # PATCH OIDC + AAK_OAUTH_GROUP_ID_CLAIM, # PATCH OIDC + AAK_OAUTH_GROUP_ID_SEPARATOR, # PATCH OIDC + AAK_OAUTH_DEBUG_FORCE_ROLE, # PATCH OIDC OAUTH_ADMIN_ROLES, OAUTH_ALLOWED_DOMAINS, OAUTH_ALLOWED_ROLES, @@ -247,6 +252,111 @@ def _normalize_token_expiry(token: dict) -> dict: return token +# PATCH OIDC +def set_aak_groups(user_data: UserInfo) -> UserInfo: + """ + Set AAK groups based on AAK claims. AAK groups need to be parsed from a collection of AAK claims, + so we cannot rely on Open WebUI's claims mapping. Parses the relevant AAK claims and adds them + to the "groups" list. This enables us to rely on Open WebUI's role management for user role assignment. + To ensure unique group names, they are constructed as " ()". + + Example claims: + "companyname": [ + "Aarhus Kommune" + ], + "division": [ + "Kultur og Borgerservice" + ], + "department": [ + "Borgerservice og Biblioteker" + ], + "extensionAttribute12": [ + "ITK" + ], + "Office": [ + "ITK Development" + ], + "extensionAttribute7": [ + "1001;1004;1012;1103;6530" + ] + + The ID's for the departments are given sequentially in "extensionAttribute7". Users in management postitions will + not have five levels of AAK groups. This will show in the length of "extensionAttribute7" but will not show in the + other claims. In the above example a manager will still have the "Office" claim, but it will repeat the value from + "extensionAttribute12" and "extensionAttribute7 will only contain "1001;1004;1012;1103" + + Configuration via environment variables: + - AAK_OAUTH_GROUP_CLAIMS: JSON array of claim names in hierarchical order + Default: '["companyname", "division", "department", "extensionAttribute12", "Office"]' + - AAK_OAUTH_GROUP_ID_CLAIM: Claim containing separated IDs (default: extensionAttribute7) + - AAK_OAUTH_GROUP_ID_SEPARATOR: Separator for IDs (default: ;) + + Note: ENABLE_OAUTH_GROUP_MANAGEMENT and ENABLE_OAUTH_GROUP_CREATION must be set to 'true' + + Args: + user_data (dict): The decoded OIDC token + + Returns: + The decoded OIDC token with the AAK group names added to the "groups" list. + """ + log.debug("Running AAK Group management") + log.debug(user_data) + + user_data['groups'] = [] + + # Parse configured claim names from JSON + try: + group_claims = json.loads(AAK_OAUTH_GROUP_CLAIMS) + except json.JSONDecodeError as e: + log.error(f"Failed to parse AAK_OAUTH_GROUP_CLAIMS: {e}") + return user_data + + # Get IDs from configured claim + dept_ids = user_data.get(AAK_OAUTH_GROUP_ID_CLAIM, "").split(AAK_OAUTH_GROUP_ID_SEPARATOR) + dept_depth = len(dept_ids) + + # Process each configured level + for level, claim_name in enumerate(group_claims): + if claim_name and claim_name in user_data and dept_depth >= (level + 1): + name = user_data.get(claim_name, "") + group_id = dept_ids[level] + user_data['groups'].append(f"{name} ({group_id})") + + log.debug(f"Using groups {user_data.get('groups', '')}.") + + return user_data + + +def set_aak_role(user_data: UserInfo) -> UserInfo: + """ + Set the AAK role based on AAK claims. For "builders" we cannot map to a native Open WebUI role. + Instead, we add the role "Builder" to the list of groups. + + Note: ENABLE_OAUTH_GROUP_MANAGEMENT and ENABLE_OAUTH_GROUP_CREATION must be set to 'true' + + Args: + user_data (dict): The decoded OIDC token + + Returns: + The decoded OIDC token with the AAK role added to the "groups" list. + """ + + log.debug("Running AAK Role management") + log.debug(user_data) + + claims_roles = user_data.get("role", "") + + log.debug(f"Using aak_claims_role {claims_roles}.") + + if "builder" in claims_roles: + user_data['groups'].append("Builder") + + log.debug(f"Using role-groups {user_data.get('groups', '')}.") + + return user_data +# //PATCH OIDC + + FERNET = None if len(OAUTH_CLIENT_INFO_ENCRYPTION_KEY) != 44: @@ -1508,6 +1618,11 @@ async def get_user_role(self, user, user_data): elif isinstance(claim_data, int): oauth_roles = [str(claim_data)] + # Debug: Override roles if AAK_OAUTH_DEBUG_FORCE_ROLE is set + if AAK_OAUTH_DEBUG_FORCE_ROLE: + oauth_roles = [r.strip() for r in AAK_OAUTH_DEBUG_FORCE_ROLE.split(",") if r.strip()] + log.warning(f'AAK_OAUTH_DEBUG_FORCE_ROLE is set, overriding oauth_roles to: {oauth_roles}') + log.debug(f'Oauth Roles claim: {oauth_claim}') log.debug(f'User roles from oauth: {oauth_roles}') log.debug(f'Accepted user roles: {oauth_allowed_roles}') @@ -1823,6 +1938,13 @@ async def handle_callback(self, request, provider, response, db=None): log.warning(f'OAuth callback failed, user data is missing: {token}') raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + # PATCH OIDC + # Set AAK role and groups + if AAK_OAUTH_ENABLE_ROLE_GROUPS_MAPPING: + user_data = set_aak_groups(user_data=user_data) + user_data = set_aak_role(user_data=user_data) + # //PATCH OIDC + # Extract the "sub" claim, using custom claim if configured if auth_config.OAUTH_SUB_CLAIM: sub = user_data.get(auth_config.OAUTH_SUB_CLAIM)