permissionsql

package module
v0.0.0-...-9df91b9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 2, 2022 License: MIT Imports: 13 Imported by: 0

README

PermissionSQL GoDoc

Middleware for keeping track of users, login states and permissions.

Online API Documentation

godoc.org

Features and limitations

  • Uses secure cookies and stores user information in a MariaDB/MySQL database.
  • Suitable for running a local MariaDB/MySQL server, registering/confirming users and managing public/user/admin pages.
  • Also supports connecting to remote MariaDB/MySQL servers.
  • Supports registration and confirmation via generated confirmation codes.
  • Tries to keep things simple.
  • Only supports "public", "user" and "admin" permissions out of the box, but offers functionality for implementing more fine grained permissions, if so desired.
  • Supports Negroni, Martini, Gin and Macaron.
  • Should also work with other frameworks, since the standard http.HandlerFunc is used everywhere.
  • The default permissions can be cleared with the Clear() function.

Connecting

For connecting to a MariaDB/MySQL host that is running locally, the New function can be used. For connecting to a remote server, the NewWithDSN function can be used.

Requirements

  • MariaDB or MySQL
  • Go >= 1.9

Examples

Example for Gin
package main

import (
    "fmt"
    "log"
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
    "github.com/xyproto/permissionsql"
)

func main() {
    g := gin.New()

    // New permissionsql middleware
    perm, err := permissionsql.New()
    if err != nil {
        log.Fatalln(err)
    }

    // Blank slate, no default permissions
    //perm.Clear()

    // Set up a middleware handler for Gin, with a custom "permission denied" message.
    permissionHandler := func(c *gin.Context) {
        // Check if the user has the right admin/user rights
        if perm.Rejected(c.Writer, c.Request) {
            // Deny the request, don't call other middleware handlers
            c.AbortWithStatus(http.StatusForbidden)
            fmt.Fprint(c.Writer, "Permission denied!")
            return
        }
        // Call the next middleware handler
        c.Next()
    }

    // Logging middleware
    g.Use(gin.Logger())

    // Enable the permissionsql middleware, must come before recovery
    g.Use(permissionHandler)

    // Recovery middleware
    g.Use(gin.Recovery())

    // Get the userstate, used in the handlers below
    userstate := perm.UserState()

    g.GET("/", func(c *gin.Context) {
        msg := ""
        msg += fmt.Sprintf("Has user bob: %v\n", userstate.HasUser("bob"))
        msg += fmt.Sprintf("Logged in on server: %v\n", userstate.IsLoggedIn("bob"))
        msg += fmt.Sprintf("Is confirmed: %v\n", userstate.IsConfirmed("bob"))
        msg += fmt.Sprintf("Username stored in cookies (or blank): %v\n", userstate.Username(c.Request))
        msg += fmt.Sprintf("Current user is logged in, has a valid cookie and *user rights*: %v\n", userstate.UserRights(c.Request))
        msg += fmt.Sprintf("Current user is logged in, has a valid cookie and *admin rights*: %v\n", userstate.AdminRights(c.Request))
        msg += fmt.Sprintln("\nTry: /register, /confirm, /remove, /login, /logout, /makeadmin, /clear, /data and /admin")
        c.String(http.StatusOK, msg)
    })

    g.GET("/register", func(c *gin.Context) {
        userstate.AddUser("bob", "hunter1", "bob@zombo.com")
        c.String(http.StatusOK, fmt.Sprintf("User bob was created: %v\n", userstate.HasUser("bob")))
    })

    g.GET("/confirm", func(c *gin.Context) {
        userstate.MarkConfirmed("bob")
        c.String(http.StatusOK, fmt.Sprintf("User bob was confirmed: %v\n", userstate.IsConfirmed("bob")))
    })

    g.GET("/remove", func(c *gin.Context) {
        userstate.RemoveUser("bob")
        c.String(http.StatusOK, fmt.Sprintf("User bob was removed: %v\n", !userstate.HasUser("bob")))
    })

    g.GET("/login", func(c *gin.Context) {
        // Headers will be written, for storing a cookie
        userstate.Login(c.Writer, "bob")
        c.String(http.StatusOK, fmt.Sprintf("bob is now logged in: %v\n", userstate.IsLoggedIn("bob")))
    })

    g.GET("/logout", func(c *gin.Context) {
        userstate.Logout("bob")
        c.String(http.StatusOK, fmt.Sprintf("bob is now logged out: %v\n", !userstate.IsLoggedIn("bob")))
    })

    g.GET("/makeadmin", func(c *gin.Context) {
        userstate.SetAdminStatus("bob")
        c.String(http.StatusOK, fmt.Sprintf("bob is now administrator: %v\n", userstate.IsAdmin("bob")))
    })

    g.GET("/clear", func(c *gin.Context) {
        userstate.ClearCookie(c.Writer)
        c.String(http.StatusOK, "Clearing cookie")
    })

    g.GET("/data", func(c *gin.Context) {
        c.String(http.StatusOK, "user page that only logged in users must see!")
    })

    g.GET("/admin", func(c *gin.Context) {
        c.String(http.StatusOK, "super secret information that only logged in administrators must see!\n\n")
        if usernames, err := userstate.AllUsernames(); err == nil {
            c.String(http.StatusOK, "list of all users: "+strings.Join(usernames, ", "))
        }
    })

    // Serve
    g.Run(":3000")
}
Example for just net/http
package main

