grim/hgkeeper

f82b7c397849
Parents 7dd7f9cf8e7b
Children 7b6de7b2cfd5
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 @@
package access
import (
- "io"
- "os"
- "path/filepath"
"sync"
-
- "github.com/go-yaml/yaml"
- log "github.com/sirupsen/logrus"
- _ "golang.org/x/crypto/ssh"
-)
-
-const (
- // AccessFile is the base name of an access control file.
- AccessFile = "access.yml"
)
const (
- // Public is a reserved hgkeeper name and is not valid in the patterns
- // or keys of an access file.
- Public = "public"
+ modelFilename = "model.conf"
+ policyFilename = "policy.csv"
)
-var publicBytes = []byte(Public)
-
-type (
- groups map[string][]string
- permissions [numPerms][]string
+var (
+ accessLock sync.Mutex
)
-// Access represents a parsed access file.
-type Access struct {
- lock sync.RWMutex
-
- repoPath string
-
- Global Acl
- 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 {
+ accessLock.Lock()
+ defer accessLock.Unlock()
-func New(adminRepo string) *Access {
- return &Access{
- repoPath: adminRepo,
- 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)
- if err != nil {
- return err
- }
- defer reader.Close()
-
- return a.load(reader)
-}
-
-// loadFile reads the config from r into the basic structure.
-func (a *Access) loadFile(r io.Reader) error {
- data := file{}
- if err := yaml.NewDecoder(r).Decode(&data); err != nil {
+ if err := refreshEnforcer(repoPath); err != nil {
return err
}
- a.Global = data.Global
- a.groups = data.Groups
- a.patterns = data.Patterns
+ if err := refreshKeys(repoPath); err != nil {
+ return err
+ }
return nil
}
-// load will load the access from file and reindex everything.
-func (a *Access) load(r io.Reader) error {
- a.lock.Lock()
- defer a.lock.Unlock()
+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 {
- return err
- }
+func CanWrite(user, repo string) bool {
+ return check(user, repo, "write")
+}
- RefreshKeys(a.repoPath)
-
- log.Infof("keys: %#v", keys)
-
- return nil
+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 @@
-package access
-
-type Acl struct {
- 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 {
- users[name] = true
- }
-
- for _, name := range a.Read {
- users[name] = true
- }
-
- for _, name := range a.Write {
- users[name] = true
- }
-
- slice := make([]string, len(users))
- idx := 0
- for user, _ := range users {
- slice[idx] = user
- idx++
- }
-
- return slice
-}
-
-func (a *Acl) CanRead(username string) bool {
- if a.CanWrite(username) || a.CanInit(username) {
- return true
- }
-
- for _, user := range a.Read {
- if user == username {
- return true
- }
- }
-
- return false
-}
-
-func (a *Acl) CanWrite(username string) bool {
- if a.CanInit(username) {
- return true
- }
-
- for _, user := range a.Write {
- if user == username {
- return true
- }
- }
-
- return false
-}
-
-func (a *Acl) CanInit(username string) bool {
- for _, user := range a.Init {
- if user == username {
- return true
- }
- }
-
- return false
-}
--- /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 @@
+package access
+
+import (
+ "path/filepath"
+ "sync"
+
+ "github.com/casbin/casbin"
+)
+
+var (
+ enforcer *casbin.Enforcer
+ enforcerLock sync.Mutex
+)
+
+func accessMatch(key1, key2 string) bool {
+ switch key2 {
+ case "deny":
+ return false
+ case "init":
+ return key1 == "init" || key1 == "write" || key1 == "read"
+ case "write":
+ return key1 == "write" || key1 == "read"
+ case "read":
+ return key1 == "read"
+ }
+
+ return false
+}
+
+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 {
+ enforcerLock.Lock()
+ defer enforcerLock.Unlock()
+
+ modelFile := filepath.Join(repoPath, modelFilename)
+ policyFile := filepath.Join(repoPath, policyFilename)
+
+ e := casbin.NewEnforcer(modelFile, policyFile)
+
+ e.AddFunction("access", accessMatchFunc)
+
+ enforcer = e
+
+ return nil
+}
--- 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 @@
-package access
-
-// A Perm represents one or more access permissions: reading, writing, etc.
-type Perm uint32
-
-// All permisions constants
-const (
- Init Perm = 1 << iota
- Read
- Write
- numPerms = 3
-)
-
-// String returns a textual representation of the perm.
-func (p Perm) String() string {
- var r string
- if p.can(Init) {
- r += "init|"
- }
- if p.can(Read) {
- r += "read|"
- }
- if p.can(Write) {
- r += "write|"
- }
- if len(r) == 0 {
- return "none"
- }
- return r[:len(r)-1]
-}
-
-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 @@
-package access
-
-type file struct {
- 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 @@
keysLock sync.Mutex
)
-func RefreshKeys(repoPath string) error {
+func refreshKeys(repoPath string) error {
keysLock.Lock()
defer keysLock.Unlock()
--- 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"
)
type cli struct {
@@ -24,7 +22,6 @@
type Command interface {
Run(conn ssh.Channel, serverConn *ssh.ServerConn, username string, req *ssh.Request) error
- CheckAccess(access *access.Access, username string) bool
String() string
}
@@ -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)
if err != nil {
return nil, err
@@ -54,9 +51,9 @@
switch pcmd {
case "hg serve":
- return NewServe(reposPath, values.Hg.Repo, access), nil
+ return NewServe(reposPath, values.Hg.Repo), nil
case "hg init <repo>":
- return NewInit(reposPath, values.Hg.Init.Repo, access), nil
+ return NewInit(reposPath, values.Hg.Init.Repo), nil
default:
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 @@
package commands
import (
+ "fmt"
+ "path/filepath"
+
"golang.org/x/crypto/ssh"
- "path/filepath"
"bitbucket.org/rw_grim/hgkeeper/access"
"bitbucket.org/rw_grim/hgkeeper/hg"
@@ -11,23 +13,21 @@
type Init struct {
repoPath string
repoName string
- access *access.Access
}
-func NewInit(reposPath, repoName string, access *access.Access) *Init {
+func NewInit(reposPath, repoName string) *Init {
return &Init{
repoPath: filepath.Join(reposPath, repoName),
repoName: repoName,
- access: access,
}
}
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 @@
package commands
import (
+ "fmt"
"path/filepath"
"golang.org/x/crypto/ssh"
@@ -12,27 +13,25 @@
type Serve struct {
repoPath string
repoName string
- access *access.Access
}
-func NewServe(reposPath, repoName string, access *access.Access) *Serve {
+func NewServe(reposPath, repoName string) *Serve {
return &Serve{
repoPath: filepath.Join(reposPath, repoName),
repoName: repoName,
- access: access,
}
}
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 {
return "hg serve"
}
--- 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 @@
require (
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 @@
package serve
import (
+ "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 {
+ return err
+ }
+
s, err := ssh.NewServer(c.SSHHostKeysPath, g.ReposPath, g.AdminRepo)
if err != nil {
return err
--- 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 @@
import (
"fmt"
"net"
- "path/filepath"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
@@ -15,7 +14,6 @@
type Server struct {
reposPath string
cfg *ssh.ServerConfig
- a *access.Access
listener net.Listener
}
@@ -34,12 +32,6 @@
return nil, err
}
- s.a = access.New(filepath.Join(s.reposPath, adminRepo))
-
- if err = s.a.Reload(); err != nil {
- return nil, err
- }
-
return s, nil
}
@@ -77,11 +69,7 @@
username, err := access.UsernameFromPubkey(key)
log.Infof("username: %q; err %v", username, err)
if err != nil {
- if s.a.Global.CanRead(access.Public) {
- username = access.Public
- } else {
- return nil, err
- }
+ return nil, err
}
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)
if err != nil {
log.Warnf("failed to find command for %q, %v", rawCmd, err)
req.Reply(false, nil)
@@ -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)
- req.Reply(false, nil)
-
- continue
- }
-
if err := cmd.Run(conn, serverConn, username, req); err != nil {
log.Warnf("%s command %q failed: %v", serverConn.RemoteAddr(), rawCmd, err)
req.Reply(false, nil)