Add support for PostgreSQL.

pull/447/head
Clement Michaud 2019-11-16 20:50:58 +01:00 committed by Clément Michaud
parent b2ced06402
commit 6303485fd2
40 changed files with 427 additions and 101 deletions

View File

@ -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)
}

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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"`
}

View File

@ -30,4 +30,6 @@ func Validate(configuration *schema.Configuration, validator *schema.StructValid
configuration.TOTP = &schema.TOTPConfiguration{}
ValidateTOTP(configuration.TOTP, validator)
}
ValidateSQLStorage(configuration.Storage, validator)
}

View File

@ -19,6 +19,11 @@ func newDefaultConfig() schema.Configuration {
Name: "authelia_session",
Secret: "secret",
}
config.Storage = &schema.StorageConfiguration{
Local: &schema.LocalStorageConfiguration{
Path: "abc",
},
}
return config
}

View File

@ -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'"))

View File

@ -0,0 +1,10 @@
version: "3"
services:
postgres:
image: postgres:12
environment:
- POSTGRES_PASSWORD=password
- POSTGRES_USER=admin
- POSTGRES_DB=authelia
networks:
- authelianet

View File

@ -89,7 +89,7 @@ regulation:
ban_time: 300
storage:
sql:
mysql:
host: mariadb-service
port: 3306
database: authelia

3
go.mod
View File

@ -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

6
go.sum
View File

@ -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=

View File

@ -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()

View File

@ -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 {

View File

@ -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 {

View File

@ -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()

View File

@ -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)

View File

@ -0,0 +1,7 @@
package storage
var preferencesTableName = "PreferencesTableName"
var identityVerificationTokensTableName = "IdentityVerificationTokens"
var totpSecretsTableName = "TOTPSecrets"
var u2fDeviceHandlesTableName = "U2FDeviceHandles"
var authenticationLogsTableName = "AuthenticationLogs"

View File

@ -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)
}

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
})
}

View File

@ -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())
}

View File

@ -105,7 +105,7 @@ regulation:
ban_time: 300
storage:
sql:
mysql:
host: 127.0.0.1
port: 3306
database: authelia

View File

@ -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)

View File

@ -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))