import (
    "fmt"
    "log"
    "net/http"
    "strings"
    "time"

    "github.com/xyproto/permissionsql"
    "github.com/xyproto/pinterface"
)

type permissionHandler struct {
    // perm is a Permissions structure that can be used to deny requests
    // and acquire the UserState. By using `pinterface.IPermissions` instead
    // of `*permissions.Permissions`, the code is compatible with not only
    // `permissions2`, but also other modules that uses other database
    // backends, like `permissionbolt` which uses Bolt.
    perm pinterface.IPermissions

    // The HTTP multiplexer
    mux *http.ServeMux
}

// Implement the ServeHTTP method to make a permissionHandler a http.Handler
func (ph *permissionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    // Check if the user has the right admin/user rights
    if ph.perm.Rejected(w, req) {
        // Let the user know, by calling the custom "permission denied" function
        ph.perm.DenyFunction()(w, req)
        // Reject the request
        return
    }
    // Serve the requested page if permissions were granted
    ph.mux.ServeHTTP(w, req)
}

func main() {
    mux := http.NewServeMux()

    // New permissions middleware
    perm, err := permissionsql.New()
    if err != nil {
        log.Fatalln(err)
    }

    // Blank slate, no default permissions
    //perm.Clear()

    // Get the userstate, used in the handlers below
    userstate := perm.UserState()

    mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintf(w, "Has user bob: %v\n", userstate.HasUser("bob"))
        fmt.Fprintf(w, "Logged in on server: %v\n", userstate.IsLoggedIn("bob"))
        fmt.Fprintf(w, "Is confirmed: %v\n", userstate.IsConfirmed("bob"))
        fmt.Fprintf(w, "Username stored in cookies (or blank): %v\n", userstate.Username(req))
        fmt.Fprintf(w, "Current user is logged in, has a valid cookie and *user rights*: %v\n", userstate.UserRights(req))
        fmt.Fprintf(w, "Current user is logged in, has a valid cookie and *admin rights*: %v\n", userstate.AdminRights(req))
        fmt.Fprintf(w, "\nTry: /register, /confirm, /remove, /login, /logout, /makeadmin, /clear, /data and /admin")
    })

    mux.HandleFunc("/register", func(w http.ResponseWriter, req *http.Request) {
        userstate.AddUser("bob", "hunter1", "bob@zombo.com")
        fmt.Fprintf(w, "User bob was created: %v\n", userstate.HasUser("bob"))
    })

    mux.HandleFunc("/confirm", func(w http.ResponseWriter, req *http.Request) {
        userstate.MarkConfirmed("bob")
        fmt.Fprintf(w, "User bob was confirmed: %v\n", userstate.IsConfirmed("bob"))
    })

    mux.HandleFunc("/remove", func(w http.ResponseWriter, req *http.Request) {
        userstate.RemoveUser("bob")
        fmt.Fprintf(w, "User bob was removed: %v\n", !userstate.HasUser("bob"))
    })

    mux.HandleFunc("/login", func(w http.ResponseWriter, req *http.Request) {
        userstate.Login(w, "bob")
        fmt.Fprintf(w, "bob is now logged in: %v\n", userstate.IsLoggedIn("bob"))
    })

    mux.HandleFunc("/logout", func(w http.ResponseWriter, req *http.Request) {
        userstate.Logout("bob")
        fmt.Fprintf(w, "bob is now logged out: %v\n", !userstate.IsLoggedIn("bob"))
    })

    mux.HandleFunc("/makeadmin", func(w http.ResponseWriter, req *http.Request) {
        userstate.SetAdminStatus("bob")
        fmt.Fprintf(w, "bob is now administrator: %v\n", userstate.IsAdmin("bob"))
    })

    mux.HandleFunc("/clear", func(w http.ResponseWriter, req *http.Request) {
        userstate.ClearCookie(w)
        fmt.Fprintf(w, "Clearing cookie")
    })

    mux.HandleFunc("/data", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintf(w, "user page that only logged in users must see!")
    })

    mux.HandleFunc("/admin", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintf(w, "super secret information that only logged in administrators must see!\n\n")
        if usernames, err := userstate.AllUsernames(); err == nil {
            fmt.Fprintf(w, "list of all users: "+strings.Join(usernames, ", "))
        }
    })

    // Custom handler for when permissions are denied
    perm.SetDenyFunction(func(w http.ResponseWriter, req *http.Request) {
        http.Error(w, "Permission denied!", http.StatusForbidden)
    })

    // Configure the HTTP server and permissionHandler struct
    s := &http.Server{
        Addr:           ":3000",
        Handler:        &permissionHandler{perm, mux},
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }

    log.Println("Listening for requests on port 3000")

    // Start listening
    log.Fatal(s.ListenAndServe())
}

