diff --git a/docs/components.d.ts b/docs/components.d.ts index 7396b959..a0c35567 100644 --- a/docs/components.d.ts +++ b/docs/components.d.ts @@ -1,10 +1,10 @@ /* eslint-disable */ -/* prettier-ignore */ // @ts-nocheck // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 export {} +/* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { BrowserWindow: typeof import('./.vitepress/theme/components/BrowserWindow.vue')['default'] diff --git a/docs/guide/authentication.md b/docs/guide/authentication.md index 6a17c372..ae24ab6c 100644 --- a/docs/guide/authentication.md +++ b/docs/guide/authentication.md @@ -24,9 +24,10 @@ users: email: me@email.net ``` -Dozzle uses `email` to generate avatars using [Gravatar](https://gravatar.com/). It is optional. +> [!TIP] +> This file can be generated with `docker run amir20/dozzle generate` with v6.6.x. See [below](#generating-users-yml) for more details. -The password is hashed using `sha256` which can be generated with `echo -n 'secret-password' | shasum -a 256` or `echo -n 'secret-password' | sha256sum` on linux. +Dozzle uses `email` to generate avatars using [Gravatar](https://gravatar.com/). It is optional. The password is hashed using `sha256` which can be generated with `echo -n 'secret-password' | shasum -a 256` or `echo -n 'secret-password' | sha256sum` on linux. You will need to mount this file for Dozzle to find it. Here is an example: @@ -64,6 +65,16 @@ users: Dozzle uses [JWT](https://en.wikipedia.org/wiki/JSON_Web_Token) to generate tokens for authentication. This token is saved in a cookie. +### Generating users.yml + +Starting with version `v6.6.x`, Dozzle has a builtin `generate` sub-command which can be used to create a `users.yml`. + +```sh +docker run amir20/dozzle generate admin --password password --email test@email.net --name "John Doe" > 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. + ## Forward Proxy Dozzle can be configured to read proxy headers by setting `--auth-provider` to `forward-proxy`. diff --git a/internal/auth/users.go b/internal/auth/users.go index 0ff86f26..d8706dab 100644 --- a/internal/auth/users.go +++ b/internal/auth/users.go @@ -1,6 +1,7 @@ package auth import ( + "bytes" "context" "crypto/sha256" "encoding/hex" @@ -16,7 +17,7 @@ import ( ) type User struct { - Username string `json:"username"` + Username string `json:"username" yaml:"-"` Email string `json:"email" yaml:"email"` Name string `json:"name" yaml:"name"` Password string `json:"-" yaml:"password"` @@ -56,6 +57,24 @@ func ReadUsersFromFile(path string) (UserDatabase, error) { return users, nil } +func GenerateUsers(user User, hashPassword bool) *bytes.Buffer { + buffer := &bytes.Buffer{} + + if hashPassword { + user.Password = sha256sum(user.Password) + } + + users := UserDatabase{ + Users: map[string]*User{ + user.Username: &user, + }, + } + + yaml.NewEncoder(buffer).Encode(users) + + return buffer +} + func decodeUsersFromFile(path string) (UserDatabase, error) { users := UserDatabase{} file, err := os.Open(path) diff --git a/main.go b/main.go index f3c2dda8..3df36fee 100644 --- a/main.go +++ b/main.go @@ -33,8 +33,6 @@ type args struct { Base string `arg:"env:DOZZLE_BASE" default:"/" help:"sets the base for http router."` Hostname string `arg:"env:DOZZLE_HOSTNAME" help:"sets the hostname for display. This is useful with multiple Dozzle instances."` Level string `arg:"env:DOZZLE_LEVEL" default:"info" help:"set Dozzle log level. Use debug for more logging."` - Username string `arg:"env:DOZZLE_USERNAME" help:"sets the username for auth."` - Password string `arg:"env:DOZZLE_PASSWORD" help:"sets password for auth"` AuthProvider string `arg:"--auth-provider,env:DOZZLE_AUTH_PROVIDER" default:"none" help:"sets the auth provider to use. Currently only forward-proxy is supported."` 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."` 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."` @@ -43,14 +41,23 @@ type args struct { EnableActions bool `arg:"--enable-actions,env:DOZZLE_ENABLE_ACTIONS" default:"false" help:"enables essential actions on containers from the web interface."` FilterStrings []string `arg:"env:DOZZLE_FILTER,--filter,separate" help:"filters docker containers using Docker syntax."` Filter map[string][]string `arg:"-"` - Healthcheck *HealthcheckCmd `arg:"subcommand:healthcheck" help:"checks if the server is running."` RemoteHost []string `arg:"env:DOZZLE_REMOTE_HOST,--remote-host,separate" help:"list of hosts to connect remotely"` NoAnalytics bool `arg:"--no-analytics,env:DOZZLE_NO_ANALYTICS" help:"disables anonymous analytics"` + + Healthcheck *HealthcheckCmd `arg:"subcommand:healthcheck" help:"checks if the server is running"` + Generate *GenerateCmd `arg:"subcommand:generate" help:"generates a configuration file for simple auth"` } type HealthcheckCmd struct { } +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"` +} + func (args) Version() string { return version } @@ -59,12 +66,32 @@ func (args) Version() string { var content embed.FS func main() { - args := parseArgs() + args, subcommand := parseArgs() validateEnvVars() - if args.Healthcheck != nil { - if err := healthcheck.HttpRequest(args.Addr, args.Base); err != nil { - log.Fatal(err) + if subcommand != nil { + switch subcommand.(type) { + case *HealthcheckCmd: + if err := healthcheck.HttpRequest(args.Addr, args.Base); err != nil { + log.Fatal(err) + } + + case *GenerateCmd: + if args.Generate.Username == "" || args.Generate.Password == "" { + log.Fatal("Username and password are required") + } + + buffer := auth.GenerateUsers(auth.User{ + Username: args.Generate.Username, + Password: args.Generate.Password, + Name: args.Generate.Name, + Email: args.Generate.Email, + }, true) + + if _, err := os.Stdout.Write(buffer.Bytes()); err != nil { + log.Fatal(err) + } } + os.Exit(0) } @@ -257,7 +284,7 @@ func createLocalClient(args args, localClientFactory func(map[string][]string) ( return nil, errors.New("could not connect to local Docker Engine") } -func parseArgs() args { +func parseArgs() (args, interface{}) { var args args parser := arg.MustParse(&args) @@ -275,10 +302,7 @@ func parseArgs() args { args.Filter[key] = append(args.Filter[key], val) } - if args.Username != "" || args.Password != "" { - log.Fatal("Using --username and --password is removed on v6.x. See https://github.com/amir20/dozzle/issues/2630 for details.") - } - return args + return args, parser.Subcommand() } func configureLogger(level string) {