2022-10-17 10:51:59 +00:00
package authentication
import (
"fmt"
"os"
2022-10-18 00:57:08 +00:00
"strings"
2022-10-17 10:51:59 +00:00
"sync"
"github.com/asaskevich/govalidator"
"github.com/go-crypt/crypt"
2022-12-04 22:37:08 +00:00
"github.com/go-crypt/crypt/algorithm"
2022-10-18 00:57:08 +00:00
"gopkg.in/yaml.v3"
2023-01-26 02:23:47 +00:00
"github.com/authelia/authelia/v4/internal/configuration/schema"
2022-10-17 10:51:59 +00:00
)
2023-01-26 02:23:47 +00:00
type FileUserProviderDatabase interface {
2023-05-25 01:17:35 +00:00
Save ( ) ( err error )
Load ( ) ( err error )
2023-01-26 02:23:47 +00:00
GetUserDetails ( username string ) ( user FileUserDatabaseUserDetails , err error )
SetUserDetails ( username string , details * FileUserDatabaseUserDetails )
2023-05-25 01:17:35 +00:00
}
2023-01-26 02:23:47 +00:00
// NewFileUserDatabase creates a new FileUserDatabase.
func NewFileUserDatabase ( filePath string , searchEmail , searchCI bool ) ( database * FileUserDatabase ) {
return & FileUserDatabase {
2022-10-18 00:57:08 +00:00
RWMutex : & sync . RWMutex { } ,
Path : filePath ,
2023-01-26 02:23:47 +00:00
Users : map [ string ] FileUserDatabaseUserDetails { } ,
2022-10-18 00:57:08 +00:00
Emails : map [ string ] string { } ,
Aliases : map [ string ] string { } ,
SearchEmail : searchEmail ,
SearchCI : searchCI ,
2022-10-17 10:51:59 +00:00
}
}
2023-01-26 02:23:47 +00:00
// FileUserDatabase is a user details database that is concurrency safe database and can be reloaded.
type FileUserDatabase struct {
* sync . RWMutex ` json:"-" `
Users map [ string ] FileUserDatabaseUserDetails ` json:"users" jsonschema:"required,title=Users" jsonschema_description:"The dictionary of users" `
2022-10-17 10:51:59 +00:00
2023-01-26 02:23:47 +00:00
Path string ` json:"-" `
Emails map [ string ] string ` json:"-" `
Aliases map [ string ] string ` json:"-" `
2022-10-18 00:57:08 +00:00
2023-01-26 02:23:47 +00:00
SearchEmail bool ` json:"-" `
SearchCI bool ` json:"-" `
2022-10-17 10:51:59 +00:00
}
// Save the database to disk.
2023-01-26 02:23:47 +00:00
func ( m * FileUserDatabase ) Save ( ) ( err error ) {
2022-10-17 10:51:59 +00:00
m . RLock ( )
defer m . RUnlock ( )
if err = m . ToDatabaseModel ( ) . Write ( m . Path ) ; err != nil {
return err
}
return nil
}
// Load the database from disk.
2023-01-26 02:23:47 +00:00
func ( m * FileUserDatabase ) Load ( ) ( err error ) {
yml := & FileDatabaseModel { Users : map [ string ] FileDatabaseUserDetailsModel { } }
2022-10-17 10:51:59 +00:00
if err = yml . Read ( m . Path ) ; err != nil {
return fmt . Errorf ( "error reading the authentication database: %w" , err )
}
m . Lock ( )
defer m . Unlock ( )
if err = yml . ReadToFileUserDatabase ( m ) ; err != nil {
return fmt . Errorf ( "error decoding the authentication database: %w" , err )
}
2022-10-18 00:57:08 +00:00
return m . LoadAliases ( )
}
// LoadAliases performs the loading of alias information from the database.
2023-01-26 02:23:47 +00:00
func ( m * FileUserDatabase ) LoadAliases ( ) ( err error ) {
2022-10-18 00:57:08 +00:00
if m . SearchEmail || m . SearchCI {
for k , user := range m . Users {
if m . SearchEmail && user . Email != "" {
if err = m . loadAliasEmail ( k , user ) ; err != nil {
return err
}
}
if m . SearchCI {
if err = m . loadAlias ( k ) ; err != nil {
return err
}
}
}
}
return nil
}
2023-01-26 02:23:47 +00:00
func ( m * FileUserDatabase ) loadAlias ( k string ) ( err error ) {
2022-10-18 00:57:08 +00:00
u := strings . ToLower ( k )
if u != k {
return fmt . Errorf ( "error loading authentication database: username '%s' is not lowercase but this is required when case-insensitive search is enabled" , k )
}
for username , details := range m . Users {
if k == username {
continue
}
if strings . EqualFold ( u , details . Email ) {
return fmt . Errorf ( "error loading authentication database: username '%s' is configured as an email for user with username '%s' which isn't allowed when case-insensitive search is enabled" , u , username )
}
}
m . Aliases [ u ] = k
return nil
}
2023-01-26 02:23:47 +00:00
func ( m * FileUserDatabase ) loadAliasEmail ( k string , user FileUserDatabaseUserDetails ) ( err error ) {
2022-10-18 00:57:08 +00:00
e := strings . ToLower ( user . Email )
var duplicates [ ] string
for username , details := range m . Users {
if k == username {
continue
}
if strings . EqualFold ( e , details . Email ) {
duplicates = append ( duplicates , username )
}
}
if len ( duplicates ) != 0 {
duplicates = append ( duplicates , k )
return fmt . Errorf ( "error loading authentication database: email '%s' is configured for for more than one user (users are '%s') which isn't allowed when email search is enabled" , e , strings . Join ( duplicates , "', '" ) )
}
if _ , ok := m . Users [ e ] ; ok && k != e {
return fmt . Errorf ( "error loading authentication database: email '%s' is also a username which isn't allowed when email search is enabled" , e )
}
m . Emails [ e ] = k
2022-10-17 10:51:59 +00:00
return nil
}
2023-01-26 02:23:47 +00:00
// GetUserDetails get a FileUserDatabaseUserDetails given a username as a value type where the username must be the users actual
2022-10-17 10:51:59 +00:00
// username.
2023-01-26 02:23:47 +00:00
func ( m * FileUserDatabase ) GetUserDetails ( username string ) ( user FileUserDatabaseUserDetails , err error ) {
2022-10-17 10:51:59 +00:00
m . RLock ( )
defer m . RUnlock ( )
2022-10-18 00:57:08 +00:00
u := strings . ToLower ( username )
if m . SearchEmail {
if key , ok := m . Emails [ u ] ; ok {
return m . Users [ key ] , nil
}
}
if m . SearchCI {
if key , ok := m . Aliases [ u ] ; ok {
return m . Users [ key ] , nil
}
}
2022-10-17 10:51:59 +00:00
if details , ok := m . Users [ username ] ; ok {
return details , nil
}
return user , ErrUserNotFound
}
2023-01-26 02:23:47 +00:00
// SetUserDetails sets the FileUserDatabaseUserDetails for a given user.
func ( m * FileUserDatabase ) SetUserDetails ( username string , details * FileUserDatabaseUserDetails ) {
2022-10-17 10:51:59 +00:00
if details == nil {
return
}
m . Lock ( )
m . Users [ username ] = * details
m . Unlock ( )
}
2023-01-26 02:23:47 +00:00
// ToDatabaseModel converts the FileUserDatabase into the FileDatabaseModel for saving.
func ( m * FileUserDatabase ) ToDatabaseModel ( ) ( model * FileDatabaseModel ) {
model = & FileDatabaseModel {
Users : map [ string ] FileDatabaseUserDetailsModel { } ,
2022-10-17 10:51:59 +00:00
}
m . RLock ( )
for user , details := range m . Users {
model . Users [ user ] = details . ToUserDetailsModel ( )
}
m . RUnlock ( )
return model
}
2023-01-26 02:23:47 +00:00
// FileUserDatabaseUserDetails is the model of user details in the file database.
type FileUserDatabaseUserDetails struct {
Username string ` json:"-" `
Password * schema . PasswordDigest ` json:"password" jsonschema:"required,title=Password" jsonschema_description:"The hashed password for the user" `
DisplayName string ` json:"displayname" jsonschema:"required,title=Display Name" jsonschema_description:"The display name for the user" `
Email string ` json:"email" jsonschema:"title=Email" jsonschema_description:"The email for the user" `
Groups [ ] string ` json:"groups" jsonschema:"title=Groups" jsonschema_description:"The groups list for the user" `
Disabled bool ` json:"disabled" jsonschema:"default=false,title=Disabled" jsonschema_description:"The disabled status for the user" `
2022-10-17 10:51:59 +00:00
}
2023-01-26 02:23:47 +00:00
// ToUserDetails converts FileUserDatabaseUserDetails into a *UserDetails given a username.
func ( m FileUserDatabaseUserDetails ) ToUserDetails ( ) ( details * UserDetails ) {
2022-10-17 10:51:59 +00:00
return & UserDetails {
Username : m . Username ,
DisplayName : m . DisplayName ,
Emails : [ ] string { m . Email } ,
Groups : m . Groups ,
}
}
2023-01-26 02:23:47 +00:00
// ToUserDetailsModel converts FileUserDatabaseUserDetails into a FileDatabaseUserDetailsModel.
func ( m FileUserDatabaseUserDetails ) ToUserDetailsModel ( ) ( model FileDatabaseUserDetailsModel ) {
return FileDatabaseUserDetailsModel {
Password : m . Password . Encode ( ) ,
DisplayName : m . DisplayName ,
Email : m . Email ,
Groups : m . Groups ,
2022-10-17 10:51:59 +00:00
}
}
2023-01-26 02:23:47 +00:00
// FileDatabaseModel is the model of users file database.
type FileDatabaseModel struct {
Users map [ string ] FileDatabaseUserDetailsModel ` yaml:"users" json:"users" valid:"required" jsonschema:"required,title=Users" jsonschema_description:"The dictionary of users" `
2022-10-17 10:51:59 +00:00
}
2023-01-26 02:23:47 +00:00
// ReadToFileUserDatabase reads the FileDatabaseModel into a FileUserDatabase.
func ( m * FileDatabaseModel ) ReadToFileUserDatabase ( db * FileUserDatabase ) ( err error ) {
users := map [ string ] FileUserDatabaseUserDetails { }
2022-10-17 10:51:59 +00:00
2023-01-26 02:23:47 +00:00
var udm * FileUserDatabaseUserDetails
2022-10-17 10:51:59 +00:00
for user , details := range m . Users {
if udm , err = details . ToDatabaseUserDetailsModel ( user ) ; err != nil {
return fmt . Errorf ( "failed to parse hash for user '%s': %w" , user , err )
}
users [ user ] = * udm
}
db . Users = users
return nil
}
2023-01-26 02:23:47 +00:00
// Read a FileDatabaseModel from disk.
func ( m * FileDatabaseModel ) Read ( filePath string ) ( err error ) {
2022-10-17 10:51:59 +00:00
var (
content [ ] byte
ok bool
)
if content , err = os . ReadFile ( filePath ) ; err != nil {
return fmt . Errorf ( "failed to read the '%s' file: %w" , filePath , err )
}
2022-10-17 11:31:23 +00:00
if len ( content ) == 0 {
return ErrNoContent
}
2022-10-17 10:51:59 +00:00
if err = yaml . Unmarshal ( content , m ) ; err != nil {
return fmt . Errorf ( "could not parse the YAML database: %w" , err )
}
if ok , err = govalidator . ValidateStruct ( m ) ; err != nil {
return fmt . Errorf ( "could not validate the schema: %w" , err )
}
if ! ok {
return fmt . Errorf ( "the schema is invalid" )
}
return nil
}
2023-01-26 02:23:47 +00:00
// Write a FileDatabaseModel to disk.
func ( m * FileDatabaseModel ) Write ( fileName string ) ( err error ) {
2022-10-17 10:51:59 +00:00
var (
data [ ] byte
)
if data , err = yaml . Marshal ( m ) ; err != nil {
return err
}
return os . WriteFile ( fileName , data , fileAuthenticationMode )
}
2023-01-26 02:23:47 +00:00
// FileDatabaseUserDetailsModel is the model of user details in the file database.
type FileDatabaseUserDetailsModel struct {
Password string ` yaml:"password" valid:"required" `
DisplayName string ` yaml:"displayname" valid:"required" `
Email string ` yaml:"email" `
Groups [ ] string ` yaml:"groups" `
Disabled bool ` yaml:"disabled" `
2022-10-17 10:51:59 +00:00
}
2023-01-26 02:23:47 +00:00
// ToDatabaseUserDetailsModel converts a FileDatabaseUserDetailsModel into a *FileUserDatabaseUserDetails.
func ( m FileDatabaseUserDetailsModel ) ToDatabaseUserDetailsModel ( username string ) ( model * FileUserDatabaseUserDetails , err error ) {
2022-12-04 22:37:08 +00:00
var d algorithm . Digest
2022-10-17 10:51:59 +00:00
2023-01-26 02:23:47 +00:00
if d , err = crypt . Decode ( m . Password ) ; err != nil {
2022-10-17 10:51:59 +00:00
return nil , err
}
2023-01-26 02:23:47 +00:00
return & FileUserDatabaseUserDetails {
2022-10-17 10:51:59 +00:00
Username : username ,
2023-01-26 02:23:47 +00:00
Password : schema . NewPasswordDigest ( d ) ,
2022-10-18 00:57:08 +00:00
Disabled : m . Disabled ,
2022-10-17 10:51:59 +00:00
DisplayName : m . DisplayName ,
Email : m . Email ,
Groups : m . Groups ,
} , nil
}