package permissionsql import ( "errors" "fmt" "math/rand" "net/http" "time" "github.com/xyproto/cookie" // For cookies "github.com/xyproto/pinterface" // For interfaces db "github.com/xyproto/simplemaria" // MariaDB/MySQL database wrapper ) const ( // username:password@host:port/database defaultConnectionString = "localhost:3306/" ) var ( minConfirmationCodeLength = 20 // minimum length of the confirmation code ErrCookieGetUsername = errors.New("Could not retrieve the username from browser cookie") ErrCookieEmptyUsername = errors.New("Can't set cookie for empty username") ErrCookieUserMissing = errors.New("Can't store cookie for non-existsing user") ErrOutOfConfirmationCodes = errors.New("Too many generated confirmation codes are not unique") ErrAllUsersConfirmedAlready = errors.New("All existing users are already confirmed") ErrConfirmationCodeExpired = errors.New("The confirmation code is no longer valid") ErrMissingUserAtConfirm = errors.New("The user that is to be confirmed no longer exists") ErrInvalidCharacters = errors.New("Only letters, numbers and underscore are allowed in usernames") ErrUsernameAsPassword = errors.New("Username and password must be different, try another password") ) type UserState struct { users *db.HashMap // Hash map of users, with several different fields per user ("loggedin", "confirmed", "email" etc) usernames *db.Set // A list of all usernames, for easy enumeration unconfirmed *db.Set // A list of unconfirmed usernames, for easy enumeration host *db.Host // A database host cookieSecret string // Secret for storing secure cookies cookieTime int64 // How long a cookie should last, in seconds passwordAlgorithm string // The hashing algorithm to utilize default: "bcrypt+" allowed: ("sha256", "bcrypt", "bcrypt+") } // Create a new *UserState that can be used for managing users. // The random number generator will be seeded after generating the cookie secret. // A Host* for the local MariaDB/MySQL server will be created. func NewUserStateSimple() (*UserState, error) { // connection string | initialize random generator after generating the cookie secret return NewUserState(defaultConnectionString, true) } // Create a new *UserState that can be used for managing users. // connectionString may be on the form "username:password@host:port/database". // If randomseed is true, the random number generator will be seeded after generating the cookie secret (true is a good default value). func NewUserStateWithDSN(connectionString string, database_name string, randomseed bool) (*UserState, error) { // Test connection if err := db.TestConnectionHostWithDSN(connectionString); err != nil { return nil, err } host := db.NewHostWithDSN(connectionString, database_name) state := new(UserState) var err error state.users, err = db.NewHashMap(host, "users") if err != nil { return nil, err } state.usernames, err = db.NewSet(host, "usernames") if err != nil { return nil, err } state.unconfirmed, err = db.NewSet(host, "unconfirmed") if err != nil { return nil, err } state.host = host // For the secure cookies // This must happen before the random seeding, or // else people will have to log in again after every server restart state.cookieSecret = cookie.RandomCookieFriendlyString(30) // Seed the random number generator if randomseed { rand.Seed(time.Now().UnixNano()) } // Cookies lasts for 24 hours by default. Specified in seconds. state.cookieTime = 3600 * 24 // Default password hashing algorithm is "bcrypt+", which is the same as // "bcrypt", but with backwards compatibility for checking sha256 hashes. state.passwordAlgorithm = "bcrypt+" // "bcrypt+", "bcrypt" or "sha256" if err := host.Ping(); err != nil { defer host.Close() return nil, fmt.Errorf("Error when pinging %s: %s", connectionString, err) } return state, nil } // Create a new *UserState that can be used for managing users. // connectionString may be on the form "username:password@host:port/database". // If randomseed is true, the random number generator will be seeded after generating the cookie secret (true is a good default value). func NewUserState(connectionString string, randomseed bool) (*UserState, error) { // Test connection if err := db.TestConnectionHost(connectionString); err != nil { return nil, err } host := db.NewHost(connectionString) state := new(UserState) var err error state.users, err = db.NewHashMap(host, "users") if err != nil { return nil, err } state.usernames, err = db.NewSet(host, "usernames") if err != nil { return nil, err } state.unconfirmed, err = db.NewSet(host, "unconfirmed") if err != nil { return nil, err } state.host = host // For the secure cookies // This must happen before the random seeding, or // else people will have to log in again after every server restart state.cookieSecret = cookie.RandomCookieFriendlyString(30) // Seed the random number generator if randomseed { rand.Seed(time.Now().UnixNano()) } // Cookies lasts for 24 hours by default. Specified in seconds. state.cookieTime = 3600 * 24 // Default password hashing algorithm is "bcrypt+", which is the same as // "bcrypt", but with backwards compatibility for checking sha256 hashes. state.passwordAlgorithm = "bcrypt+" // "bcrypt+", "bcrypt" or "sha256" if err := host.Ping(); err != nil { defer host.Close() return nil, fmt.Errorf("Error when pinging %s: %s", connectionString, err) } return state, nil } // Get the database host func (state *UserState) Host() pinterface.IHost { return state.host } // Close the connection to the database host func (state *UserState) Close() { state.host.Close() } // Check if the current user is logged in and has user rights. func (state *UserState) UserRights(req *http.Request) bool { username, err := state.UsernameCookie(req) if err != nil { return false } return state.IsLoggedIn(username) } // Check if the given username exists. func (state *UserState) HasUser(username string) bool { val, err := state.usernames.Has(username) if err != nil { // This happened at concurrent connections before introducing the connection pool panic("ERROR: Lost connection to database?") } return val } // Return the boolean value for a given username and fieldname. // If the user or field is missing, false will be returned. // Useful for states where it makes sense that the returned value is not true // unless everything is in order. func (state *UserState) BooleanField(username, fieldname string) bool { hasUser := state.HasUser(username) if !hasUser { return false } value, err := state.users.Get(username, fieldname) if err != nil { return false } return value == "true" } // Store a boolean value for the given username and custom fieldname. func (state *UserState) SetBooleanField(username, fieldname string, val bool) { strval := "false" if val { strval = "true" } state.users.Set(username, fieldname, strval) } // Check if the given username is confirmed. func (state *UserState) IsConfirmed(username string) bool { return state.BooleanField(username, "confirmed") } // Checks if the given username is logged in. func (state *UserState) IsLoggedIn(username string) bool { if !state.HasUser(username) { return false } status, err := state.users.Get(username, "loggedin") if err != nil { // Returns "no" if the status can not be retrieved return false } return status == "true" } // Check if the current user is logged in and has administrator rights. func (state *UserState) AdminRights(req *http.Request) bool { username, err := state.UsernameCookie(req) if err != nil { return false } return state.IsLoggedIn(username) && state.IsAdmin(username) } // Check if the given username is an administrator. func (state *UserState) IsAdmin(username string) bool { if !state.HasUser(username) { return false } status, err := state.users.Get(username, "admin") if err != nil { return false } return status == "true" } // Retrieve the username that is stored in a cookie in the browser, if available. func (state *UserState) UsernameCookie(req *http.Request) (string, error) { username, ok := cookie.SecureCookie(req, "user", state.cookieSecret) if ok && (username != "") { return username, nil } return "", ErrCookieGetUsername } // Store the given username in a cookie in the browser, if possible. // The user must exist. func (state *UserState) SetUsernameCookie(w http.ResponseWriter, username string) error { if username == "" { return ErrCookieEmptyUsername } if !state.HasUser(username) { return ErrCookieUserMissing } // Create a cookie that lasts for a while ("timeout" seconds), // this is the equivivalent of a session for a given username. cookie.SetSecureCookiePath(w, "user", username, state.cookieTime, "/", state.cookieSecret) return nil } // Get a list of all usernames. func (state *UserState) AllUsernames() ([]string, error) { return state.usernames.GetAll() } // Get the email for the given username. func (state *UserState) Email(username string) (string, error) { return state.users.Get(username, "email") } // Get the password hash for the given username. func (state *UserState) PasswordHash(username string) (string, error) { return state.users.Get(username, "password") } // Get all registered users that are not yet confirmed. func (state *UserState) AllUnconfirmedUsernames() ([]string, error) { return state.unconfirmed.GetAll() } // Get the confirmation code for a specific user. func (state *UserState) ConfirmationCode(username string) (string, error) { return state.users.Get(username, "confirmationCode") } // Get the users HashMap. func (state *UserState) Users() pinterface.IHashMap { return state.users } // Add a user that is registered but not confirmed. func (state *UserState) AddUnconfirmed(username, confirmationCode string) { state.unconfirmed.Add(username) state.users.Set(username, "confirmationCode", confirmationCode) } // Remove a user that is registered but not confirmed. func (state *UserState) RemoveUnconfirmed(username string) { state.unconfirmed.Del(username) state.users.DelKey(username, "confirmationCode") } // Mark a user as confirmed. func (state *UserState) MarkConfirmed(username string) { state.users.Set(username, "confirmed", "true") } // Remove user and login status. func (state *UserState) RemoveUser(username string) { state.usernames.Del(username) // Remove additional data as well //state.users.DelKey(username, "loggedin") state.users.Del(username) } // Mark user as an administrator. func (state *UserState) SetAdminStatus(username string) { state.users.Set(username, "admin", "true") } // Mark user as a regular user. func (state *UserState) RemoveAdminStatus(username string) { state.users.Set(username, "admin", "false") } // Creates a user from the username and password hash, does not check for rights. func (state *UserState) addUserUnchecked(username, passwordHash, email string) { // Add the user state.usernames.Add(username) // Add password and email state.users.Set(username, "password", passwordHash) state.users.Set(username, "email", email) // Addditional fields additionalfields := []string{"loggedin", "confirmed", "admin"} for _, fieldname := range additionalfields { state.users.Set(username, fieldname, "false") } } // Creates a user and hashes the password, does not check for rights. // The given data must be valid. func (state *UserState) AddUser(username, password, email string) { passwordHash := state.HashPassword(username, password) state.addUserUnchecked(username, passwordHash, email) } // Mark the user as logged in. Use the Login function instead, unless cookies are not involved. func (state *UserState) SetLoggedIn(username string) { state.users.Set(username, "loggedin", "true") } // Mark the user as logged out. func (state *UserState) SetLoggedOut(username string) { state.users.Set(username, "loggedin", "false") } // Convenience function for logging a user in and storing the username in a cookie. // Returns an error if the cookie could not be set. func (state *UserState) Login(w http.ResponseWriter, username string) error { state.SetLoggedIn(username) return state.SetUsernameCookie(w, username) } // Try to clear the user cookie by setting it to expired. // Some browsers *may* be configured to keep cookies even after this. func (state *UserState) ClearCookie(w http.ResponseWriter) { cookie.ClearCookie(w, "user", "/") } // Convenience function for logging a user out. func (state *UserState) Logout(username string) { state.SetLoggedOut(username) } // Convenience function that will return a username (from the browser cookie) or an empty string. func (state *UserState) Username(req *http.Request) string { username, err := state.UsernameCookie(req) if err != nil { return "" } return username } // Get how long a login cookie should last, in seconds. func (state *UserState) CookieTimeout(username string) int64 { return state.cookieTime } // Set how long a login cookie should last, in seconds. func (state *UserState) SetCookieTimeout(cookieTime int64) { state.cookieTime = cookieTime } // CookieSecret returns the current cookie secret func (state *UserState) CookieSecret() string { return state.cookieSecret } // SetCookieSecret sets the current cookie secret func (state *UserState) SetCookieSecret(cookieSecret string) { state.cookieSecret = cookieSecret } // PasswordAlgo returns the current password hashing algorithm. func (state *UserState) PasswordAlgo() string { return state.passwordAlgorithm } // Set the password hashing algorithm that should be used. // The default is "bcrypt+". // Possible values are: // bcrypt -> Store and check passwords with the bcrypt hash. // sha256 -> Store and check passwords with the sha256 hash. // bcrypt+ -> Store passwords with bcrypt, but check with both // bcrypt and sha256, for backwards compatibility // with old passwords that has been stored as sha256. func (state *UserState) SetPasswordAlgo(algorithm string) error { switch algorithm { case "sha256", "bcrypt", "bcrypt+": state.passwordAlgorithm = algorithm default: return errors.New("Permissions: " + algorithm + " is an unsupported encryption algorithm") } return nil } // Hash the password (takes a username as well, it can be used for salting). func (state *UserState) HashPassword(username, password string) string { switch state.passwordAlgorithm { case "sha256": return string(hash_sha256(state.cookieSecret, username, password)) case "bcrypt", "bcrypt+": return string(hash_bcrypt(password)) } // Only valid password algorithms should be allowed to set return "" } // SetPassword sets/changes the password for a user. // Does not take a password hash, will hash the password string. func (state *UserState) SetPassword(username, password string) { state.users.Set(username, "password", state.HashPassword(username, password)) } // Return the stored hash, or an empty byte slice. func (state *UserState) stored_hash(username string) []byte { hashString, err := state.PasswordHash(username) if err != nil { return []byte{} } return []byte(hashString) } // Check if a password is correct. username is needed because it is part of the hash. func (state *UserState) CorrectPassword(username, password string) bool { if !state.HasUser(username) { return false } // Retrieve the stored password hash hash := state.stored_hash(username) if len(hash) == 0 { return false } // Check the password with the right password algorithm switch state.passwordAlgorithm { case "sha256": return correct_sha256(hash, state.cookieSecret, username, password) case "bcrypt": return correct_bcrypt(hash, password) case "bcrypt+": // for backwards compatibility with sha256 if is_sha256(hash) && correct_sha256(hash, state.cookieSecret, username, password) { return true } return correct_bcrypt(hash, password) } return false } // Goes through all the confirmationCodes of all the unconfirmed users // and checks if this confirmationCode already is in use. func (state *UserState) AlreadyHasConfirmationCode(confirmationCode string) bool { unconfirmedUsernames, err := state.AllUnconfirmedUsernames() if err != nil { return false } for _, aUsername := range unconfirmedUsernames { aConfirmationCode, err := state.ConfirmationCode(aUsername) if err != nil { // If the confirmation code can not be found, that's okay too return false } if confirmationCode == aConfirmationCode { // Found it return true } } return false } // Given a unique confirmation code, find the corresponding username. func (state *UserState) FindUserByConfirmationCode(confirmationcode string) (string, error) { unconfirmedUsernames, err := state.AllUnconfirmedUsernames() if err != nil { return "", ErrAllUsersConfirmedAlready } // Find the username by looking up the confirmationcode on unconfirmed users username := "" for _, aUsername := range unconfirmedUsernames { aConfirmationCode, err := state.ConfirmationCode(aUsername) if err != nil { // If the confirmation code can not be found, just skip this one continue } if confirmationcode == aConfirmationCode { // Found the right user username = aUsername break } } // Check that the user is there if username == "" { return username, ErrConfirmationCodeExpired } if !state.HasUser(username) { return username, ErrMissingUserAtConfirm } return username, nil } // Remove the username from the list of unconfirmed users and mark the user as confirmed. func (state *UserState) Confirm(username string) { // Remove from the list of unconfirmed usernames state.RemoveUnconfirmed(username) // Mark user as confirmed state.MarkConfirmed(username) } // Take a confirmation code and mark the corresponding unconfirmed user as confirmed. func (state *UserState) ConfirmUserByConfirmationCode(confirmationcode string) error { username, err := state.FindUserByConfirmationCode(confirmationcode) if err != nil { return err } state.Confirm(username) return nil } // Set the minimum length of the user confirmation code. The default is 20. func (state *UserState) SetMinimumConfirmationCodeLength(length int) { minConfirmationCodeLength = length } // Generate a unique confirmation code that can be used for confirming users. func (state *UserState) GenerateUniqueConfirmationCode() (string, error) { const maxConfirmationCodeLength = 100 // when are the generated confirmation codes unreasonably long length := minConfirmationCodeLength confirmationCode := cookie.RandomHumanFriendlyString(length) for state.AlreadyHasConfirmationCode(confirmationCode) { // Increase the length of the confirmationCode random string every time there is a collision length++ confirmationCode = cookie.RandomHumanFriendlyString(length) if length > maxConfirmationCodeLength { // This should never happen return confirmationCode, ErrOutOfConfirmationCodes } } return confirmationCode, nil } // Check that the given username and password are different. // Also check if the chosen username only contains letters, numbers and/or underscore. // Use the "CorrectPassword" function for checking if the password is correct. func ValidUsernamePassword(username, password string) error { const allowed_letters = "abcdefghijklmnopqrstuvwxyzæøåABCDEFGHIJKLMNOPQRSTUVWXYZÆØÅ_0123456789" NEXT: for _, letter := range username { for _, allowedLetter := range allowed_letters { if letter == allowedLetter { continue NEXT // check the next letter in the username } } return ErrInvalidCharacters } if username == password { return ErrUsernameAsPassword } return nil } // Return a struct for creating datastructures func (state *UserState) Creator() pinterface.ICreator { return db.NewCreator(state.host) }