feat(auth): support excluding roles with ^ prefix (#4959)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ac6d4c6ff3
commit
bed7e7a226
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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},
|
||||
|
||||
Reference in New Issue
Block a user