Default permissions

  • Visiting the /admin path prefix requires the user to be logged in with admin rights, by default.
  • These path prefixes requires the user to be logged in, by default: /repo and /data
  • These path prefixes are public by default: /, /login, /register, /style, /img, /js, /favicon.ico, /robots.txt and /sitemap_index.xml

The default permissions can be cleared with the Clear() function.

Coding style

  • The code shall always be formatted with go fmt.

Password hashing

  • bcrypt is used by default for hashing passwords. sha256 is also supported.
  • By default, all new password will be hashed with bcrypt.
  • For backwards compatibility, old password hashes with the length of a sha256 hash will be checked with sha256. To disable this behavior, and only ever use bcrypt, add this line: userstate.SetPasswordAlgo("bcrypt")

Setting and getting properties for users

  • Setting a property:
username := "bob"
propertyName := "clever"
propertyValue := "yes"

userstate.Users().Set(username, propertyName, propertyValue)
  • Getting a property:
username := "bob"
propertyName := "clever"
propertyValue, err := userstate.Users().Get(username, propertyName)
if err != nil {
    log.Print(err)
    return err
}
fmt.Printf("%s is %s: %s\n", username, propertyName, propertyValue)

Passing userstate between functions, files and to other Go packages

Using the *pinterface.IUserState type (from the pinterface package) makes it possible to pass UserState structs between functions, also in other packages. By using this interface, it is possible to seamlessly change the database backend from, for instance, PostgreSQL (pstore) to BoltDB (permissionbolt) or Redis (permissions2).

pstore, permissionsql, permissionbolt and permissions2 are interchangeable.

General information

Documentation

Overview

Package permissionsql provides a way to keeping track of users, login states and permissions.

Index

Constants

View Source
const (
	// Version number. Stable API within major version numbers.
	Version = 2.1
)

Variables

View Source
var (
	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")
)

Functions

func PermissionDenied

func PermissionDenied(w http.ResponseWriter, req *http.Request)

The default "permission denied" http handler.

func ValidUsernamePassword

func ValidUsernamePassword(username, password string) error

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.

Types

type Permissions

type Permissions struct {
	// contains filtered or unexported fields
}

The structure that keeps track of the permissions for various path prefixes

func New

func New() (*Permissions, error)

Initialize a Permissions struct with all the default settings. This will also connect to the database host at port 3306.

func NewPermissions

func NewPermissions(state *UserState) *Permissions

Initialize a Permissions struct with the given UserState and a few default paths for admin/user/public path prefixes.

func NewWithConf

func NewWithConf(connectionString string) (*Permissions, error)

Initialize a Permissions struct with a database connection string

func NewWithDSN

func NewWithDSN(connectionString string, database_name string) (*Permissions, error)

Initialize a Permissions struct with a dsn

func (*Permissions) AddAdminPath

func (perm *Permissions) AddAdminPath(prefix string)

Add an url path prefix that is a page for the logged in administrators

