diff --git a/cmd/authelia-scripts/cmd_suites.go b/cmd/authelia-scripts/cmd_suites.go index b4127fabf..05a18d5c1 100644 --- a/cmd/authelia-scripts/cmd_suites.go +++ b/cmd/authelia-scripts/cmd_suites.go @@ -136,7 +136,7 @@ func runSuiteSetupTeardown(command string, suite string) error { s := suites.GlobalRegistry.Get(selectedSuite) - cmd := utils.CommandWithStdout("bash", "-c", "go run cmd/authelia-suites/*.go "+command+" "+selectedSuite) + cmd := utils.CommandWithStdout("go", "run", "cmd/authelia-suites/main.go", command, selectedSuite) cmd.Env = os.Environ() return utils.RunCommandWithTimeout(cmd, s.SetUpTimeout) } diff --git a/cmd/authelia-scripts/cmd_unittest.go b/cmd/authelia-scripts/cmd_unittest.go index a191018f7..fd27551c5 100644 --- a/cmd/authelia-scripts/cmd_unittest.go +++ b/cmd/authelia-scripts/cmd_unittest.go @@ -8,6 +8,7 @@ import ( // RunUnitTest run the unit tests func RunUnitTest(cobraCmd *cobra.Command, args []string) { + log.SetLevel(log.TraceLevel) err := utils.Shell("go test $(go list ./... | grep -v suites)").Run() if err != nil { log.Fatal(err) diff --git a/cmd/authelia/main.go b/cmd/authelia/main.go index f2d5cf949..8041f1aa1 100644 --- a/cmd/authelia/main.go +++ b/cmd/authelia/main.go @@ -54,6 +54,9 @@ func main() { logging.SetLevel(logrus.InfoLevel) break case "debug": + logging.SetLevel(logrus.DebugLevel) + break + case "trace": logging.SetLevel(logrus.TraceLevel) } @@ -68,8 +71,10 @@ func main() { } var storageProvider storage.Provider - if config.Storage.SQL != nil { - storageProvider = storage.NewSQLProvider(*config.Storage.SQL) + if config.Storage.PostgreSQL != nil { + storageProvider = storage.NewPostgreSQLProvider(*config.Storage.PostgreSQL) + } else if config.Storage.MySQL != nil { + storageProvider = storage.NewMySQLProvider(*config.Storage.MySQL) } else if config.Storage.Local != nil { storageProvider = storage.NewSQLiteProvider(config.Storage.Local.Path) } else { diff --git a/config.template.yml b/config.template.yml index b0877dfd7..d5df6e74a 100644 --- a/config.template.yml +++ b/config.template.yml @@ -249,14 +249,22 @@ storage: ## local: ## path: /var/lib/authelia/db.sqlite3 - # Settings to connect to SQL server - sql: + # Settings to connect to MySQL server + mysql: host: 127.0.0.1 port: 3306 database: authelia username: authelia password: mypassword + # Settings to connect to MySQL server + # postgres: + # host: 127.0.0.1 + # port: 3306 + # database: authelia + # username: authelia + # password: mypassword + # Configuration of the notification system. # # Notifications are sent to users when they require a password reset, a u2f diff --git a/configuration/schema/storage.go b/configuration/schema/storage.go index 0f63d5ef1..35bb74a3b 100644 --- a/configuration/schema/storage.go +++ b/configuration/schema/storage.go @@ -14,8 +14,20 @@ type SQLStorageConfiguration struct { Password string `yaml:"password"` } +// MySQLStorageConfiguration represents the configuration of a MySQL database +type MySQLStorageConfiguration struct { + SQLStorageConfiguration `yaml:",inline"` +} + +// PostgreSQLStorageConfiguration represents the configuration of a Postgres database +type PostgreSQLStorageConfiguration struct { + SQLStorageConfiguration `yaml:",inline"` + SSLMode string `yaml:"sslmode"` +} + // StorageConfiguration represents the configuration of the storage backend. type StorageConfiguration struct { - Local *LocalStorageConfiguration `yaml:"local"` - SQL *SQLStorageConfiguration `yaml:"sql"` + Local *LocalStorageConfiguration `yaml:"local"` + MySQL *MySQLStorageConfiguration `yaml:"mysql"` + PostgreSQL *PostgreSQLStorageConfiguration `yaml:"postgres"` } diff --git a/configuration/validator/configuration.go b/configuration/validator/configuration.go index c4837cc14..c15480c85 100644 --- a/configuration/validator/configuration.go +++ b/configuration/validator/configuration.go @@ -30,4 +30,6 @@ func Validate(configuration *schema.Configuration, validator *schema.StructValid configuration.TOTP = &schema.TOTPConfiguration{} ValidateTOTP(configuration.TOTP, validator) } + + ValidateSQLStorage(configuration.Storage, validator) } diff --git a/configuration/validator/configuration_test.go b/configuration/validator/configuration_test.go index c29feee0b..b529029bb 100644 --- a/configuration/validator/configuration_test.go +++ b/configuration/validator/configuration_test.go @@ -19,6 +19,11 @@ func newDefaultConfig() schema.Configuration { Name: "authelia_session", Secret: "secret", } + config.Storage = &schema.StorageConfiguration{ + Local: &schema.LocalStorageConfiguration{ + Path: "abc", + }, + } return config } diff --git a/configuration/validator/storage.go b/configuration/validator/storage.go index 3633be38d..0cce5fa70 100644 --- a/configuration/validator/storage.go +++ b/configuration/validator/storage.go @@ -8,12 +8,14 @@ import ( // ValidateSQLStorage validates storage configuration. func ValidateSQLStorage(configuration *schema.StorageConfiguration, validator *schema.StructValidator) { - if configuration.Local == nil && configuration.SQL == nil { - validator.Push(errors.New("A storage configuration must be provided. It could be 'local' or 'sql'")) + if configuration.Local == nil && configuration.MySQL == nil && configuration.PostgreSQL == nil { + validator.Push(errors.New("A storage configuration must be provided. It could be 'local', 'mysql' or 'postgres'")) } - if configuration.SQL != nil { - validateSQLConfiguration(configuration.SQL, validator) + if configuration.MySQL != nil { + validateSQLConfiguration(&configuration.MySQL.SQLStorageConfiguration, validator) + } else if configuration.PostgreSQL != nil { + validatePostgreSQLConfiguration(configuration.PostgreSQL, validator) } else if configuration.Local != nil { validateLocalStorageConfiguration(configuration.Local, validator) } @@ -29,6 +31,19 @@ func validateSQLConfiguration(configuration *schema.SQLStorageConfiguration, val } } +func validatePostgreSQLConfiguration(configuration *schema.PostgreSQLStorageConfiguration, validator *schema.StructValidator) { + validateSQLConfiguration(&configuration.SQLStorageConfiguration, validator) + + if configuration.SSLMode == "" { + configuration.SSLMode = "disable" + } + + if !(configuration.SSLMode == "disable" || configuration.SSLMode == "require" || + configuration.SSLMode == "verify-ca" || configuration.SSLMode == "verify-full") { + validator.Push(errors.New("SSL mode must be 'disable', 'require', 'verify-ca' or 'verify-full'")) + } +} + func validateLocalStorageConfiguration(configuration *schema.LocalStorageConfiguration, validator *schema.StructValidator) { if configuration.Path == "" { validator.Push(errors.New("A file path must be provided with key 'path'")) diff --git a/example/compose/postgres/docker-compose.yml b/example/compose/postgres/docker-compose.yml new file mode 100644 index 000000000..7a3385adb --- /dev/null +++ b/example/compose/postgres/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3" +services: + postgres: + image: postgres:12 + environment: + - POSTGRES_PASSWORD=password + - POSTGRES_USER=admin + - POSTGRES_DB=authelia + networks: + - authelianet \ No newline at end of file diff --git a/example/kube/authelia/configs/config.yml b/example/kube/authelia/configs/config.yml index c7ad71e8b..06cad571e 100644 --- a/example/kube/authelia/configs/config.yml +++ b/example/kube/authelia/configs/config.yml @@ -89,7 +89,7 @@ regulation: ban_time: 300 storage: - sql: + mysql: host: mariadb-service port: 3306 database: authelia diff --git a/go.mod b/go.mod index ede1c8e1a..d51f20c0c 100644 --- a/go.mod +++ b/go.mod @@ -3,17 +3,20 @@ module github.com/clems4ever/authelia go 1.13 require ( + github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 github.com/Workiva/go-datastructures v1.0.50 github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a github.com/cespare/reflex v0.2.0 // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74 + github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/fasthttp/router v0.5.2 github.com/fasthttp/session v1.1.3 github.com/go-sql-driver/mysql v1.4.1 github.com/golang/mock v1.3.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kr/pty v1.1.8 // indirect + github.com/lib/pq v1.2.0 github.com/mattn/go-sqlite3 v1.11.0 github.com/ogier/pflag v0.0.1 // indirect github.com/onsi/ginkgo v1.10.3 // indirect diff --git a/go.sum b/go.sum index bef7c81e4..9559b1e43 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOC github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e h1:4ZrkT/RzpnROylmoQL57iVUL57wGKTR5O6KpVnbm2tA= github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= +github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 h1:vdT7QwBhJJEVNFMBNhRSFDRCB6O16T28VhvqRgqFyn8= +github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4/go.mod h1:SvXOG8ElV28oAiG9zv91SDe5+9PfIr7PPccpr8YyXNs= github.com/Workiva/go-datastructures v1.0.50 h1:slDmfW6KCHcC7U+LP3DDBbm4fqTwZGn1beOFPfGaLvo= github.com/Workiva/go-datastructures v1.0.50/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -35,6 +37,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74 h1:2MIhn2R6oXQbgW5yHfS+d6YqyMfXiu2L55rFZC4UD/M= github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74/go.mod h1:UqXY1lYT/ERa4OEAywUqdok1T4RCRdArkhic1Opuavo= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= github.com/fasthttp/router v0.5.2 h1:xdmx8uYc9IFDtlbG2/FhE1Gyowv7/sqMgMonRjoW0Yo= github.com/fasthttp/router v0.5.2/go.mod h1:Y5JAeRTSPwSLoUgH4x75UnT1j1IcAgVshMDMMrnNmKQ= github.com/fasthttp/session v1.1.3 h1:2qjxNltI7iv0yh7frsIdhbsGmSoRnTajU8xtpC6Hd80= @@ -87,6 +91,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= diff --git a/handlers/handler_firstfactor.go b/handlers/handler_firstfactor.go index d4615a825..3d4eef683 100644 --- a/handlers/handler_firstfactor.go +++ b/handlers/handler_firstfactor.go @@ -5,13 +5,11 @@ import ( "net/url" "time" - "github.com/clems4ever/authelia/regulation" - - "github.com/clems4ever/authelia/session" - "github.com/clems4ever/authelia/authentication" "github.com/clems4ever/authelia/authorization" "github.com/clems4ever/authelia/middlewares" + "github.com/clems4ever/authelia/regulation" + "github.com/clems4ever/authelia/session" ) // FirstFactorPost is the handler performing the first factory. @@ -51,7 +49,7 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) { } if !userPasswordOk { - ctx.Error(fmt.Errorf("Credentials are wrong for user %s", bodyJSON.Username), authenticationFailedMessage) + ctx.ReplyError(fmt.Errorf("Credentials are wrong for user %s", bodyJSON.Username), authenticationFailedMessage) return } @@ -89,7 +87,7 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) { return } - ctx.Logger.Debugf("Details for user %s => groups: %s, emails %s", bodyJSON.Username, userDetails.Groups, userDetails.Emails) + ctx.Logger.Tracef("Details for user %s => groups: %s, emails %s", bodyJSON.Username, userDetails.Groups, userDetails.Emails) // And set those information in the new session. userSession := ctx.GetSession() diff --git a/handlers/handler_logout.go b/handlers/handler_logout.go index e64cde038..76d2b2b69 100644 --- a/handlers/handler_logout.go +++ b/handlers/handler_logout.go @@ -8,7 +8,7 @@ import ( // LogoutPost is the handler logging out the user attached to the given cookie. func LogoutPost(ctx *middlewares.AutheliaCtx) { - ctx.Logger.Debug("Destroy session") + ctx.Logger.Tracef("Destroy session") err := ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx) if err != nil { diff --git a/handlers/handler_verify.go b/handlers/handler_verify.go index d62ed65a0..521dbd418 100644 --- a/handlers/handler_verify.go +++ b/handlers/handler_verify.go @@ -158,7 +158,7 @@ func hasUserBeenInactiveLongEnough(ctx *middlewares.AutheliaCtx) (bool, error) { lastActivity := ctx.GetSession().LastActivity inactivityPeriod := time.Now().Unix() - lastActivity - ctx.Logger.Debugf("Inactivity report: Inactivity=%d, MaxInactivity=%d", + ctx.Logger.Tracef("Inactivity report: Inactivity=%d, MaxInactivity=%d", inactivityPeriod, maxInactivityPeriod) if inactivityPeriod > maxInactivityPeriod { diff --git a/logging/logger.go b/logging/logger.go index 34eb4b9a9..a90453542 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -1,10 +1,17 @@ package logging import ( + logrus_stack "github.com/Gurpartap/logrus-stack" "github.com/sirupsen/logrus" "github.com/valyala/fasthttp" ) +func init() { + callerLevels := []logrus.Level{} + stackLevels := []logrus.Level{logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel} + logrus.AddHook(logrus_stack.NewHook(callerLevels, stackLevels)) +} + // Logger return the standard logrues logger. func Logger() *logrus.Logger { return logrus.StandardLogger() diff --git a/middlewares/authelia_context.go b/middlewares/authelia_context.go index 50c80ad3c..806a5449b 100644 --- a/middlewares/authelia_context.go +++ b/middlewares/authelia_context.go @@ -45,6 +45,7 @@ func AutheliaMiddleware(configuration schema.Configuration, providers Providers) } } +// Error reply with an error and display the stack trace in the logs. func (c *AutheliaCtx) Error(err error, message string) { b, _ := json.Marshal(ErrorResponse{Status: "KO", Message: message}) c.SetContentType("application/json") @@ -52,6 +53,14 @@ func (c *AutheliaCtx) Error(err error, message string) { c.Logger.Error(err) } +// ReplyError reply with an error but does not display any stack trace in the logs +func (c *AutheliaCtx) ReplyError(err error, message string) { + b, _ := json.Marshal(ErrorResponse{Status: "KO", Message: message}) + c.SetContentType("application/json") + c.SetBody(b) + c.Logger.Debug(err) +} + // ReplyUnauthorized response sent when user is unauthorized func (c *AutheliaCtx) ReplyUnauthorized() { c.RequestCtx.Error(fasthttp.StatusMessage(fasthttp.StatusUnauthorized), fasthttp.StatusUnauthorized) diff --git a/storage/constants.go b/storage/constants.go new file mode 100644 index 000000000..6ce860dfd --- /dev/null +++ b/storage/constants.go @@ -0,0 +1,7 @@ +package storage + +var preferencesTableName = "PreferencesTableName" +var identityVerificationTokensTableName = "IdentityVerificationTokens" +var totpSecretsTableName = "TOTPSecrets" +var u2fDeviceHandlesTableName = "U2FDeviceHandles" +var authenticationLogsTableName = "AuthenticationLogs" diff --git a/storage/mysql_provider.go b/storage/mysql_provider.go index 6503f0df7..8c5866602 100644 --- a/storage/mysql_provider.go +++ b/storage/mysql_provider.go @@ -14,8 +14,8 @@ type MySQLProvider struct { SQLProvider } -// NewSQLProvider a SQL provider -func NewSQLProvider(configuration schema.SQLStorageConfiguration) *MySQLProvider { +// NewMySQLProvider a MySQL provider +func NewMySQLProvider(configuration schema.MySQLStorageConfiguration) *MySQLProvider { connectionString := configuration.Username if configuration.Password != "" { @@ -36,14 +36,30 @@ func NewSQLProvider(configuration schema.SQLStorageConfiguration) *MySQLProvider connectionString += fmt.Sprintf("/%s", configuration.Database) } - fmt.Println(connectionString) - db, err := sql.Open("mysql", connectionString) if err != nil { logging.Logger().Fatalf("Unable to connect to SQL database: %v", err) } - provider := MySQLProvider{} + provider := MySQLProvider{ + SQLProvider{ + sqlGetPreferencesByUsername: fmt.Sprintf("SELECT second_factor_method FROM %s WHERE username=?", preferencesTableName), + sqlUpsertSecondFactorPreference: fmt.Sprintf("REPLACE INTO %s (username, second_factor_method) VALUES (?, ?)", preferencesTableName), + + sqlTestIdentityVerificationTokenExistence: fmt.Sprintf("SELECT EXISTS (SELECT * FROM %s WHERE token=?)", identityVerificationTokensTableName), + sqlInsertIdentityVerificationToken: fmt.Sprintf("INSERT INTO %s (token) VALUES (?)", identityVerificationTokensTableName), + sqlDeleteIdentityVerificationToken: fmt.Sprintf("DELETE FROM %s WHERE token=?", identityVerificationTokensTableName), + + sqlGetTOTPSecretByUsername: fmt.Sprintf("SELECT secret FROM %s WHERE username=?", totpSecretsTableName), + sqlUpsertTOTPSecret: fmt.Sprintf("REPLACE INTO %s (username, secret) VALUES (?, ?)", totpSecretsTableName), + + sqlGetU2FDeviceHandleByUsername: fmt.Sprintf("SELECT deviceHandle FROM %s WHERE username=?", u2fDeviceHandlesTableName), + sqlUpsertU2FDeviceHandle: fmt.Sprintf("REPLACE INTO %s (username, deviceHandle) VALUES (?, ?)", u2fDeviceHandlesTableName), + + sqlInsertAuthenticationLog: fmt.Sprintf("INSERT INTO %s (username, successful, time) VALUES (?, ?, ?)", authenticationLogsTableName), + sqlGetLatestAuthenticationLogs: fmt.Sprintf("SELECT successful, time FROM %s WHERE time>? AND username=? ORDER BY time DESC", authenticationLogsTableName), + }, + } if err := provider.initialize(db); err != nil { logging.Logger().Fatalf("Unable to initialize SQL database: %v", err) } diff --git a/storage/postgres_provider.go b/storage/postgres_provider.go new file mode 100644 index 000000000..3f2aa825d --- /dev/null +++ b/storage/postgres_provider.go @@ -0,0 +1,75 @@ +package storage + +import ( + "database/sql" + "fmt" + "strings" + + "github.com/clems4ever/authelia/configuration/schema" + "github.com/clems4ever/authelia/logging" + _ "github.com/lib/pq" // Load the PostgreSQL Driver used in the connection string. +) + +// PostgreSQLProvider is a Postrgres provider +type PostgreSQLProvider struct { + SQLProvider +} + +// NewPostgreSQLProvider a SQL provider +func NewPostgreSQLProvider(configuration schema.PostgreSQLStorageConfiguration) *PostgreSQLProvider { + args := make([]string, 0) + if configuration.Username != "" { + args = append(args, fmt.Sprintf("user='%s'", configuration.Username)) + } + + if configuration.Password != "" { + args = append(args, fmt.Sprintf("password='%s'", configuration.Password)) + } + + if configuration.Host != "" { + args = append(args, fmt.Sprintf("host=%s", configuration.Host)) + } + + if configuration.Port > 0 { + args = append(args, fmt.Sprintf("port=%d", configuration.Port)) + } + + if configuration.Database != "" { + args = append(args, fmt.Sprintf("dbname=%s", configuration.Database)) + } + + if configuration.SSLMode != "" { + args = append(args, fmt.Sprintf("sslmode=%s", configuration.SSLMode)) + } + + connectionString := strings.Join(args, " ") + + db, err := sql.Open("postgres", connectionString) + if err != nil { + logging.Logger().Fatalf("Unable to connect to SQL database: %v", err) + } + + provider := PostgreSQLProvider{ + SQLProvider{ + sqlGetPreferencesByUsername: fmt.Sprintf("SELECT second_factor_method FROM %s WHERE username=$1", preferencesTableName), + sqlUpsertSecondFactorPreference: fmt.Sprintf("INSERT INTO %s (username, second_factor_method) VALUES ($1, $2) ON CONFLICT (username) DO UPDATE SET method=$2", preferencesTableName), + + sqlTestIdentityVerificationTokenExistence: fmt.Sprintf("SELECT EXISTS (SELECT * FROM %s WHERE token=$1)", identityVerificationTokensTableName), + sqlInsertIdentityVerificationToken: fmt.Sprintf("INSERT INTO %s (token) VALUES ($1)", identityVerificationTokensTableName), + sqlDeleteIdentityVerificationToken: fmt.Sprintf("DELETE FROM %s WHERE token=$1", identityVerificationTokensTableName), + + sqlGetTOTPSecretByUsername: fmt.Sprintf("SELECT secret FROM %s WHERE username=$1", totpSecretsTableName), + sqlUpsertTOTPSecret: fmt.Sprintf("INSERT INTO %s (username, secret) VALUES ($1, $2) ON CONFLICT (username) DO UPDATE SET secret=$2", totpSecretsTableName), + + sqlGetU2FDeviceHandleByUsername: fmt.Sprintf("SELECT deviceHandle FROM %s WHERE username=$1", u2fDeviceHandlesTableName), + sqlUpsertU2FDeviceHandle: fmt.Sprintf("INSERT INTO %s (username, deviceHandle) VALUES ($1, $2) ON CONFLICT (username) DO UPDATE SET deviceHandle=$2", u2fDeviceHandlesTableName), + + sqlInsertAuthenticationLog: fmt.Sprintf("INSERT INTO %s (username, successful, time) VALUES ($1, $2, $3)", authenticationLogsTableName), + sqlGetLatestAuthenticationLogs: fmt.Sprintf("SELECT successful, time FROM %s WHERE time>$1 AND username=$2 ORDER BY time DESC", authenticationLogsTableName), + }, + } + if err := provider.initialize(db); err != nil { + logging.Logger().Fatalf("Unable to initialize SQL database: %v", err) + } + return &provider +} diff --git a/storage/sql_provider.go b/storage/sql_provider.go index b8d284768..4a023ea01 100644 --- a/storage/sql_provider.go +++ b/storage/sql_provider.go @@ -2,6 +2,8 @@ package storage import ( "database/sql" + "encoding/base64" + "fmt" "time" "github.com/clems4ever/authelia/models" @@ -10,42 +12,58 @@ import ( // SQLProvider is a storage provider persisting data in a SQL database. type SQLProvider struct { db *sql.DB + + sqlGetPreferencesByUsername string + sqlUpsertSecondFactorPreference string + + sqlTestIdentityVerificationTokenExistence string + sqlInsertIdentityVerificationToken string + sqlDeleteIdentityVerificationToken string + + sqlGetTOTPSecretByUsername string + sqlUpsertTOTPSecret string + + sqlGetU2FDeviceHandleByUsername string + sqlUpsertU2FDeviceHandle string + + sqlInsertAuthenticationLog string + sqlGetLatestAuthenticationLogs string } func (p *SQLProvider) initialize(db *sql.DB) error { p.db = db - _, err := db.Exec("CREATE TABLE IF NOT EXISTS SecondFactorPreferences (username VARCHAR(100) PRIMARY KEY, method VARCHAR(10))") + _, err := db.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (username VARCHAR(100) PRIMARY KEY, second_factor_method VARCHAR(10))", preferencesTableName)) if err != nil { return err } - _, err = db.Exec("CREATE TABLE IF NOT EXISTS IdentityVerificationTokens (token VARCHAR(512))") + _, err = db.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (token VARCHAR(512))", identityVerificationTokensTableName)) if err != nil { return err } - _, err = db.Exec("CREATE TABLE IF NOT EXISTS TOTPSecrets (username VARCHAR(100) PRIMARY KEY, secret VARCHAR(64))") + _, err = db.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (username VARCHAR(100) PRIMARY KEY, secret VARCHAR(64))", totpSecretsTableName)) if err != nil { return err } - _, err = db.Exec("CREATE TABLE IF NOT EXISTS U2FDeviceHandles (username VARCHAR(100) PRIMARY KEY, deviceHandle BLOB)") + _, err = db.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (username VARCHAR(100) PRIMARY KEY, deviceHandle TEXT)", u2fDeviceHandlesTableName)) if err != nil { return err } - _, err = db.Exec("CREATE TABLE IF NOT EXISTS AuthenticationLogs (username VARCHAR(100), successful BOOL, time INTEGER)") + _, err = db.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (username VARCHAR(100), successful BOOL, time INTEGER)", authenticationLogsTableName)) if err != nil { return err } - _, err = db.Exec("CREATE INDEX IF NOT EXISTS time ON AuthenticationLogs (time);") + _, err = db.Exec(fmt.Sprintf("CREATE INDEX IF NOT EXISTS time ON %s (time);", authenticationLogsTableName)) if err != nil { return err } - _, err = db.Exec("CREATE INDEX IF NOT EXISTS username ON AuthenticationLogs (username);") + _, err = db.Exec(fmt.Sprintf("CREATE INDEX IF NOT EXISTS username ON %s (username);", authenticationLogsTableName)) if err != nil { return err } @@ -54,11 +72,7 @@ func (p *SQLProvider) initialize(db *sql.DB) error { // LoadPrefered2FAMethod load the prefered method for 2FA from sqlite db. func (p *SQLProvider) LoadPrefered2FAMethod(username string) (string, error) { - stmt, err := p.db.Prepare("SELECT method FROM SecondFactorPreferences WHERE username=?") - if err != nil { - return "", err - } - rows, err := stmt.Query(username) + rows, err := p.db.Query(p.sqlGetPreferencesByUsername, username) defer rows.Close() if err != nil { return "", err @@ -76,70 +90,42 @@ func (p *SQLProvider) LoadPrefered2FAMethod(username string) (string, error) { // SavePrefered2FAMethod save the prefered method for 2FA in sqlite db. func (p *SQLProvider) SavePrefered2FAMethod(username string, method string) error { - stmt, err := p.db.Prepare("REPLACE INTO SecondFactorPreferences (username, method) VALUES (?, ?)") - if err != nil { - return err - } - _, err = stmt.Exec(username, method) + _, err := p.db.Exec(p.sqlUpsertSecondFactorPreference, username, method) return err } // FindIdentityVerificationToken look for an identity verification token in DB. func (p *SQLProvider) FindIdentityVerificationToken(token string) (bool, error) { - stmt, err := p.db.Prepare("SELECT token FROM IdentityVerificationTokens WHERE token=?") + var found bool + err := p.db.QueryRow(p.sqlTestIdentityVerificationTokenExistence, token).Scan(&found) if err != nil { return false, err } - var found string - err = stmt.QueryRow(token).Scan(&found) - if err != nil { - if err == sql.ErrNoRows { - return false, nil - } - return false, err - } - return true, nil + return found, nil } // SaveIdentityVerificationToken save an identity verification token in DB. func (p *SQLProvider) SaveIdentityVerificationToken(token string) error { - stmt, err := p.db.Prepare("INSERT INTO IdentityVerificationTokens (token) VALUES (?)") - if err != nil { - return err - } - _, err = stmt.Exec(token) + _, err := p.db.Exec(p.sqlInsertIdentityVerificationToken, token) return err } // RemoveIdentityVerificationToken remove an identity verification token from the DB. func (p *SQLProvider) RemoveIdentityVerificationToken(token string) error { - stmt, err := p.db.Prepare("DELETE FROM IdentityVerificationTokens WHERE token=?") - if err != nil { - return err - } - _, err = stmt.Exec(token) + _, err := p.db.Exec(p.sqlDeleteIdentityVerificationToken, token) return err } // SaveTOTPSecret save a TOTP secret of a given user. func (p *SQLProvider) SaveTOTPSecret(username string, secret string) error { - stmt, err := p.db.Prepare("REPLACE INTO TOTPSecrets (username, secret) VALUES (?, ?)") - if err != nil { - return err - } - _, err = stmt.Exec(username, secret) + _, err := p.db.Exec(p.sqlUpsertTOTPSecret, username, secret) return err } // LoadTOTPSecret load a TOTP secret given a username. func (p *SQLProvider) LoadTOTPSecret(username string) (string, error) { - stmt, err := p.db.Prepare("SELECT secret FROM TOTPSecrets WHERE username=?") - if err != nil { - return "", err - } var secret string - err = stmt.QueryRow(username).Scan(&secret) - if err != nil { + if err := p.db.QueryRow(p.sqlGetTOTPSecretByUsername, username).Scan(&secret); err != nil { if err == sql.ErrNoRows { return "", nil } @@ -150,45 +136,32 @@ func (p *SQLProvider) LoadTOTPSecret(username string) (string, error) { // SaveU2FDeviceHandle save a registered U2F device registration blob. func (p *SQLProvider) SaveU2FDeviceHandle(username string, keyHandle []byte) error { - stmt, err := p.db.Prepare("REPLACE INTO U2FDeviceHandles (username, deviceHandle) VALUES (?, ?)") - if err != nil { - return err - } - _, err = stmt.Exec(username, keyHandle) + _, err := p.db.Exec(p.sqlUpsertU2FDeviceHandle, username, base64.StdEncoding.EncodeToString(keyHandle)) return err } // LoadU2FDeviceHandle load a U2F device registration blob for a given username. func (p *SQLProvider) LoadU2FDeviceHandle(username string) ([]byte, error) { - stmt, err := p.db.Prepare("SELECT deviceHandle FROM U2FDeviceHandles WHERE username=?") - if err != nil { - return nil, err - } - var deviceHandle []byte - err = stmt.QueryRow(username).Scan(&deviceHandle) - if err != nil { + var deviceHandle string + if err := p.db.QueryRow(p.sqlGetU2FDeviceHandleByUsername, username).Scan(&deviceHandle); err != nil { if err == sql.ErrNoRows { return nil, ErrNoU2FDeviceHandle } return nil, err } - return deviceHandle, nil + + return base64.StdEncoding.DecodeString(deviceHandle) } // AppendAuthenticationLog append a mark to the authentication log. func (p *SQLProvider) AppendAuthenticationLog(attempt models.AuthenticationAttempt) error { - stmt, err := p.db.Prepare("INSERT INTO AuthenticationLogs (username, successful, time) VALUES (?, ?, ?)") - if err != nil { - return err - } - _, err = stmt.Exec(attempt.Username, attempt.Successful, attempt.Time.Unix()) + _, err := p.db.Exec(p.sqlInsertAuthenticationLog, attempt.Username, attempt.Successful, attempt.Time.Unix()) return err } // LoadLatestAuthenticationLogs retrieve the latest marks from the authentication log. func (p *SQLProvider) LoadLatestAuthenticationLogs(username string, fromDate time.Time) ([]models.AuthenticationAttempt, error) { - rows, err := p.db.Query("SELECT successful, time FROM AuthenticationLogs WHERE time>? AND username=? ORDER BY time DESC", - fromDate.Unix(), username) + rows, err := p.db.Query(p.sqlGetLatestAuthenticationLogs, fromDate.Unix(), username) if err != nil { return nil, err diff --git a/storage/sqlite_provider.go b/storage/sqlite_provider.go index 6efe70035..342f851a6 100644 --- a/storage/sqlite_provider.go +++ b/storage/sqlite_provider.go @@ -2,6 +2,7 @@ package storage import ( "database/sql" + "fmt" "github.com/clems4ever/authelia/logging" _ "github.com/mattn/go-sqlite3" // Load the SQLite Driver used in the connection string. @@ -19,7 +20,25 @@ func NewSQLiteProvider(path string) *SQLiteProvider { logging.Logger().Fatalf("Unable to create SQLite database %s: %s", path, err) } - provider := SQLiteProvider{} + provider := SQLiteProvider{ + SQLProvider{ + sqlGetPreferencesByUsername: fmt.Sprintf("SELECT second_factor_method FROM %s WHERE username=?", preferencesTableName), + sqlUpsertSecondFactorPreference: fmt.Sprintf("REPLACE INTO %s (username, second_factor_method) VALUES (?, ?)", preferencesTableName), + + sqlTestIdentityVerificationTokenExistence: fmt.Sprintf("SELECT EXISTS (SELECT * FROM %s WHERE token=?)", identityVerificationTokensTableName), + sqlInsertIdentityVerificationToken: fmt.Sprintf("INSERT INTO %s (token) VALUES (?)", identityVerificationTokensTableName), + sqlDeleteIdentityVerificationToken: fmt.Sprintf("DELETE FROM %s WHERE token=?", identityVerificationTokensTableName), + + sqlGetTOTPSecretByUsername: fmt.Sprintf("SELECT secret FROM %s WHERE username=?", totpSecretsTableName), + sqlUpsertTOTPSecret: fmt.Sprintf("REPLACE INTO %s (username, secret) VALUES (?, ?)", totpSecretsTableName), + + sqlGetU2FDeviceHandleByUsername: fmt.Sprintf("SELECT deviceHandle FROM %s WHERE username=?", u2fDeviceHandlesTableName), + sqlUpsertU2FDeviceHandle: fmt.Sprintf("REPLACE INTO %s (username, deviceHandle) VALUES (?, ?)", u2fDeviceHandlesTableName), + + sqlInsertAuthenticationLog: fmt.Sprintf("INSERT INTO %s (username, successful, time) VALUES (?, ?, ?)", authenticationLogsTableName), + sqlGetLatestAuthenticationLogs: fmt.Sprintf("SELECT successful, time FROM %s WHERE time>? AND username=? ORDER BY time DESC", authenticationLogsTableName), + }, + } if err := provider.initialize(db); err != nil { logging.Logger().Fatalf("Unable to initialize SQLite database %s: %s", path, err) } diff --git a/suites/HighAvailability/configuration.yml b/suites/HighAvailability/configuration.yml index 4548261b5..64c7525ff 100644 --- a/suites/HighAvailability/configuration.yml +++ b/suites/HighAvailability/configuration.yml @@ -223,7 +223,7 @@ regulation: # You must use only an available configuration: local, sql storage: # Settings to connect to mariadb server - sql: + mysql: host: mariadb port: 3306 database: authelia diff --git a/suites/Mariadb/configuration.yml b/suites/Mariadb/configuration.yml index 1aafd427c..c07ea901d 100644 --- a/suites/Mariadb/configuration.yml +++ b/suites/Mariadb/configuration.yml @@ -22,7 +22,7 @@ session: # Configuration of the storage backend used to store data and secrets. i.e. totp data storage: - sql: + mysql: host: mariadb port: 3306 database: authelia diff --git a/suites/Postgres/configuration.yml b/suites/Postgres/configuration.yml new file mode 100644 index 000000000..39da51523 --- /dev/null +++ b/suites/Postgres/configuration.yml @@ -0,0 +1,67 @@ +############################################################### +# Authelia minimal configuration # +############################################################### + +port: 9091 + +logs_level: debug + +default_redirection_url: https://home.example.com:8080/ + +jwt_secret: very_important_secret + +authentication_backend: + file: + path: users.yml + +session: + secret: unsecure_session_secret + domain: example.com + expiration: 3600 # 1 hour + inactivity: 300 # 5 minutes + +# Configuration of the storage backend used to store data and secrets. i.e. totp data +storage: + postgres: + host: postgres + port: 5432 + database: authelia + username: admin + password: password + +# TOTP Issuer Name +# +# This will be the issuer name displayed in Google Authenticator +# See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format for more info on issuer names +totp: + issuer: example.com + +access_control: + default_policy: deny + rules: + - domain: "public.example.com" + policy: bypass + - domain: "admin.example.com" + policy: two_factor + - domain: "secure.example.com" + policy: two_factor + - domain: "singlefactor.example.com" + policy: one_factor + +# Configuration of the authentication regulation mechanism. +regulation: + # Set it to 0 to disable max_retries. + max_retries: 3 + + # The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window. + find_time: 8 + + # The length of time before a banned user can login again. + ban_time: 10 + +notifier: + # Use a SMTP server for sending notifications + smtp: + host: smtp + port: 1025 + sender: admin@example.com diff --git a/suites/Postgres/users.yml b/suites/Postgres/users.yml new file mode 100644 index 000000000..6fe7a384d --- /dev/null +++ b/suites/Postgres/users.yml @@ -0,0 +1,29 @@ +############################################################### +# Users Database # +############################################################### + +# This file can be used if you do not have an LDAP set up. + +# List of users +users: + john: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + email: john.doe@authelia.com + groups: + - admins + - dev + + harry: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + email: harry.potter@authelia.com + groups: [] + + bob: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + email: bob.dylan@authelia.com + groups: + - dev + + james: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + email: james.dean@authelia.com \ No newline at end of file diff --git a/suites/suite_postgres.go b/suites/suite_postgres.go new file mode 100644 index 000000000..89688abc1 --- /dev/null +++ b/suites/suite_postgres.go @@ -0,0 +1,40 @@ +package suites + +import ( + "time" +) + +var postgresSuiteName = "Postgres" + +func init() { + dockerEnvironment := NewDockerEnvironment([]string{ + "docker-compose.yml", + "example/compose/authelia/docker-compose.backend.yml", + "example/compose/authelia/docker-compose.frontend.yml", + "example/compose/nginx/backend/docker-compose.yml", + "example/compose/nginx/portal/docker-compose.yml", + "example/compose/smtp/docker-compose.yml", + "example/compose/postgres/docker-compose.yml", + "example/compose/ldap/docker-compose.yml", + }) + + setup := func(suitePath string) error { + if err := dockerEnvironment.Up(suitePath); err != nil { + return err + } + + return waitUntilAutheliaIsReady(dockerEnvironment) + } + + teardown := func(suitePath string) error { + err := dockerEnvironment.Down(suitePath) + return err + } + + GlobalRegistry.Register(postgresSuiteName, Suite{ + SetUp: setup, + SetUpTimeout: 3 * time.Minute, + TearDown: teardown, + TearDownTimeout: 2 * time.Minute, + }) +} diff --git a/suites/suite_postgres_test.go b/suites/suite_postgres_test.go new file mode 100644 index 000000000..49aab7887 --- /dev/null +++ b/suites/suite_postgres_test.go @@ -0,0 +1,20 @@ +package suites + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type PostgresSuite struct { + *SeleniumSuite +} + +func NewPostgresSuite() *PostgresSuite { + return &PostgresSuite{SeleniumSuite: new(SeleniumSuite)} +} + +func TestPostgresSuite(t *testing.T) { + suite.Run(t, NewOneFactorSuite()) + suite.Run(t, NewTwoFactorSuite()) +} diff --git a/test/resources/config.yml b/test/resources/config.yml index 27d328faf..41f10eb1c 100644 --- a/test/resources/config.yml +++ b/test/resources/config.yml @@ -105,7 +105,7 @@ regulation: ban_time: 300 storage: - sql: + mysql: host: 127.0.0.1 port: 3306 database: authelia diff --git a/test/suites/Basic/scenarii/AlreadyLoggedIn.ts b/test/suites/Standalone/scenarii/AlreadyLoggedIn.ts similarity index 100% rename from test/suites/Basic/scenarii/AlreadyLoggedIn.ts rename to test/suites/Standalone/scenarii/AlreadyLoggedIn.ts diff --git a/test/suites/Basic/scenarii/BackendProtection.ts b/test/suites/Standalone/scenarii/BackendProtection.ts similarity index 100% rename from test/suites/Basic/scenarii/BackendProtection.ts rename to test/suites/Standalone/scenarii/BackendProtection.ts diff --git a/test/suites/Basic/scenarii/BadPassword.ts b/test/suites/Standalone/scenarii/BadPassword.ts similarity index 100% rename from test/suites/Basic/scenarii/BadPassword.ts rename to test/suites/Standalone/scenarii/BadPassword.ts diff --git a/test/suites/Basic/scenarii/BypassPolicy.ts b/test/suites/Standalone/scenarii/BypassPolicy.ts similarity index 100% rename from test/suites/Basic/scenarii/BypassPolicy.ts rename to test/suites/Standalone/scenarii/BypassPolicy.ts diff --git a/test/suites/Basic/scenarii/NoDuoPushOption.ts b/test/suites/Standalone/scenarii/NoDuoPushOption.ts similarity index 100% rename from test/suites/Basic/scenarii/NoDuoPushOption.ts rename to test/suites/Standalone/scenarii/NoDuoPushOption.ts diff --git a/test/suites/Basic/scenarii/RegisterTotp.ts b/test/suites/Standalone/scenarii/RegisterTotp.ts similarity index 100% rename from test/suites/Basic/scenarii/RegisterTotp.ts rename to test/suites/Standalone/scenarii/RegisterTotp.ts diff --git a/test/suites/Basic/scenarii/ResetPassword.ts b/test/suites/Standalone/scenarii/ResetPassword.ts similarity index 100% rename from test/suites/Basic/scenarii/ResetPassword.ts rename to test/suites/Standalone/scenarii/ResetPassword.ts diff --git a/test/suites/Basic/scenarii/TOTPValidation.ts b/test/suites/Standalone/scenarii/TOTPValidation.ts similarity index 100% rename from test/suites/Basic/scenarii/TOTPValidation.ts rename to test/suites/Standalone/scenarii/TOTPValidation.ts diff --git a/test/suites/Basic/scenarii/VerifyEndpoint.ts b/test/suites/Standalone/scenarii/VerifyEndpoint.ts similarity index 100% rename from test/suites/Basic/scenarii/VerifyEndpoint.ts rename to test/suites/Standalone/scenarii/VerifyEndpoint.ts diff --git a/test/suites/Basic/test.ts b/test/suites/Standalone/test.ts similarity index 86% rename from test/suites/Basic/test.ts rename to test/suites/Standalone/test.ts index 5e079e5d1..5ff4f3855 100644 --- a/test/suites/Basic/test.ts +++ b/test/suites/Standalone/test.ts @@ -10,11 +10,11 @@ import { exec } from '../../helpers/utils/exec'; import BypassPolicy from "./scenarii/BypassPolicy"; import NoDuoPushOption from "./scenarii/NoDuoPushOption"; -AutheliaSuite("/tmp/authelia/suites/Basic/", function() { +AutheliaSuite("/tmp/authelia/suites/Standalone/", function() { this.timeout(10000); beforeEach(async function() { - await exec(`cp ${__dirname}/../../../suites/Basic/users.yml /tmp/authelia/suites/Basic/users.yml`); + await exec(`cp ${__dirname}/../../../suites/Standalone/users.yml /tmp/authelia/suites/Standalone/users.yml`); }); describe('Bypass policy', BypassPolicy) diff --git a/utils/exec.go b/utils/exec.go index 3e2a23ba2..d228d026e 100644 --- a/utils/exec.go +++ b/utils/exec.go @@ -114,9 +114,8 @@ func RunCommandWithTimeout(cmd *exec.Cmd, timeout time.Duration) error { select { case <-time.After(timeout): fmt.Printf("Timeout of %ds reached... Killing process...\n", int64(timeout/time.Second)) - err := cmd.Process.Kill() - if err != nil { + if err := cmd.Process.Kill(); err != nil { return err } return fmt.Errorf("timeout of %ds reached", int64(timeout/time.Second))