Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,14 @@ means **two components may never share a basename**, at any depth.
- Three modes: none, simple (file-based users.yml), forward-proxy (e.g., Authelia)
- JWT tokens for simple auth with configurable TTL
- User file location: `./data/users.yml` or `./data/users.yaml`
- **Roles and filters are separate checks, and a new route needs both.** Roles say what a
user may do; a per-user `filter` says which containers. An empty `roles:` means `all`, so
holding every role never means admin. Per-container routes resolve through
`h.resolveLabels(r)`. Anything reaching past the containers a user can see (host-wide or
instance-wide actions, setup, settings, anything that hands out a credential) must also
refuse `h.restrictedUser(r)`, as `pruneHost` and `setupCanWrite` do. The `notifications`
and `cloud` roles are the documented, tested exceptions; don't add another without
documenting it in `docs/guide/authentication/simple.md`.

### Testing

Expand Down
4 changes: 3 additions & 1 deletion docs/de/guide/authentication/simple.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Einfache Authentifizierung
description: Richte die einfache Authentifizierung von Dozzle mit einer users.yml ein und lege mit Filtern und Rollen fest, welche Docker-Container jeder Benutzer sieht und was er tun darf.
sourceHash: 42be19df819b
sourceHash: 6669a5ecbae2
---

# <Icon icon="mdi:account-cog-outline" inline /> Einfache Authentifizierung
Expand Down Expand Up @@ -137,6 +137,8 @@ users:

In diesem Beispiel hat der Benutzer `admin` keinen Filter und sieht damit alle Container. Der Benutzer `guest` sieht nur Container mit dem Label `com.example.app`. Das ist nützlich, um den Zugriff auf bestimmte Container einzuschränken.

Ein Benutzer mit Filter kann unabhängig von seinen Rollen das Setup nicht ändern, weil es die gesamte Instanz betrifft.

> [!NOTE]
> Filter können auch [global](/de/guide/filters) mit dem Flag `--filter` gesetzt werden. Dieses Flag gilt für alle Benutzer. Hat ein Benutzer einen eigenen Filter, überschreibt dieser den globalen Filter.

Expand Down
4 changes: 3 additions & 1 deletion docs/es/guide/authentication/simple.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Autenticación simple
description: Configura la autenticación simple de Dozzle con un archivo users.yml y limita con filtros y roles qué contenedores Docker ve cada usuario y qué puede hacer con ellos.
sourceHash: 42be19df819b
sourceHash: 6669a5ecbae2
---

# <Icon icon="mdi:account-cog-outline" inline /> Autenticación simple
Expand Down Expand Up @@ -137,6 +137,8 @@ users:

En este ejemplo, el usuario `admin` no tiene filtro, así que ve todos los contenedores. El usuario `guest` solo ve los contenedores con la etiqueta `com.example.app`. Esto es útil para restringir el acceso a contenedores concretos.

Un usuario con un filtro no puede cambiar la configuración inicial, sean cuales sean sus roles, porque eso afecta a toda la instancia.

> [!NOTE]
> Los filtros también se pueden definir [globalmente](/es/guide/filters) con el flag `--filter`. Ese flag se aplica a todos los usuarios. Si un usuario tiene un filtro definido, este tiene prioridad sobre el global.

Expand Down
4 changes: 3 additions & 1 deletion docs/fr/guide/authentication/simple.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Authentification simple
description: Configurez l'authentification simple de Dozzle avec un fichier users.yml, et limitez par des filtres et des rôles les conteneurs Docker que voit chaque utilisateur et ce qu'il peut faire.
sourceHash: 42be19df819b
sourceHash: 6669a5ecbae2
---

# <Icon icon="mdi:account-cog-outline" inline /> Authentification simple
Expand Down Expand Up @@ -137,6 +137,8 @@ users:

Dans cet exemple, l'utilisateur `admin` n'a aucun filtre, il voit donc tous les conteneurs. L'utilisateur `guest` ne voit que les conteneurs portant le label `com.example.app`. C'est pratique pour restreindre l'accès à certains conteneurs.

Un utilisateur ayant un filtre ne peut pas modifier la configuration, quels que soient ses rôles, car cela affecte toute l'instance.

> [!NOTE]
> Les filtres peuvent aussi être définis [globalement](/fr/guide/filters) avec le flag `--filter`. Ce flag s'applique à tous les utilisateurs. Si un utilisateur a un filtre défini, celui-ci remplace le filtre global.

Expand Down
2 changes: 2 additions & 0 deletions docs/guide/authentication/simple.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ users:

In this example, the `admin` user has no filter, so they can see all containers. The `guest` user can only see containers with the label `com.example.app`. This is useful for restricting access to specific containers.

A user with a filter can't change setup, whatever their roles, because it affects the whole instance.

> [!NOTE]
> Filters can also be set [globally](/guide/filters) with the `--filter` flag. This flag is applied to all users. If a user has a filter set, it will override the global filter.

Expand Down
4 changes: 3 additions & 1 deletion docs/zh/guide/authentication/simple.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: 简单模式身份验证
description: 使用 users.yml 文件配置 Dozzle 的简单模式身份验证,并通过过滤器和角色限制每个用户能看到哪些 Docker 容器、能执行哪些操作。
sourceHash: 42be19df819b
sourceHash: 6669a5ecbae2
---

# <Icon icon="mdi:account-cog-outline" inline /> 简单模式身份验证
Expand Down Expand Up @@ -137,6 +137,8 @@ users:

在这个例子中,`admin` 用户没有过滤器,因此能看到所有容器。`guest` 用户只能看到带有 `com.example.app` 标签的容器。这样就能把访问限制在特定容器上。

设置了过滤器的用户无论拥有什么角色,都无法更改设置,因为这会影响整个实例。

> [!NOTE]
> 过滤器也可以用 `--filter` 参数[全局设置](/zh/guide/filters)。该参数对所有用户生效。如果某个用户设置了自己的过滤器,则会覆盖全局过滤器。

Expand Down
102 changes: 102 additions & 0 deletions internal/web/filtered_user_admin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package web

import (
"context"
"crypto/tls"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"testing/fstest"
"time"

"github.com/amir20/dozzle/internal/auth"
"github.com/amir20/dozzle/internal/hostservice"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// No Remote-Roles header means every role, the same as an empty roles key in
// users.yml, so the filter is all that separates these two.
var (
filteredUser = []string{"Remote-User", "guest", "Remote-Filter", "label=com.example.app"}
unfiltered = []string{"Remote-User", "amir"}
)

func proxyConfig() *Config {
return &Config{Base: "/", Mode: "server",
Authorization: Authorization{
Provider: FORWARD_PROXY,
Authorizer: auth.NewForwardProxyAuth("Remote-User", "Remote-Email", "Remote-Name", "Remote-Filter", "Remote-Roles"),
},
Setup: SetupConfig{StartedAt: time.Now()},
}
}

func proxyAgentsHandler(hosts *fakeAgentHosts) *chi.Mux {
manager := hostservice.NewRetriableClientManager(nil, nil, time.Second, tls.Certificate{})
hosts.MultiHostService = hostservice.NewMultiHostService(manager, time.Second)
return createRouter(&handler{
hostService: hosts,
content: fstest.MapFS{"index.html": {Data: []byte("index page")}},
config: proxyConfig(),
})
}

func TestSetup_FilteredUserCannotWrite(t *testing.T) {
setupTestEnv(t, true)
h := proxyAgentsHandler(&fakeAgentHosts{connected: map[string]string{}})

writes := []struct{ method, path, body string }{
{"POST", "/api/setup/agent-cert", ""},
{"PATCH", "/api/setup/config", `{"enableShell":true}`},
{"PATCH", "/api/setup/config", `{"autoPrune":{"mode":"daily","time":"03:00"}}`},
{"POST", "/api/setup/restart", ""},
{"POST", "/api/setup/agents", `{"address":"nas:7007"}`},
{"DELETE", "/api/setup/agents", `{"endpoint":"nas:7007"}`},
}
for _, write := range writes {
rr := doSetup(h, write.method, write.path, write.body, filteredUser...)
assert.Equal(t, http.StatusForbidden, rr.Code, "%s %s %s", write.method, write.path, write.body)
assert.NotContains(t, rr.Body.String(), "PRIVATE KEY")
}

// Writing the roles out changes nothing: the filter is what disqualifies.
explicit := append([]string{"Remote-Roles", "all"}, filteredUser...)
assert.Equal(t, http.StatusForbidden, doSetup(h, "POST", "/api/setup/agent-cert", "", explicit...).Code)

rr := doSetup(h, "GET", "/api/setup", "", filteredUser...)
require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
var state setupState
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &state))
assert.False(t, state.CanWrite)
assert.False(t, state.CanWriteUpdates)
}

func TestSetup_UnfilteredUserStillWrites(t *testing.T) {
setupTestEnv(t, true)
h := proxyAgentsHandler(&fakeAgentHosts{connected: map[string]string{}})

rr := doSetup(h, "POST", "/api/setup/agent-cert", "", unfiltered...)
require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
var pair setupAgentCertResponse
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &pair))
assert.Contains(t, pair.Key, "PRIVATE KEY")

assert.Equal(t, http.StatusNoContent, doSetup(h, "PATCH", "/api/setup/config", `{"enableShell":true}`, unfiltered...).Code)
}

func Test_setupCanWrite_EveryLoginProvider(t *testing.T) {
for _, provider := range []AuthProvider{SIMPLE, OIDC, FORWARD_PROXY} {
h := &handler{config: &Config{Authorization: Authorization{Provider: provider}}}
canWrite := func(user auth.User) bool {
req := httptest.NewRequest(http.MethodPost, "/api/setup/agent-cert", nil)
return h.setupCanWrite(req.WithContext(auth.WithUser(context.Background(), user)))
}

assert.True(t, canWrite(auth.User{Roles: auth.All}), string(provider))
assert.False(t, canWrite(auth.User{Roles: auth.All, ContainerLabels: devLabels}), string(provider))
assert.False(t, canWrite(auth.User{Roles: auth.Actions}), string(provider))
}
}
7 changes: 4 additions & 3 deletions internal/web/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,10 @@ func (h *handler) setupCanWrite(r *http.Request) bool {
return h.setupWindowOpen()
}
user := auth.UserFromContext(r.Context())
// Role.Has matches any bit, so compare the whole mask: setup is for
// someone who holds every role.
return user != nil && user.Roles&auth.All == auth.All
// Setup is for someone who holds every role and sees every container. An
// empty roles key means all, so the roles alone can't tell an admin from a
// user confined by a filter, and setup applies to the whole instance.
return user != nil && user.Roles&auth.All == auth.All && !h.restrictedUser(r)
}

// setupCanWriteUpdates is setupCanWrite for the auto-update settings. Without
Expand Down
Loading