1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-21 21:33:18 +01:00

feat: Add user roles (#4133)

This commit is contained in:
Dmitry Mazurov
2025-09-22 00:51:16 +03:00
committed by GitHub
parent 54799b420c
commit 5f24657b41
17 changed files with 207 additions and 79 deletions

View File

@@ -15,7 +15,7 @@
<KeyShortcut char="k" :modifiers="['shift', 'meta']" /> <KeyShortcut char="k" :modifiers="['shift', 'meta']" />
</a> </a>
</li> </li>
<li> <li v-if="enableDownload">
<a :href="downloadUrl" download> <octicon:download-24 /> {{ $t("toolbar.download") }} </a> <a :href="downloadUrl" download> <octicon:download-24 /> {{ $t("toolbar.download") }} </a>
</li> </li>
<li v-if="!historical"> <li v-if="!historical">
@@ -167,7 +167,7 @@ import LogAnalytics from "../LogViewer/LogAnalytics.vue";
import Terminal from "@/components/Terminal.vue"; import Terminal from "@/components/Terminal.vue";
const { showSearch } = useSearchFilter(); const { showSearch } = useSearchFilter();
const { enableActions, enableShell } = config; const { enableActions, enableShell, enableDownload } = config;
const { streamConfig, hasComplexLogs, levels } = useLoggingContext(); const { streamConfig, hasComplexLogs, levels } = useLoggingContext();
const showDrawer = useDrawer(); const showDrawer = useDrawer();

View File

@@ -15,7 +15,7 @@
<KeyShortcut char="k" :modifiers="['shift', 'meta']" /> <KeyShortcut char="k" :modifiers="['shift', 'meta']" />
</a> </a>
</li> </li>
<li> <li v-if="enableDownload">
<a :href="downloadUrl" download> <octicon:download-24 /> {{ $t("toolbar.download") }} </a> <a :href="downloadUrl" download> <octicon:download-24 /> {{ $t("toolbar.download") }} </a>
</li> </li>
<li> <li>
@@ -88,7 +88,7 @@
<script lang="ts" setup> <script lang="ts" setup>
const { showSearch } = useSearchFilter(); const { showSearch } = useSearchFilter();
const { enableDownload } = config;
const clear = defineEmit(); const clear = defineEmit();
const { streamConfig, showHostname, showContainerName, containers } = useLoggingContext(); const { streamConfig, showHostname, showContainerName, containers } = useLoggingContext();

View File

@@ -12,6 +12,7 @@ export interface Config {
authProvider: "simple" | "none" | "forward-proxy"; authProvider: "simple" | "none" | "forward-proxy";
enableActions: boolean; enableActions: boolean;
enableShell: boolean; enableShell: boolean;
enableDownload: boolean;
disableAvatars: boolean; disableAvatars: boolean;
releaseCheckMode: "automatic" | "manual"; releaseCheckMode: "automatic" | "manual";
user?: { user?: {

View File

@@ -28,6 +28,7 @@ users:
# Generate with docker run -it --rm amir20/dozzle generate --name Admin --email me@email.net --password secret admin # Generate with docker run -it --rm amir20/dozzle generate --name Admin --email me@email.net --password secret admin
password: $2a$11$9ho4vY2LdJ/WBopFcsAS0uORC0x2vuFHQgT/yBqZyzclhHsoaIkzK password: $2a$11$9ho4vY2LdJ/WBopFcsAS0uORC0x2vuFHQgT/yBqZyzclhHsoaIkzK
filter: filter:
roles:
``` ```
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`. 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`.
@@ -139,15 +140,43 @@ In this example, the `admin` user has no filter, so they can see all containers.
> [!NOTE] > [!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. > 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.
### Setting specific roles for users
Dozzle allows assigning roles to users. Roles define what actions a user can perform on containers. Roles are configured in the users.yml file.
```yaml
users:
admin:
email:
name: Admin
password: $2a$11$9ho4vY2LdJ/WBopFcsAS0uORC0x2vuFHQgT/yBqZyzclhHsoaIkzK
roles:
guest:
email:
name: Guest
password: $2a$11$9ho4vY2LdJ/WBopFcsAS0uORC0x2vuFHQgT/yBqZyzclhHsoaIkzK
roles: shell
```
In this example, the `admin` user has no roles specified, so they have full access to all container actions. The `guest` user has the shell role, meaning they can only open a shell in the containers. Roles make it easy to control and restrict what users can do in Dozzle.
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.
- **none** - denies all actions.
## Generating users.yml ## Generating users.yml
Dozzle has a built-in `generate` command to generate `users.yml`. Here is an example: Dozzle has a built-in `generate` command to generate `users.yml`. Here is an example:
```sh ```sh
docker run -it --rm amir20/dozzle generate admin --password password --email test@email.net --name "John Doe" --user-filter name=foo > users.yml docker run -it --rm amir20/dozzle generate admin --password password --email test@email.net --name "John Doe" --user-filter name=foo --user-roles shell > users.yml
``` ```
In this example, `admin` is the username. Email and name are optional but recommended to display accurate avatars. `docker run -it --rm amir20/dozzle generate --help` displays all options. The `--user-filter` flag is a comma-separated list of filters. In this example, `admin` is the username. Email and name are optional but recommended to display accurate avatars. `docker run -it --rm amir20/dozzle generate --help` displays all options. The `--user-filter` flag is a comma-separated list of filters. The `--user-roles` flag is a comma-separated list of roles.
## Forward Proxy ## Forward Proxy
@@ -179,6 +208,7 @@ In this mode, Dozzle expects the following headers:
- `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. - `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.
- `Remote-Name` to be a display name like `John Doe` - `Remote-Name` to be a display name like `John Doe`
- `Remote-Filter` to be a comma-separated list of filters allowed for user. - `Remote-Filter` to be a comma-separated list of filters allowed for user.
- `Remote-Roles` to be a comma-separated list of roles allowed for user.
### Setting up Dozzle with Authelia ### Setting up Dozzle with Authelia

View File

@@ -7,7 +7,7 @@ title: Environment Variables and Subcommands
Configurations can be done with flags or environment variables. The table below outlines all supported options and their respective env vars. Configurations can be done with flags or environment variables. The table below outlines all supported options and their respective env vars.
| Flag | Env Variable | Default | | Flag | Env Variable | Default |
| ---------------------- | --------------------------- | -------------- | |------------------------|-----------------------------|-----------------|
| `--addr` | `DOZZLE_ADDR` | `:8080` | | `--addr` | `DOZZLE_ADDR` | `:8080` |
| `--base` | `DOZZLE_BASE` | `/` | | `--base` | `DOZZLE_BASE` | `/` |
| `--hostname` | `DOZZLE_HOSTNAME` | `""` | | `--hostname` | `DOZZLE_HOSTNAME` | `""` |
@@ -16,6 +16,8 @@ Configurations can be done with flags or environment variables. The table below
| `--auth-header-user` | `DOZZLE_AUTH_HEADER_USER` | `Remote-User` | | `--auth-header-user` | `DOZZLE_AUTH_HEADER_USER` | `Remote-User` |
| `--auth-header-email` | `DOZZLE_AUTH_HEADER_EMAIL` | `Remote-Email` | | `--auth-header-email` | `DOZZLE_AUTH_HEADER_EMAIL` | `Remote-Email` |
| `--auth-header-name` | `DOZZLE_AUTH_HEADER_NAME` | `Remote-Name` | | `--auth-header-name` | `DOZZLE_AUTH_HEADER_NAME` | `Remote-Name` |
| `--auth-header-filter` | `DOZZLE_AUTH_HEADER_FILTER` | `Remote-Filter` |
| `--auth-header-roles` | `DOZZLE_AUTH_HEADER_ROLES` | `Remote-Roles` |
| `--enable-actions` | `DOZZLE_ENABLE_ACTIONS` | `false` | | `--enable-actions` | `DOZZLE_ENABLE_ACTIONS` | `false` |
| `--enable-shell` | `DOZZLE_ENABLE_SHELL` | `false` | | `--enable-shell` | `DOZZLE_ENABLE_SHELL` | `false` |
| `--disable-avatars` | `DOZZLE_DISABLE_AVATARS` | `false` | | `--disable-avatars` | `DOZZLE_DISABLE_AVATARS` | `false` |
@@ -36,16 +38,18 @@ Configurations can be done with flags or environment variables. The table below
Dozzle supports generating `users.yml` file. This file is used to authenticate users. Here is an example: Dozzle supports generating `users.yml` file. This file is used to authenticate users. Here is an example:
```sh ```sh
docker run -it --rm amir20/dozzle generate admin --password password --email test@email.net --name "John Doe" > users.yml docker run -it --rm amir20/dozzle generate admin --password password --email test@email.net --name "John Doe" --user-filter name=foo --user-roles shell > users.yml
``` ```
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. 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.
| Flag | Description | Default | | Flag | Description | Default |
| ------------ | ---------------- | ------- | |-----------------|------------------| ------- |
| `--password` | User's password | | | `--password` | User's password | |
| `--email` | User's email | | | `--email` | User's email | |
| `--name` | User's full name | | | `--name` | User's full name | |
| `--user-filter` | User's filters | |
| `--user-roles` | User's roles | |
See [authentication](/guide/authentication) for more information. See [authentication](/guide/authentication) for more information.

View File

@@ -20,6 +20,7 @@ type proxyAuthContext struct {
headerEmail string headerEmail string
headerName string headerName string
headerFilter string headerFilter string
headerRoles string
} }
func hashEmail(email string) string { func hashEmail(email string) string {
@@ -30,12 +31,13 @@ func hashEmail(email string) string {
return hex.EncodeToString(hash[:]) return hex.EncodeToString(hash[:])
} }
func NewForwardProxyAuth(userHeader, emailHeader, nameHeader, filterHeader string) *proxyAuthContext { func NewForwardProxyAuth(userHeader, emailHeader, nameHeader, filterHeader, rolesHeader string) *proxyAuthContext {
return &proxyAuthContext{ return &proxyAuthContext{
headerUser: userHeader, headerUser: userHeader,
headerEmail: emailHeader, headerEmail: emailHeader,
headerName: nameHeader, headerName: nameHeader,
headerFilter: filterHeader, headerFilter: filterHeader,
headerRoles: rolesHeader,
} }
} }
@@ -46,7 +48,8 @@ func (p *proxyAuthContext) AuthMiddleware(next http.Handler) http.Handler {
if err != nil { if err != nil {
log.Fatal().Str("filter", r.Header.Get(p.headerFilter)).Msg("Failed to parse container filter") log.Fatal().Str("filter", r.Header.Get(p.headerFilter)).Msg("Failed to parse container filter")
} }
user := newUser(r.Header.Get(p.headerUser), r.Header.Get(p.headerEmail), r.Header.Get(p.headerName), containerFilter) userRoles := ParseRole(r.Header.Get(p.headerRoles))
user := newUser(r.Header.Get(p.headerUser), r.Header.Get(p.headerEmail), r.Header.Get(p.headerName), containerFilter, userRoles)
ctx := context.WithValue(r.Context(), remoteUser, user) ctx := context.WithValue(r.Context(), remoteUser, user)
next.ServeHTTP(w, r.WithContext(ctx)) next.ServeHTTP(w, r.WithContext(ctx))
} else { } else {

42
internal/auth/roles.go Normal file
View File

@@ -0,0 +1,42 @@
package auth
import (
"strings"
)
type Role int
const (
NONE Role = 0
Shell Role = 1 << iota
Actions
Download
)
const AllRole = Shell | Actions | Download
func ParseRole(commaValues string) Role {
if commaValues == "" {
return AllRole
}
var roles Role
for r := range strings.SplitSeq(commaValues, ",") {
role := strings.TrimSpace(strings.ToLower(r))
switch role {
case "shell":
roles |= Shell
case "actions":
roles |= Actions
case "download":
roles |= Download
case "none":
return NONE
}
}
return roles
}
func (roles Role) Has(role Role) bool {
return roles&role != 0
}

View File

@@ -38,7 +38,7 @@ func (a *simpleAuthContext) CreateToken(username, password string) (string, erro
return "", ErrInvalidCredentials return "", ErrInvalidCredentials
} }
claims := map[string]interface{}{"username": user.Username, "email": user.Email, "name": user.Name, "filter": user.Filter} claims := map[string]interface{}{"username": user.Username, "email": user.Email, "name": user.Name, "filter": user.Filter, "roles": user.RolesConfigured}
jwtauth.SetIssuedNow(claims) jwtauth.SetIssuedNow(claims)
if a.ttl > 0 { if a.ttl > 0 {

View File

@@ -24,7 +24,9 @@ type User struct {
Name string `json:"name" yaml:"name"` Name string `json:"name" yaml:"name"`
Password string `json:"-" yaml:"password"` Password string `json:"-" yaml:"password"`
Filter string `json:"-" yaml:"filter"` Filter string `json:"-" yaml:"filter"`
RolesConfigured string `json:"-" yaml:"roles"`
ContainerLabels container.ContainerLabels `json:"-" yaml:"-"` ContainerLabels container.ContainerLabels `json:"-" yaml:"-"`
Roles Role `json:"-" yaml:"-"`
} }
func (u User) AvatarURL() string { func (u User) AvatarURL() string {
@@ -35,12 +37,13 @@ func (u User) AvatarURL() string {
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)) 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))
} }
func newUser(username, email, name string, labels container.ContainerLabels) User { func newUser(username, email, name string, labels container.ContainerLabels, roles Role) User {
return User{ return User{
Username: username, Username: username,
Email: email, Email: email,
Name: name, Name: name,
ContainerLabels: labels, ContainerLabels: labels,
Roles: roles,
} }
} }
@@ -198,14 +201,18 @@ func UserFromContext(ctx context.Context) *User {
email := claims["email"].(string) email := claims["email"].(string)
name := claims["name"].(string) name := claims["name"].(string)
containerFilter := container.ContainerLabels{} containerFilter := container.ContainerLabels{}
roles := AllRole
if filter, ok := claims["filter"].(string); ok { if filter, ok := claims["filter"].(string); ok {
containerFilter, err = container.ParseContainerFilter(filter) containerFilter, err = container.ParseContainerFilter(filter)
if err != nil { if err != nil {
log.Fatal().Err(err).Str("filter", filter).Msg("Failed to parse container filter") log.Fatal().Err(err).Str("filter", filter).Msg("Failed to parse container filter")
} }
} }
if role, ok := claims["roles"].(string); ok {
roles = ParseRole(role)
}
user := newUser(username, email, name, containerFilter) user := newUser(username, email, name, containerFilter, roles)
return &user return &user
} }
return nil return nil

View File

@@ -21,6 +21,7 @@ type Args struct {
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."` 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."`
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."` 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."`
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."` 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."`
AuthHeaderRoles string `arg:"--auth-header-roles,env:DOZZLE_AUTH_HEADER_ROLES" default:"Remote-Roles" help:"sets the HTTP Header to use for roles in Forward Proxy configuration."`
EnableActions bool `arg:"--enable-actions,env:DOZZLE_ENABLE_ACTIONS" default:"false" help:"enables essential actions on containers from the web interface."` EnableActions bool `arg:"--enable-actions,env:DOZZLE_ENABLE_ACTIONS" default:"false" help:"enables essential actions on containers from the web interface."`
EnableShell bool `arg:"--enable-shell,env:DOZZLE_ENABLE_SHELL" default:"false" help:"enables shell access to containers from the web interface."` EnableShell bool `arg:"--enable-shell,env:DOZZLE_ENABLE_SHELL" default:"false" help:"enables shell access to containers from the web interface."`
DisableAvatars bool `arg:"--disable-avatars,env:DOZZLE_DISABLE_AVATARS" default:"false" help:"disables avatars for authenticated users."` DisableAvatars bool `arg:"--disable-avatars,env:DOZZLE_DISABLE_AVATARS" default:"false" help:"disables avatars for authenticated users."`

View File

@@ -16,6 +16,7 @@ type GenerateCmd struct {
Name string `arg:"--name, -n" help:"sets the display name for the user"` Name string `arg:"--name, -n" help:"sets the display name for the user"`
Email string `arg:"--email, -e" help:"sets the email for the user"` Email string `arg:"--email, -e" help:"sets the email for the user"`
Filter string `arg:"--user-filter" help:"sets the filter for the user. This can be a comma separated list of filters."` Filter string `arg:"--user-filter" help:"sets the filter for the user. This can be a comma separated list of filters."`
RolesConfigured string `arg:"--user-roles" help:"sets the roles for the user. This can be a comma separated list of roles."`
} }
func (g *GenerateCmd) Run(args Args, embeddedCerts embed.FS) error { func (g *GenerateCmd) Run(args Args, embeddedCerts embed.FS) error {
@@ -32,6 +33,7 @@ func (g *GenerateCmd) Run(args Args, embeddedCerts embed.FS) error {
Name: args.Generate.Name, Name: args.Generate.Name,
Email: args.Generate.Email, Email: args.Generate.Email,
Filter: args.Generate.Filter, Filter: args.Generate.Filter,
RolesConfigured: args.Generate.RolesConfigured,
}, true) }, true)
if _, err := os.Stdout.Write(buffer.Bytes()); err != nil { if _, err := os.Stdout.Write(buffer.Bytes()); err != nil {

View File

@@ -14,11 +14,19 @@ func (h *handler) containerActions(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id") id := chi.URLParam(r, "id")
userLabels := h.config.Labels userLabels := h.config.Labels
permit := true
if h.config.Authorization.Provider != NONE { if h.config.Authorization.Provider != NONE {
user := auth.UserFromContext(r.Context()) user := auth.UserFromContext(r.Context())
if user.ContainerLabels.Exists() { if user.ContainerLabels.Exists() {
userLabels = user.ContainerLabels userLabels = user.ContainerLabels
} }
permit = user.Roles.Has(auth.Actions)
}
if !permit {
log.Warn().Msg("user is not permitted to perform actions on container")
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
} }
containerService, err := h.hostService.FindContainer(hostKey(r), id, userLabels) containerService, err := h.hostService.FindContainer(hostKey(r), id, userLabels)

View File

@@ -20,7 +20,7 @@ func Test_createRoutes_proxy_missing_headers(t *testing.T) {
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/", handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/",
Authorization: Authorization{ Authorization: Authorization{
Provider: FORWARD_PROXY, Provider: FORWARD_PROXY,
Authorizer: auth.NewForwardProxyAuth("Remote-User", "Remote-Email", "Remote-Name", "Remote-Filter"), Authorizer: auth.NewForwardProxyAuth("Remote-User", "Remote-Email", "Remote-Name", "Remote-Filter", "Remote-RolesConfigured"),
}, },
}) })
req, err := http.NewRequest("GET", "/", nil) req, err := http.NewRequest("GET", "/", nil)
@@ -39,7 +39,7 @@ func Test_createRoutes_proxy_happy(t *testing.T) {
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/", handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/",
Authorization: Authorization{ Authorization: Authorization{
Provider: FORWARD_PROXY, Provider: FORWARD_PROXY,
Authorizer: auth.NewForwardProxyAuth("Remote-User", "Remote-Email", "Remote-Name", "Remote-Filter"), Authorizer: auth.NewForwardProxyAuth("Remote-User", "Remote-Email", "Remote-Name", "Remote-Filter", "Remote-RolesConfigured"),
}, },
}) })
req, err := http.NewRequest("GET", "/", nil) req, err := http.NewRequest("GET", "/", nil)

View File

@@ -23,11 +23,19 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
} }
userLabels := h.config.Labels userLabels := h.config.Labels
permit := true
if h.config.Authorization.Provider != NONE { if h.config.Authorization.Provider != NONE {
user := auth.UserFromContext(r.Context()) user := auth.UserFromContext(r.Context())
if user.ContainerLabels.Exists() { if user.ContainerLabels.Exists() {
userLabels = user.ContainerLabels userLabels = user.ContainerLabels
} }
permit = user.Roles.Has(auth.Download)
}
if !permit {
log.Warn().Msg("user is not permitted to download logs from container")
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
} }
now := time.Now() now := time.Now()

View File

@@ -12,7 +12,6 @@ import (
"github.com/amir20/dozzle/internal/auth" "github.com/amir20/dozzle/internal/auth"
"github.com/amir20/dozzle/internal/profile" "github.com/amir20/dozzle/internal/profile"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@@ -45,12 +44,20 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
user := auth.UserFromContext(req.Context()) user := auth.UserFromContext(req.Context())
if h.config.Authorization.Provider == NONE || user != nil { if h.config.Authorization.Provider == NONE || user != nil {
if user != nil {
config["enableShell"] = h.config.EnableShell && user.Roles.Has(auth.Shell)
config["enableActions"] = h.config.EnableActions && user.Roles.Has(auth.Actions)
config["enableDownload"] = user.Roles.Has(auth.Download)
} else {
config["enableShell"] = h.config.EnableShell
config["enableActions"] = h.config.EnableActions
config["enableDownload"] = true
}
config["authProvider"] = h.config.Authorization.Provider config["authProvider"] = h.config.Authorization.Provider
config["version"] = h.config.Version config["version"] = h.config.Version
config["hostname"] = h.config.Hostname config["hostname"] = h.config.Hostname
config["hosts"] = hosts config["hosts"] = hosts
config["enableActions"] = h.config.EnableActions
config["enableShell"] = h.config.EnableShell
config["disableAvatars"] = h.config.DisableAvatars config["disableAvatars"] = h.config.DisableAvatars
config["releaseCheckMode"] = h.config.ReleaseCheckMode config["releaseCheckMode"] = h.config.ReleaseCheckMode
} }

View File

@@ -27,15 +27,22 @@ func (h *handler) attach(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id") id := chi.URLParam(r, "id")
userLabels := h.config.Labels userLabels := h.config.Labels
permit := true
if h.config.Authorization.Provider != NONE { if h.config.Authorization.Provider != NONE {
user := auth.UserFromContext(r.Context()) user := auth.UserFromContext(r.Context())
if user.ContainerLabels.Exists() { if user.ContainerLabels.Exists() {
userLabels = user.ContainerLabels userLabels = user.ContainerLabels
} }
permit = user.Roles.Has(auth.Shell)
}
if !permit {
log.Warn().Msg("user is not permitted to attach to container")
conn.WriteMessage(websocket.TextMessage, []byte("⛔ Access denied: attaching to this container is forbidden\r\n"))
return
} }
containerService, err := h.hostService.FindContainer(hostKey(r), id, userLabels) containerService, err := h.hostService.FindContainer(hostKey(r), id, userLabels)
if err != nil { if err != nil {
log.Error().Err(err).Msg("error while trying to find container") log.Error().Err(err).Msg("error while trying to find container")
return return
@@ -60,11 +67,19 @@ func (h *handler) exec(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id") id := chi.URLParam(r, "id")
userLabels := h.config.Labels userLabels := h.config.Labels
permit := true
if h.config.Authorization.Provider != NONE { if h.config.Authorization.Provider != NONE {
user := auth.UserFromContext(r.Context()) user := auth.UserFromContext(r.Context())
if user.ContainerLabels.Exists() { if user.ContainerLabels.Exists() {
userLabels = user.ContainerLabels userLabels = user.ContainerLabels
} }
permit = user.Roles.Has(auth.Shell)
}
if !permit {
log.Warn().Msg("user is not permitted to exec into container")
conn.WriteMessage(websocket.TextMessage, []byte("⛔ Access denied: attaching to this container is forbidden\r\n"))
return
} }
containerService, err := h.hostService.FindContainer(hostKey(r), id, userLabels) containerService, err := h.hostService.FindContainer(hostKey(r), id, userLabels)

View File

@@ -156,7 +156,7 @@ func createServer(args cli.Args, hostService web.HostService) *http.Server {
if args.AuthProvider == "forward-proxy" { if args.AuthProvider == "forward-proxy" {
log.Debug().Msg("Using forward proxy authentication") log.Debug().Msg("Using forward proxy authentication")
provider = web.FORWARD_PROXY provider = web.FORWARD_PROXY
authorizer = auth.NewForwardProxyAuth(args.AuthHeaderUser, args.AuthHeaderEmail, args.AuthHeaderName, args.AuthHeaderFilter) authorizer = auth.NewForwardProxyAuth(args.AuthHeaderUser, args.AuthHeaderEmail, args.AuthHeaderName, args.AuthHeaderFilter, args.AuthHeaderRoles)
} else if args.AuthProvider == "simple" { } else if args.AuthProvider == "simple" {
log.Debug().Msg("Using simple authentication") log.Debug().Msg("Using simple authentication")
provider = web.SIMPLE provider = web.SIMPLE