func (*Permissions) AddPublicPath

func (perm *Permissions) AddPublicPath(prefix string)

Add an url path prefix that is a public page

func (*Permissions) AddUserPath

func (perm *Permissions) AddUserPath(prefix string)

Add an url path prefix that is a page for the logged in users

func (*Permissions) Clear

func (perm *Permissions) Clear()

Set everything to public

func (*Permissions) DenyFunction

func (perm *Permissions) DenyFunction() http.HandlerFunc

Get the current http.HandlerFunc for when permissions are denied

func (*Permissions) Rejected

func (perm *Permissions) Rejected(w http.ResponseWriter, req *http.Request) bool

Check if a given request should be rejected.

func (*Permissions) ServeHTTP

func (perm *Permissions) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.HandlerFunc)

Middleware handler (compatible with Negroni)

func (*Permissions) SetAdminPath

func (perm *Permissions) SetAdminPath(pathPrefixes []string)

Set all url path prefixes that are for the logged in administrator pages

func (*Permissions) SetDenyFunction

func (perm *Permissions) SetDenyFunction(f http.HandlerFunc)

Specify the http.HandlerFunc for when the permissions are denied

func (*Permissions) SetPublicPath

func (perm *Permissions) SetPublicPath(pathPrefixes []string)

Set all url path prefixes that are for the public pages

func (*Permissions) SetUserPath

func (perm *Permissions) SetUserPath(pathPrefixes []string)

Set all url path prefixes that are for the logged in user pages

func (*Permissions) UserState

func (perm *Permissions) UserState() pinterface.IUserState

Retrieve the UserState struct

type UserState

type UserState struct {
	// contains filtered or unexported fields
}

func NewUserState

func NewUserState(connectionString string, randomseed bool) (*UserState, error)

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 NewUserStateSimple

func NewUserStateSimple() (*UserState, error)

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 NewUserStateWithDSN

func NewUserStateWithDSN(connectionString string, database_name string, randomseed bool) (*UserState, error)

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 (*UserState) AddUnconfirmed

func (state *UserState) AddUnconfirmed(username, confirmationCode string)

Add a user that is registered but not confirmed.

func (*UserState) AddUser

func (state *UserState) AddUser(username, password, email string)

Creates a user and hashes the password, does not check for rights. The given data must be valid.

func (*UserState) AdminRights

func (state *UserState) AdminRights(req *http.Request) bool

Check if the current user is logged in and has administrator rights.

func (*UserState) AllUnconfirmedUsernames

func (state *UserState) AllUnconfirmedUsernames() ([]string, error)

Get all registered users that are not yet confirmed.

func (*UserState) AllUsernames

func (state *UserState) AllUsernames() ([]string, error)

Get a list of all usernames.

func (*UserState) AlreadyHasConfirmationCode

func (state *UserState) AlreadyHasConfirmationCode(confirmationCode string) bool

Goes through all the confirmationCodes of all the unconfirmed users and checks if this confirmationCode already is in use.

func (*UserState) BooleanField

func (state *UserState) BooleanField(username, fieldname string) bool

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 (*UserState) ClearCookie

func (state *UserState) ClearCookie(w http.ResponseWriter)

Try to clear the user cookie by setting it to expired. Some browsers *may* be configured to keep cookies even after this.

func (*UserState) Close

func (state *UserState) Close()

Close the connection to the database host

func (*UserState) Confirm

func (state *UserState) Confirm(username string)

Remove the username from the list of unconfirmed users and mark the user as confirmed.

func (*UserState) ConfirmUserByConfirmationCode

func (state *UserState) ConfirmUserByConfirmationCode(confirmationcode string) error

Take a confirmation code and mark the corresponding unconfirmed user as confirmed.

func (*UserState) ConfirmationCode

func (state *UserState) ConfirmationCode(username string) (string, error)

Get the confirmation code for a specific user.

func (*UserState) CookieSecret

func (state *UserState) CookieSecret() string

CookieSecret returns the current cookie secret

func (*UserState) CookieTimeout

func (state *UserState) CookieTimeout(username string) int64

Get how long a login cookie should last, in seconds.

func (*UserState) CorrectPassword

func (state *UserState) CorrectPassword(username, password string) bool

Check if a password is correct. username is needed because it is part of the hash.

func (*UserState) Creator

func (state *UserState) Creator() pinterface.ICreator

