Add role map support for proxy auth (#19758)

* update config

* add role map support

* docs
This commit is contained in:
Josh Hawkins 2025-08-25 17:58:41 -05:00 committed by GitHub
parent ed9d031e80
commit 22e981c38c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 80 additions and 20 deletions

View File

@ -59,6 +59,7 @@ The default session length for user authentication in Frigate is 24 hours. This
While the default provides a balance of security and convenience, you can customize this duration to suit your specific security requirements and user experience preferences. The session length is configured in seconds.
The default value of `86400` will expire the authentication session after 24 hours. Some other examples:
- `0`: Setting the session length to 0 will require a user to log in every time they access the application or after a very short, immediate timeout.
- `604800`: Setting the session length to 604800 will require a user to log in if the token is not refreshed for 7 days.
@ -133,6 +134,31 @@ proxy:
default_role: viewer
```
## Role mapping
In some environments, upstream identity providers (OIDC, SAML, LDAP, etc.) do not pass a Frigate-compatible role directly, but instead pass one or more group claims. To handle this, Frigate supports a `role_map` that translates upstream group names into Frigates internal roles (`admin` or `viewer`).
```yaml
proxy:
...
header_map:
user: x-forwarded-user
role: x-forwarded-groups
role_map:
admin:
- sysadmins
- access-level-security
viewer:
- camera-viewer
```
In this example:
- If the proxy passes a role header containing `sysadmins` or `access-level-security`, the user is assigned the `admin` role.
- If the proxy passes a role header containing `camera-viewer`, the user is assigned the `viewer` role.
- 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` or `viewer`.
#### Port Considerations
**Authenticated Port (8971)**

View File

@ -88,7 +88,13 @@ proxy:
# See the docs for more info.
header_map:
user: x-forwarded-user
role: x-forwarded-role
role: x-forwarded-groups
role_map:
admin:
- sysadmins
- access-level-security
viewer:
- camera-viewer
# Optional: Url for logging out a user. This sets the location of the logout url in
# the UI.
logout_url: /api/logout

View File

@ -217,15 +217,23 @@ def require_role(required_roles: List[str]):
if not roles:
raise HTTPException(status_code=403, detail="Role not provided")
# Check if any role matches required_roles
if not any(role in required_roles for role in roles):
# enforce VALID_ROLES
valid_roles = [r for r in roles if r in VALID_ROLES]
if not valid_roles:
raise HTTPException(
status_code=403,
detail=f"Role {', '.join(roles)} not authorized. Required: {', '.join(required_roles)}",
detail=f"No valid roles found in {roles}. Required: {', '.join(required_roles)}",
)
# Return the first matching role
return next((role for role in roles if role in required_roles), roles[0])
if not any(role in required_roles for role in valid_roles):
raise HTTPException(
status_code=403,
detail=f"Role {', '.join(valid_roles)} not authorized. Required: {', '.join(required_roles)}",
)
return next(
(role for role in valid_roles if role in required_roles), valid_roles[0]
)
return role_checker
@ -266,22 +274,38 @@ def auth(request: Request):
else "anonymous"
)
# start with default_role
role = proxy_config.default_role
# first try: explicit role header
role_header = proxy_config.header_map.role
role = (
request.headers.get(role_header, default=proxy_config.default_role)
if role_header
else proxy_config.default_role
)
# if comma-separated with "admin", use "admin",
# if comma-separated with "viewer", use "viewer",
# else use default role
roles = [r.strip() for r in role.split(proxy_config.separator)] if role else []
success_response.headers["remote-role"] = next(
(r for r in VALID_ROLES if r in roles), proxy_config.default_role
)
if role_header:
raw_value = request.headers.get(role_header, "")
if proxy_config.header_map.role_map and raw_value:
# treat as group claim
groups = [
g.strip()
for g in raw_value.replace(" ", ",").split(",")
if g.strip()
]
for (
candidate_role,
required_groups,
) in proxy_config.header_map.role_map.items():
if any(group in groups for group in required_groups):
role = candidate_role
break
elif raw_value:
normalized_role = raw_value.strip().lower()
if normalized_role in VALID_ROLES:
role = normalized_role
else:
logger.warning(
f"Provided proxy role header contains invalid value '{raw_value}'. Using default role '{proxy_config.default_role}'."
)
role = proxy_config.default_role
success_response.headers["remote-role"] = role
return success_response
# now apply authentication

View File

@ -16,6 +16,10 @@ class HeaderMappingConfig(FrigateBaseModel):
default=None,
title="Header name from upstream proxy to identify user role.",
)
role_map: Optional[dict[str, list[str]]] = Field(
default_factory=dict,
title=("Mapping of Frigate roles to upstream group values. "),
)
class ProxyConfig(FrigateBaseModel):