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:
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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?: {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
42
internal/auth/roles.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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."`
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user