feat(storage): encrypted secret values (#2588)
This adds an AES-GCM 256bit encryption layer for storage for sensitive items. This is only TOTP secrets for the time being but this may be expanded later. This will require a configuration change as per https://www.authelia.com/docs/configuration/migration.html#4330. Closes #682pull/2632/head
parent
eb94960348
commit
347bd1be77
|
@ -507,6 +507,10 @@ regulation:
|
|||
##
|
||||
## The available providers are: `local`, `mysql`, `postgres`. You must use one and only one of these providers.
|
||||
storage:
|
||||
## The encryption key that is used to encrypt sensitive information in the database. Must be a string with a minimum
|
||||
## length of 20. Please see the docs if you configure this with an undesirable key and need to change it.
|
||||
# encryption_key: you_must_generate_a_random_string_of_more_than_twenty_chars_and_configure_this
|
||||
|
||||
##
|
||||
## Local (Storage Provider)
|
||||
##
|
||||
|
|
|
@ -36,6 +36,7 @@ other configuration using the environment but instead of loading a file the valu
|
|||
|session.secret |AUTHELIA_SESSION_SECRET_FILE |
|
||||
|session.redis.password |AUTHELIA_SESSION_REDIS_PASSWORD_FILE |
|
||||
|session.redis.high_availability.sentinel_password|AUTHELIA_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE |
|
||||
|storage.encryption_key |AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE |
|
||||
|storage.mysql.password |AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE |
|
||||
|storage.postgres.password |AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE |
|
||||
|notifier.smtp.password |AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE |
|
||||
|
|
|
@ -6,9 +6,43 @@ nav_order: 14
|
|||
has_children: true
|
||||
---
|
||||
|
||||
# Storage backends
|
||||
|
||||
**Authelia** supports multiple storage backends. The backend is used to store user preferences, 2FA device handles and
|
||||
secrets, authentication logs, etc...
|
||||
|
||||
The available storage backends are listed in the table of contents below.
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
storage:
|
||||
encryption_key: a_very_important_secret
|
||||
local: {}
|
||||
mysql: {}
|
||||
postgres: {}
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### encryption_key
|
||||
<div markdown="1">
|
||||
type: string
|
||||
{: .label .label-config .label-purple }
|
||||
required: yes
|
||||
{: .label .label-config .label-red }
|
||||
</div>
|
||||
|
||||
The encryption key used to encrypt data in the database. It has a minimum length of 20 and must be provided. We encrypt
|
||||
data by creating a sha256 checksum of the provided value, and use that to encrypt the data with the AES-GCM 256bit
|
||||
algorithm.
|
||||
|
||||
The encrypted data in the database is as follows:
|
||||
- TOTP Secret
|
||||
|
||||
### local
|
||||
See [SQLite](./sqlite.md).
|
||||
|
||||
### mysql
|
||||
See [MySQL](./mysql.md).
|
||||
|
||||
### postgres
|
||||
See [PostgreSQL](./postgres.md).
|
|
@ -14,6 +14,7 @@ The MySQL storage provider also serves as a MariaDB provider.
|
|||
|
||||
```yaml
|
||||
storage:
|
||||
encryption_key: a_very_important_secret
|
||||
mysql:
|
||||
host: 127.0.0.1
|
||||
port: 3306
|
||||
|
@ -24,6 +25,9 @@ storage:
|
|||
|
||||
## Options
|
||||
|
||||
### encryption_key
|
||||
See the [encryption_key docs](./index.md#encryption_key).
|
||||
|
||||
### host
|
||||
<div markdown="1">
|
||||
type: string
|
||||
|
|
|
@ -14,6 +14,7 @@ The MySQL storage provider.
|
|||
|
||||
```yaml
|
||||
storage:
|
||||
encryption_key: a_very_important_secret
|
||||
mysql:
|
||||
host: 127.0.0.1
|
||||
port: 3306
|
||||
|
@ -25,6 +26,9 @@ storage:
|
|||
|
||||
## Options
|
||||
|
||||
### encryption_key
|
||||
See the [encryption_key docs](./index.md#encryption_key).
|
||||
|
||||
### host
|
||||
<div markdown="1">
|
||||
type: string
|
||||
|
|
|
@ -14,6 +14,7 @@ The PostgreSQL storage provider.
|
|||
|
||||
```yaml
|
||||
storage:
|
||||
encryption_key: a_very_important_secret
|
||||
postgres:
|
||||
host: 127.0.0.1
|
||||
port: 5432
|
||||
|
@ -25,6 +26,9 @@ storage:
|
|||
|
||||
## Options
|
||||
|
||||
### encryption_key
|
||||
See the [encryption_key docs](./index.md#encryption_key).
|
||||
|
||||
### host
|
||||
<div markdown="1">
|
||||
type: string
|
||||
|
|
|
@ -20,12 +20,16 @@ requires you setup an external database.
|
|||
|
||||
```yaml
|
||||
storage:
|
||||
encryption_key: a_very_important_secret
|
||||
local:
|
||||
path: /config/db.sqlite3
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### encryption_key
|
||||
See the [encryption_key docs](./index.md#encryption_key).
|
||||
|
||||
### path
|
||||
<div markdown="1">
|
||||
type: string
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
const cmdAutheliaExample = `authelia --config /etc/authelia/config.yml --config /etc/authelia/access-control.yml
|
||||
authelia --config /etc/authelia/config.yml,/etc/authelia/access-control.yml
|
||||
authelia --config /etc/authelia/config/
|
||||
|
@ -80,3 +84,12 @@ const (
|
|||
storageMigrateDirectionUp = "up"
|
||||
storageMigrateDirectionDown = "down"
|
||||
)
|
||||
|
||||
const (
|
||||
storageExportFormatCSV = "csv"
|
||||
storageExportFormatURI = "uri"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoStorageProvider = errors.New("no storage provider configured")
|
||||
)
|
||||
|
|
|
@ -1,28 +1,84 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authentication"
|
||||
"github.com/authelia/authelia/v4/internal/authorization"
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/notification"
|
||||
"github.com/authelia/authelia/v4/internal/ntp"
|
||||
"github.com/authelia/authelia/v4/internal/oidc"
|
||||
"github.com/authelia/authelia/v4/internal/regulation"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
"github.com/authelia/authelia/v4/internal/storage"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
func getStorageProvider() (provider storage.Provider, err error) {
|
||||
func getStorageProvider() (provider storage.Provider) {
|
||||
switch {
|
||||
case config.Storage.PostgreSQL != nil:
|
||||
provider = storage.NewPostgreSQLProvider(*config.Storage.PostgreSQL)
|
||||
return storage.NewPostgreSQLProvider(*config.Storage.PostgreSQL, config.Storage.EncryptionKey)
|
||||
case config.Storage.MySQL != nil:
|
||||
provider = storage.NewMySQLProvider(*config.Storage.MySQL)
|
||||
return storage.NewMySQLProvider(*config.Storage.MySQL, config.Storage.EncryptionKey)
|
||||
case config.Storage.Local != nil:
|
||||
provider = storage.NewSQLiteProvider(config.Storage.Local.Path)
|
||||
return storage.NewSQLiteProvider(config.Storage.Local.Path, config.Storage.EncryptionKey)
|
||||
default:
|
||||
return nil, errors.New("no storage provider configured")
|
||||
return nil
|
||||
}
|
||||
|
||||
if (config.Storage.MySQL != nil && config.Storage.PostgreSQL != nil) ||
|
||||
(config.Storage.MySQL != nil && config.Storage.Local != nil) ||
|
||||
(config.Storage.PostgreSQL != nil && config.Storage.Local != nil) {
|
||||
return nil, errors.New("multiple storage providers are configured but should only configure one")
|
||||
}
|
||||
|
||||
return provider, err
|
||||
}
|
||||
|
||||
func getProviders() (providers middlewares.Providers, warnings []error, errors []error) {
|
||||
// TODO: Adjust this so the CertPool can be used like a provider.
|
||||
autheliaCertPool, warnings, errors := utils.NewX509CertPool(config.CertificatesDirectory)
|
||||
if len(warnings) != 0 || len(errors) != 0 {
|
||||
return providers, warnings, errors
|
||||
}
|
||||
|
||||
storageProvider := getStorageProvider()
|
||||
|
||||
var (
|
||||
userProvider authentication.UserProvider
|
||||
err error
|
||||
)
|
||||
|
||||
switch {
|
||||
case config.AuthenticationBackend.File != nil:
|
||||
userProvider = authentication.NewFileUserProvider(config.AuthenticationBackend.File)
|
||||
case config.AuthenticationBackend.LDAP != nil:
|
||||
userProvider = authentication.NewLDAPUserProvider(config.AuthenticationBackend, autheliaCertPool)
|
||||
}
|
||||
|
||||
var notifier notification.Notifier
|
||||
|
||||
switch {
|
||||
case config.Notifier.SMTP != nil:
|
||||
notifier = notification.NewSMTPNotifier(config.Notifier.SMTP, autheliaCertPool)
|
||||
case config.Notifier.FileSystem != nil:
|
||||
notifier = notification.NewFileNotifier(*config.Notifier.FileSystem)
|
||||
}
|
||||
|
||||
var ntpProvider *ntp.Provider
|
||||
if config.NTP != nil {
|
||||
ntpProvider = ntp.NewProvider(config.NTP)
|
||||
}
|
||||
|
||||
clock := utils.RealClock{}
|
||||
authorizer := authorization.NewAuthorizer(config)
|
||||
sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
|
||||
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
|
||||
|
||||
oidcProvider, err := oidc.NewOpenIDConnectProvider(config.IdentityProviders.OIDC)
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
|
||||
return middlewares.Providers{
|
||||
Authorizer: authorizer,
|
||||
UserProvider: userProvider,
|
||||
Regulator: regulator,
|
||||
OpenIDConnect: oidcProvider,
|
||||
StorageProvider: storageProvider,
|
||||
NTP: ntpProvider,
|
||||
Notifier: notifier,
|
||||
SessionProvider: sessionProvider,
|
||||
}, warnings, errors
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
)
|
||||
|
||||
func TestGetStorageProvider(t *testing.T) {
|
||||
config = &schema.Configuration{}
|
||||
|
||||
assert.Nil(t, getStorageProvider())
|
||||
}
|
|
@ -8,19 +8,11 @@ import (
|
|||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authentication"
|
||||
"github.com/authelia/authelia/v4/internal/authorization"
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/logging"
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/models"
|
||||
"github.com/authelia/authelia/v4/internal/notification"
|
||||
"github.com/authelia/authelia/v4/internal/ntp"
|
||||
"github.com/authelia/authelia/v4/internal/oidc"
|
||||
"github.com/authelia/authelia/v4/internal/regulation"
|
||||
"github.com/authelia/authelia/v4/internal/server"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
"github.com/authelia/authelia/v4/internal/storage"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
|
@ -67,7 +59,7 @@ func cmdRootRun(_ *cobra.Command, _ []string) {
|
|||
logger.Fatalf("Cannot initialize logger: %v", err)
|
||||
}
|
||||
|
||||
providers, warnings, errors := getProviders(config)
|
||||
providers, warnings, errors := getProviders()
|
||||
if len(warnings) != 0 {
|
||||
for _, err := range warnings {
|
||||
logger.Warn(err)
|
||||
|
@ -87,72 +79,6 @@ func cmdRootRun(_ *cobra.Command, _ []string) {
|
|||
server.Start(*config, providers)
|
||||
}
|
||||
|
||||
func getProviders(config *schema.Configuration) (providers middlewares.Providers, warnings []error, errors []error) {
|
||||
// TODO: Adjust this so the CertPool can be used like a provider.
|
||||
autheliaCertPool, warnings, errors := utils.NewX509CertPool(config.CertificatesDirectory)
|
||||
if len(warnings) != 0 || len(errors) != 0 {
|
||||
return providers, warnings, errors
|
||||
}
|
||||
|
||||
var storageProvider storage.Provider
|
||||
|
||||
switch {
|
||||
case config.Storage.PostgreSQL != nil:
|
||||
storageProvider = storage.NewPostgreSQLProvider(*config.Storage.PostgreSQL)
|
||||
case config.Storage.MySQL != nil:
|
||||
storageProvider = storage.NewMySQLProvider(*config.Storage.MySQL)
|
||||
case config.Storage.Local != nil:
|
||||
storageProvider = storage.NewSQLiteProvider(config.Storage.Local.Path)
|
||||
}
|
||||
|
||||
var (
|
||||
userProvider authentication.UserProvider
|
||||
err error
|
||||
)
|
||||
|
||||
switch {
|
||||
case config.AuthenticationBackend.File != nil:
|
||||
userProvider = authentication.NewFileUserProvider(config.AuthenticationBackend.File)
|
||||
case config.AuthenticationBackend.LDAP != nil:
|
||||
userProvider = authentication.NewLDAPUserProvider(config.AuthenticationBackend, autheliaCertPool)
|
||||
}
|
||||
|
||||
var notifier notification.Notifier
|
||||
|
||||
switch {
|
||||
case config.Notifier.SMTP != nil:
|
||||
notifier = notification.NewSMTPNotifier(config.Notifier.SMTP, autheliaCertPool)
|
||||
case config.Notifier.FileSystem != nil:
|
||||
notifier = notification.NewFileNotifier(*config.Notifier.FileSystem)
|
||||
}
|
||||
|
||||
var ntpProvider *ntp.Provider
|
||||
if config.NTP != nil {
|
||||
ntpProvider = ntp.NewProvider(config.NTP)
|
||||
}
|
||||
|
||||
clock := utils.RealClock{}
|
||||
authorizer := authorization.NewAuthorizer(config)
|
||||
sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
|
||||
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
|
||||
|
||||
oidcProvider, err := oidc.NewOpenIDConnectProvider(config.IdentityProviders.OIDC)
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
|
||||
return middlewares.Providers{
|
||||
Authorizer: authorizer,
|
||||
UserProvider: userProvider,
|
||||
Regulator: regulator,
|
||||
OpenIDConnect: oidcProvider,
|
||||
StorageProvider: storageProvider,
|
||||
NTP: ntpProvider,
|
||||
Notifier: notifier,
|
||||
SessionProvider: sessionProvider,
|
||||
}, warnings, errors
|
||||
}
|
||||
|
||||
func doStartupChecks(config *schema.Configuration, providers *middlewares.Providers) {
|
||||
logger := logging.Logger()
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@ func NewStorageCmd() (cmd *cobra.Command) {
|
|||
|
||||
cmd.PersistentFlags().StringSliceP("config", "c", []string{"config.yml"}, "configuration file to load for the storage migration")
|
||||
|
||||
cmd.PersistentFlags().String("encryption-key", "", "the storage encryption key to use")
|
||||
|
||||
cmd.PersistentFlags().String("sqlite.path", "", "the SQLite database path")
|
||||
|
||||
cmd.PersistentFlags().String("mysql.host", "", "the MySQL hostname")
|
||||
|
@ -32,11 +34,74 @@ func NewStorageCmd() (cmd *cobra.Command) {
|
|||
cmd.AddCommand(
|
||||
newStorageMigrateCmd(),
|
||||
newStorageSchemaInfoCmd(),
|
||||
newStorageEncryptionCmd(),
|
||||
newStorageExportCmd(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newStorageEncryptionCmd() (cmd *cobra.Command) {
|
||||
cmd = &cobra.Command{
|
||||
Use: "encryption",
|
||||
Short: "Manages encryption",
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
newStorageEncryptionChangeKeyCmd(),
|
||||
newStorageEncryptionCheckCmd(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newStorageEncryptionCheckCmd() (cmd *cobra.Command) {
|
||||
cmd = &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Checks the encryption key against the database data",
|
||||
RunE: storageSchemaEncryptionCheckRunE,
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("verbose", false, "enables verbose checking of every row of encrypted data")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newStorageEncryptionChangeKeyCmd() (cmd *cobra.Command) {
|
||||
cmd = &cobra.Command{
|
||||
Use: "change-key",
|
||||
Short: "Changes the encryption key",
|
||||
RunE: storageSchemaEncryptionChangeKeyRunE,
|
||||
}
|
||||
|
||||
cmd.Flags().String("new-encryption-key", "", "the new key to encrypt the data with")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newStorageExportCmd() (cmd *cobra.Command) {
|
||||
cmd = &cobra.Command{
|
||||
Use: "export",
|
||||
Short: "Performs exports",
|
||||
}
|
||||
|
||||
cmd.AddCommand(newStorageExportTOTPConfigurationsCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newStorageExportTOTPConfigurationsCmd() (cmd *cobra.Command) {
|
||||
cmd = &cobra.Command{
|
||||
Use: "totp-configurations",
|
||||
Short: "Performs exports of the totp configurations",
|
||||
RunE: storageExportTOTPConfigurationsRunE,
|
||||
}
|
||||
|
||||
cmd.Flags().String("format", storageExportFormatCSV, "changes the format of the export, options are csv and uri")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newStorageSchemaInfoCmd() (cmd *cobra.Command) {
|
||||
cmd = &cobra.Command{
|
||||
Use: "schema-info",
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/configuration"
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/configuration/validator"
|
||||
"github.com/authelia/authelia/v4/internal/models"
|
||||
"github.com/authelia/authelia/v4/internal/storage"
|
||||
)
|
||||
|
||||
|
@ -38,6 +39,7 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
|
|||
}
|
||||
|
||||
mapping := map[string]string{
|
||||
"encryption-key": "storage.encryption_key",
|
||||
"sqlite.path": "storage.local.path",
|
||||
"mysql.host": "storage.mysql.host",
|
||||
"mysql.port": "storage.mysql.port",
|
||||
|
@ -100,17 +102,179 @@ func storagePersistentPreRunE(cmd *cobra.Command, _ []string) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
func storageSchemaEncryptionCheckRunE(cmd *cobra.Command, args []string) (err error) {
|
||||
var (
|
||||
provider storage.Provider
|
||||
ctx = context.Background()
|
||||
)
|
||||
|
||||
provider = getStorageProvider()
|
||||
if provider == nil {
|
||||
return errNoStorageProvider
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = provider.Close()
|
||||
}()
|
||||
|
||||
verbose, err := cmd.Flags().GetBool("verbose")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = provider.SchemaEncryptionCheckKey(ctx, verbose); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, storage.ErrSchemaEncryptionVersionUnsupported):
|
||||
fmt.Printf("Could not check encryption key for validity. The schema version doesn't support encryption.\n")
|
||||
case errors.Is(err, storage.ErrSchemaEncryptionInvalidKey):
|
||||
fmt.Printf("Encryption key validation: failed.\n\nError: %v.\n", err)
|
||||
default:
|
||||
fmt.Printf("Could not check encryption key for validity.\n\nError: %v.\n", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Encryption key validation: success.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func storageSchemaEncryptionChangeKeyRunE(cmd *cobra.Command, args []string) (err error) {
|
||||
var (
|
||||
provider storage.Provider
|
||||
ctx = context.Background()
|
||||
)
|
||||
|
||||
provider = getStorageProvider()
|
||||
if provider == nil {
|
||||
return errNoStorageProvider
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = provider.Close()
|
||||
}()
|
||||
|
||||
if err = checkStorageSchemaUpToDate(ctx, provider); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
version, err := provider.SchemaVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if version <= 0 {
|
||||
return errors.New("schema version must be at least version 1 to change the encryption key")
|
||||
}
|
||||
|
||||
key, err := cmd.Flags().GetString("new-encryption-key")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if key == "" {
|
||||
return errors.New("you must set the --new-encryption-key flag")
|
||||
}
|
||||
|
||||
if len(key) < 20 {
|
||||
return errors.New("the encryption key must be at least 20 characters")
|
||||
}
|
||||
|
||||
if err = provider.SchemaEncryptionChangeKey(ctx, key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Completed the encryption key change. Please adjust your configuration to use the new key.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func storageExportTOTPConfigurationsRunE(cmd *cobra.Command, args []string) (err error) {
|
||||
var (
|
||||
provider storage.Provider
|
||||
ctx = context.Background()
|
||||
)
|
||||
|
||||
provider = getStorageProvider()
|
||||
if provider == nil {
|
||||
return errNoStorageProvider
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = provider.Close()
|
||||
}()
|
||||
|
||||
if err = checkStorageSchemaUpToDate(ctx, provider); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
format, err := cmd.Flags().GetString("format")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch format {
|
||||
case storageExportFormatCSV, storageExportFormatURI:
|
||||
break
|
||||
default:
|
||||
return errors.New("format must be csv or uri")
|
||||
}
|
||||
|
||||
limit := 10
|
||||
|
||||
var configurations []models.TOTPConfiguration
|
||||
|
||||
for page := 0; true; page++ {
|
||||
configurations, err = provider.LoadTOTPConfigurations(ctx, limit, page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if page == 0 && format == storageExportFormatCSV {
|
||||
fmt.Printf("issuer,username,algorithm,digits,period,secret\n")
|
||||
}
|
||||
|
||||
for _, c := range configurations {
|
||||
switch format {
|
||||
case storageExportFormatCSV:
|
||||
fmt.Printf("%s,%s,%s,%d,%d,%s\n", "Authelia", c.Username, c.Algorithm, c.Digits, c.Period, string(c.Secret))
|
||||
case storageExportFormatURI:
|
||||
fmt.Printf("otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=%s&digits=%d&period=%d\n", "Authelia", c.Username, string(c.Secret), "Authelia", c.Algorithm, c.Digits, c.Period)
|
||||
}
|
||||
}
|
||||
|
||||
if len(configurations) < limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func storageMigrateHistoryRunE(_ *cobra.Command, _ []string) (err error) {
|
||||
var (
|
||||
provider storage.Provider
|
||||
ctx = context.Background()
|
||||
)
|
||||
|
||||
provider, err = getStorageProvider()
|
||||
provider = getStorageProvider()
|
||||
if provider == nil {
|
||||
return errNoStorageProvider
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = provider.Close()
|
||||
}()
|
||||
|
||||
version, err := provider.SchemaVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if version <= 0 {
|
||||
fmt.Println("No migration history is available for schemas that not version 1 or above.")
|
||||
return
|
||||
}
|
||||
|
||||
migrations, err := provider.SchemaMigrationHistory(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -138,11 +302,15 @@ func newStorageMigrateListRunE(up bool) func(cmd *cobra.Command, args []string)
|
|||
directionStr string
|
||||
)
|
||||
|
||||
provider, err = getStorageProvider()
|
||||
if err != nil {
|
||||
return err
|
||||
provider = getStorageProvider()
|
||||
if provider == nil {
|
||||
return errNoStorageProvider
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = provider.Close()
|
||||
}()
|
||||
|
||||
if up {
|
||||
migrations, err = provider.SchemaMigrationsUp(ctx, 0)
|
||||
directionStr = "Up"
|
||||
|
@ -151,13 +319,7 @@ func newStorageMigrateListRunE(up bool) func(cmd *cobra.Command, args []string)
|
|||
directionStr = "Down"
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "cannot migrate to the same version as prior" {
|
||||
fmt.Printf("No %s migrations found\n", directionStr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil && !errors.Is(err, storage.ErrNoAvailableMigrations) && !errors.Is(err, storage.ErrMigrateCurrentVersionSameAsTarget) {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -182,11 +344,15 @@ func newStorageMigrationRunE(up bool) func(cmd *cobra.Command, args []string) (e
|
|||
ctx = context.Background()
|
||||
)
|
||||
|
||||
provider, err = getStorageProvider()
|
||||
if err != nil {
|
||||
return err
|
||||
provider = getStorageProvider()
|
||||
if provider == nil {
|
||||
return errNoStorageProvider
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = provider.Close()
|
||||
}()
|
||||
|
||||
target, err := cmd.Flags().GetInt("target")
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -253,11 +419,15 @@ func storageSchemaInfoRunE(_ *cobra.Command, _ []string) (err error) {
|
|||
tablesStr string
|
||||
)
|
||||
|
||||
provider, err = getStorageProvider()
|
||||
if err != nil {
|
||||
return err
|
||||
provider = getStorageProvider()
|
||||
if provider == nil {
|
||||
return errNoStorageProvider
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = provider.Close()
|
||||
}()
|
||||
|
||||
version, err := provider.SchemaVersion(ctx)
|
||||
if err != nil && err.Error() != "unknown schema state" {
|
||||
return err
|
||||
|
@ -285,7 +455,37 @@ func storageSchemaInfoRunE(_ *cobra.Command, _ []string) (err error) {
|
|||
upgradeStr = "no"
|
||||
}
|
||||
|
||||
fmt.Printf("Schema Version: %s\nSchema Upgrade Available: %s\nSchema Tables: %s\n", storage.SchemaVersionToString(version), upgradeStr, tablesStr)
|
||||
var encryption string
|
||||
|
||||
if err = provider.SchemaEncryptionCheckKey(ctx, false); err != nil {
|
||||
if errors.Is(err, storage.ErrSchemaEncryptionVersionUnsupported) {
|
||||
encryption = "unsupported (schema version)"
|
||||
} else {
|
||||
encryption = "invalid"
|
||||
}
|
||||
} else {
|
||||
encryption = "valid"
|
||||
}
|
||||
|
||||
fmt.Printf("Schema Version: %s\nSchema Upgrade Available: %s\nSchema Tables: %s\nSchema Encryption Key: %s\n", storage.SchemaVersionToString(version), upgradeStr, tablesStr, encryption)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkStorageSchemaUpToDate(ctx context.Context, provider storage.Provider) (err error) {
|
||||
version, err := provider.SchemaVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
latest, err := provider.SchemaLatestVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if version != latest {
|
||||
return fmt.Errorf("schema is version %d which is outdated please migrate to version %d in order to use this command or use an older binary", version, latest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -507,6 +507,10 @@ regulation:
|
|||
##
|
||||
## The available providers are: `local`, `mysql`, `postgres`. You must use one and only one of these providers.
|
||||
storage:
|
||||
## The encryption key that is used to encrypt sensitive information in the database. Must be a string with a minimum
|
||||
## length of 20. Please see the docs if you configure this with an undesirable key and need to change it.
|
||||
# encryption_key: you_must_generate_a_random_string_of_more_than_twenty_chars_and_configure_this
|
||||
|
||||
##
|
||||
## Local (Storage Provider)
|
||||
##
|
||||
|
|
|
@ -138,6 +138,7 @@ func TestShouldValidateAndRaiseErrorsOnNormalConfigurationAndSecret(t *testing.T
|
|||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "an env storage mysql password"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET_FILE", "./test_resources/example_secret"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD", "an env authentication backend ldap password"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_ENCRYPTION_KEY", "a_very_bad_encryption_key"))
|
||||
|
||||
val := schema.NewStructValidator()
|
||||
_, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
|
||||
|
@ -152,6 +153,7 @@ func TestShouldValidateAndRaiseErrorsOnNormalConfigurationAndSecret(t *testing.T
|
|||
assert.Equal(t, "example_secret value", config.Session.Secret)
|
||||
assert.Equal(t, "an env storage mysql password", config.Storage.MySQL.Password)
|
||||
assert.Equal(t, "an env authentication backend ldap password", config.AuthenticationBackend.LDAP.Password)
|
||||
assert.Equal(t, "a_very_bad_encryption_key", config.Storage.EncryptionKey)
|
||||
}
|
||||
|
||||
func TestShouldRaiseIOErrOnUnreadableFile(t *testing.T) {
|
||||
|
@ -184,6 +186,7 @@ func TestShouldValidateConfigurationWithEnvSecrets(t *testing.T) {
|
|||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD_FILE", "./test_resources/example_secret"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET_FILE", "./test_resources/example_secret"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", "./test_resources/example_secret"))
|
||||
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_ENCRYPTION_KEY_FILE", "./test_resources/example_secret"))
|
||||
|
||||
val := schema.NewStructValidator()
|
||||
_, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
|
||||
|
@ -196,6 +199,7 @@ func TestShouldValidateConfigurationWithEnvSecrets(t *testing.T) {
|
|||
assert.Equal(t, "example_secret value", config.Session.Secret)
|
||||
assert.Equal(t, "example_secret value", config.AuthenticationBackend.LDAP.Password)
|
||||
assert.Equal(t, "example_secret value", config.Storage.MySQL.Password)
|
||||
assert.Equal(t, "example_secret value", config.Storage.EncryptionKey)
|
||||
}
|
||||
|
||||
func TestShouldValidateAndRaiseErrorsOnBadConfiguration(t *testing.T) {
|
||||
|
@ -275,6 +279,7 @@ func testReset() {
|
|||
testUnsetEnvName("PORT")
|
||||
testUnsetEnvName("IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY")
|
||||
testUnsetEnvName("IDENTITY_PROVIDERS_OIDC_HMAC_SECRET")
|
||||
testUnsetEnvName("STORAGE_ENCRYPTION_KEY")
|
||||
}
|
||||
|
||||
func testUnsetEnvName(name string) {
|
||||
|
|
|
@ -33,6 +33,8 @@ type StorageConfiguration struct {
|
|||
Local *LocalStorageConfiguration `koanf:"local"`
|
||||
MySQL *MySQLStorageConfiguration `koanf:"mysql"`
|
||||
PostgreSQL *PostgreSQLStorageConfiguration `koanf:"postgres"`
|
||||
|
||||
EncryptionKey string `koanf:"encryption_key"`
|
||||
}
|
||||
|
||||
// DefaultPostgreSQLStorageConfiguration represents the default PostgreSQL configuration.
|
||||
|
|
|
@ -28,6 +28,7 @@ func newDefaultConfig() schema.Configuration {
|
|||
Name: "authelia_session",
|
||||
Secret: "secret",
|
||||
}
|
||||
config.Storage.EncryptionKey = testEncryptionKey
|
||||
config.Storage.Local = &schema.LocalStorageConfiguration{
|
||||
Path: "abc",
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ const (
|
|||
testModeDisabled = "disable"
|
||||
testTLSCert = "/tmp/cert.pem"
|
||||
testTLSKey = "/tmp/key.pem"
|
||||
testEncryptionKey = "a_not_so_secure_encryption_key"
|
||||
)
|
||||
|
||||
// Notifier Error constants.
|
||||
|
@ -206,6 +207,8 @@ var ValidKeys = []string{
|
|||
"session.redis.timeouts.read",
|
||||
"session.redis.timeouts.write",
|
||||
|
||||
"storage.encryption_key",
|
||||
|
||||
// Local Storage Keys.
|
||||
"storage.local.path",
|
||||
|
||||
|
|
|
@ -20,6 +20,12 @@ func ValidateStorage(configuration schema.StorageConfiguration, validator *schem
|
|||
case configuration.Local != nil:
|
||||
validateLocalStorageConfiguration(configuration.Local, validator)
|
||||
}
|
||||
|
||||
if configuration.EncryptionKey == "" {
|
||||
validator.Push(errors.New("the configuration option storage.encryption_key must be provided"))
|
||||
} else if len(configuration.EncryptionKey) < 20 {
|
||||
validator.Push(errors.New("the configuration option storage.encryption_key must be 20 characters or longer"))
|
||||
}
|
||||
}
|
||||
|
||||
func validateMySQLConfiguration(configuration *schema.SQLStorageConfiguration, validator *schema.StructValidator) {
|
||||
|
|
|
@ -16,6 +16,7 @@ type StorageSuite struct {
|
|||
|
||||
func (suite *StorageSuite) SetupTest() {
|
||||
suite.validator = schema.NewStructValidator()
|
||||
suite.configuration.EncryptionKey = testEncryptionKey
|
||||
suite.configuration.Local = &schema.LocalStorageConfiguration{
|
||||
Path: "/this/is/a/path",
|
||||
}
|
||||
|
@ -106,6 +107,26 @@ func (suite *StorageSuite) TestShouldValidatePostgresSSLModeMustBeValid() {
|
|||
suite.Assert().EqualError(suite.validator.Errors()[0], "SSL mode must be 'disable', 'require', 'verify-ca', or 'verify-full'")
|
||||
}
|
||||
|
||||
func (suite *StorageSuite) TestShouldRaiseErrorOnNoEncryptionKey() {
|
||||
suite.configuration.EncryptionKey = ""
|
||||
|
||||
ValidateStorage(suite.configuration, suite.validator)
|
||||
|
||||
suite.Assert().False(suite.validator.HasWarnings())
|
||||
suite.Require().Len(suite.validator.Errors(), 1)
|
||||
suite.Assert().EqualError(suite.validator.Errors()[0], "the configuration option storage.encryption_key must be provided")
|
||||
}
|
||||
|
||||
func (suite *StorageSuite) TestShouldRaiseErrorOnShortEncryptionKey() {
|
||||
suite.configuration.EncryptionKey = "abc"
|
||||
|
||||
ValidateStorage(suite.configuration, suite.validator)
|
||||
|
||||
suite.Assert().False(suite.validator.HasWarnings())
|
||||
suite.Require().Len(suite.validator.Errors(), 1)
|
||||
suite.Assert().EqualError(suite.validator.Errors()[0], "the configuration option storage.encryption_key must be 20 characters or longer")
|
||||
}
|
||||
|
||||
func TestShouldRunStorageSuite(t *testing.T) {
|
||||
suite.Run(t, new(StorageSuite))
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ func secondFactorTOTPIdentityFinish(ctx *middlewares.AutheliaCtx, username strin
|
|||
Username: username,
|
||||
Algorithm: otpAlgoToString(algorithm),
|
||||
Digits: 6,
|
||||
Secret: key.Secret(),
|
||||
Secret: []byte(key.Secret()),
|
||||
Period: key.Period(),
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ func (s *HandlerSignTOTPSuite) TearDownTest() {
|
|||
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
|
||||
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
|
||||
|
||||
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: "secret", Period: 30, Algorithm: "SHA1"}
|
||||
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
|
||||
|
||||
s.mock.StorageProviderMock.EXPECT().
|
||||
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
|
||||
|
@ -65,7 +65,7 @@ func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToDefaultURL() {
|
|||
func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
|
||||
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
|
||||
|
||||
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: "secret", Period: 30, Algorithm: "SHA1"}
|
||||
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
|
||||
|
||||
s.mock.StorageProviderMock.EXPECT().
|
||||
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
|
||||
|
@ -88,7 +88,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotReturnRedirectURL() {
|
|||
func (s *HandlerSignTOTPSuite) TestShouldRedirectUserToSafeTargetURL() {
|
||||
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
|
||||
|
||||
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: "secret", Period: 30, Algorithm: "SHA1"}
|
||||
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
|
||||
|
||||
s.mock.StorageProviderMock.EXPECT().
|
||||
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
|
||||
|
@ -116,10 +116,10 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
|
|||
|
||||
s.mock.StorageProviderMock.EXPECT().
|
||||
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
|
||||
Return(&models.TOTPConfiguration{Secret: "secret"}, nil)
|
||||
Return(&models.TOTPConfiguration{Secret: []byte("secret")}, nil)
|
||||
|
||||
verifier.EXPECT().
|
||||
Verify(gomock.Eq(&models.TOTPConfiguration{Secret: "secret"}), gomock.Eq("abc")).
|
||||
Verify(gomock.Eq(&models.TOTPConfiguration{Secret: []byte("secret")}), gomock.Eq("abc")).
|
||||
Return(true, nil)
|
||||
|
||||
bodyBytes, err := json.Marshal(signTOTPRequestBody{
|
||||
|
@ -136,7 +136,7 @@ func (s *HandlerSignTOTPSuite) TestShouldNotRedirectToUnsafeURL() {
|
|||
func (s *HandlerSignTOTPSuite) TestShouldRegenerateSessionForPreventingSessionFixation() {
|
||||
verifier := NewMockTOTPVerifier(s.mock.Ctrl)
|
||||
|
||||
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: "secret", Period: 30, Algorithm: "SHA1"}
|
||||
config := models.TOTPConfiguration{ID: 1, Username: "john", Digits: 6, Secret: []byte("secret"), Period: 30, Algorithm: "SHA1"}
|
||||
|
||||
s.mock.StorageProviderMock.EXPECT().
|
||||
LoadTOTPConfiguration(s.mock.Ctx, gomock.Any()).
|
||||
|
|
|
@ -34,7 +34,7 @@ func (tv *TOTPVerifierImpl) Verify(config *models.TOTPConfiguration, token strin
|
|||
Algorithm: otpStringToAlgo(config.Algorithm),
|
||||
}
|
||||
|
||||
return totp.ValidateCustom(token, config.Secret, time.Now().UTC(), opts)
|
||||
return totp.ValidateCustom(token, string(config.Secret), time.Now().UTC(), opts)
|
||||
}
|
||||
|
||||
func otpAlgoToString(algorithm otp.Algorithm) (out string) {
|
||||
|
|
|
@ -7,5 +7,5 @@ type TOTPConfiguration struct {
|
|||
Algorithm string `db:"algorithm"`
|
||||
Digits int `db:"digits"`
|
||||
Period uint64 `db:"totp_period"`
|
||||
Secret string `db:"secret"`
|
||||
Secret []byte `db:"secret"`
|
||||
}
|
||||
|
|
|
@ -12,10 +12,15 @@ const (
|
|||
tableDUODevices = "duo_devices"
|
||||
tableAuthenticationLogs = "authentication_logs"
|
||||
tableMigrations = "migrations"
|
||||
tableEncryption = "encryption"
|
||||
|
||||
tablePrefixBackup = "_bkp_"
|
||||
)
|
||||
|
||||
const (
|
||||
encryptionNameCheck = "check"
|
||||
)
|
||||
|
||||
// WARNING: Do not change/remove these consts. They are used for Pre1 migrations.
|
||||
const (
|
||||
tablePre1TOTPSecrets = "totp_secrets"
|
||||
|
|
|
@ -8,17 +8,31 @@ var (
|
|||
// ErrNoU2FDeviceHandle error thrown when no U2F device handle has been found in DB.
|
||||
ErrNoU2FDeviceHandle = errors.New("no U2F device handle found")
|
||||
|
||||
// ErrNoAuthenticationLogs error thrown when no matching authentication logs hve been found in DB.
|
||||
ErrNoAuthenticationLogs = errors.New("no matching authentication logs found")
|
||||
|
||||
// ErrNoTOTPSecret error thrown when no TOTP secret has been found in DB.
|
||||
ErrNoTOTPSecret = errors.New("no TOTP secret registered")
|
||||
|
||||
// ErrNoAvailableMigrations is returned when no available migrations can be found.
|
||||
ErrNoAvailableMigrations = errors.New("no available migrations")
|
||||
|
||||
// ErrMigrateCurrentVersionSameAsTarget is returned when the target version is the same as the current.
|
||||
ErrMigrateCurrentVersionSameAsTarget = errors.New("current version is same as migration target, no action being taken")
|
||||
|
||||
// ErrSchemaAlreadyUpToDate is returned when the schema is already up to date.
|
||||
ErrSchemaAlreadyUpToDate = errors.New("schema already up to date")
|
||||
|
||||
// ErrNoMigrationsFound is returned when no migrations were found.
|
||||
ErrNoMigrationsFound = errors.New("no schema migrations found")
|
||||
|
||||
// ErrSchemaEncryptionVersionUnsupported is returned when the schema is checked if the encryption key is valid for
|
||||
// the database but the schema doesn't support encryption.
|
||||
ErrSchemaEncryptionVersionUnsupported = errors.New("schema version doesn't support encryption")
|
||||
|
||||
// ErrSchemaEncryptionInvalidKey is returned when the schema is checked if the encryption key is valid for
|
||||
// the database but the key doesn't appear to be valid.
|
||||
ErrSchemaEncryptionInvalidKey = errors.New("the encryption key is not valid against the schema check value")
|
||||
)
|
||||
|
||||
// Error formats for the storage provider.
|
||||
|
|
|
@ -85,7 +85,7 @@ func loadMigration(providerName string, version int, up bool) (migration *Schema
|
|||
// this indicates the database zero state.
|
||||
func loadMigrations(providerName string, prior, target int) (migrations []SchemaMigration, err error) {
|
||||
if prior == target && (prior != -1 || target != -1) {
|
||||
return nil, errors.New("cannot migrate to the same version as prior")
|
||||
return nil, ErrMigrateCurrentVersionSameAsTarget
|
||||
}
|
||||
|
||||
entries, err := migrationsFS.ReadDir("migrations")
|
||||
|
|
|
@ -4,3 +4,4 @@ DROP TABLE IF EXISTS totp_configurations;
|
|||
DROP TABLE IF EXISTS u2f_devices;
|
||||
DROP TABLE IF EXISTS user_preferences;
|
||||
DROP TABLE IF EXISTS migrations;
|
||||
DROP TABLE IF EXISTS encryption;
|
|
@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS totp_configurations (
|
|||
algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
|
||||
digits INTEGER NOT NULL DEFAULT 6,
|
||||
totp_period INTEGER NOT NULL DEFAULT 30,
|
||||
secret VARCHAR(64) NOT NULL,
|
||||
secret BLOB NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (username)
|
||||
);
|
||||
|
@ -53,3 +53,11 @@ CREATE TABLE IF NOT EXISTS migrations (
|
|||
application_version VARCHAR(128) NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS encryption (
|
||||
id INTEGER AUTO_INCREMENT,
|
||||
name VARCHAR(100),
|
||||
value BLOB NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (name)
|
||||
);
|
||||
|
|
|
@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS totp_configurations (
|
|||
algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
|
||||
digits INTEGER NOT NULL DEFAULT 6,
|
||||
totp_period INTEGER NOT NULL DEFAULT 30,
|
||||
secret VARCHAR(64) NOT NULL,
|
||||
secret BYTEA NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (username)
|
||||
);
|
||||
|
@ -53,3 +53,11 @@ CREATE TABLE IF NOT EXISTS migrations (
|
|||
application_version VARCHAR(128) NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS encryption (
|
||||
id SERIAL,
|
||||
name VARCHAR(100),
|
||||
value BYTEA NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (name)
|
||||
);
|
|
@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS totp_configurations (
|
|||
algorithm VARCHAR(6) NOT NULL DEFAULT 'SHA1',
|
||||
digits INTEGER(1) NOT NULL DEFAULT 6,
|
||||
totp_period INTEGER NOT NULL DEFAULT 30,
|
||||
secret VARCHAR(64) NOT NULL,
|
||||
secret BLOB NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (username)
|
||||
);
|
||||
|
@ -52,3 +52,11 @@ CREATE TABLE IF NOT EXISTS migrations (
|
|||
application_version VARCHAR(128) NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS encryption (
|
||||
id INTEGER,
|
||||
name VARCHAR(100),
|
||||
value BLOB NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (name)
|
||||
);
|
||||
|
|
|
@ -24,6 +24,8 @@ type Provider interface {
|
|||
SaveTOTPConfiguration(ctx context.Context, config models.TOTPConfiguration) (err error)
|
||||
DeleteTOTPConfiguration(ctx context.Context, username string) (err error)
|
||||
LoadTOTPConfiguration(ctx context.Context, username string) (config *models.TOTPConfiguration, err error)
|
||||
LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []models.TOTPConfiguration, err error)
|
||||
UpdateTOTPConfigurationSecret(ctx context.Context, config models.TOTPConfiguration) (err error)
|
||||
|
||||
SaveU2FDevice(ctx context.Context, device models.U2FDevice) (err error)
|
||||
LoadU2FDevice(ctx context.Context, username string) (device *models.U2FDevice, err error)
|
||||
|
@ -33,9 +35,14 @@ type Provider interface {
|
|||
SchemaMigrate(ctx context.Context, up bool, version int) (err error)
|
||||
SchemaMigrationHistory(ctx context.Context) (migrations []models.Migration, err error)
|
||||
|
||||
SchemaEncryptionChangeKey(ctx context.Context, encryptionKey string) (err error)
|
||||
SchemaEncryptionCheckKey(ctx context.Context, verbose bool) (err error)
|
||||
|
||||
SchemaLatestVersion() (version int, err error)
|
||||
SchemaMigrationsUp(ctx context.Context, version int) (migrations []SchemaMigration, err error)
|
||||
SchemaMigrationsDown(ctx context.Context, version int) (migrations []SchemaMigration, err error)
|
||||
|
||||
Close() (err error)
|
||||
}
|
||||
|
||||
// RegulatorProvider is an interface providing storage capabilities for persisting any kind of data related to the regulator.
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
"context"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
"github.com/golang/mock/gomock"
|
||||
|
||||
models "github.com/authelia/authelia/v4/internal/models"
|
||||
"github.com/authelia/authelia/v4/internal/models"
|
||||
)
|
||||
|
||||
// MockProvider is a mock of Provider interface.
|
||||
|
@ -50,6 +50,20 @@ func (mr *MockProviderMockRecorder) AppendAuthenticationLog(arg0, arg1 interface
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendAuthenticationLog", reflect.TypeOf((*MockProvider)(nil).AppendAuthenticationLog), arg0, arg1)
|
||||
}
|
||||
|
||||
// Close mocks base method.
|
||||
func (m *MockProvider) Close() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Close")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Close indicates an expected call of Close.
|
||||
func (mr *MockProviderMockRecorder) Close() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockProvider)(nil).Close))
|
||||
}
|
||||
|
||||
// DeleteTOTPConfiguration mocks base method.
|
||||
func (m *MockProvider) DeleteTOTPConfiguration(arg0 context.Context, arg1 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -124,6 +138,21 @@ func (mr *MockProviderMockRecorder) LoadTOTPConfiguration(arg0, arg1 interface{}
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadTOTPConfiguration", reflect.TypeOf((*MockProvider)(nil).LoadTOTPConfiguration), arg0, arg1)
|
||||
}
|
||||
|
||||
// LoadTOTPConfigurations mocks base method.
|
||||
func (m *MockProvider) LoadTOTPConfigurations(arg0 context.Context, arg1, arg2 int) ([]models.TOTPConfiguration, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LoadTOTPConfigurations", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].([]models.TOTPConfiguration)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// LoadTOTPConfigurations indicates an expected call of LoadTOTPConfigurations.
|
||||
func (mr *MockProviderMockRecorder) LoadTOTPConfigurations(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadTOTPConfigurations", reflect.TypeOf((*MockProvider)(nil).LoadTOTPConfigurations), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// LoadU2FDevice mocks base method.
|
||||
func (m *MockProvider) LoadU2FDevice(arg0 context.Context, arg1 string) (*models.U2FDevice, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -224,6 +253,34 @@ func (mr *MockProviderMockRecorder) SaveU2FDevice(arg0, arg1 interface{}) *gomoc
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveU2FDevice", reflect.TypeOf((*MockProvider)(nil).SaveU2FDevice), arg0, arg1)
|
||||
}
|
||||
|
||||
// SchemaEncryptionChangeKey mocks base method.
|
||||
func (m *MockProvider) SchemaEncryptionChangeKey(arg0 context.Context, arg1 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SchemaEncryptionChangeKey", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SchemaEncryptionChangeKey indicates an expected call of SchemaEncryptionChangeKey.
|
||||
func (mr *MockProviderMockRecorder) SchemaEncryptionChangeKey(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaEncryptionChangeKey", reflect.TypeOf((*MockProvider)(nil).SchemaEncryptionChangeKey), arg0, arg1)
|
||||
}
|
||||
|
||||
// SchemaEncryptionCheckKey mocks base method.
|
||||
func (m *MockProvider) SchemaEncryptionCheckKey(arg0 context.Context, arg1 bool) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SchemaEncryptionCheckKey", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SchemaEncryptionCheckKey indicates an expected call of SchemaEncryptionCheckKey.
|
||||
func (mr *MockProviderMockRecorder) SchemaEncryptionCheckKey(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchemaEncryptionCheckKey", reflect.TypeOf((*MockProvider)(nil).SchemaEncryptionCheckKey), arg0, arg1)
|
||||
}
|
||||
|
||||
// SchemaLatestVersion mocks base method.
|
||||
func (m *MockProvider) SchemaLatestVersion() (int, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -341,3 +398,17 @@ func (mr *MockProviderMockRecorder) StartupCheck() *gomock.Call {
|
|||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartupCheck", reflect.TypeOf((*MockProvider)(nil).StartupCheck))
|
||||
}
|
||||
|
||||
// UpdateTOTPConfigurationSecret mocks base method.
|
||||
func (m *MockProvider) UpdateTOTPConfigurationSecret(arg0 context.Context, arg1 models.TOTPConfiguration) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateTOTPConfigurationSecret", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateTOTPConfigurationSecret indicates an expected call of UpdateTOTPConfigurationSecret.
|
||||
func (mr *MockProviderMockRecorder) UpdateTOTPConfigurationSecret(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTOTPConfigurationSecret", reflect.TypeOf((*MockProvider)(nil).UpdateTOTPConfigurationSecret), arg0, arg1)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package storage
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -16,15 +17,16 @@ import (
|
|||
)
|
||||
|
||||
// NewSQLProvider generates a generic SQLProvider to be used with other SQL provider NewUp's.
|
||||
func NewSQLProvider(name, driverName, dataSourceName string) (provider SQLProvider) {
|
||||
func NewSQLProvider(name, driverName, dataSourceName, encryptionKey string) (provider SQLProvider) {
|
||||
db, err := sqlx.Open(driverName, dataSourceName)
|
||||
|
||||
provider = SQLProvider{
|
||||
db: db,
|
||||
key: sha256.Sum256([]byte(encryptionKey)),
|
||||
name: name,
|
||||
driverName: driverName,
|
||||
db: db,
|
||||
log: logging.Logger(),
|
||||
errOpen: err,
|
||||
log: logging.Logger(),
|
||||
|
||||
sqlInsertAuthenticationAttempt: fmt.Sprintf(queryFmtInsertAuthenticationLogEntry, tableAuthenticationLogs),
|
||||
sqlSelectAuthenticationAttemptsByUsername: fmt.Sprintf(queryFmtSelect1FAAuthenticationLogEntryByUsername, tableAuthenticationLogs),
|
||||
|
@ -36,6 +38,10 @@ func NewSQLProvider(name, driverName, dataSourceName string) (provider SQLProvid
|
|||
sqlUpsertTOTPConfig: fmt.Sprintf(queryFmtUpsertTOTPConfiguration, tableTOTPConfigurations),
|
||||
sqlDeleteTOTPConfig: fmt.Sprintf(queryFmtDeleteTOTPConfiguration, tableTOTPConfigurations),
|
||||
sqlSelectTOTPConfig: fmt.Sprintf(queryFmtSelectTOTPConfiguration, tableTOTPConfigurations),
|
||||
sqlSelectTOTPConfigs: fmt.Sprintf(queryFmtSelectTOTPConfigurations, tableTOTPConfigurations),
|
||||
|
||||
sqlUpdateTOTPConfigSecret: fmt.Sprintf(queryFmtUpdateTOTPConfigurationSecret, tableTOTPConfigurations),
|
||||
sqlUpdateTOTPConfigSecretByUsername: fmt.Sprintf(queryFmtUpdateTOTPConfigurationSecretByUsername, tableTOTPConfigurations),
|
||||
|
||||
sqlUpsertU2FDevice: fmt.Sprintf(queryFmtUpsertU2FDevice, tableU2FDevices),
|
||||
sqlSelectU2FDevice: fmt.Sprintf(queryFmtSelectU2FDevice, tableU2FDevices),
|
||||
|
@ -48,20 +54,29 @@ func NewSQLProvider(name, driverName, dataSourceName string) (provider SQLProvid
|
|||
sqlSelectMigrations: fmt.Sprintf(queryFmtSelectMigrations, tableMigrations),
|
||||
sqlSelectLatestMigration: fmt.Sprintf(queryFmtSelectLatestMigration, tableMigrations),
|
||||
|
||||
sqlUpsertEncryptionValue: fmt.Sprintf(queryFmtUpsertEncryptionValue, tableEncryption),
|
||||
sqlSelectEncryptionValue: fmt.Sprintf(queryFmtSelectEncryptionValue, tableEncryption),
|
||||
|
||||
sqlFmtRenameTable: queryFmtRenameTable,
|
||||
}
|
||||
|
||||
key := sha256.Sum256([]byte(encryptionKey))
|
||||
|
||||
provider.key = key
|
||||
|
||||
return provider
|
||||
}
|
||||
|
||||
// SQLProvider is a storage provider persisting data in a SQL database.
|
||||
type SQLProvider struct {
|
||||
db *sqlx.DB
|
||||
log *logrus.Logger
|
||||
key [32]byte
|
||||
name string
|
||||
driverName string
|
||||
errOpen error
|
||||
|
||||
log *logrus.Logger
|
||||
|
||||
// Table: authentication_logs.
|
||||
sqlInsertAuthenticationAttempt string
|
||||
sqlSelectAuthenticationAttemptsByUsername string
|
||||
|
@ -75,6 +90,10 @@ type SQLProvider struct {
|
|||
sqlUpsertTOTPConfig string
|
||||
sqlDeleteTOTPConfig string
|
||||
sqlSelectTOTPConfig string
|
||||
sqlSelectTOTPConfigs string
|
||||
|
||||
sqlUpdateTOTPConfigSecret string
|
||||
sqlUpdateTOTPConfigSecretByUsername string
|
||||
|
||||
// Table: u2f_devices.
|
||||
sqlUpsertU2FDevice string
|
||||
|
@ -90,21 +109,29 @@ type SQLProvider struct {
|
|||
sqlSelectMigrations string
|
||||
sqlSelectLatestMigration string
|
||||
|
||||
// Table: encryption.
|
||||
sqlUpsertEncryptionValue string
|
||||
sqlSelectEncryptionValue string
|
||||
|
||||
// Utility.
|
||||
sqlSelectExistingTables string
|
||||
sqlFmtRenameTable string
|
||||
}
|
||||
|
||||
// Close the underlying database connection.
|
||||
func (p *SQLProvider) Close() (err error) {
|
||||
return p.db.Close()
|
||||
}
|
||||
|
||||
// StartupCheck implements the provider startup check interface.
|
||||
func (p *SQLProvider) StartupCheck() (err error) {
|
||||
if p.errOpen != nil {
|
||||
return p.errOpen
|
||||
return fmt.Errorf("error opening database: %w", p.errOpen)
|
||||
}
|
||||
|
||||
// TODO: Decide if this is needed, or if it should be configurable.
|
||||
for i := 0; i < 19; i++ {
|
||||
err = p.db.Ping()
|
||||
if err == nil {
|
||||
if err = p.db.Ping(); err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -112,13 +139,17 @@ func (p *SQLProvider) StartupCheck() (err error) {
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("error pinging database: %w", err)
|
||||
}
|
||||
|
||||
p.log.Infof("Storage schema is being checked for updates")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if err = p.SchemaEncryptionCheckKey(ctx, false); err != nil && !errors.Is(err, ErrSchemaEncryptionVersionUnsupported) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = p.SchemaMigrate(ctx, true, SchemaLatest)
|
||||
|
||||
switch err {
|
||||
|
@ -128,7 +159,7 @@ func (p *SQLProvider) StartupCheck() (err error) {
|
|||
case nil:
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
return fmt.Errorf("error during schema migrate: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,13 +174,13 @@ func (p *SQLProvider) SavePreferred2FAMethod(ctx context.Context, username strin
|
|||
func (p *SQLProvider) LoadPreferred2FAMethod(ctx context.Context, username string) (method string, err error) {
|
||||
err = p.db.GetContext(ctx, &method, p.sqlSelectPreferred2FAMethod, username)
|
||||
|
||||
switch err {
|
||||
case sql.ErrNoRows:
|
||||
switch {
|
||||
case err == nil:
|
||||
return method, nil
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return "", nil
|
||||
case nil:
|
||||
return method, err
|
||||
default:
|
||||
return "", err
|
||||
return "", fmt.Errorf("error selecting preferred two factor method for user '%s': %w", username, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,120 +192,98 @@ func (p *SQLProvider) LoadUserInfo(ctx context.Context, username string) (info m
|
|||
case err == nil:
|
||||
return info, nil
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
_, err = p.db.ExecContext(ctx, p.sqlUpsertPreferred2FAMethod, username, authentication.PossibleMethods[0])
|
||||
if err != nil {
|
||||
return models.UserInfo{}, err
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlUpsertPreferred2FAMethod, username, authentication.PossibleMethods[0]); err != nil {
|
||||
return models.UserInfo{}, fmt.Errorf("error upserting preferred two factor method while selecting user info for user '%s': %w", username, err)
|
||||
}
|
||||
|
||||
err = p.db.GetContext(ctx, &info, p.sqlSelectUserInfo, username, username, username)
|
||||
if err != nil {
|
||||
return models.UserInfo{}, err
|
||||
if err = p.db.GetContext(ctx, &info, p.sqlSelectUserInfo, username, username, username); err != nil {
|
||||
return models.UserInfo{}, fmt.Errorf("error selecting user info for user '%s': %w", username, err)
|
||||
}
|
||||
|
||||
return info, nil
|
||||
default:
|
||||
return models.UserInfo{}, err
|
||||
return models.UserInfo{}, fmt.Errorf("error selecting user info for user '%s': %w", username, err)
|
||||
}
|
||||
}
|
||||
|
||||
// SaveIdentityVerification save an identity verification record to the database.
|
||||
func (p *SQLProvider) SaveIdentityVerification(ctx context.Context, verification models.IdentityVerification) (err error) {
|
||||
_, err = p.db.ExecContext(ctx, p.sqlInsertIdentityVerification, verification.Token)
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlInsertIdentityVerification, verification.Token); err != nil {
|
||||
return fmt.Errorf("error inserting identity verification: %w", err)
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveIdentityVerification remove an identity verification record from the database.
|
||||
func (p *SQLProvider) RemoveIdentityVerification(ctx context.Context, token string) (err error) {
|
||||
_, err = p.db.ExecContext(ctx, p.sqlDeleteIdentityVerification, token)
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlDeleteIdentityVerification, token); err != nil {
|
||||
return fmt.Errorf("error updating identity verification: %w", err)
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindIdentityVerification checks if an identity verification record is in the database and active.
|
||||
func (p *SQLProvider) FindIdentityVerification(ctx context.Context, jti string) (found bool, err error) {
|
||||
err = p.db.GetContext(ctx, &found, p.sqlSelectExistsIdentityVerification, jti)
|
||||
if err != nil {
|
||||
return false, err
|
||||
func (p *SQLProvider) FindIdentityVerification(ctx context.Context, token string) (found bool, err error) {
|
||||
if err = p.db.GetContext(ctx, &found, p.sqlSelectExistsIdentityVerification, token); err != nil {
|
||||
return false, fmt.Errorf("error selecting identity verification exists: %w", err)
|
||||
}
|
||||
|
||||
return found, nil
|
||||
}
|
||||
|
||||
// SaveTOTPConfiguration save a TOTP config of a given user in the database.
|
||||
// SaveTOTPConfiguration save a TOTP configuration of a given user in the database.
|
||||
func (p *SQLProvider) SaveTOTPConfiguration(ctx context.Context, config models.TOTPConfiguration) (err error) {
|
||||
// TODO: Encrypt config.Secret here.
|
||||
_, err = p.db.ExecContext(ctx, p.sqlUpsertTOTPConfig,
|
||||
config.Username,
|
||||
config.Algorithm,
|
||||
config.Digits,
|
||||
config.Period,
|
||||
config.Secret,
|
||||
)
|
||||
if config.Secret, err = p.encrypt(config.Secret); err != nil {
|
||||
return fmt.Errorf("error encrypting the TOTP configuration secret: %v", err)
|
||||
}
|
||||
|
||||
return err
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlUpsertTOTPConfig,
|
||||
config.Username, config.Algorithm, config.Digits, config.Period, config.Secret); err != nil {
|
||||
return fmt.Errorf("error upserting TOTP configuration: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteTOTPConfiguration delete a TOTP secret from the database given a username.
|
||||
// DeleteTOTPConfiguration delete a TOTP configuration from the database given a username.
|
||||
func (p *SQLProvider) DeleteTOTPConfiguration(ctx context.Context, username string) (err error) {
|
||||
_, err = p.db.ExecContext(ctx, p.sqlDeleteTOTPConfig, username)
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlDeleteTOTPConfig, username); err != nil {
|
||||
return fmt.Errorf("error deleting TOTP configuration: %w", err)
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadTOTPConfiguration load a TOTP secret given a username from the database.
|
||||
// LoadTOTPConfiguration load a TOTP configuration given a username from the database.
|
||||
func (p *SQLProvider) LoadTOTPConfiguration(ctx context.Context, username string) (config *models.TOTPConfiguration, err error) {
|
||||
config = &models.TOTPConfiguration{}
|
||||
|
||||
err = p.db.QueryRowxContext(ctx, p.sqlSelectTOTPConfig, username).StructScan(config)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
if err = p.db.QueryRowxContext(ctx, p.sqlSelectTOTPConfig, username).StructScan(config); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNoTOTPSecret
|
||||
}
|
||||
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("error selecting TOTP configuration: %w", err)
|
||||
}
|
||||
|
||||
if config.Secret, err = p.decrypt(config.Secret); err != nil {
|
||||
return nil, fmt.Errorf("error decrypting the TOTP secret: %v", err)
|
||||
}
|
||||
|
||||
// TODO: Decrypt config.Secret here.
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// SaveU2FDevice saves a registered U2F device.
|
||||
func (p *SQLProvider) SaveU2FDevice(ctx context.Context, device models.U2FDevice) (err error) {
|
||||
_, err = p.db.ExecContext(ctx, p.sqlUpsertU2FDevice, device.Username, device.KeyHandle, device.PublicKey)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadU2FDevice loads a U2F device registration for a given username.
|
||||
func (p *SQLProvider) LoadU2FDevice(ctx context.Context, username string) (device *models.U2FDevice, err error) {
|
||||
device = &models.U2FDevice{
|
||||
Username: username,
|
||||
}
|
||||
|
||||
err = p.db.GetContext(ctx, device, p.sqlSelectU2FDevice, username)
|
||||
// LoadTOTPConfigurations load a set of TOTP configurations.
|
||||
func (p *SQLProvider) LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []models.TOTPConfiguration, err error) {
|
||||
rows, err := p.db.QueryxContext(ctx, p.sqlSelectTOTPConfigs, limit, limit*page)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrNoU2FDeviceHandle
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return device, nil
|
||||
}
|
||||
|
||||
// AppendAuthenticationLog append a mark to the authentication log.
|
||||
func (p *SQLProvider) AppendAuthenticationLog(ctx context.Context, attempt models.AuthenticationAttempt) (err error) {
|
||||
_, err = p.db.ExecContext(ctx, p.sqlInsertAuthenticationAttempt, attempt.Time, attempt.Successful, attempt.Username)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadAuthenticationLogs retrieve the latest failed authentications from the authentication log.
|
||||
func (p *SQLProvider) LoadAuthenticationLogs(ctx context.Context, username string, fromDate time.Time, limit, page int) (attempts []models.AuthenticationAttempt, err error) {
|
||||
rows, err := p.db.QueryxContext(ctx, p.sqlSelectAuthenticationAttemptsByUsername, fromDate, username, limit, limit*page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("error selecting TOTP configurations: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
|
@ -283,13 +292,99 @@ func (p *SQLProvider) LoadAuthenticationLogs(ctx context.Context, username strin
|
|||
}
|
||||
}()
|
||||
|
||||
configs = make([]models.TOTPConfiguration, 0, limit)
|
||||
|
||||
var config models.TOTPConfiguration
|
||||
|
||||
for rows.Next() {
|
||||
if err = rows.StructScan(&config); err != nil {
|
||||
return nil, fmt.Errorf("error scanning TOTP configuration to struct: %w", err)
|
||||
}
|
||||
|
||||
if config.Secret, err = p.decrypt(config.Secret); err != nil {
|
||||
return nil, fmt.Errorf("error decrypting the TOTP secret: %v", err)
|
||||
}
|
||||
|
||||
configs = append(configs, config)
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// UpdateTOTPConfigurationSecret updates a TOTP configuration secret.
|
||||
func (p *SQLProvider) UpdateTOTPConfigurationSecret(ctx context.Context, config models.TOTPConfiguration) (err error) {
|
||||
switch config.ID {
|
||||
case 0:
|
||||
_, err = p.db.ExecContext(ctx, p.sqlUpdateTOTPConfigSecretByUsername, config.Secret, config.Username)
|
||||
default:
|
||||
_, err = p.db.ExecContext(ctx, p.sqlUpdateTOTPConfigSecret, config.Secret, config.ID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating TOTP configuration secret: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveU2FDevice saves a registered U2F device.
|
||||
func (p *SQLProvider) SaveU2FDevice(ctx context.Context, device models.U2FDevice) (err error) {
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlUpsertU2FDevice, device.Username, device.KeyHandle, device.PublicKey); err != nil {
|
||||
return fmt.Errorf("error upserting U2F device secret: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadU2FDevice loads a U2F device registration for a given username.
|
||||
func (p *SQLProvider) LoadU2FDevice(ctx context.Context, username string) (device *models.U2FDevice, err error) {
|
||||
device = &models.U2FDevice{
|
||||
Username: username,
|
||||
}
|
||||
|
||||
if err = p.db.GetContext(ctx, device, p.sqlSelectU2FDevice, username); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNoU2FDeviceHandle
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("error selecting U2F device: %w", err)
|
||||
}
|
||||
|
||||
return device, nil
|
||||
}
|
||||
|
||||
// AppendAuthenticationLog append a mark to the authentication log.
|
||||
func (p *SQLProvider) AppendAuthenticationLog(ctx context.Context, attempt models.AuthenticationAttempt) (err error) {
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlInsertAuthenticationAttempt, attempt.Time, attempt.Successful, attempt.Username); err != nil {
|
||||
return fmt.Errorf("error inserting authentiation attempt: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAuthenticationLogs retrieve the latest failed authentications from the authentication log.
|
||||
func (p *SQLProvider) LoadAuthenticationLogs(ctx context.Context, username string, fromDate time.Time, limit, page int) (attempts []models.AuthenticationAttempt, err error) {
|
||||
rows, err := p.db.QueryxContext(ctx, p.sqlSelectAuthenticationAttemptsByUsername, fromDate, username, limit, limit*page)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNoAuthenticationLogs
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("error selecting authentication logs: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
p.log.Errorf(logFmtErrClosingConn, err)
|
||||
}
|
||||
}()
|
||||
|
||||
var attempt models.AuthenticationAttempt
|
||||
|
||||
attempts = make([]models.AuthenticationAttempt, 0, limit)
|
||||
|
||||
for rows.Next() {
|
||||
var attempt models.AuthenticationAttempt
|
||||
|
||||
err = rows.StructScan(&attempt)
|
||||
if err != nil {
|
||||
if err = rows.StructScan(&attempt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
@ -15,9 +15,9 @@ type MySQLProvider struct {
|
|||
}
|
||||
|
||||
// NewMySQLProvider a MySQL provider.
|
||||
func NewMySQLProvider(config schema.MySQLStorageConfiguration) (provider *MySQLProvider) {
|
||||
func NewMySQLProvider(config schema.MySQLStorageConfiguration, encryptionKey string) (provider *MySQLProvider) {
|
||||
provider = &MySQLProvider{
|
||||
SQLProvider: NewSQLProvider(providerMySQL, providerMySQL, dataSourceNameMySQL(config)),
|
||||
SQLProvider: NewSQLProvider(providerMySQL, providerMySQL, dataSourceNameMySQL(config), encryptionKey),
|
||||
}
|
||||
|
||||
// All providers have differing SELECT existing table statements.
|
||||
|
|
|
@ -16,9 +16,9 @@ type PostgreSQLProvider struct {
|
|||
}
|
||||
|
||||
// NewPostgreSQLProvider a PostgreSQL provider.
|
||||
func NewPostgreSQLProvider(config schema.PostgreSQLStorageConfiguration) (provider *PostgreSQLProvider) {
|
||||
func NewPostgreSQLProvider(config schema.PostgreSQLStorageConfiguration, encryptionKey string) (provider *PostgreSQLProvider) {
|
||||
provider = &PostgreSQLProvider{
|
||||
SQLProvider: NewSQLProvider(providerPostgres, "pgx", dataSourceNamePostgreSQL(config)),
|
||||
SQLProvider: NewSQLProvider(providerPostgres, "pgx", dataSourceNamePostgreSQL(config), encryptionKey),
|
||||
}
|
||||
|
||||
// All providers have differing SELECT existing table statements.
|
||||
|
@ -29,6 +29,7 @@ func NewPostgreSQLProvider(config schema.PostgreSQLStorageConfiguration) (provid
|
|||
provider.sqlUpsertU2FDevice = fmt.Sprintf(queryFmtPostgresUpsertU2FDevice, tableU2FDevices)
|
||||
provider.sqlUpsertTOTPConfig = fmt.Sprintf(queryFmtPostgresUpsertTOTPConfiguration, tableTOTPConfigurations)
|
||||
provider.sqlUpsertPreferred2FAMethod = fmt.Sprintf(queryFmtPostgresUpsertPreferred2FAMethod, tableUserPreferences)
|
||||
provider.sqlUpsertEncryptionValue = fmt.Sprintf(queryFmtPostgresUpsertEncryptionValue, tableEncryption)
|
||||
|
||||
// PostgreSQL requires rebinding of any query that contains a '?' placeholder to use the '$#' notation placeholders.
|
||||
provider.sqlFmtRenameTable = provider.db.Rebind(provider.sqlFmtRenameTable)
|
||||
|
@ -40,10 +41,14 @@ func NewPostgreSQLProvider(config schema.PostgreSQLStorageConfiguration) (provid
|
|||
provider.sqlSelectTOTPConfig = provider.db.Rebind(provider.sqlSelectTOTPConfig)
|
||||
provider.sqlUpsertTOTPConfig = provider.db.Rebind(provider.sqlUpsertTOTPConfig)
|
||||
provider.sqlDeleteTOTPConfig = provider.db.Rebind(provider.sqlDeleteTOTPConfig)
|
||||
provider.sqlSelectTOTPConfigs = provider.db.Rebind(provider.sqlSelectTOTPConfigs)
|
||||
provider.sqlUpdateTOTPConfigSecret = provider.db.Rebind(provider.sqlUpdateTOTPConfigSecret)
|
||||
provider.sqlUpdateTOTPConfigSecretByUsername = provider.db.Rebind(provider.sqlUpdateTOTPConfigSecretByUsername)
|
||||
provider.sqlSelectU2FDevice = provider.db.Rebind(provider.sqlSelectU2FDevice)
|
||||
provider.sqlInsertAuthenticationAttempt = provider.db.Rebind(provider.sqlInsertAuthenticationAttempt)
|
||||
provider.sqlSelectAuthenticationAttemptsByUsername = provider.db.Rebind(provider.sqlSelectAuthenticationAttemptsByUsername)
|
||||
provider.sqlInsertMigration = provider.db.Rebind(provider.sqlInsertMigration)
|
||||
provider.sqlSelectEncryptionValue = provider.db.Rebind(provider.sqlSelectEncryptionValue)
|
||||
|
||||
return provider
|
||||
}
|
||||
|
|
|
@ -10,9 +10,9 @@ type SQLiteProvider struct {
|
|||
}
|
||||
|
||||
// NewSQLiteProvider constructs a SQLite provider.
|
||||
func NewSQLiteProvider(path string) (provider *SQLiteProvider) {
|
||||
func NewSQLiteProvider(path, encryptionKey string) (provider *SQLiteProvider) {
|
||||
provider = &SQLiteProvider{
|
||||
SQLProvider: NewSQLProvider(providerSQLite, "sqlite3", path),
|
||||
SQLProvider: NewSQLProvider(providerSQLite, "sqlite3", path, encryptionKey),
|
||||
}
|
||||
|
||||
// All providers have differing SELECT existing table statements.
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/models"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// SchemaEncryptionChangeKey uses the currently configured key to decrypt values in the database and the key provided
|
||||
// by this command to encrypt the values again and update them using a transaction.
|
||||
func (p *SQLProvider) SchemaEncryptionChangeKey(ctx context.Context, encryptionKey string) (err error) {
|
||||
tx, err := p.db.Beginx()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error beginning transaction to change encryption key: %w", err)
|
||||
}
|
||||
|
||||
key := sha256.Sum256([]byte(encryptionKey))
|
||||
|
||||
var configs []models.TOTPConfiguration
|
||||
|
||||
for page := 0; true; page++ {
|
||||
if configs, err = p.LoadTOTPConfigurations(ctx, 10, page); err != nil {
|
||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||
return fmt.Errorf("rollback error %v: rollback due to error: %w", rollbackErr, err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("rollback due to error: %w", err)
|
||||
}
|
||||
|
||||
for _, config := range configs {
|
||||
if config.Secret, err = utils.Encrypt(config.Secret, &key); err != nil {
|
||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||
return fmt.Errorf("rollback error %v: rollback due to error: %w", rollbackErr, err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("rollback due to error: %w", err)
|
||||
}
|
||||
|
||||
if err = p.UpdateTOTPConfigurationSecret(ctx, config); err != nil {
|
||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||
return fmt.Errorf("rollback error %v: rollback due to error: %w", rollbackErr, err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("rollback due to error: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(configs) != 10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err = p.setNewEncryptionCheckValue(ctx, &key, tx); err != nil {
|
||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||
return fmt.Errorf("rollback error %v: rollback due to error: %w", rollbackErr, err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("rollback due to error: %w", err)
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// SchemaEncryptionCheckKey checks the encryption key configured is valid for the database.
|
||||
func (p *SQLProvider) SchemaEncryptionCheckKey(ctx context.Context, verbose bool) (err error) {
|
||||
version, err := p.SchemaVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if version < 1 {
|
||||
return ErrSchemaEncryptionVersionUnsupported
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
if _, err = p.getEncryptionValue(ctx, encryptionNameCheck); err != nil {
|
||||
errs = append(errs, ErrSchemaEncryptionInvalidKey)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
var (
|
||||
config models.TOTPConfiguration
|
||||
row int
|
||||
invalid int
|
||||
total int
|
||||
)
|
||||
|
||||
pageSize := 10
|
||||
|
||||
var rows *sqlx.Rows
|
||||
|
||||
for page := 0; true; page++ {
|
||||
if rows, err = p.db.QueryxContext(ctx, p.sqlSelectTOTPConfigs, pageSize, pageSize*page); err != nil {
|
||||
_ = rows.Close()
|
||||
|
||||
return fmt.Errorf("error selecting TOTP configurations: %w", err)
|
||||
}
|
||||
|
||||
row = 0
|
||||
|
||||
for rows.Next() {
|
||||
total++
|
||||
row++
|
||||
|
||||
if err = rows.StructScan(&config); err != nil {
|
||||
_ = rows.Close()
|
||||
return fmt.Errorf("error scanning TOTP configuration to struct: %w", err)
|
||||
}
|
||||
|
||||
if _, err = p.decrypt(config.Secret); err != nil {
|
||||
invalid++
|
||||
}
|
||||
}
|
||||
|
||||
_ = rows.Close()
|
||||
|
||||
if row < pageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if invalid != 0 {
|
||||
errs = append(errs, fmt.Errorf("%d of %d total TOTP secrets were invalid", invalid, total))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
for i, e := range errs {
|
||||
if i == 0 {
|
||||
err = e
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
err = fmt.Errorf("%w, %v", err, e)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p SQLProvider) encrypt(clearText []byte) (cipherText []byte, err error) {
|
||||
return utils.Encrypt(clearText, &p.key)
|
||||
}
|
||||
|
||||
func (p SQLProvider) decrypt(cipherText []byte) (clearText []byte, err error) {
|
||||
return utils.Decrypt(cipherText, &p.key)
|
||||
}
|
||||
|
||||
func (p *SQLProvider) getEncryptionValue(ctx context.Context, name string) (value []byte, err error) {
|
||||
var encryptedValue []byte
|
||||
|
||||
err = p.db.GetContext(ctx, &encryptedValue, p.sqlSelectEncryptionValue, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.decrypt(encryptedValue)
|
||||
}
|
||||
|
||||
func (p *SQLProvider) setNewEncryptionCheckValue(ctx context.Context, key *[32]byte, e sqlx.ExecerContext) (err error) {
|
||||
valueClearText := uuid.New()
|
||||
|
||||
value, err := utils.Encrypt([]byte(valueClearText.String()), key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if e != nil {
|
||||
_, err = e.ExecContext(ctx, p.sqlUpsertEncryptionValue, encryptionNameCheck, value)
|
||||
} else {
|
||||
_, err = p.db.ExecContext(ctx, p.sqlUpsertEncryptionValue, encryptionNameCheck, value)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
|
@ -78,6 +78,24 @@ const (
|
|||
FROM %s
|
||||
WHERE username = ?;`
|
||||
|
||||
queryFmtSelectTOTPConfigurations = `
|
||||
SELECT id, username, algorithm, digits, totp_period, secret
|
||||
FROM %s
|
||||
LIMIT ?
|
||||
OFFSET ?;`
|
||||
|
||||
//nolint:gosec // These are not hardcoded credentials it's a query to obtain credentials.
|
||||
queryFmtUpdateTOTPConfigurationSecret = `
|
||||
UPDATE %s
|
||||
SET secret = ?
|
||||
WHERE id = ?;`
|
||||
|
||||
//nolint:gosec // These are not hardcoded credentials it's a query to obtain credentials.
|
||||
queryFmtUpdateTOTPConfigurationSecretByUsername = `
|
||||
UPDATE %s
|
||||
SET secret = ?
|
||||
WHERE username = ?;`
|
||||
|
||||
queryFmtUpsertTOTPConfiguration = `
|
||||
REPLACE INTO %s (username, algorithm, digits, totp_period, secret)
|
||||
VALUES (?, ?, ?, ?, ?);`
|
||||
|
@ -123,3 +141,20 @@ const (
|
|||
LIMIT ?
|
||||
OFFSET ?;`
|
||||
)
|
||||
|
||||
const (
|
||||
queryFmtSelectEncryptionValue = `
|
||||
SELECT (value)
|
||||
FROM %s
|
||||
WHERE name = ?`
|
||||
|
||||
queryFmtUpsertEncryptionValue = `
|
||||
REPLACE INTO %s (name, value)
|
||||
VALUES (?, ?);`
|
||||
|
||||
queryFmtPostgresUpsertEncryptionValue = `
|
||||
INSERT INTO %s (name, value)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (name)
|
||||
DO UPDATE SET value=$2;`
|
||||
)
|
||||
|
|
|
@ -63,8 +63,6 @@ func (p *SQLProvider) SchemaVersion(ctx context.Context) (version int, err error
|
|||
return -1, nil
|
||||
}
|
||||
|
||||
// TODO: Decide if we want to support external tables.
|
||||
// return -2, ErrUnknownSchemaState
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
|
@ -213,7 +211,18 @@ func (p *SQLProvider) schemaMigrateApply(ctx context.Context, migration SchemaMi
|
|||
return fmt.Errorf(errFmtFailedMigration, migration.Version, migration.Name, err)
|
||||
}
|
||||
|
||||
if migration.Version == 1 {
|
||||
// Skip the migration history insertion in a migration to v0.
|
||||
if !migration.Up {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add the schema encryption value if upgrading to v1.
|
||||
if err = p.setNewEncryptionCheckValue(ctx, &p.key, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if migration.Version == 1 && !migration.Up {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -49,6 +49,10 @@ func (p *SQLProvider) schemaMigratePre1To1(ctx context.Context) (err error) {
|
|||
return fmt.Errorf(errFmtFailedMigration, migration.Version, migration.Name, err)
|
||||
}
|
||||
|
||||
if err = p.setNewEncryptionCheckValue(ctx, &p.key, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = p.db.ExecContext(ctx, fmt.Sprintf(p.db.Rebind(queryFmtPre1InsertUserPreferencesFromSelect),
|
||||
tableUserPreferences, tablePrefixBackup+tableUserPreferences)); err != nil {
|
||||
return err
|
||||
|
@ -213,8 +217,10 @@ func (p *SQLProvider) schemaMigratePre1To1TOTP(ctx context.Context) (err error)
|
|||
return err
|
||||
}
|
||||
|
||||
// TODO: Add encryption migration here.
|
||||
encryptedSecret := "encrypted:" + secret
|
||||
encryptedSecret, err := p.encrypt([]byte(secret))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totpConfigs = append(totpConfigs, models.TOTPConfiguration{Username: username, Secret: encryptedSecret})
|
||||
}
|
||||
|
@ -288,6 +294,7 @@ func (p *SQLProvider) schemaMigrate1ToPre1(ctx context.Context) (err error) {
|
|||
tableDUODevices,
|
||||
tableUserPreferences,
|
||||
tableAuthenticationLogs,
|
||||
tableEncryption,
|
||||
}
|
||||
|
||||
if err = p.schemaMigratePre1Rename(ctx, tables, tablesRename); err != nil {
|
||||
|
@ -388,18 +395,22 @@ func (p *SQLProvider) schemaMigrate1ToPre1TOTP(ctx context.Context) (err error)
|
|||
}()
|
||||
|
||||
for rows.Next() {
|
||||
var username, encryptedSecret string
|
||||
var (
|
||||
username string
|
||||
secretCipherText []byte
|
||||
)
|
||||
|
||||
err = rows.Scan(&username, &encryptedSecret)
|
||||
err = rows.Scan(&username, &secretCipherText)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Fix.
|
||||
// TODO: Add DECRYPTION migration here.
|
||||
decryptedSecret := strings.Replace(encryptedSecret, "encrypted:", "", 1)
|
||||
secretClearText, err := p.decrypt(secretCipherText)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totpConfigs = append(totpConfigs, models.TOTPConfiguration{Username: username, Secret: decryptedSecret})
|
||||
totpConfigs = append(totpConfigs, models.TOTPConfiguration{Username: username, Secret: secretClearText})
|
||||
}
|
||||
|
||||
for _, config := range totpConfigs {
|
||||
|
|
|
@ -37,6 +37,7 @@ session:
|
|||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /config/db.sqlite3
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ session:
|
|||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /config/db.sqlite
|
||||
|
||||
|
|
|
@ -26,8 +26,9 @@ session:
|
|||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /config/db.sqlite
|
||||
path: /tmp/db.sqlite
|
||||
|
||||
access_control:
|
||||
default_policy: bypass
|
||||
|
@ -43,5 +44,5 @@ access_control:
|
|||
|
||||
notifier:
|
||||
filesystem:
|
||||
filename: /config/notification.txt
|
||||
filename: /tmp/notification.txt
|
||||
...
|
||||
|
|
|
@ -4,6 +4,9 @@ services:
|
|||
authelia-backend:
|
||||
volumes:
|
||||
- './CLI/configuration.yml:/config/configuration.yml:ro'
|
||||
- './CLI/storage.yml:/config/configuration.storage.yml:ro'
|
||||
- './CLI/users.yml:/config/users.yml'
|
||||
- './common/ssl:/config/ssl:ro'
|
||||
- '/tmp:/tmp'
|
||||
user: ${USER_ID}:${GROUP_ID}
|
||||
...
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
storage:
|
||||
encryption_key: a_cli_encryption_key_which_isnt_secure
|
||||
local:
|
||||
path: /tmp/db.sqlite3
|
||||
...
|
|
@ -27,6 +27,7 @@ session:
|
|||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /config/db.sqlite3
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ session:
|
|||
|
||||
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /config/db.sqlite
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ session:
|
|||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /config/db.sqlite
|
||||
|
||||
|
|
|
@ -110,6 +110,7 @@ regulation:
|
|||
ban_time: 10
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
mysql:
|
||||
host: mariadb
|
||||
port: 3306
|
||||
|
|
|
@ -41,6 +41,7 @@ session:
|
|||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /config/db.sqlite3
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ session:
|
|||
|
||||
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
mysql:
|
||||
host: mariadb
|
||||
port: 3306
|
||||
|
|
|
@ -29,6 +29,7 @@ session:
|
|||
|
||||
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
mysql:
|
||||
host: mysql
|
||||
port: 3306
|
||||
|
|
|
@ -27,6 +27,7 @@ session:
|
|||
|
||||
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /config/db.sqlite
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ session:
|
|||
port: 6379
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /config/db.sqlite
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ session:
|
|||
port: 6379
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /config/db.sqlite
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ session:
|
|||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /config/db.sqlite
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ session:
|
|||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /config/db.sqlite
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ session:
|
|||
|
||||
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
postgres:
|
||||
host: postgres
|
||||
port: 5432
|
||||
|
|
|
@ -27,6 +27,7 @@ session:
|
|||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /config/db.sqlite
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ session:
|
|||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /tmp/db.sqlite3
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ session:
|
|||
remember_me_duration: 1y
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /config/db.sqlite
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ session:
|
|||
password: redis-user-password
|
||||
|
||||
storage:
|
||||
encryption_key: a_not_so_secure_encryption_key
|
||||
local:
|
||||
path: /config/db.sqlite
|
||||
|
||||
|
|
|
@ -40,6 +40,8 @@ spec:
|
|||
value: /app/secrets/session
|
||||
- name: AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE
|
||||
value: /app/secrets/sql_password
|
||||
- name: AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE
|
||||
value: /app/secrets/encryption_key
|
||||
- name: ENVIRONMENT
|
||||
value: dev
|
||||
volumes:
|
||||
|
@ -69,4 +71,6 @@ spec:
|
|||
path: sql_password
|
||||
- key: ldap_password
|
||||
path: ldap_password
|
||||
- key: encryption_key
|
||||
path: encryption_key
|
||||
...
|
||||
|
|
|
@ -12,4 +12,5 @@ data:
|
|||
ldap_password: cGFzc3dvcmQ= # password
|
||||
session: dW5zZWN1cmVfcGFzc3dvcmQ= # unsecure_password
|
||||
sql_password: cGFzc3dvcmQ= # password
|
||||
encryption_key: YV9ub3Rfc29fc2VjdXJlX2VuY3J5cHRpb25fa2V5
|
||||
...
|
||||
|
|
|
@ -2,6 +2,7 @@ package suites
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -35,6 +36,9 @@ func init() {
|
|||
|
||||
teardown := func(suitePath string) error {
|
||||
err := dockerEnvironment.Down()
|
||||
_ = os.Remove("/tmp/db.sqlite3")
|
||||
_ = os.Remove("/tmp/db.sqlite")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
package suites
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/models"
|
||||
"github.com/authelia/authelia/v4/internal/storage"
|
||||
)
|
||||
|
||||
type CLISuite struct {
|
||||
|
@ -40,7 +47,7 @@ func (s *CLISuite) SetupTest() {
|
|||
|
||||
func (s *CLISuite) TestShouldPrintBuildInformation() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "build-info"})
|
||||
s.Assert().Nil(err)
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Last Tag: ")
|
||||
s.Assert().Contains(output, "State: ")
|
||||
s.Assert().Contains(output, "Branch: ")
|
||||
|
@ -55,13 +62,13 @@ func (s *CLISuite) TestShouldPrintBuildInformation() {
|
|||
|
||||
func (s *CLISuite) TestShouldPrintVersion() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "--version"})
|
||||
s.Assert().Nil(err)
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "authelia version")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldValidateConfig() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "validate-config", "/config/configuration.yml"})
|
||||
s.Assert().Nil(err)
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Configuration parsed successfully without errors")
|
||||
}
|
||||
|
||||
|
@ -73,33 +80,33 @@ func (s *CLISuite) TestShouldFailValidateConfig() {
|
|||
|
||||
func (s *CLISuite) TestShouldHashPasswordArgon2id() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "hash-password", "test", "-m", "32", "-s", "test1234"})
|
||||
s.Assert().Nil(err)
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Password hash: $argon2id$v=19$m=32768,t=1,p=8")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldHashPasswordSHA512() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "hash-password", "test", "-z"})
|
||||
s.Assert().Nil(err)
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Password hash: $6$rounds=50000")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldGenerateCertificateRSA() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/"})
|
||||
s.Assert().Nil(err)
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldGenerateCertificateRSAWithIPAddress() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "127.0.0.1", "--dir", "/tmp/"})
|
||||
s.Assert().Nil(err)
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldGenerateCertificateRSAWithStartDate() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--start-date", "'Jan 1 15:04:05 2011'"})
|
||||
s.Assert().Nil(err)
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
@ -112,14 +119,14 @@ func (s *CLISuite) TestShouldFailGenerateCertificateRSAWithStartDate() {
|
|||
|
||||
func (s *CLISuite) TestShouldGenerateCertificateCA() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ca"})
|
||||
s.Assert().Nil(err)
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldGenerateCertificateEd25519() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ed25519"})
|
||||
s.Assert().Nil(err)
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
@ -132,32 +139,238 @@ func (s *CLISuite) TestShouldFailGenerateCertificateECDSA() {
|
|||
|
||||
func (s *CLISuite) TestShouldGenerateCertificateECDSAP224() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P224"})
|
||||
s.Assert().Nil(err)
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldGenerateCertificateECDSAP256() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P256"})
|
||||
s.Assert().Nil(err)
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldGenerateCertificateECDSAP384() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P384"})
|
||||
s.Assert().Nil(err)
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestShouldGenerateCertificateECDSAP521() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P521"})
|
||||
s.Assert().Nil(err)
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
|
||||
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestStorageShouldShowErrWithoutConfig() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info"})
|
||||
s.Assert().EqualError(err, "exit status 1")
|
||||
|
||||
s.Assert().Contains(output, "Error: A storage configuration must be provided. It could be 'local', 'mysql' or 'postgres', the configuration option storage.encryption_key must be provided\n")
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "history"})
|
||||
s.Assert().EqualError(err, "exit status 1")
|
||||
|
||||
s.Assert().Contains(output, "Error: A storage configuration must be provided. It could be 'local', 'mysql' or 'postgres', the configuration option storage.encryption_key must be provided\n")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestStorage00ShouldShowCorrectPreInitInformation() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config", "/config/configuration.storage.yml"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
pattern := regexp.MustCompile(`^Schema Version: N/A\nSchema Upgrade Available: yes - version \d+\nSchema Tables: N/A\nSchema Encryption Key: unsupported \(schema version\)`)
|
||||
|
||||
s.Assert().Regexp(pattern, output)
|
||||
|
||||
patternOutdated := regexp.MustCompile(`Error: schema is version \d+ which is outdated please migrate to version \d+ in order to use this command or use an older binary`)
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "export", "totp-configurations", "--config", "/config/configuration.storage.yml"})
|
||||
s.Assert().EqualError(err, "exit status 1")
|
||||
s.Assert().Regexp(patternOutdated, output)
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "change-key", "--config", "/config/configuration.storage.yml"})
|
||||
s.Assert().EqualError(err, "exit status 1")
|
||||
s.Assert().Regexp(patternOutdated, output)
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--config", "/config/configuration.storage.yml"})
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Contains(output, "Could not check encryption key for validity. The schema version doesn't support encryption.")
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "down", "--target", "0", "--destroy-data", "--config", "/config/configuration.storage.yml"})
|
||||
s.Assert().EqualError(err, "exit status 1")
|
||||
s.Assert().Contains(output, "Error: schema migration target version 0 is the same current version 0")
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "up", "--target", "2147483640", "--config", "/config/configuration.storage.yml"})
|
||||
s.Assert().EqualError(err, "exit status 1")
|
||||
s.Assert().Contains(output, "Error: schema up migration target version 2147483640 is greater then the latest version ")
|
||||
s.Assert().Contains(output, " which indicates it doesn't exist")
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "history"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
s.Assert().Contains(output, "No migration history is available for schemas that not version 1 or above.\n")
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "list-up"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
s.Assert().Contains(output, "Storage Schema Migration List (Up)\n\nVersion\t\tDescription\n1\t\tInitial Schema\n")
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "list-down"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
s.Assert().Contains(output, "Storage Schema Migration List (Down)\n\nNo Migrations Available\n")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestStorage01ShouldMigrateUp() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "up"})
|
||||
s.Require().NoError(err)
|
||||
|
||||
pattern0 := regexp.MustCompile(`"Storage schema migration from \d+ to \d+ is being attempted"`)
|
||||
pattern1 := regexp.MustCompile(`"Storage schema migration from \d+ to \d+ is complete"`)
|
||||
|
||||
s.Regexp(pattern0, output)
|
||||
s.Regexp(pattern1, output)
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "up"})
|
||||
s.Assert().EqualError(err, "exit status 1")
|
||||
|
||||
s.Assert().Contains(output, "Error: schema already up to date\n")
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "history"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
s.Assert().Contains(output, "Migration History:\n\nID\tDate\t\t\t\tBefore\tAfter\tAuthelia Version\n")
|
||||
s.Assert().Contains(output, "0\t1")
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "list-up"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
s.Assert().Contains(output, "Storage Schema Migration List (Up)\n\nNo Migrations Available")
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "--config", "/config/configuration.storage.yml", "migrate", "list-down"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
s.Assert().Contains(output, "Storage Schema Migration List (Down)\n\nVersion\t\tDescription\n")
|
||||
s.Assert().Contains(output, "1\t\tInitial Schema")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestStorage02ShouldShowSchemaInfo() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config", "/config/configuration.storage.yml"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
pattern := regexp.MustCompile(`^Schema Version: \d+\nSchema Upgrade Available: no\nSchema Tables: authentication_logs, identity_verification_tokens, totp_configurations, u2f_devices, user_preferences, migrations, encryption\nSchema Encryption Key: valid`)
|
||||
|
||||
s.Assert().Regexp(pattern, output)
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestStorage03ShouldExportTOTP() {
|
||||
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_cli_encryption_key_which_isnt_secure")
|
||||
|
||||
s.Require().NoError(provider.StartupCheck())
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
var (
|
||||
err error
|
||||
key *otp.Key
|
||||
config models.TOTPConfiguration
|
||||
)
|
||||
|
||||
var (
|
||||
expectedLines = make([]string, 0, 3)
|
||||
expectedLinesCSV = make([]string, 0, 4)
|
||||
output string
|
||||
)
|
||||
|
||||
expectedLinesCSV = append(expectedLinesCSV, "issuer,username,algorithm,digits,period,secret")
|
||||
|
||||
for _, name := range []string{"john", "mary", "fred"} {
|
||||
key, err = totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "Authelia",
|
||||
AccountName: name,
|
||||
Period: uint(30),
|
||||
SecretSize: 32,
|
||||
Digits: otp.Digits(6),
|
||||
Algorithm: otp.AlgorithmSHA1,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
|
||||
config = models.TOTPConfiguration{
|
||||
Username: name,
|
||||
Algorithm: "SHA1",
|
||||
Digits: 6,
|
||||
Secret: []byte(key.Secret()),
|
||||
Period: key.Period(),
|
||||
}
|
||||
|
||||
expectedLinesCSV = append(expectedLinesCSV, fmt.Sprintf("%s,%s,%s,%d,%d,%s", "Authelia", config.Username, config.Algorithm, config.Digits, config.Period, string(config.Secret)))
|
||||
expectedLines = append(expectedLines, fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=%s&digits=%d&period=%d", "Authelia", config.Username, string(config.Secret), "Authelia", config.Algorithm, config.Digits, config.Period))
|
||||
|
||||
s.Require().NoError(provider.SaveTOTPConfiguration(ctx, config))
|
||||
}
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "export", "totp-configurations", "--format", "uri", "--config", "/config/configuration.storage.yml"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
for _, expectedLine := range expectedLines {
|
||||
s.Assert().Contains(output, expectedLine)
|
||||
}
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "export", "totp-configurations", "--format", "csv", "--config", "/config/configuration.storage.yml"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
for _, expectedLine := range expectedLinesCSV {
|
||||
s.Assert().Contains(output, expectedLine)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestStorage04ShouldChangeEncryptionKey() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "change-key", "--new-encryption-key", "apple-apple-apple-apple", "--config", "/config/configuration.storage.yml"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
s.Assert().Contains(output, "Completed the encryption key change. Please adjust your configuration to use the new key.\n")
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info", "--config", "/config/configuration.storage.yml"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
pattern := regexp.MustCompile(`Schema Version: \d+\nSchema Upgrade Available: no\nSchema Tables: authentication_logs, identity_verification_tokens, totp_configurations, u2f_devices, user_preferences, migrations, encryption\nSchema Encryption Key: invalid`)
|
||||
s.Assert().Regexp(pattern, output)
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--config", "/config/configuration.storage.yml"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
s.Assert().Contains(output, "Encryption key validation: failed.\n\nError: the encryption key is not valid against the schema check value.\n")
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--verbose", "--config", "/config/configuration.storage.yml"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
s.Assert().Contains(output, "Encryption key validation: failed.\n\nError: the encryption key is not valid against the schema check value, 3 of 3 total TOTP secrets were invalid.\n")
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--encryption-key", "apple-apple-apple-apple", "--config", "/config/configuration.storage.yml"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
s.Assert().Contains(output, "Encryption key validation: success.\n")
|
||||
|
||||
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--verbose", "--encryption-key", "apple-apple-apple-apple", "--config", "/config/configuration.storage.yml"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
s.Assert().Contains(output, "Encryption key validation: success.\n")
|
||||
}
|
||||
|
||||
func (s *CLISuite) TestStorage05ShouldMigrateDown() {
|
||||
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "down", "--target", "0", "--destroy-data", "--config", "/config/configuration.storage.yml"})
|
||||
s.Assert().NoError(err)
|
||||
|
||||
pattern0 := regexp.MustCompile(`"Storage schema migration from \d+ to \d+ is being attempted"`)
|
||||
pattern1 := regexp.MustCompile(`"Storage schema migration from \d+ to \d+ is complete"`)
|
||||
|
||||
s.Regexp(pattern0, output)
|
||||
s.Regexp(pattern1, output)
|
||||
}
|
||||
|
||||
func TestCLISuite(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping suite test in short mode")
|
||||
|
|
|
@ -121,7 +121,7 @@ func (s *StandaloneWebDriverSuite) TestShouldCheckUserIsAskedToRegisterDevice()
|
|||
password := "password"
|
||||
|
||||
// Clean up any TOTP secret already in DB.
|
||||
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3")
|
||||
provider := storage.NewSQLiteProvider("/tmp/db.sqlite3", "a_not_so_secure_encryption_key")
|
||||
|
||||
require.NoError(s.T(), provider.StartupCheck())
|
||||
require.NoError(s.T(), provider.DeleteTOTPConfiguration(ctx, username))
|
||||
|
|
Loading…
Reference in New Issue