diff --git a/docs/docs/configuration/authentication.md b/docs/docs/configuration/authentication.md index 474998263..38602a497 100644 --- a/docs/docs/configuration/authentication.md +++ b/docs/docs/configuration/authentication.md @@ -166,6 +166,12 @@ In this example: - If no mapping matches, Frigate falls back to `default_role` if configured. - If `role_map` is not defined, Frigate assumes the role header directly contains `admin`, `viewer`, or a custom role name. +**Note on matching semantics:** + +- Admin precedence: if the `admin` mapping matches , Frigate resolves the session to `admin` to avoid accidental downgrade + when a user belongs to multiple groups (for example both admin and viewer + groups). + #### Port Considerations **Authenticated Port (8971)** diff --git a/frigate/api/auth.py b/frigate/api/auth.py index bfb3b81a1..e0a6ec924 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -439,10 +439,11 @@ def resolve_role( Determine the effective role for a request based on proxy headers and configuration. Order of resolution: - 1. If a role header is defined in proxy_config.header_map.role: - - If a role_map is configured, treat the header as group claims - (split by proxy_config.separator) and map to roles. - - If no role_map is configured, treat the header as role names directly. + 1. If a role header is defined in proxy_config.header_map.role: + - If a role_map is configured, treat the header as group claims + (split by proxy_config.separator) and map to roles. + Admin matches short-circuit to admin. + - If no role_map is configured, treat the header as role names directly. 2. If no valid role is found, return proxy_config.default_role if it's valid in config_roles, else 'viewer'. Args: @@ -492,6 +493,12 @@ def resolve_role( } logger.debug("Matched roles from role_map: %s", matched_roles) + # If admin matches, prioritize it to avoid accidental downgrade when + # users belong to both admin and lower-privilege groups. + if "admin" in matched_roles and "admin" in config_roles: + logger.debug("Resolved role (with role_map) to 'admin'.") + return "admin" + if matched_roles: resolved = next( (r for r in config_roles if r in matched_roles), validated_default diff --git a/frigate/test/test_proxy_auth.py b/frigate/test/test_proxy_auth.py index 61955486a..2ffad957c 100644 --- a/frigate/test/test_proxy_auth.py +++ b/frigate/test/test_proxy_auth.py @@ -31,6 +31,21 @@ class TestProxyRoleResolution(unittest.TestCase): role = resolve_role(headers, self.proxy_config, self.config_roles) self.assertEqual(role, "admin") + def test_role_map_or_matching(self): + config = self.proxy_config + config.header_map.role_map = { + "admin": ["group_admin", "group_privileged"], + } + + # OR semantics: a single matching group should map to the role + headers = {"x-remote-role": "group_admin"} + role = resolve_role(headers, config, self.config_roles) + self.assertEqual(role, "admin") + + headers = {"x-remote-role": "group_admin|group_privileged"} + role = resolve_role(headers, config, self.config_roles) + self.assertEqual(role, "admin") + def test_direct_role_header_with_separator(self): config = self.proxy_config config.header_map.role_map = None # disable role_map