mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-09-27 03:46:15 +08:00
Add role map support for proxy auth (#19758)
* update config * add role map support * docs
This commit is contained in:
@@ -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.
|
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:
|
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.
|
- `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.
|
- `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
|
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 Frigate’s 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
|
#### Port Considerations
|
||||||
|
|
||||||
**Authenticated Port (8971)**
|
**Authenticated Port (8971)**
|
||||||
|
@@ -88,7 +88,13 @@ proxy:
|
|||||||
# See the docs for more info.
|
# See the docs for more info.
|
||||||
header_map:
|
header_map:
|
||||||
user: x-forwarded-user
|
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
|
# Optional: Url for logging out a user. This sets the location of the logout url in
|
||||||
# the UI.
|
# the UI.
|
||||||
logout_url: /api/logout
|
logout_url: /api/logout
|
||||||
|
@@ -217,15 +217,23 @@ def require_role(required_roles: List[str]):
|
|||||||
if not roles:
|
if not roles:
|
||||||
raise HTTPException(status_code=403, detail="Role not provided")
|
raise HTTPException(status_code=403, detail="Role not provided")
|
||||||
|
|
||||||
# Check if any role matches required_roles
|
# enforce VALID_ROLES
|
||||||
if not any(role in required_roles for role in roles):
|
valid_roles = [r for r in roles if r in VALID_ROLES]
|
||||||
|
if not valid_roles:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=403,
|
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
|
if not any(role in required_roles for role in valid_roles):
|
||||||
return next((role for role in roles if role in required_roles), roles[0])
|
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
|
return role_checker
|
||||||
|
|
||||||
@@ -266,22 +274,38 @@ def auth(request: Request):
|
|||||||
else "anonymous"
|
else "anonymous"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# start with default_role
|
||||||
|
role = proxy_config.default_role
|
||||||
|
|
||||||
|
# first try: explicit role header
|
||||||
role_header = proxy_config.header_map.role
|
role_header = proxy_config.header_map.role
|
||||||
role = (
|
if role_header:
|
||||||
request.headers.get(role_header, default=proxy_config.default_role)
|
raw_value = request.headers.get(role_header, "")
|
||||||
if role_header
|
if proxy_config.header_map.role_map and raw_value:
|
||||||
else proxy_config.default_role
|
# treat as group claim
|
||||||
)
|
groups = [
|
||||||
|
g.strip()
|
||||||
# if comma-separated with "admin", use "admin",
|
for g in raw_value.replace(" ", ",").split(",")
|
||||||
# if comma-separated with "viewer", use "viewer",
|
if g.strip()
|
||||||
# else use default role
|
]
|
||||||
|
for (
|
||||||
roles = [r.strip() for r in role.split(proxy_config.separator)] if role else []
|
candidate_role,
|
||||||
success_response.headers["remote-role"] = next(
|
required_groups,
|
||||||
(r for r in VALID_ROLES if r in roles), proxy_config.default_role
|
) 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
|
return success_response
|
||||||
|
|
||||||
# now apply authentication
|
# now apply authentication
|
||||||
|
@@ -16,6 +16,10 @@ class HeaderMappingConfig(FrigateBaseModel):
|
|||||||
default=None,
|
default=None,
|
||||||
title="Header name from upstream proxy to identify user role.",
|
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):
|
class ProxyConfig(FrigateBaseModel):
|
||||||
|
Reference in New Issue
Block a user