Move from our custom yaml access setup to casbin
--- a/access/access.go Sun Sep 08 05:44:05 2019 -0500
+++ b/access/access.go Mon Sep 09 05:04:32 2019 -0500
@@ -1,94 +1,48 @@
- "github.com/go-yaml/yaml"
- log "github.com/sirupsen/logrus"
- _ "golang.org/x/crypto/ssh"
- // AccessFile is the base name of an access control file.
- AccessFile = "access.yml"
- // Public is a reserved hgkeeper name and is not valid in the patterns
- // or keys of an access file.
+ modelFilename = "model.conf" + policyFilename = "policy.csv" -var publicBytes = []byte(Public)
- groups map[string][]string
- permissions [numPerms][]string
-// Access represents a parsed access file.
- groups map[string][]string
- patterns map[string]Acl
- // index ssh key fingerprint to users
- fingerprintIndex map[string]string
+// Refresh will try to reload the casbin model and policies followed by SSH +// keys. If there is an error it's possible that the casbin model and polcies +// could have been updated but the ssh keys were not. +func Refresh(repoPath string) error { + defer accessLock.Unlock() -func New(adminRepo string) *Access {
- groups: map[string][]string{},
- patterns: map[string]Acl{},
- fingerprintIndex: map[string]string{},
-func (a *Access) Reload() error {
- path := filepath.Join(a.repoPath, AccessFile)
- reader, err := os.Open(path)
-// loadFile reads the config from r into the basic structure.
-func (a *Access) loadFile(r io.Reader) error {
- if err := yaml.NewDecoder(r).Decode(&data); err != nil {
+ if err := refreshEnforcer(repoPath); err != nil {
- a.patterns = data.Patterns
+ if err := refreshKeys(repoPath); err != nil { -// load will load the access from file and reindex everything.
-func (a *Access) load(r io.Reader) error {
+func check(user, repo, action string) bool { + return enforcer.Enforce(user, repo, action) +func CanRead(user, repo string) bool { + return check(user, repo, "read") - if err := a.loadFile(r); err != nil {
+func CanWrite(user, repo string) bool { + return check(user, repo, "write") - RefreshKeys(a.repoPath)
- log.Infof("keys: %#v", keys)
+func CanInit(user, repo string) bool { + return check(user, repo, "init") --- a/access/acl.go Sun Sep 08 05:44:05 2019 -0500
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
- Init []string `yaml:"init"`
- Read []string `yaml:"read"`
- Write []string `yaml:"write"`
-func (a *Acl) Users() []string {
- users := map[string]bool{}
- for _, name := range a.Init {
- for _, name := range a.Read {
- for _, name := range a.Write {
- slice := make([]string, len(users))
- for user, _ := range users {
-func (a *Acl) CanRead(username string) bool {
- if a.CanWrite(username) || a.CanInit(username) {
- for _, user := range a.Read {
-func (a *Acl) CanWrite(username string) bool {
- if a.CanInit(username) {
- for _, user := range a.Write {
-func (a *Acl) CanInit(username string) bool {
- for _, user := range a.Init {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/access/enforcer.go Mon Sep 09 05:04:32 2019 -0500
@@ -0,0 +1,51 @@
+ "github.com/casbin/casbin" + enforcer *casbin.Enforcer + enforcerLock sync.Mutex +func accessMatch(key1, key2 string) bool { + return key1 == "init" || key1 == "write" || key1 == "read" + return key1 == "write" || key1 == "read" +func accessMatchFunc(args ...interface{}) (interface{}, error) { + key1 := args[0].(string) + key2 := args[1].(string) + return (bool)(accessMatch(key1, key2)), nil +func refreshEnforcer(repoPath string) error { + defer enforcerLock.Unlock() + modelFile := filepath.Join(repoPath, modelFilename) + policyFile := filepath.Join(repoPath, policyFilename) + e := casbin.NewEnforcer(modelFile, policyFile) + e.AddFunction("access", accessMatchFunc) --- a/access/permissions.go Sun Sep 08 05:44:05 2019 -0500
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-// A Perm represents one or more access permissions: reading, writing, etc.
-// All permisions constants
-// String returns a textual representation of the perm.
-func (p Perm) String() string {
-func (p *Perm) can(i Perm) bool { return *p&i != 0 }
-func (p *Perm) clear(i Perm) { *p &^= i }
-func (p *Perm) set(i Perm) { *p |= i }
--- a/access/types.go Sun Sep 08 05:44:05 2019 -0500
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
- Global Acl `yaml:"global"`
- Groups map[string][]string `yaml:"groups"`
- Patterns map[string]Acl `yaml:"patterns"`
--- a/access/users.go Sun Sep 08 05:44:05 2019 -0500
+++ b/access/users.go Mon Sep 09 05:04:32 2019 -0500
@@ -19,7 +19,7 @@
-func RefreshKeys(repoPath string) error {
+func refreshKeys(repoPath string) error { --- a/commands/commands.go Sun Sep 08 05:44:05 2019 -0500
+++ b/commands/commands.go Mon Sep 09 05:04:32 2019 -0500
@@ -6,8 +6,6 @@
"github.com/alecthomas/kong"
"github.com/kballard/go-shellquote"
"golang.org/x/crypto/ssh"
- "bitbucket.org/rw_grim/hgkeeper/access"
@@ -24,7 +22,6 @@
Run(conn ssh.Channel, serverConn *ssh.ServerConn, username string, req *ssh.Request) error
- CheckAccess(access *access.Access, username string) bool
@@ -46,7 +43,7 @@
return values, ctx.Command(), nil
-func Find(cmd, reposPath string, access *access.Access) (Command, error) {
+func Find(cmd, reposPath string) (Command, error) { values, pcmd, err := parse(cmd)
@@ -54,9 +51,9 @@
- return NewServe(reposPath, values.Hg.Repo, access), nil
+ return NewServe(reposPath, values.Hg.Repo), nil - return NewInit(reposPath, values.Hg.Init.Repo, access), nil
+ return NewInit(reposPath, values.Hg.Init.Repo), nil return nil, fmt.Errorf("unknown command %s", cmd)
--- a/commands/init.go Sun Sep 08 05:44:05 2019 -0500
+++ b/commands/init.go Mon Sep 09 05:04:32 2019 -0500
@@ -1,8 +1,10 @@
"golang.org/x/crypto/ssh"
"bitbucket.org/rw_grim/hgkeeper/access"
"bitbucket.org/rw_grim/hgkeeper/hg"
@@ -11,23 +13,21 @@
-func NewInit(reposPath, repoName string, access *access.Access) *Init {
+func NewInit(reposPath, repoName string) *Init { repoPath: filepath.Join(reposPath, repoName),
func (i *Init) Run(conn ssh.Channel, serverConn *ssh.ServerConn, username string, req *ssh.Request) error {
- return run(hg.Init(i.repoPath), conn, serverConn, req)
+ if !access.CanInit(username, i.repoName) { + return fmt.Errorf("access denied") -func (i *Init) CheckAccess(access *access.Access, username string) bool {
- return access.Global.CanInit(username)
+ return run(hg.Init(i.repoPath), conn, serverConn, req) func (i *Init) String() string {
--- a/commands/serve.go Sun Sep 08 05:44:05 2019 -0500
+++ b/commands/serve.go Mon Sep 09 05:04:32 2019 -0500
@@ -1,6 +1,7 @@
"golang.org/x/crypto/ssh"
@@ -12,27 +13,25 @@
-func NewServe(reposPath, repoName string, access *access.Access) *Serve {
+func NewServe(reposPath, repoName string) *Serve { repoPath: filepath.Join(reposPath, repoName),
func (s *Serve) Run(conn ssh.Channel, serverConn *ssh.ServerConn, username string, req *ssh.Request) error {
- writeable := s.access.Global.CanWrite(username)
+ if !access.CanRead(username, s.repoName) { + return fmt.Errorf("repository %q not found", s.repoName) + writeable := access.CanWrite(username, s.repoName) return run(hg.Serve(s.repoPath, writeable), conn, serverConn, req)
-func (s *Serve) CheckAccess(access *access.Access, username string) bool {
- return access.Global.CanRead(username)
func (s *Serve) String() string {
--- a/go.mod Sun Sep 08 05:44:05 2019 -0500
+++ b/go.mod Mon Sep 09 05:04:32 2019 -0500
@@ -2,12 +2,9 @@
github.com/alecthomas/kong v0.1.16
- github.com/go-yaml/yaml v2.1.0+incompatible
+ github.com/casbin/casbin v1.9.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
- github.com/kr/pretty v0.1.0 // indirect
github.com/sirupsen/logrus v1.4.1
github.com/stretchr/testify v1.3.0
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a
- gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
- gopkg.in/yaml.v2 v2.2.2 // indirect
--- a/go.sum Sun Sep 08 05:44:05 2019 -0500
+++ b/go.sum Mon Sep 09 05:04:32 2019 -0500
@@ -1,21 +1,17 @@
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/alecthomas/kong v0.1.16 h1:BheBKuvr6FE1unlZVdqkdZo/D/eDu90rrVIlpPbOdgw=
github.com/alecthomas/kong v0.1.16/go.mod h1:0m2VYms8rH0qbCqVB2gvGHk74bqLIq0HXjCs5bNbNQU=
+github.com/casbin/casbin v1.9.1 h1:ucjbS5zTrmSLtH4XogqOG920Poe6QatdXtz1FEbApeM= +github.com/casbin/casbin v1.9.1/go.mod h1:z8uPsfBJGUsnkagrt3G8QvjgTKFMBJ32UP8HpZllfog= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
-github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
@@ -31,8 +27,3 @@
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
--- a/serve/command.go Sun Sep 08 05:44:05 2019 -0500
+++ b/serve/command.go Mon Sep 09 05:04:32 2019 -0500
@@ -1,6 +1,7 @@
+ "bitbucket.org/rw_grim/hgkeeper/access" "bitbucket.org/rw_grim/hgkeeper/globals"
"bitbucket.org/rw_grim/hgkeeper/ssh"
@@ -11,6 +12,10 @@
func (c *Command) Run(g *globals.Globals) error {
+ if err := access.Refresh(g.AdminRepo); err != nil { s, err := ssh.NewServer(c.SSHHostKeysPath, g.ReposPath, g.AdminRepo)
--- a/ssh/server.go Sun Sep 08 05:44:05 2019 -0500
+++ b/ssh/server.go Mon Sep 09 05:04:32 2019 -0500
@@ -3,7 +3,6 @@
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
@@ -15,7 +14,6 @@
@@ -34,12 +32,6 @@
- s.a = access.New(filepath.Join(s.reposPath, adminRepo))
- if err = s.a.Reload(); err != nil {
@@ -77,11 +69,7 @@
username, err := access.UsernameFromPubkey(key)
log.Infof("username: %q; err %v", username, err)
- if s.a.Global.CanRead(access.Public) {
- username = access.Public
log.Infof("returning username: %q", username)
@@ -124,7 +112,7 @@
log.Infof("%s requested command %q", serverConn.RemoteAddr(), rawCmd)
- cmd, err := commands.Find(rawCmd, s.reposPath, s.a)
+ cmd, err := commands.Find(rawCmd, s.reposPath) log.Warnf("failed to find command for %q, %v", rawCmd, err)
@@ -135,13 +123,6 @@
username := serverConn.Permissions.Extensions["username"]
log.Infof("username in exec: %q", username)
- if !cmd.CheckAccess(s.a, username) {
- log.Warnf("User %s is not allowed to run %s", username, cmd)
if err := cmd.Run(conn, serverConn, username, req); err != nil {
log.Warnf("%s command %q failed: %v", serverConn.RemoteAddr(), rawCmd, err)