mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-21 13:23:07 +01:00
feat: Add user roles (#4133)
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
<KeyShortcut char="k" :modifiers="['shift', 'meta']" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<li v-if="enableDownload">
|
||||
<a :href="downloadUrl" download> <octicon:download-24 /> {{ $t("toolbar.download") }} </a>
|
||||
</li>
|
||||
<li v-if="!historical">
|
||||
@@ -167,7 +167,7 @@ import LogAnalytics from "../LogViewer/LogAnalytics.vue";
|
||||
import Terminal from "@/components/Terminal.vue";
|
||||
|
||||
const { showSearch } = useSearchFilter();
|
||||
const { enableActions, enableShell } = config;
|
||||
const { enableActions, enableShell, enableDownload } = config;
|
||||
const { streamConfig, hasComplexLogs, levels } = useLoggingContext();
|
||||
const showDrawer = useDrawer();
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<KeyShortcut char="k" :modifiers="['shift', 'meta']" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<li v-if="enableDownload">
|
||||
<a :href="downloadUrl" download> <octicon:download-24 /> {{ $t("toolbar.download") }} </a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -88,7 +88,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { showSearch } = useSearchFilter();
|
||||
|
||||
const { enableDownload } = config;
|
||||
const clear = defineEmit();
|
||||
|
||||
const { streamConfig, showHostname, showContainerName, containers } = useLoggingContext();
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface Config {
|
||||
authProvider: "simple" | "none" | "forward-proxy";
|
||||
enableActions: boolean;
|
||||
enableShell: boolean;
|
||||
enableDownload: boolean;
|
||||
disableAvatars: boolean;
|
||||
releaseCheckMode: "automatic" | "manual";
|
||||
user?: {
|
||||
|
||||
@@ -28,6 +28,7 @@ users:
|
||||
# Generate with docker run -it --rm amir20/dozzle generate --name Admin --email me@email.net --password secret admin
|
||||
password: $2a$11$9ho4vY2LdJ/WBopFcsAS0uORC0x2vuFHQgT/yBqZyzclhHsoaIkzK
|
||||
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`.
|
||||
@@ -139,15 +140,43 @@ In this example, the `admin` user has no filter, so they can see all containers.
|
||||
> [!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.
|
||||
|
||||
### 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
|
||||
|
||||
Dozzle has a built-in `generate` command to generate `users.yml`. Here is an example:
|
||||
|
||||
```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
|
||||
|
||||
@@ -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-Name` to be a display name like `John Doe`
|
||||
- `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
|
||||
|
||||
|
||||
@@ -6,27 +6,29 @@ 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.
|
||||
|
||||
| Flag | Env Variable | Default |
|
||||
| ---------------------- | --------------------------- | -------------- |
|
||||
| `--addr` | `DOZZLE_ADDR` | `:8080` |
|
||||
| `--base` | `DOZZLE_BASE` | `/` |
|
||||
| `--hostname` | `DOZZLE_HOSTNAME` | `""` |
|
||||
| `--level` | `DOZZLE_LEVEL` | `info` |
|
||||
| `--auth-provider` | `DOZZLE_AUTH_PROVIDER` | `none` |
|
||||
| `--auth-header-user` | `DOZZLE_AUTH_HEADER_USER` | `Remote-User` |
|
||||
| `--auth-header-email` | `DOZZLE_AUTH_HEADER_EMAIL` | `Remote-Email` |
|
||||
| `--auth-header-name` | `DOZZLE_AUTH_HEADER_NAME` | `Remote-Name` |
|
||||
| `--enable-actions` | `DOZZLE_ENABLE_ACTIONS` | `false` |
|
||||
| `--enable-shell` | `DOZZLE_ENABLE_SHELL` | `false` |
|
||||
| `--disable-avatars` | `DOZZLE_DISABLE_AVATARS` | `false` |
|
||||
| `--filter` | `DOZZLE_FILTER` | `""` |
|
||||
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | `false` |
|
||||
| `--mode` | `DOZZLE_MODE` | `server` |
|
||||
| `--release-check-mode` | `DOZZLE_RELEASE_CHECK_MODE` | `automatic` |
|
||||
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
|
||||
| `--remote-agent` | `DOZZLE_REMOTE_AGENT` | |
|
||||
| `--timeout` | `DOZZLE_TIMEOUT` | `10s` |
|
||||
| `--namespace` | `DOZZLE_NAMESPACE` | `""` |
|
||||
| Flag | Env Variable | Default |
|
||||
|------------------------|-----------------------------|-----------------|
|
||||
| `--addr` | `DOZZLE_ADDR` | `:8080` |
|
||||
| `--base` | `DOZZLE_BASE` | `/` |
|
||||
| `--hostname` | `DOZZLE_HOSTNAME` | `""` |
|
||||
| `--level` | `DOZZLE_LEVEL` | `info` |
|
||||
| `--auth-provider` | `DOZZLE_AUTH_PROVIDER` | `none` |
|
||||
| `--auth-header-user` | `DOZZLE_AUTH_HEADER_USER` | `Remote-User` |
|
||||
| `--auth-header-email` | `DOZZLE_AUTH_HEADER_EMAIL` | `Remote-Email` |
|
||||
| `--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-shell` | `DOZZLE_ENABLE_SHELL` | `false` |
|
||||
| `--disable-avatars` | `DOZZLE_DISABLE_AVATARS` | `false` |
|
||||
| `--filter` | `DOZZLE_FILTER` | `""` |
|
||||
| `--no-analytics` | `DOZZLE_NO_ANALYTICS` | `false` |
|
||||
| `--mode` | `DOZZLE_MODE` | `server` |
|
||||
| `--release-check-mode` | `DOZZLE_RELEASE_CHECK_MODE` | `automatic` |
|
||||
| `--remote-host` | `DOZZLE_REMOTE_HOST` | |
|
||||
| `--remote-agent` | `DOZZLE_REMOTE_AGENT` | |
|
||||
| `--timeout` | `DOZZLE_TIMEOUT` | `10s` |
|
||||
| `--namespace` | `DOZZLE_NAMESPACE` | `""` |
|
||||
|
||||
> [!TIP]
|
||||
> Some flags like `--remote-host` or `--remote-agent` can be used multiple times. For example, `--remote-agent 167.99.1.1:7007 --remote-agent 167.99.1.2:7007` or comma-separated `DOZZLE_REMOTE_AGENT=167.99.1.1:7007,167.99.1.2:7007`.
|
||||
@@ -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:
|
||||
|
||||
```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.
|
||||
|
||||
| Flag | Description | Default |
|
||||
| ------------ | ---------------- | ------- |
|
||||
| `--password` | User's password | |
|
||||
| `--email` | User's email | |
|
||||
| `--name` | User's full name | |
|
||||
| Flag | Description | Default |
|
||||
|-----------------|------------------| ------- |
|
||||
| `--password` | User's password | |
|
||||
| `--email` | User's email | |
|
||||
| `--name` | User's full name | |
|
||||
| `--user-filter` | User's filters | |
|
||||
| `--user-roles` | User's roles | |
|
||||
|
||||
See [authentication](/guide/authentication) for more information.
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ type proxyAuthContext struct {
|
||||
headerEmail string
|
||||
headerName string
|
||||
headerFilter string
|
||||
headerRoles string
|
||||
}
|
||||
|
||||
func hashEmail(email string) string {
|
||||
@@ -30,12 +31,13 @@ func hashEmail(email string) string {
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func NewForwardProxyAuth(userHeader, emailHeader, nameHeader, filterHeader string) *proxyAuthContext {
|
||||
func NewForwardProxyAuth(userHeader, emailHeader, nameHeader, filterHeader, rolesHeader string) *proxyAuthContext {
|
||||
return &proxyAuthContext{
|
||||
headerUser: userHeader,
|
||||
headerEmail: emailHeader,
|
||||
headerName: nameHeader,
|
||||
headerFilter: filterHeader,
|
||||
headerRoles: rolesHeader,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +48,8 @@ func (p *proxyAuthContext) AuthMiddleware(next http.Handler) http.Handler {
|
||||
if err != nil {
|
||||
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)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
} 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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if a.ttl > 0 {
|
||||
|
||||
@@ -24,7 +24,9 @@ type User struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Password string `json:"-" yaml:"password"`
|
||||
Filter string `json:"-" yaml:"filter"`
|
||||
RolesConfigured string `json:"-" yaml:"roles"`
|
||||
ContainerLabels container.ContainerLabels `json:"-" yaml:"-"`
|
||||
Roles Role `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
func newUser(username, email, name string, labels container.ContainerLabels) User {
|
||||
func newUser(username, email, name string, labels container.ContainerLabels, roles Role) User {
|
||||
return User{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Name: name,
|
||||
ContainerLabels: labels,
|
||||
Roles: roles,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,14 +201,18 @@ func UserFromContext(ctx context.Context) *User {
|
||||
email := claims["email"].(string)
|
||||
name := claims["name"].(string)
|
||||
containerFilter := container.ContainerLabels{}
|
||||
roles := AllRole
|
||||
if filter, ok := claims["filter"].(string); ok {
|
||||
containerFilter, err = container.ParseContainerFilter(filter)
|
||||
if err != nil {
|
||||
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 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."`
|
||||
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."`
|
||||
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."`
|
||||
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."`
|
||||
|
||||
@@ -11,11 +11,12 @@ import (
|
||||
)
|
||||
|
||||
type GenerateCmd struct {
|
||||
Username string `arg:"positional"`
|
||||
Password string `arg:"--password, -p" help:"sets the password 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"`
|
||||
Filter string `arg:"--user-filter" help:"sets the filter for the user. This can be a comma separated list of filters."`
|
||||
Username string `arg:"positional"`
|
||||
Password string `arg:"--password, -p" help:"sets the password 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"`
|
||||
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 {
|
||||
@@ -27,11 +28,12 @@ func (g *GenerateCmd) Run(args Args, embeddedCerts embed.FS) error {
|
||||
}
|
||||
|
||||
buffer := auth.GenerateUsers(auth.User{
|
||||
Username: args.Generate.Username,
|
||||
Password: args.Generate.Password,
|
||||
Name: args.Generate.Name,
|
||||
Email: args.Generate.Email,
|
||||
Filter: args.Generate.Filter,
|
||||
Username: args.Generate.Username,
|
||||
Password: args.Generate.Password,
|
||||
Name: args.Generate.Name,
|
||||
Email: args.Generate.Email,
|
||||
Filter: args.Generate.Filter,
|
||||
RolesConfigured: args.Generate.RolesConfigured,
|
||||
}, true)
|
||||
|
||||
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")
|
||||
|
||||
userLabels := h.config.Labels
|
||||
permit := true
|
||||
if h.config.Authorization.Provider != NONE {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
if user.ContainerLabels.Exists() {
|
||||
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)
|
||||
|
||||
@@ -20,7 +20,7 @@ func Test_createRoutes_proxy_missing_headers(t *testing.T) {
|
||||
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/",
|
||||
Authorization: Authorization{
|
||||
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)
|
||||
@@ -39,7 +39,7 @@ func Test_createRoutes_proxy_happy(t *testing.T) {
|
||||
handler := createHandler(nil, afero.NewIOFS(fs), Config{Base: "/",
|
||||
Authorization: Authorization{
|
||||
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)
|
||||
|
||||
@@ -23,11 +23,19 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
userLabels := h.config.Labels
|
||||
permit := true
|
||||
if h.config.Authorization.Provider != NONE {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
if user.ContainerLabels.Exists() {
|
||||
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()
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/profile"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -45,12 +44,20 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
|
||||
user := auth.UserFromContext(req.Context())
|
||||
|
||||
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["version"] = h.config.Version
|
||||
config["hostname"] = h.config.Hostname
|
||||
config["hosts"] = hosts
|
||||
config["enableActions"] = h.config.EnableActions
|
||||
config["enableShell"] = h.config.EnableShell
|
||||
config["disableAvatars"] = h.config.DisableAvatars
|
||||
config["releaseCheckMode"] = h.config.ReleaseCheckMode
|
||||
}
|
||||
|
||||
@@ -27,15 +27,22 @@ func (h *handler) attach(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
id := chi.URLParam(r, "id")
|
||||
userLabels := h.config.Labels
|
||||
permit := true
|
||||
if h.config.Authorization.Provider != NONE {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
if user.ContainerLabels.Exists() {
|
||||
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)
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error while trying to find container")
|
||||
return
|
||||
@@ -60,11 +67,19 @@ func (h *handler) exec(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
id := chi.URLParam(r, "id")
|
||||
userLabels := h.config.Labels
|
||||
permit := true
|
||||
if h.config.Authorization.Provider != NONE {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
if user.ContainerLabels.Exists() {
|
||||
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)
|
||||
|
||||
50
main.go
50
main.go
@@ -130,11 +130,11 @@ func main() {
|
||||
}
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
_, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return err == nil
|
||||
_, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func createServer(args cli.Args, hostService web.HostService) *http.Server {
|
||||
@@ -143,12 +143,12 @@ func createServer(args cli.Args, hostService web.HostService) *http.Server {
|
||||
var releaseCheckMode web.ReleaseCheckMode = web.Automatic
|
||||
|
||||
switch args.ReleaseCheckMode {
|
||||
case "automatic":
|
||||
releaseCheckMode = web.Automatic
|
||||
case "manual":
|
||||
releaseCheckMode = web.Manual
|
||||
default:
|
||||
log.Fatal().Str("releaseCheckMode", args.ReleaseCheckMode).Msg("Invalid release check mode")
|
||||
case "automatic":
|
||||
releaseCheckMode = web.Automatic
|
||||
case "manual":
|
||||
releaseCheckMode = web.Manual
|
||||
default:
|
||||
log.Fatal().Str("releaseCheckMode", args.ReleaseCheckMode).Msg("Invalid release check mode")
|
||||
}
|
||||
|
||||
var provider web.AuthProvider = web.NONE
|
||||
@@ -156,25 +156,25 @@ func createServer(args cli.Args, hostService web.HostService) *http.Server {
|
||||
if args.AuthProvider == "forward-proxy" {
|
||||
log.Debug().Msg("Using forward proxy authentication")
|
||||
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" {
|
||||
log.Debug().Msg("Using simple authentication")
|
||||
provider = web.SIMPLE
|
||||
|
||||
userFilePath := "./data/users.yml"
|
||||
if !fileExists(userFilePath) {
|
||||
userFilePath = "./data/users.yaml"
|
||||
if !fileExists(userFilePath) {
|
||||
log.Fatal().Msg("No users.yaml or users.yml file found.")
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Reading %s file", filepath.Base(userFilePath))
|
||||
userFilePath := "./data/users.yml"
|
||||
if !fileExists(userFilePath) {
|
||||
userFilePath = "./data/users.yaml"
|
||||
if !fileExists(userFilePath) {
|
||||
log.Fatal().Msg("No users.yaml or users.yml file found.")
|
||||
}
|
||||
}
|
||||
|
||||
db, err := auth.ReadUsersFromFile(userFilePath)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msgf("Could not read users file: %s", userFilePath)
|
||||
}
|
||||
log.Debug().Msgf("Reading %s file", filepath.Base(userFilePath))
|
||||
|
||||
db, err := auth.ReadUsersFromFile(userFilePath)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msgf("Could not read users file: %s", userFilePath)
|
||||
}
|
||||
|
||||
log.Debug().Int("users", len(db.Users)).Msg("Loaded users")
|
||||
ttl := time.Duration(0)
|
||||
|
||||
Reference in New Issue
Block a user