Return a struct for creating datastructures

func (*UserState) Email

func (state *UserState) Email(username string) (string, error)

Get the email for the given username.

func (*UserState) FindUserByConfirmationCode

func (state *UserState) FindUserByConfirmationCode(confirmationcode string) (string, error)

Given a unique confirmation code, find the corresponding username.

func (*UserState) GenerateUniqueConfirmationCode

func (state *UserState) GenerateUniqueConfirmationCode() (string, error)

Generate a unique confirmation code that can be used for confirming users.

func (*UserState) HasUser

func (state *UserState) HasUser(username string) bool

Check if the given username exists.

func (*UserState) HashPassword

func (state *UserState) HashPassword(username, password string) string

Hash the password (takes a username as well, it can be used for salting).

func (*UserState) Host

func (state *UserState) Host() pinterface.IHost

Get the database host

func (*UserState) IsAdmin

func (state *UserState) IsAdmin(username string) bool

Check if the given username is an administrator.

func (*UserState) IsConfirmed

func (state *UserState) IsConfirmed(username string) bool

Check if the given username is confirmed.

func (*UserState) IsLoggedIn

func (state *UserState) IsLoggedIn(username string) bool

Checks if the given username is logged in.

func (*UserState) Login

func (state *UserState) Login(w http.ResponseWriter, username string) error

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 (*UserState) Logout

func (state *UserState) Logout(username string)

Convenience function for logging a user out.

func (*UserState) MarkConfirmed

func (state *UserState) MarkConfirmed(username string)

Mark a user as confirmed.

func (*UserState) PasswordAlgo

func (state *UserState) PasswordAlgo() string

PasswordAlgo returns the current password hashing algorithm.

func (*UserState) PasswordHash

func (state *UserState) PasswordHash(username string) (string, error)

Get the password hash for the given username.

func (*UserState) RemoveAdminStatus

func (state *UserState) RemoveAdminStatus(username string)

Mark user as a regular user.

func (*UserState) RemoveUnconfirmed

func (state *UserState) RemoveUnconfirmed(username string)

Remove a user that is registered but not confirmed.

func (*UserState) RemoveUser

func (state *UserState) RemoveUser(username string)

Remove user and login status.

func (*UserState) SetAdminStatus

func (state *UserState) SetAdminStatus(username string)

Mark user as an administrator.

func (*UserState) SetBooleanField

func (state *UserState) SetBooleanField(username, fieldname string, val bool)

Store a boolean value for the given username and custom fieldname.

func (*UserState) SetCookieSecret

func (state *UserState) SetCookieSecret(cookieSecret string)

SetCookieSecret sets the current cookie secret

func (*UserState) SetCookieTimeout

func (state *UserState) SetCookieTimeout(cookieTime int64)

Set how long a login cookie should last, in seconds.

func (*UserState) SetLoggedIn

func (state *UserState) SetLoggedIn(username string)

Mark the user as logged in. Use the Login function instead, unless cookies are not involved.

func (*UserState) SetLoggedOut

func (state *UserState) SetLoggedOut(username string)

Mark the user as logged out.

func (*UserState) SetMinimumConfirmationCodeLength

func (state *UserState) SetMinimumConfirmationCodeLength(length int)

Set the minimum length of the user confirmation code. The default is 20.

func (*UserState) SetPassword

func (state *UserState) SetPassword(username, password string)

SetPassword sets/changes the password for a user. Does not take a password hash, will hash the password string.

func (*UserState) SetPasswordAlgo

func (state *UserState) SetPasswordAlgo(algorithm string) error

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 (*UserState) SetUsernameCookie

func (state *UserState) SetUsernameCookie(w http.ResponseWriter, username string) error

Store the given username in a cookie in the browser, if possible. The user must exist.

func (*UserState) UserRights

func (state *UserState) UserRights(req *http.Request) bool

Check if the current user is logged in and has user rights.

func (*UserState) Username

func (state *UserState) Username(req *http.Request) string

Convenience function that will return a username (from the browser cookie) or an empty string.

func (*UserState) UsernameCookie

func (state *UserState) UsernameCookie(req *http.Request) (string, error)

Retrieve the username that is stored in a cookie in the browser, if available.

func (*UserState) Users

func (state *UserState) Users() pinterface.IHashMap

Get the users HashMap.

Directories

Path Synopsis
examples
gin

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL