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