├── .gitignore ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── README.md ├── auth_model.conf ├── authorization └── authorization.go ├── casbin-http-role-example ├── main.go ├── model └── model.go └── policy.csv /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | vendor 16 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/Knetic/govaluate" 6 | packages = ["."] 7 | revision = "d216395917cc49052c7c7094cf57f09657ca08a8" 8 | version = "v3.0.0" 9 | 10 | [[projects]] 11 | name = "github.com/alexedwards/scs" 12 | packages = ["engine/memstore","session"] 13 | revision = "0fdd3ef0cedf798548a2a904932bc0b4e13ee91b" 14 | version = "v0.1.1" 15 | 16 | [[projects]] 17 | name = "github.com/casbin/casbin" 18 | packages = [".","config","file-adapter","model","persist","rbac","util"] 19 | revision = "9566491e9fe1898000031bba3e2b354dd633904a" 20 | version = "v1.3.0" 21 | 22 | [[projects]] 23 | name = "github.com/patrickmn/go-cache" 24 | packages = ["."] 25 | revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0" 26 | version = "v2.1.0" 27 | 28 | [solve-meta] 29 | analyzer-name = "dep" 30 | analyzer-version = 1 31 | inputs-digest = "f43a2728630887923eac9e3666f80c2b0befda9a7bcc27b7ea74cc74ecf623bc" 32 | solver-name = "gps-cdcl" 33 | solver-version = 1 34 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "github.com/alexedwards/scs" 26 | version = "0.1.1" 27 | 28 | [[constraint]] 29 | name = "github.com/casbin/casbin" 30 | version = "1.3.0" 31 | 32 | [[constraint]] 33 | name = "github.com/go-chi/render" 34 | version = "1.0.0" 35 | 36 | [[constraint]] 37 | name = "github.com/pkg/errors" 38 | version = "0.8.0" 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 mario zupan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # casbin-http-role-exampe 2 | 3 | Simplistic Example of role-based HTTP Authorization with [casbin](https://github.com/casbin/casbin) using [scs](https://github.com/alexedwards/scs) for session handling. 4 | 5 | Run with 6 | 7 | ```bash 8 | dep ensure 9 | go run main.go 10 | ``` 11 | 12 | Which starts a server at `http://localhost:8080` with the following routes: 13 | 14 | * `POST /login` - accessible if not logged in 15 | * takes `name` as a form-data parameter - there is no password 16 | * Valid Users: 17 | * `Admin` ID: `1`, Role: `admin` 18 | * `Sabine` ID: `2`, Role: `member` 19 | * `Sepp` ID: `3`, Role: `member` 20 | * `POST /logout` - accessible if logged in 21 | * `GET /member/current` - accessible if logged in as a member 22 | * `GET /member/role` - accessible if logged in as a member 23 | * `GET /admin/stuff` - accessible if logged in as an admin 24 | -------------------------------------------------------------------------------- /auth_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [policy_effect] 8 | e = some(where (p.eft == allow)) 9 | 10 | [matchers] 11 | m = r.sub == p.sub && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*") 12 | 13 | -------------------------------------------------------------------------------- /authorization/authorization.go: -------------------------------------------------------------------------------- 1 | package authorization 2 | 3 | import ( 4 | "errors" 5 | "github.com/alexedwards/scs/session" 6 | "github.com/casbin/casbin" 7 | "github.com/zupzup/casbin-http-role-example/model" 8 | "log" 9 | "net/http" 10 | ) 11 | 12 | // Authorizer is a middleware for authorization 13 | func Authorizer(e *casbin.Enforcer, users model.Users) func(next http.Handler) http.Handler { 14 | return func(next http.Handler) http.Handler { 15 | fn := func(w http.ResponseWriter, r *http.Request) { 16 | role, err := session.GetString(r, "role") 17 | if err != nil { 18 | writeError(http.StatusInternalServerError, "ERROR", w, err) 19 | return 20 | } 21 | if role == "" { 22 | role = "anonymous" 23 | } 24 | // if it's a member, check if the user still exists 25 | if role == "member" { 26 | uid, err := session.GetInt(r, "userID") 27 | if err != nil { 28 | writeError(http.StatusInternalServerError, "ERROR", w, err) 29 | return 30 | } 31 | exists := users.Exists(uid) 32 | if !exists { 33 | writeError(http.StatusForbidden, "FORBIDDEN", w, errors.New("user does not exist")) 34 | return 35 | } 36 | } 37 | // casbin enforce 38 | res, err := e.EnforceSafe(role, r.URL.Path, r.Method) 39 | if err != nil { 40 | writeError(http.StatusInternalServerError, "ERROR", w, err) 41 | return 42 | } 43 | if res { 44 | next.ServeHTTP(w, r) 45 | } else { 46 | writeError(http.StatusForbidden, "FORBIDDEN", w, errors.New("unauthorized")) 47 | return 48 | } 49 | } 50 | 51 | return http.HandlerFunc(fn) 52 | } 53 | } 54 | 55 | func writeError(status int, message string, w http.ResponseWriter, err error) { 56 | log.Print("ERROR: ", err.Error()) 57 | w.WriteHeader(status) 58 | w.Write([]byte(message)) 59 | } 60 | -------------------------------------------------------------------------------- /casbin-http-role-example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zupzup/casbin-http-role-example/90e0d5af6869ad6a3ea9ee5c08eec18911b58714/casbin-http-role-example -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/alexedwards/scs/engine/memstore" 6 | "github.com/alexedwards/scs/session" 7 | "github.com/casbin/casbin" 8 | "github.com/zupzup/casbin-http-role-example/authorization" 9 | "github.com/zupzup/casbin-http-role-example/model" 10 | "log" 11 | "net/http" 12 | "time" 13 | ) 14 | 15 | func main() { 16 | // setup casbin auth rules 17 | authEnforcer, err := casbin.NewEnforcerSafe("./auth_model.conf", "./policy.csv") 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | // setup session store 23 | engine := memstore.New(30 * time.Minute) 24 | sessionManager := session.Manage(engine, session.IdleTimeout(30*time.Minute), session.Persist(true), session.Secure(true)) 25 | users := createUsers() 26 | 27 | // setup routes 28 | mux := http.NewServeMux() 29 | mux.HandleFunc("/login", loginHandler(users)) 30 | mux.HandleFunc("/logout", logoutHandler()) 31 | mux.HandleFunc("/member/current", currentMemberHandler()) 32 | mux.HandleFunc("/member/role", memberRoleHandler()) 33 | mux.HandleFunc("/admin/stuff", adminHandler()) 34 | 35 | log.Print("Server started on localhost:8080") 36 | log.Fatal(http.ListenAndServe(":8080", sessionManager(authorization.Authorizer(authEnforcer, users)(mux)))) 37 | 38 | } 39 | 40 | func createUsers() model.Users { 41 | users := model.Users{} 42 | users = append(users, model.User{ID: 1, Name: "Admin", Role: "admin"}) 43 | users = append(users, model.User{ID: 2, Name: "Sabine", Role: "member"}) 44 | users = append(users, model.User{ID: 3, Name: "Sepp", Role: "member"}) 45 | return users 46 | } 47 | 48 | func loginHandler(users model.Users) http.HandlerFunc { 49 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 50 | name := r.PostFormValue("name") 51 | user, err := users.FindByName(name) 52 | if err != nil { 53 | writeError(http.StatusBadRequest, "WRONG_CREDENTIALS", w, err) 54 | return 55 | } 56 | // setup ession 57 | if err := session.RegenerateToken(r); err != nil { 58 | writeError(http.StatusInternalServerError, "ERROR", w, err) 59 | return 60 | } 61 | session.PutInt(r, "userID", user.ID) 62 | session.PutString(r, "role", user.Role) 63 | writeSuccess("SUCCESS", w) 64 | }) 65 | } 66 | 67 | func logoutHandler() http.HandlerFunc { 68 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 69 | if err := session.Renew(r); err != nil { 70 | writeError(http.StatusInternalServerError, "ERROR", w, err) 71 | return 72 | } 73 | writeSuccess("SUCCESS", w) 74 | }) 75 | } 76 | 77 | func currentMemberHandler() http.HandlerFunc { 78 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 79 | uid, err := session.GetInt(r, "userID") 80 | if err != nil { 81 | writeError(http.StatusInternalServerError, "ERROR", w, err) 82 | return 83 | } 84 | writeSuccess(fmt.Sprintf("User with ID: %d", uid), w) 85 | }) 86 | } 87 | 88 | func memberRoleHandler() http.HandlerFunc { 89 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 90 | role, err := session.GetString(r, "role") 91 | if err != nil { 92 | writeError(http.StatusInternalServerError, "ERROR", w, err) 93 | return 94 | } 95 | writeSuccess(fmt.Sprintf("User with Role: %s", role), w) 96 | }) 97 | } 98 | 99 | func adminHandler() http.HandlerFunc { 100 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 101 | writeSuccess("I'm an Admin!", w) 102 | }) 103 | } 104 | 105 | func writeError(status int, message string, w http.ResponseWriter, err error) { 106 | log.Print("ERROR: ", err.Error()) 107 | w.WriteHeader(status) 108 | w.Write([]byte(message)) 109 | } 110 | 111 | func writeSuccess(message string, w http.ResponseWriter) { 112 | w.WriteHeader(http.StatusOK) 113 | w.Write([]byte(message)) 114 | } 115 | -------------------------------------------------------------------------------- /model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "errors" 4 | 5 | // User is a user 6 | type User struct { 7 | ID int 8 | Name string 9 | Role string 10 | } 11 | 12 | // Users is a list of users 13 | type Users []User 14 | 15 | // Exists checks if a user with the given id exists in the list 16 | func (u Users) Exists(id int) bool { 17 | exists := false 18 | for _, user := range u { 19 | if user.ID == id { 20 | return true 21 | } 22 | } 23 | return exists 24 | } 25 | 26 | // FindByName returns the user with the given name, or returns an error 27 | func (u Users) FindByName(name string) (User, error) { 28 | for _, user := range u { 29 | if user.Name == name { 30 | return user, nil 31 | } 32 | } 33 | return User{}, errors.New("USER_NOT_FOUND") 34 | } 35 | -------------------------------------------------------------------------------- /policy.csv: -------------------------------------------------------------------------------- 1 | p, admin, /*, * 2 | p, anonymous, /login, * 3 | p, member, /logout, * 4 | p, member, /member/*, * 5 | 6 | --------------------------------------------------------------------------------