# PermissionSQL [![Build Status](https://travis-ci.org/xyproto/permissionsql.svg?branch=master)](https://travis-ci.org/xyproto/permissionsql) [![GoDoc](https://godoc.org/github.com/xyproto/permissionsql?status.svg)](http://godoc.org/github.com/xyproto/permissionsql) Middleware for keeping track of users, login states and permissions. ## Online API Documentation [godoc.org](http://godoc.org/github.com/xyproto/permissionsql) ## 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](https://github.com/codegangsta/negroni), [Martini](https://github.com/go-martini/martini), [Gin](https://github.com/gin-gonic/gin) and [Macaron](https://github.com/Unknwon/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.7 ## Examples ### Example for [Gin](https://github.com/gin-gonic/gin) ~~~ go 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` ~~~ go 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](https://github.com/xyproto/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](https://github.com/xyproto/pstore)) to BoltDB ([permissionbolt](https://github.com/xyproto/permissionbolt)) or Redis ([permissions2](https://github.com/xyproto/permissions2)). [pstore](https://github.com/xyproto/pstore), [permissionsql](https://github.com/xyproto/permissionsql), [permissionbolt](https://github.com/xyproto/permissionbolt) and [permissions2](https://github.com/xyproto/permissions2) are interchangeable. ## General information * Version: 2.1.0 * License: MIT * Alexander F Rødseth <xyproto@archlinux.org>