feat(auth): support excluding roles with ^ prefix (#4959)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Amir Raminfar
2026-08-28 13:58:01 +00:00
committed by GitHub
co-authored by Claude Opus 5
parent ac6d4c6ff3
commit bed7e7a226
3 changed files with 61 additions and 14 deletions
+16 -6
View File
@@ -173,16 +173,26 @@ In this example, the `admin` user has no roles specified, so they have full acce
Dozzle supports the following roles:
- **shell** - allows attach and exec in the container
- **actions** - allows performing container actions (start, stop, restart)
- **download** - allows downloading container logs
- **notifications** - allows creating and editing notification rules and destinations
- **none** - denies all actions
- **all** - allows all actions (default)
| Role | Also accepted | Grants |
| --------------- | ---------------------- | ----------------------------------------------------------------------------------------- |
| `shell` | `dozzle_shell` | Attach to a container and open an exec session. The instance also needs `--enable-shell`. |
| `actions` | `dozzle_actions` | Start, stop and restart containers. The instance also needs `--enable-actions`. |
| `download` | `dozzle_download` | Download container logs as a file. |
| `notifications` | `dozzle_notifications` | Create and edit notification rules and destinations. |
| `all` | `dozzle_all` | Every role above. This is the default when `roles` is empty. |
| `none` | `dozzle_none` | No roles. Logs are still viewable, subject to the user's filter. Overrides anything else. |
Roles are separated by commas or pipes (`shell,actions` or `shell|actions`), and a JSON array works too (`["shell", "actions"]`). Names are case insensitive. The `dozzle_` prefixed aliases exist so group names from an identity provider can be passed through unchanged in forward proxy mode.
> [!WARNING]
> Notification rules are instance wide. A rule matches containers by expression, not by the user's filter, so a user with the `notifications` role can create a rule for containers their filter otherwise hides and receive those log lines at a destination they control. Only grant it to users you trust with every container on the instance.
Any role can be prefixed with `^` to exclude it. Exclusions are applied last, so order doesn't matter:
```yaml
roles: all,^shell # everything except shell
```
## <Icon icon="mdi:file-document-edit-outline" inline /> Generating users.yml
Dozzle has a built-in `generate` command to generate `users.yml`. Here is an example:
+29 -8
View File
@@ -20,8 +20,11 @@ const (
const All = Shell | Actions | Download | Notifications
// ParseRole parses a comma-separated string of roles and returns the corresponding Role.
// Roles prefixed with ^ are excluded after all other roles are applied, so "all,^shell"
// grants everything except shell.
func ParseRole(input string) Role {
var roles Role
var excluded Role
var parts []string
// Check if input is valid JSON
@@ -43,24 +46,42 @@ func ParseRole(input string) Role {
for _, r := range parts {
role := strings.TrimSpace(strings.ToLower(r))
negated := strings.HasPrefix(role, "^")
if negated {
role = strings.TrimSpace(strings.TrimPrefix(role, "^"))
}
var bits Role
switch role {
case "shell", "dozzle_shell":
roles |= Shell
bits = Shell
case "actions", "dozzle_actions":
roles |= Actions
bits = Actions
case "download", "dozzle_download":
roles |= Download
bits = Download
case "notifications", "dozzle_notifications":
roles |= Notifications
case "none", "dozzle_none":
return None
bits = Notifications
case "all", "dozzle_all":
return All
bits = All
case "none", "dozzle_none":
if negated {
log.Debug().Str("role", role).Msg("none cannot be negated")
continue
}
return None
default:
log.Debug().Str("role", role).Msg("invalid role")
continue
}
if negated {
excluded |= bits
} else {
roles |= bits
}
}
return roles
return roles &^ excluded
}
func (roles Role) Has(role Role) bool {
+16
View File
@@ -67,6 +67,22 @@ func TestParseRole(t *testing.T) {
{"Dozzle_none overrides others", "dozzle_shell,dozzle_none,dozzle_actions", None},
{"Dozzle_all overrides others", "dozzle_shell,dozzle_all,dozzle_actions", All},
// Negated roles
{"All except shell", "all,^shell", All &^ Shell},
{"All except shell with pipe", "all | ^shell", All &^ Shell},
{"All except two", "all,^shell,^actions", All &^ (Shell | Actions)},
{"Negate everything", "all,^all", None},
{"Negation order does not matter", "^shell,all", All &^ Shell},
{"Negate unset role", "shell,^actions", Shell},
{"Negate with spaces", "all, ^ shell ", All &^ Shell},
{"Negated dozzle prefixed role", "dozzle_all,^dozzle_shell", All &^ Shell},
{"Negated uppercase", "ALL,^SHELL", All &^ Shell},
{"Negated invalid role", "all,^invalid", All},
{"Negated none is ignored", "all,^none", All},
{"None still wins over negation", "all,^shell,none", None},
{"All except notifications", "all,^notifications", All &^ Notifications},
{"JSON with negation", `["all", "^shell"]`, All &^ Shell},
// Invalid JSON
{"Invalid JSON format", `["shell"`, None},
{"Malformed JSON", `{shell: "test"}`, None},