authelia/internal/authentication/file_user_provider_test.go
Amir Zarrinkafsh ff7f9a50ab
[FEATURE] Docker simplification and configuration generation (#1113)
* [FEATURE] Docker simplification and configuration generation
The Authelia binary now will attempt to generate configuration based on the latest template assuming that the config location specified on startup does not exist. If a file based backend is selected and the backend cannot be found similarly it will generate a `user_database.yml` based a template.

This will allow more seamless bootstrapping of an environment no matter the deployment method.

We have also squashed the Docker volume requirement down to just `/config` thus removing the requirement for `/var/lib/authelia` this is primarily in attempts to simplify the Docker deployment.

Users with the old volume mappings have two options:
1. Change their mappings to conform to `/config`
2. Change the container entrypoint from `authelia --config /config/configuration.yml` to their old mapping

* Adjust paths relative to `/etc/authelia` and simplify to single volume for compose
* Add generation for file backend based user database
* Refactor Docker volumes and paths to /config
* Refactor Docker WORKDIR to /app
* Fix integration tests
* Update BREAKING.md for v4.20.0
* Run go mod tidy
* Fix log_file_path in miscellaneous.md docs
* Generate config and userdb with 0600 permissions
* Fix log_file_path in config.template.yml
2020-06-17 16:25:35 +10:00

394 lines
13 KiB
Go

package authentication
import (
"io/ioutil"
"log"
"os"
"strings"
"testing"
"aletheia.icu/broccoli/fs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/authelia/authelia/internal/configuration/schema"
)
func WithDatabase(content []byte, f func(path string)) {
tmpfile, err := ioutil.TempFile("", "users_database.*.yaml")
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
if _, err := tmpfile.Write(content); err != nil {
tmpfile.Close()
log.Fatal(err)
}
f(tmpfile.Name())
if err := tmpfile.Close(); err != nil {
log.Fatal(err)
}
}
func TestShouldErrorNoUserDBInEmbeddedFS(t *testing.T) {
oldCfg := cfg
cfg = fs.New(false, []byte("\x1b~\x00\x80\x8d\x94n\xc2|\x84J\xf7\xbfn\xfd\xf7w;.\x8d m\xb2&\xd1Z\xec\xb2\x05\xb9\xc00\x8a\xf7(\x80^78\t(\f\f\xc3p\xc2\xc1\x06[a\xa2\xb3\xa4P\xe5\xa14\xfb\x19\xb2cp\xf6\x90-Z\xb2\x11\xe0l\xa1\x80\\\x95Vh\t\xc5\x06\x16\xfa\x8c\xc0\"!\xa5\xcf\xf7$\x9a\xb2\a`\xc6\x18\xc8~\xce8\r\x16Z\x9d\xc3\xe3\xff\x00"))
errors := checkDatabase("./nonexistent.yml")
cfg = oldCfg
require.Len(t, errors, 3)
require.EqualError(t, errors[0], "Unable to find database file: ./nonexistent.yml")
require.EqualError(t, errors[1], "Generating database file: ./nonexistent.yml")
require.EqualError(t, errors[2], "Unable to open users_database.template.yml: file does not exist")
}
func TestShouldErrorPermissionsOnLocalFS(t *testing.T) {
_ = os.Mkdir("/tmp/noperms/", 0000)
errors := checkDatabase("/tmp/noperms/users_database.yml")
require.Len(t, errors, 3)
require.EqualError(t, errors[0], "Unable to find database file: /tmp/noperms/users_database.yml")
require.EqualError(t, errors[1], "Generating database file: /tmp/noperms/users_database.yml")
require.EqualError(t, errors[2], "Unable to generate /tmp/noperms/users_database.yml: open /tmp/noperms/users_database.yml: permission denied")
}
func TestShouldErrorAndGenerateUserDB(t *testing.T) {
errors := checkDatabase("./nonexistent.yml")
_ = os.Remove("./nonexistent.yml")
require.Len(t, errors, 3)
require.EqualError(t, errors[0], "Unable to find database file: ./nonexistent.yml")
require.EqualError(t, errors[1], "Generating database file: ./nonexistent.yml")
require.EqualError(t, errors[2], "Generated database at: ./nonexistent.yml")
}
func TestShouldCheckUserArgon2idPasswordIsCorrect(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("john", "password")
assert.NoError(t, err)
assert.True(t, ok)
})
}
func TestShouldCheckUserSHA512PasswordIsCorrect(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("harry", "password")
assert.NoError(t, err)
assert.True(t, ok)
})
}
func TestShouldCheckUserPasswordIsWrong(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("john", "wrong_password")
assert.NoError(t, err)
assert.False(t, ok)
})
}
func TestShouldCheckUserPasswordIsWrongForEnumerationCompare(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("enumeration", "wrong_password")
assert.NoError(t, err)
assert.False(t, ok)
})
}
func TestShouldCheckUserPasswordOfUserThatDoesNotExist(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("fake", "password")
assert.Error(t, err)
assert.Equal(t, false, ok)
assert.EqualError(t, err, "user not found")
})
}
func TestShouldRetrieveUserDetails(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
details, err := provider.GetDetails("john")
assert.NoError(t, err)
assert.Equal(t, details.Username, "john")
assert.Equal(t, details.Emails, []string{"john.doe@authelia.com"})
assert.Equal(t, details.Groups, []string{"admins", "dev"})
})
}
func TestShouldUpdatePassword(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
err := provider.UpdatePassword("harry", "newpassword")
assert.NoError(t, err)
// Reset the provider to force a read from disk.
provider = NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("harry", "newpassword")
assert.NoError(t, err)
assert.True(t, ok)
})
}
// Checks both that the hashing algo changes and that it removes {CRYPT} from the start.
func TestShouldUpdatePasswordHashingAlgorithmToArgon2id(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
assert.True(t, strings.HasPrefix(provider.database.Users["harry"].HashedPassword, "$6$"))
err := provider.UpdatePassword("harry", "newpassword")
assert.NoError(t, err)
// Reset the provider to force a read from disk.
provider = NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("harry", "newpassword")
assert.NoError(t, err)
assert.True(t, ok)
assert.True(t, strings.HasPrefix(provider.database.Users["harry"].HashedPassword, "$argon2id$"))
})
}
func TestShouldUpdatePasswordHashingAlgorithmToSHA512(t *testing.T) {
WithDatabase(UserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
config.Password.Algorithm = "sha512"
config.Password.Iterations = 50000
provider := NewFileUserProvider(&config)
assert.True(t, strings.HasPrefix(provider.database.Users["john"].HashedPassword, "$argon2id$"))
err := provider.UpdatePassword("john", "newpassword")
assert.NoError(t, err)
// Reset the provider to force a read from disk.
provider = NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("john", "newpassword")
assert.NoError(t, err)
assert.True(t, ok)
assert.True(t, strings.HasPrefix(provider.database.Users["john"].HashedPassword, "$6$"))
})
}
func TestShouldRaiseWhenLoadingMalformedDatabaseForFirstTime(t *testing.T) {
WithDatabase(MalformedUserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithError(t, "Unable to parse database: yaml: line 4: mapping values are not allowed in this context", func() {
NewFileUserProvider(&config)
})
})
}
func TestShouldRaiseWhenLoadingDatabaseWithBadSchemaForFirstTime(t *testing.T) {
WithDatabase(BadSchemaUserDatabaseContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithError(t, "Invalid schema of database: Users: non zero value required", func() {
NewFileUserProvider(&config)
})
})
}
func TestShouldRaiseWhenLoadingDatabaseWithBadSHA512HashesForTheFirstTime(t *testing.T) {
WithDatabase(BadSHA512HashContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithError(t, "Unable to parse hash of user john: Hash key is not the last parameter, the hash is likely malformed ($6$rounds00000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/)", func() {
NewFileUserProvider(&config)
})
})
}
func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSettingsForTheFirstTime(t *testing.T) {
WithDatabase(BadArgon2idHashSettingsContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithError(t, "Unable to parse hash of user john: Hash key is not the last parameter, the hash is likely malformed ($argon2id$v=19$m65536,t3,p2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM)", func() {
NewFileUserProvider(&config)
})
})
}
func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashKeyForTheFirstTime(t *testing.T) {
WithDatabase(BadArgon2idHashKeyContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithError(t, "Unable to parse hash of user john: Hash key contains invalid base64 characters", func() {
NewFileUserProvider(&config)
})
})
}
func TestShouldRaiseWhenLoadingDatabaseWithBadArgon2idHashSaltForTheFirstTime(t *testing.T) {
WithDatabase(BadArgon2idHashSaltContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
assert.PanicsWithError(t, "Unable to parse hash of user john: Salt contains invalid base64 characters", func() {
NewFileUserProvider(&config)
})
})
}
func TestShouldSupportHashPasswordWithoutCRYPT(t *testing.T) {
WithDatabase(UserDatabaseWithoutCryptContent, func(path string) {
config := DefaultFileAuthenticationBackendConfiguration
config.Path = path
provider := NewFileUserProvider(&config)
ok, err := provider.CheckUserPassword("john", "password")
assert.NoError(t, err)
assert.True(t, ok)
})
}
var (
DefaultFileAuthenticationBackendConfiguration = schema.FileAuthenticationBackendConfiguration{
Path: "",
Password: &schema.PasswordConfiguration{
Iterations: schema.DefaultCIPasswordConfiguration.Iterations,
KeyLength: schema.DefaultCIPasswordConfiguration.KeyLength,
SaltLength: schema.DefaultCIPasswordConfiguration.SaltLength,
Algorithm: schema.DefaultCIPasswordConfiguration.Algorithm,
Memory: schema.DefaultCIPasswordConfiguration.Memory,
Parallelism: schema.DefaultCIPasswordConfiguration.Parallelism,
},
}
)
var UserDatabaseContent = []byte(`
users:
john:
password: "{CRYPT}$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
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
enumeration:
password: "$argon2id$v=19$m=131072,p=8$BpLnfgDsc2WD8F2q$O126GHPeZ5fwj7OLSs7PndXsTbje76R+QW9/EGfhkJg"
email: james.dean@authelia.com
`)
var MalformedUserDatabaseContent = []byte(`
users
john
email: john.doe@authelia.com
groups:
- admins
- dev
`)
// The YAML is valid but the root key is user instead of users.
var BadSchemaUserDatabaseContent = []byte(`
user:
john:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: john.doe@authelia.com
groups:
- admins
- dev
`)
var UserDatabaseWithoutCryptContent = []byte(`
users:
john:
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: john.doe@authelia.com
groups:
- admins
- dev
james:
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: james.dean@authelia.com
`)
var BadSHA512HashContent = []byte(`
users:
john:
password: "$6$rounds00000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: john.doe@authelia.com
groups:
- admins
- dev
james:
password: "$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: james.dean@authelia.com
`)
var BadArgon2idHashSettingsContent = []byte(`
users:
john:
password: "$argon2id$v=19$m65536,t3,p2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: john.doe@authelia.com
groups:
- admins
- dev
james:
password: "$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: james.dean@authelia.com
`)
var BadArgon2idHashKeyContent = []byte(`
users:
john:
password: "$argon2id$v=19$m=65536,t=3,p=2$BpLnfgDsc2WD8F2q$^^vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: john.doe@authelia.com
groups:
- admins
- dev
`)
var BadArgon2idHashSaltContent = []byte(`
users:
john:
password: "$argon2id$v=19$m=65536,t=3,p=2$^^LnfgDsc2WD8F2q$o/vzA4myCqZZ36bUGsDY//8mKUYNZZaR0t4MFFSs+iM"
email: john.doe@authelia.com
groups:
- admins
- dev
`)