Skip to content

Commit d312871

Browse files
authored
feat: allows filters to be set at user level (#3456)
1 parent 5a5a0d3 commit d312871

File tree

12 files changed

+170
-49
lines changed

12 files changed

+170
-49
lines changed

docs/guide/agent.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,23 @@ services:
117117

118118
This will change the agent's name to `my-special-name` and will be reflected on the UI when connecting to the agent.
119119

120+
## Setting Up Filters
121+
122+
You can set up filters for the agent to limit the containers it can access. These filters are passed directly to Docker, restricting what Dozzle can view.
123+
124+
```yaml
125+
services:
126+
dozzle-agent:
127+
image: amir20/dozzle:latest
128+
command: agent
129+
environment:
130+
- DOZZLE_FILTER=label=color
131+
volumes:
132+
- /var/run/docker.sock:/var/run/docker.sock:ro
133+
```
134+
135+
This will restrict the agent to displaying only containers with the label `color`. Keep in mind that these filters are combined with the UI filters to narrow down the containers. To learn more about the different types of filters, read the [filters documentation](/guide/filters#ui-agents-and-user-filters).
136+
120137
## Custom Certificates
121138

122139
By default, Dozzle uses self-signed certificates for communication between agents. This is a private certificate which is only valid to other Dozzle instances. This is secure and recommended for most use cases. However, if Dozzle is exposed externally and an attacker knows exactly which port the agent is running on, then they can set up their own Dozzle instance and connect to the agent. To prevent this, you can provide your own certificates.
@@ -169,5 +186,6 @@ Agents are similar to remote connections, but they have some advantages. General
169186
| Permissions | Full access to Docker | Can be controlled with a proxy |
170187
| Reconnect | Automatically reconnects | Requires UI restart |
171188
| Healthcheck | Built-in healthcheck | No healthcheck |
189+
| Filters | Supports filters | No support for filters |
172190

173191
If you do plan to use remote connections, make sure to secure the connection using Docker TLS or a reverse proxy.

docs/guide/authentication.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: Authentication
33
---
44

5-
# Setting Up Authentication
5+
# Setting Up Authentication <Badge type="tip" text="Updated" />
66

77
Dozzle supports two configurations for authentication. In the first configuration, you bring your own authentication method by protecting Dozzle through a proxy. Dozzle can read appropriate headers out of the box.
88

@@ -22,6 +22,7 @@ users:
2222
name: Admin
2323
# Generate with docker run amir20/dozzle generate --name Admin --email me@email.net --password secret admin
2424
password: $2a$11$9ho4vY2LdJ/WBopFcsAS0uORC0x2vuFHQgT/yBqZyzclhHsoaIkzK
25+
filter:
2526
```
2627
2728
Dozzle uses `email` to generate avatars using [Gravatar](https://gravatar.com/). It is optional. The password is hashed using `bcrypt` which can be generated using `docker run amir20/dozzle generate`.
@@ -90,15 +91,39 @@ services:
9091

9192
Note that only duration is supported. You can only use `s`, `m`, `h` for seconds, minutes and hours respectively.
9293

94+
### Setting specific filters for users
95+
96+
Dozzle supports setting filters for users. Filters are used to restrict the containers that a user can see. Filters are set in the `users.yml` file. Here is an example:
97+
98+
```yaml
99+
users:
100+
admin:
101+
email:
102+
name: Admin
103+
password: $2a$11$9ho4vY2LdJ/WBopFcsAS0uORC0x2vuFHQgT/yBqZyzclhHsoaIkzK
104+
filter:
105+
106+
guest:
107+
email:
108+
name: Guest
109+
password: $2a$11$9ho4vY2LdJ/WBopFcsAS0uORC0x2vuFHQgT/yBqZyzclhHsoaIkzK
110+
filter: "label=com.example.app"
111+
```
112+
113+
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.
114+
115+
> [!NOTE]
116+
> 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.
117+
93118
## Generating users.yml
94119

95120
Dozzle has a built-in `generate` command to generate `users.yml`. Here is an example:
96121

97122
```sh
98-
docker run amir20/dozzle generate admin --password password --email test@email.net --name "John Doe" > users.yml
123+
docker run amir20/dozzle generate admin --password password --email test@email.net --name "John Doe" --user-filter name=foo > users.yml
99124
```
100125

101-
In this example, `admin` is the username. Email and name are optional but recommended to display accurate avatars. `docker run amir20/dozzle generate --help` displays all options.
126+
In this example, `admin` is the username. Email and name are optional but recommended to display accurate avatars. `docker run amir20/dozzle generate --help` displays all options. The `--user-filter` flag is a comma-separated list of filters.
102127

103128
## Forward Proxy
104129

@@ -129,6 +154,7 @@ In this mode, Dozzle expects the following headers:
129154
- `Remote-User` to map to the username e.g. `johndoe`
130155
- `Remote-Email` to map to the user's email address. This email is also used to find the right [Gravatar](https://gravatar.com/) for the user.
131156
- `Remote-Name` to be a display name like `John Doe`
157+
- `Remote-Filter` to be a comma-separated list of filters allowed for user.
132158

133159
### Setting up Dozzle with Authelia
134160

docs/guide/filters.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,16 @@ services:
2727
:::
2828
2929
Common filters are `name` or `label` to limit Dozzle's access to containers.
30+
31+
## UI, Agents, and User Filters <Badge type="tip" text="New" />
32+
33+
Dozzle supports multiple filters to limit the containers it can see. Filters can be set at the UI, agent, or user level.
34+
35+
1. **UI Filters**: These filters are applied to the Dozzle UI instance and sent to Docker to restrict the visible containers. They affect all agents and users who do not have their own filters.
36+
2. **Agent Filters**: These filters are set at the agent level and sent to Docker to limit the containers exposed by that agent. Agent filters and UI filters work together to restrict the containers.
37+
3. **User Filters**: These filters are set at the user level and determine which containers the user can see. If user filters are not defined, Dozzle defaults to using the UI filters.
38+
39+
For more information on setting filters for specific users, see [user filters](/guide/authentication#setting-specific-filters-for-users). For details on setting filters for agents, see [agent filters](/guide/agent#setting-up-filters).
40+
41+
> [!WARNING]
42+
> It is important to understand that multiple filters are combined to limit the containers. For example, if you set `--filter label=color` at the UI level and `--filter label=type` at the agent level, Dozzle will only display containers that have both the `color` and `type` labels.

internal/auth/proxy.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http"
88
"strings"
99

10+
"github.com/amir20/dozzle/internal/docker"
1011
"github.com/rs/zerolog/log"
1112
)
1213

@@ -15,9 +16,10 @@ type contextKey string
1516
const remoteUser contextKey = "remoteUser"
1617

1718
type proxyAuthContext struct {
18-
headerUser string
19-
headerEmail string
20-
headerName string
19+
headerUser string
20+
headerEmail string
21+
headerName string
22+
headerFilter string
2123
}
2224

2325
func hashEmail(email string) string {
@@ -28,18 +30,23 @@ func hashEmail(email string) string {
2830
return hex.EncodeToString(hash[:])
2931
}
3032

31-
func NewForwardProxyAuth(user, email, name string) *proxyAuthContext {
33+
func NewForwardProxyAuth(userHeader, emailHeader, nameHeader, filterHeader string) *proxyAuthContext {
3234
return &proxyAuthContext{
33-
headerUser: user,
34-
headerEmail: email,
35-
headerName: name,
35+
headerUser: userHeader,
36+
headerEmail: emailHeader,
37+
headerName: nameHeader,
38+
headerFilter: filterHeader,
3639
}
3740
}
3841

3942
func (p *proxyAuthContext) AuthMiddleware(next http.Handler) http.Handler {
4043
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
4144
if r.Header.Get(p.headerUser) != "" {
42-
user := newUser(r.Header.Get(p.headerUser), r.Header.Get(p.headerEmail), r.Header.Get(p.headerName))
45+
containerFilter, err := docker.ParseContainerFilter(r.Header.Get(p.headerFilter))
46+
if err != nil {
47+
log.Fatal().Str("filter", r.Header.Get(p.headerFilter)).Msg("Failed to parse container filter")
48+
}
49+
user := newUser(r.Header.Get(p.headerUser), r.Header.Get(p.headerEmail), r.Header.Get(p.headerName), containerFilter)
4350
ctx := context.WithValue(r.Context(), remoteUser, user)
4451
next.ServeHTTP(w, r.WithContext(ctx))
4552
} else {

internal/auth/simple.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func (a *simpleAuthContext) CreateToken(username, password string) (string, erro
3838
return "", ErrInvalidCredentials
3939
}
4040

41-
claims := map[string]interface{}{"username": user.Username, "email": user.Email, "name": user.Name}
41+
claims := map[string]interface{}{"username": user.Username, "email": user.Email, "name": user.Name, "filter": user.Filter}
4242
jwtauth.SetIssuedNow(claims)
4343

4444
if a.ttl > 0 {

internal/auth/users.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,20 @@ import (
1111
"os"
1212
"time"
1313

14+
"github.com/amir20/dozzle/internal/docker"
1415
"github.com/go-chi/jwtauth/v5"
1516
"github.com/rs/zerolog/log"
1617
"golang.org/x/crypto/bcrypt"
1718
"gopkg.in/yaml.v3"
1819
)
1920

2021
type User struct {
21-
Username string `json:"username" yaml:"-"`
22-
Email string `json:"email" yaml:"email"`
23-
Name string `json:"name" yaml:"name"`
24-
Password string `json:"-" yaml:"password"`
22+
Username string `json:"username" yaml:"-"`
23+
Email string `json:"email" yaml:"email"`
24+
Name string `json:"name" yaml:"name"`
25+
Password string `json:"-" yaml:"password"`
26+
Filter string `json:"-" yaml:"filter"`
27+
ContainerFilter docker.ContainerFilter `json:"-" yaml:"-"`
2528
}
2629

2730
func (u User) AvatarURL() string {
@@ -32,11 +35,12 @@ func (u User) AvatarURL() string {
3235
return fmt.Sprintf("https://gravatar.com/avatar/%s?d=https%%3A%%2F%%2Fui-avatars.com%%2Fapi%%2F/%s/128", hashEmail(u.Email), url.QueryEscape(name))
3336
}
3437

35-
func newUser(username, email, name string) User {
38+
func newUser(username, email, name string, filter docker.ContainerFilter) User {
3639
return User{
37-
Username: username,
38-
Email: email,
39-
Name: name,
40+
Username: username,
41+
Email: email,
42+
Name: name,
43+
ContainerFilter: filter,
4044
}
4145
}
4246

@@ -193,7 +197,15 @@ func UserFromContext(ctx context.Context) *User {
193197
}
194198
email := claims["email"].(string)
195199
name := claims["name"].(string)
196-
user := newUser(username, email, name)
200+
containerFilter := docker.ContainerFilter{}
201+
if filter, ok := claims["filter"].(string); ok {
202+
containerFilter, err = docker.ParseContainerFilter(filter)
203+
if err != nil {
204+
log.Fatal().Err(err).Str("filter", filter).Msg("Failed to parse container filter")
205+
}
206+
}
207+
208+
user := newUser(username, email, name, containerFilter)
197209
return &user
198210
}
199211
return nil

internal/docker/types.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package docker
33
import (
44
"fmt"
55
"math"
6+
"strings"
67
"time"
78

89
"github.com/amir20/dozzle/internal/utils"
@@ -44,6 +45,29 @@ type ContainerEvent struct {
4445

4546
type ContainerFilter map[string][]string
4647

48+
func ParseContainerFilter(commaValues string) (ContainerFilter, error) {
49+
filter := make(ContainerFilter)
50+
if commaValues == "" {
51+
return filter, nil
52+
}
53+
54+
for _, val := range strings.Split(commaValues, ",") {
55+
pos := strings.Index(val, "=")
56+
if pos == -1 {
57+
return nil, fmt.Errorf("invalid filter: %s", filter)
58+
}
59+
key := val[:pos]
60+
val := val[pos+1:]
61+
filter[key] = append(filter[key], val)
62+
}
63+
64+
return filter, nil
65+
}
66+
67+
func (f ContainerFilter) Exists() bool {
68+
return len(f) > 0
69+
}
70+
4771
func (f ContainerFilter) asArgs() filters.Args {
4872
filterArgs := filters.NewArgs()
4973
for key, values := range f {

internal/support/cli/args.go

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,29 @@ import (
1010
var Version = "head"
1111

1212
type Args struct {
13-
Addr string `arg:"env:DOZZLE_ADDR" default:":8080" help:"sets host:port to bind for server. This is rarely needed inside a docker container."`
14-
Base string `arg:"env:DOZZLE_BASE" default:"/" help:"sets the base for http router."`
15-
Hostname string `arg:"env:DOZZLE_HOSTNAME" help:"sets the hostname for display. This is useful with multiple Dozzle instances."`
16-
Level string `arg:"env:DOZZLE_LEVEL" default:"info" help:"set Dozzle log level. Use debug for more logging."`
17-
AuthProvider string `arg:"--auth-provider,env:DOZZLE_AUTH_PROVIDER" default:"none" help:"sets the auth provider to use. Currently only forward-proxy is supported."`
18-
AuthTTL string `arg:"--auth-ttl,env:DOZZLE_AUTH_TTL" default:"session" help:"sets the TTL for the auth token. Accepts duration values like 12h. Valid time units are s, m, h"`
19-
AuthHeaderUser string `arg:"--auth-header-user,env:DOZZLE_AUTH_HEADER_USER" default:"Remote-User" help:"sets the HTTP Header to use for username in Forward Proxy configuration."`
20-
AuthHeaderEmail string `arg:"--auth-header-email,env:DOZZLE_AUTH_HEADER_EMAIL" default:"Remote-Email" help:"sets the HTTP Header to use for email in Forward Proxy configuration."`
21-
AuthHeaderName string `arg:"--auth-header-name,env:DOZZLE_AUTH_HEADER_NAME" default:"Remote-Name" help:"sets the HTTP Header to use for name in Forward Proxy configuration."`
22-
EnableActions bool `arg:"--enable-actions,env:DOZZLE_ENABLE_ACTIONS" default:"false" help:"enables essential actions on containers from the web interface."`
23-
FilterStrings []string `arg:"env:DOZZLE_FILTER,--filter,separate" help:"filters docker containers using Docker syntax."`
24-
Filter map[string][]string `arg:"-"`
25-
RemoteHost []string `arg:"env:DOZZLE_REMOTE_HOST,--remote-host,separate" help:"list of hosts to connect remotely"`
26-
RemoteAgent []string `arg:"env:DOZZLE_REMOTE_AGENT,--remote-agent,separate" help:"list of agents to connect remotely"`
27-
NoAnalytics bool `arg:"--no-analytics,env:DOZZLE_NO_ANALYTICS" help:"disables anonymous analytics"`
28-
Mode string `arg:"env:DOZZLE_MODE" default:"server" help:"sets the mode to run in (server, swarm)"`
29-
TimeoutString string `arg:"--timeout,env:DOZZLE_TIMEOUT" default:"3s" help:"sets the timeout for docker client"`
30-
Timeout time.Duration `arg:"-"`
31-
Healthcheck *HealthcheckCmd `arg:"subcommand:healthcheck" help:"checks if the server is running"`
32-
Generate *GenerateCmd `arg:"subcommand:generate" help:"generates a configuration file for simple auth"`
33-
Agent *AgentCmd `arg:"subcommand:agent" help:"starts the agent"`
34-
AgentTest *AgentTestCmd `arg:"subcommand:agent-test" help:"tests an agent"`
13+
Addr string `arg:"env:DOZZLE_ADDR" default:":8080" help:"sets host:port to bind for server. This is rarely needed inside a docker container."`
14+
Base string `arg:"env:DOZZLE_BASE" default:"/" help:"sets the base for http router."`
15+
Hostname string `arg:"env:DOZZLE_HOSTNAME" help:"sets the hostname for display. This is useful with multiple Dozzle instances."`
16+
Level string `arg:"env:DOZZLE_LEVEL" default:"info" help:"set Dozzle log level. Use debug for more logging."`
17+
AuthProvider string `arg:"--auth-provider,env:DOZZLE_AUTH_PROVIDER" default:"none" help:"sets the auth provider to use. Currently only forward-proxy is supported."`
18+
AuthTTL string `arg:"--auth-ttl,env:DOZZLE_AUTH_TTL" default:"session" help:"sets the TTL for the auth token. Accepts duration values like 12h. Valid time units are s, m, h"`
19+
AuthHeaderUser string `arg:"--auth-header-user,env:DOZZLE_AUTH_HEADER_USER" default:"Remote-User" help:"sets the HTTP Header to use for username in Forward Proxy configuration."`
20+
AuthHeaderEmail string `arg:"--auth-header-email,env:DOZZLE_AUTH_HEADER_EMAIL" default:"Remote-Email" help:"sets the HTTP Header to use for email in Forward Proxy configuration."`
21+
AuthHeaderName string `arg:"--auth-header-name,env:DOZZLE_AUTH_HEADER_NAME" default:"Remote-Name" help:"sets the HTTP Header to use for name in Forward Proxy configuration."`
22+
AuthHeaderFilter string `arg:"--auth-header-filter,env:DOZZLE_AUTH_HEADER_FILTER" default:"Remote-Filter" help:"sets the HTTP Header to use for filtering in Forward Proxy configuration."`
23+
EnableActions bool `arg:"--enable-actions,env:DOZZLE_ENABLE_ACTIONS" default:"false" help:"enables essential actions on containers from the web interface."`
24+
FilterStrings []string `arg:"env:DOZZLE_FILTER,--filter,separate" help:"filters docker containers using Docker syntax."`
25+
Filter map[string][]string `arg:"-"`
26+
RemoteHost []string `arg:"env:DOZZLE_REMOTE_HOST,--remote-host,separate" help:"list of hosts to connect remotely"`
27+
RemoteAgent []string `arg:"env:DOZZLE_REMOTE_AGENT,--remote-agent,separate" help:"list of agents to connect remotely"`
28+
NoAnalytics bool `arg:"--no-analytics,env:DOZZLE_NO_ANALYTICS" help:"disables anonymous analytics"`
29+
Mode string `arg:"env:DOZZLE_MODE" default:"server" help:"sets the mode to run in (server, swarm)"`
30+
TimeoutString string `arg:"--timeout,env:DOZZLE_TIMEOUT" default:"3s" help:"sets the timeout for docker client"`
31+
Timeout time.Duration `arg:"-"`
32+
Healthcheck *HealthcheckCmd `arg:"subcommand:healthcheck" help:"checks if the server is running"`
33+
Generate *GenerateCmd `arg:"subcommand:generate" help:"generates a configuration file for simple auth"`
34+
Agent *AgentCmd `arg:"subcommand:agent" help:"starts the agent"`
35+
AgentTest *AgentTestCmd `arg:"subcommand:agent-test" help:"tests an agent"`
3536
}
3637

3738
type HealthcheckCmd struct {
@@ -50,6 +51,7 @@ type GenerateCmd struct {
5051
Password string `arg:"--password, -p" help:"sets the password for the user"`
5152
Name string `arg:"--name, -n" help:"sets the display name for the user"`
5253
Email string `arg:"--email, -e" help:"sets the email for the user"`
54+
Filter string `arg:"--user-filter" help:"sets the filter for the user. This can be a comma separated list of filters."`
5355
}
5456

5557
func (Args) Version() string {

internal/web/auth_proxy_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func Test_createRoutes_proxy_missing_headers(t *testing.T) {
2020
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/",
2121
Authorization: Authorization{
2222
Provider: FORWARD_PROXY,
23-
Authorizer: auth.NewForwardProxyAuth("Remote-User", "Remote-Email", "Remote-Name"),
23+
Authorizer: auth.NewForwardProxyAuth("Remote-User", "Remote-Email", "Remote-Name", "Remote-Filter"),
2424
},
2525
})
2626
req, err := http.NewRequest("GET", "/", nil)
@@ -39,7 +39,7 @@ func Test_createRoutes_proxy_happy(t *testing.T) {
3939
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/",
4040
Authorization: Authorization{
4141
Provider: FORWARD_PROXY,
42-
Authorizer: auth.NewForwardProxyAuth("Remote-User", "Remote-Email", "Remote-Name"),
42+
Authorizer: auth.NewForwardProxyAuth("Remote-User", "Remote-Email", "Remote-Name", "Remote-Filter"),
4343
},
4444
})
4545
req, err := http.NewRequest("GET", "/", nil)

0 commit comments

Comments
 (0)