feat(auth): Add support for multiple basic auth credentials (#25)
Reviewed-on: #25 Co-authored-by: Varakh <varakh@varakh.de> Co-committed-by: Varakh <varakh@varakh.de>
This commit is contained in:
parent
f231e66e7c
commit
faffad851c
6 changed files with 95 additions and 21 deletions
|
@ -6,10 +6,14 @@ Changes adhere to [semantic versioning](https://semver.org).
|
|||
|
||||
> This is a major version upgrade. Other versions are incompatible with this release.
|
||||
|
||||
* Added mandatory `SECRET` environment variable to encrypt some data inside the database
|
||||
* Switched to encrypting webhook tokens in database
|
||||
* Added _Actions_, a simple way to trigger notifications via [shoutrrr](https://containrrr.dev/shoutrrr) which supports secrets
|
||||
* Added new auth mode which allows setting multiple basic auth credentials
|
||||
* Added `AUTH_MODE` which can be one of `basic_single` (_default_) and `basic_credentials`
|
||||
* For `basic_credentials`: added `BASIC_AUTH_CREDENTIALS` which can be used as list of `username1=password1,...` (comma separated)
|
||||
* For `basic_single`: renamed `ADMIN_USER` and `ADMIN_PASSWORD` to `BASIC_AUTH_USER` and `BASIC_AUTH_PASSWORD`
|
||||
* Added mandatory `SECRET` environment variable to encrypt some data inside the database
|
||||
* Switched to producing events only for _Updates_
|
||||
* Switched to encrypting webhook tokens in database
|
||||
* Adapted logging which defaults to JSON encoding
|
||||
* Updated dependencies
|
||||
|
||||
|
|
|
@ -134,8 +134,11 @@ The following environment variables can be used to modify application behavior.
|
|||
|:------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `SECRET` | A 32 character long secure random secret used for encrypting some data inside the database. When data has been created inside the database, the secret cannot be changed anymore, otherwise decryption fails. | Not set by default, you need to explicitly set it, e.g., generate via `openssl rand -hex 16` |
|
||||
| `TZ` | The time zone (**recommended** to set it properly, background tasks depend on it) | Defaults to `Europe/Berlin`, can be any time zone according to _tz database_ |
|
||||
| `ADMIN_USER` | Admin user name for login | Not set by default, you need to explicitly set it to user name |
|
||||
| `ADMIN_PASSWORD` | Admin password for login | Not set by default, you need to explicitly set it to a secure random |
|
||||
| | | |
|
||||
| `AUTH_MODE` | The auth mode. Possible values are `basic_single` and `basic_credentials` | Defaults to `basic_single` |
|
||||
| `BASIC_AUTH_USER` | For auth mode `basic_single`: Username for login | Not set by default, you need to explicitly set it to user name |
|
||||
| `BASIC_AUTH_PASSWORD` | For auth mode `basic_single`: User's password for login | Not set by default, you need to explicitly set it to a secure random |
|
||||
| `BASIC_AUTH_CREDENTIALS` | For auth mode `basic_credentials`: list of comma separated credentials, e.g. `username1=password1,username2=password2` | Not set by default, you need to explicitly set it |
|
||||
| | | |
|
||||
| `DB_TYPE` | The database type (Postgres is **recommended**) | Defaults to `sqlite`, possible values are `sqlite` or `postgres` |
|
||||
| `DB_SQLITE_FILE` | Path to the SQLITE file | Defaults to `<XDG_DATA_DIR>/upda/upda.db`, e.g. `~/.local/share/upda/upda.db` |
|
||||
|
|
|
@ -58,8 +58,8 @@ services:
|
|||
- DB_POSTGRES_NAME=upda
|
||||
- DB_POSTGRES_USER=upda
|
||||
- DB_POSTGRES_PASSWORD=upda
|
||||
- ADMIN_USER=admin
|
||||
- ADMIN_PASSWORD=changeit
|
||||
- BASIC_AUTH_USER=admin
|
||||
- BASIC_AUTH_PASSWORD=changeit
|
||||
# generate 32 character long secret, e.g., with "openssl rand -hex 16"
|
||||
- SECRET=generated-secure-secret-32-chars
|
||||
restart: unless-stopped
|
||||
|
@ -121,8 +121,8 @@ services:
|
|||
image: git.myservermanager.com/varakh/upda:latest
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
- ADMIN_USER=admin
|
||||
- ADMIN_PASSWORD=changeit
|
||||
- BASIC_AUTH_USER=admin
|
||||
- BASIC_AUTH_PASSWORD=changeit
|
||||
# generate 32 character long secret, e.g., with "openssl rand -hex 16"
|
||||
- SECRET=generated-secure-secret-32-chars
|
||||
restart: unless-stopped
|
||||
|
|
|
@ -95,9 +95,19 @@ func Start() {
|
|||
|
||||
apiPublicGroup.POST("/webhooks/:id", webhookInvocationHandler.execute)
|
||||
|
||||
apiAuthGroup := router.Group("/api/v1", gin.BasicAuth(gin.Accounts{
|
||||
env.authConfig.adminUser: env.authConfig.adminPassword,
|
||||
}))
|
||||
var authMethodHandler gin.HandlerFunc
|
||||
|
||||
if authModeBasicSingle == env.authConfig.authMethod {
|
||||
authMethodHandler = gin.BasicAuth(gin.Accounts{
|
||||
env.authConfig.basicAuthUser: env.authConfig.basicAuthPassword,
|
||||
})
|
||||
} else if authModeBasicCredentials == env.authConfig.authMethod {
|
||||
authMethodHandler = gin.BasicAuth(env.authConfig.basicAuthCredentials)
|
||||
} else {
|
||||
zap.L().Fatal("No valid auth mode found")
|
||||
}
|
||||
|
||||
apiAuthGroup := router.Group("/api/v1", authMethodHandler)
|
||||
|
||||
apiAuthGroup.GET("/login", authHandler.login)
|
||||
|
||||
|
|
|
@ -16,8 +16,13 @@ const (
|
|||
envTZ = "TZ"
|
||||
tzDefault = "Europe/Berlin"
|
||||
|
||||
envAdminUser = "ADMIN_USER"
|
||||
envAdminPassword = "ADMIN_PASSWORD"
|
||||
envAuthMode = "AUTH_MODE"
|
||||
authModeDefault = authModeBasicSingle
|
||||
authModeBasicSingle = "basic_single"
|
||||
authModeBasicCredentials = "basic_credentials"
|
||||
envBasicAuthUser = "BASIC_AUTH_USER"
|
||||
envBasicAuthPassword = "BASIC_AUTH_PASSWORD"
|
||||
envBasicAuthCredentials = "BASIC_AUTH_CREDENTIALS"
|
||||
|
||||
envServerPort = "SERVER_PORT"
|
||||
envServerListen = "SERVER_LISTEN"
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -38,8 +39,10 @@ type serverConfig struct {
|
|||
}
|
||||
|
||||
type authConfig struct {
|
||||
adminUser string
|
||||
adminPassword string
|
||||
authMethod string
|
||||
basicAuthUser string
|
||||
basicAuthPassword string
|
||||
basicAuthCredentials map[string]string
|
||||
}
|
||||
|
||||
type taskConfig struct {
|
||||
|
@ -209,9 +212,25 @@ func bootstrapEnvironment() *Environment {
|
|||
corsAllowHeaders: []string{os.Getenv(envCorsAllowHeaders)},
|
||||
}
|
||||
|
||||
authMode := os.Getenv(envAuthMode)
|
||||
|
||||
if authMode != authModeBasicSingle && authMode != authModeBasicCredentials {
|
||||
zap.L().Sugar().Fatalln("Invalid auth mode. Reason: must be one of ['basic_single','basic_credentials'")
|
||||
}
|
||||
|
||||
authC := &authConfig{
|
||||
adminUser: os.Getenv(envAdminUser),
|
||||
adminPassword: os.Getenv(envAdminPassword),
|
||||
authMethod: authMode,
|
||||
}
|
||||
|
||||
if authModeBasicSingle == authMode {
|
||||
failIfEnvKeyNotPresent(envBasicAuthUser)
|
||||
failIfEnvKeyNotPresent(envBasicAuthPassword)
|
||||
authC.basicAuthUser = os.Getenv(envBasicAuthUser)
|
||||
authC.basicAuthPassword = os.Getenv(envBasicAuthPassword)
|
||||
}
|
||||
if authModeBasicCredentials == authMode {
|
||||
failIfEnvKeyNotPresent(envBasicAuthCredentials)
|
||||
authC.basicAuthCredentials = parseBasicAuthCredentials(envBasicAuthCredentials)
|
||||
}
|
||||
|
||||
// task config
|
||||
|
@ -390,13 +409,14 @@ func bootstrapEnvironment() *Environment {
|
|||
}
|
||||
|
||||
func bootstrapFromEnvironmentAndValidate() {
|
||||
failIfEnvKeyNotPresent(envSecret)
|
||||
|
||||
// auth mode
|
||||
setEnvKeyDefault(envAuthMode, authModeDefault)
|
||||
|
||||
// app
|
||||
setEnvKeyDefault(envTZ, tzDefault)
|
||||
|
||||
failIfEnvKeyNotPresent(envSecret)
|
||||
failIfEnvKeyNotPresent(envAdminUser)
|
||||
failIfEnvKeyNotPresent(envAdminPassword)
|
||||
|
||||
// webhook
|
||||
setEnvKeyDefault(envWebhooksTokenLength, webhooksTokenLengthDefault)
|
||||
|
||||
|
@ -467,3 +487,35 @@ func setEnvKeyDefault(key string, defaultValue string) {
|
|||
zap.L().Sugar().Infof("Set '%s' to '%s'", key, defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
func parseBasicAuthCredentials(envProperty string) map[string]string {
|
||||
if envProperty == "" {
|
||||
zap.L().Sugar().Fatalln("Invalid env for parsing basic auth credentials")
|
||||
}
|
||||
credentialsFromEnv := os.Getenv(envProperty)
|
||||
|
||||
var credentials []string
|
||||
credentials = strings.Split(credentialsFromEnv, ",")
|
||||
|
||||
basicAuthCredentials := make(map[string]string)
|
||||
|
||||
for _, c := range credentials {
|
||||
pair := strings.Split(c, "=")
|
||||
|
||||
if len(pair) != 2 {
|
||||
zap.L().Sugar().Fatalln("Invalid basic auth credentials. Reason: credentials must be specified with the = separator per credential entry")
|
||||
}
|
||||
|
||||
if pair[0] == "" {
|
||||
zap.L().Sugar().Fatalln("Invalid basic auth credentials. Reason: username must not be blank")
|
||||
}
|
||||
|
||||
if pair[1] == "" {
|
||||
zap.L().Sugar().Fatalln("Invalid basic auth credentials. Reason: password must not be blank")
|
||||
}
|
||||
|
||||
basicAuthCredentials[pair[0]] = pair[1]
|
||||
}
|
||||
|
||||
return basicAuthCredentials
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue