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.
|
> 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 _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 producing events only for _Updates_
|
||||||
|
* Switched to encrypting webhook tokens in database
|
||||||
* Adapted logging which defaults to JSON encoding
|
* Adapted logging which defaults to JSON encoding
|
||||||
* Updated dependencies
|
* 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` |
|
| `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_ |
|
| `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_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` |
|
| `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_NAME=upda
|
||||||
- DB_POSTGRES_USER=upda
|
- DB_POSTGRES_USER=upda
|
||||||
- DB_POSTGRES_PASSWORD=upda
|
- DB_POSTGRES_PASSWORD=upda
|
||||||
- ADMIN_USER=admin
|
- BASIC_AUTH_USER=admin
|
||||||
- ADMIN_PASSWORD=changeit
|
- BASIC_AUTH_PASSWORD=changeit
|
||||||
# generate 32 character long secret, e.g., with "openssl rand -hex 16"
|
# generate 32 character long secret, e.g., with "openssl rand -hex 16"
|
||||||
- SECRET=generated-secure-secret-32-chars
|
- SECRET=generated-secure-secret-32-chars
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
@ -121,8 +121,8 @@ services:
|
||||||
image: git.myservermanager.com/varakh/upda:latest
|
image: git.myservermanager.com/varakh/upda:latest
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Berlin
|
- TZ=Europe/Berlin
|
||||||
- ADMIN_USER=admin
|
- BASIC_AUTH_USER=admin
|
||||||
- ADMIN_PASSWORD=changeit
|
- BASIC_AUTH_PASSWORD=changeit
|
||||||
# generate 32 character long secret, e.g., with "openssl rand -hex 16"
|
# generate 32 character long secret, e.g., with "openssl rand -hex 16"
|
||||||
- SECRET=generated-secure-secret-32-chars
|
- SECRET=generated-secure-secret-32-chars
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
|
@ -95,9 +95,19 @@ func Start() {
|
||||||
|
|
||||||
apiPublicGroup.POST("/webhooks/:id", webhookInvocationHandler.execute)
|
apiPublicGroup.POST("/webhooks/:id", webhookInvocationHandler.execute)
|
||||||
|
|
||||||
apiAuthGroup := router.Group("/api/v1", gin.BasicAuth(gin.Accounts{
|
var authMethodHandler gin.HandlerFunc
|
||||||
env.authConfig.adminUser: env.authConfig.adminPassword,
|
|
||||||
}))
|
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)
|
apiAuthGroup.GET("/login", authHandler.login)
|
||||||
|
|
||||||
|
|
|
@ -16,8 +16,13 @@ const (
|
||||||
envTZ = "TZ"
|
envTZ = "TZ"
|
||||||
tzDefault = "Europe/Berlin"
|
tzDefault = "Europe/Berlin"
|
||||||
|
|
||||||
envAdminUser = "ADMIN_USER"
|
envAuthMode = "AUTH_MODE"
|
||||||
envAdminPassword = "ADMIN_PASSWORD"
|
authModeDefault = authModeBasicSingle
|
||||||
|
authModeBasicSingle = "basic_single"
|
||||||
|
authModeBasicCredentials = "basic_credentials"
|
||||||
|
envBasicAuthUser = "BASIC_AUTH_USER"
|
||||||
|
envBasicAuthPassword = "BASIC_AUTH_PASSWORD"
|
||||||
|
envBasicAuthCredentials = "BASIC_AUTH_CREDENTIALS"
|
||||||
|
|
||||||
envServerPort = "SERVER_PORT"
|
envServerPort = "SERVER_PORT"
|
||||||
envServerListen = "SERVER_LISTEN"
|
envServerListen = "SERVER_LISTEN"
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -38,8 +39,10 @@ type serverConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type authConfig struct {
|
type authConfig struct {
|
||||||
adminUser string
|
authMethod string
|
||||||
adminPassword string
|
basicAuthUser string
|
||||||
|
basicAuthPassword string
|
||||||
|
basicAuthCredentials map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type taskConfig struct {
|
type taskConfig struct {
|
||||||
|
@ -209,9 +212,25 @@ func bootstrapEnvironment() *Environment {
|
||||||
corsAllowHeaders: []string{os.Getenv(envCorsAllowHeaders)},
|
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{
|
authC := &authConfig{
|
||||||
adminUser: os.Getenv(envAdminUser),
|
authMethod: authMode,
|
||||||
adminPassword: os.Getenv(envAdminPassword),
|
}
|
||||||
|
|
||||||
|
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
|
// task config
|
||||||
|
@ -390,13 +409,14 @@ func bootstrapEnvironment() *Environment {
|
||||||
}
|
}
|
||||||
|
|
||||||
func bootstrapFromEnvironmentAndValidate() {
|
func bootstrapFromEnvironmentAndValidate() {
|
||||||
|
failIfEnvKeyNotPresent(envSecret)
|
||||||
|
|
||||||
|
// auth mode
|
||||||
|
setEnvKeyDefault(envAuthMode, authModeDefault)
|
||||||
|
|
||||||
// app
|
// app
|
||||||
setEnvKeyDefault(envTZ, tzDefault)
|
setEnvKeyDefault(envTZ, tzDefault)
|
||||||
|
|
||||||
failIfEnvKeyNotPresent(envSecret)
|
|
||||||
failIfEnvKeyNotPresent(envAdminUser)
|
|
||||||
failIfEnvKeyNotPresent(envAdminPassword)
|
|
||||||
|
|
||||||
// webhook
|
// webhook
|
||||||
setEnvKeyDefault(envWebhooksTokenLength, webhooksTokenLengthDefault)
|
setEnvKeyDefault(envWebhooksTokenLength, webhooksTokenLengthDefault)
|
||||||
|
|
||||||
|
@ -467,3 +487,35 @@ func setEnvKeyDefault(key string, defaultValue string) {
|
||||||
zap.L().Sugar().Infof("Set '%s' to '%s'", key, defaultValue)
|
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