grim/hgkeeper

access: implement
access-control
2019-04-27, Wagner Riffel
8c7e5a1b48ed
Parents 192d0f174bf4
Children bdb2c82e0679
access: implement
--- a/access/access.go Thu Apr 18 23:15:44 2019 +0000
+++ b/access/access.go Sat Apr 27 18:19:37 2019 -0300
@@ -1,27 +1,334 @@
-package main
+package access
import (
+ "bytes"
+ "fmt"
"io"
+ "io/ioutil"
+ "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"
+
+ // KeysDir is the base name of directory holding public keys
+ KeysDir = "keys"
+)
+
+var (
+ keysCacheMu sync.RWMutex
+ keysCache = make(map[string]string)
+)
+
+const (
+ // Public is a reserved hgkeeper name and is not valid in the patterns
+ // or keys of an access file.
+ Public = "public"
+)
+
+var publicBytes = []byte(Public)
+
+// set once at Init, after that it may stay read-only
+var (
+ accessFile string
+ keysDir string
+ reposDir string
+)
+
+// A Perm represents one or more access permissions: reading, writing, etc.
+type Perm uint32
+
+// All permisions constants
+const (
+ Init Perm = 1 << iota
+ Read
+ Write
)
-type acl struct {
- Init []string `yaml:"init"`
- Read []string `yaml:"read"`
- Write []string `yaml:"write"`
+// 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 }
+
+type _key struct {
+ fprint string
+ p Perm
+}
+
+// Access represents a parsed access file.
+type Access struct {
+ // pathsMu controls access to the patterns map
+ pathsMu sync.RWMutex
+
+ // paths holds all keys that to a path, indexed by
+ // path.
+ paths map[string][]_key
+
+ // global []_key
+}
+
+// parse parses access yml into a
+func (a *Access) parse(b []byte) error {
+ dec := yaml.NewDecoder(bytes.NewReader(b))
+ for {
+ err := dec.Decode(a)
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return err
+ }
+ }
+ return nil
}
-type AccessControl struct {
- Global acl `yaml:"global"`
- Groups map[string][]string `yaml:"groups"`
- Patterns map[string]acl `yaml:"patterns"`
+// UnmarshalYAML unmarshals access yaml into an internal access
+// representation
+func (a *Access) UnmarshalYAML(f func(interface{}) error) error {
+ type acl struct {
+ Init []string `yaml:"init"`
+ Read []string `yaml:"read"`
+ Write []string `yaml:"write"`
+ }
+ type file struct {
+ Global acl `yaml:"global"`
+ Groups map[string][]string `yaml:"groups"`
+ Patterns map[string]acl `yaml:"patterns"`
+ }
+ var dummy file
+ err := f(&dummy)
+ if err != nil {
+ return err
+ }
+ groups := make(map[string][]_key)
+ for i, v := range dummy.Groups {
+ if isPublic([]byte(i)) {
+ log.Errorf("access: %q group is reserved, ignored", i)
+ continue
+ }
+ _, exists := groups[i]
+ if exists {
+ log.Errorf("access: group %q registered twice", i)
+ continue
+ }
+ for _, key := range v {
+ k, err := loadKey(key)
+ if err != nil {
+ log.Errorf("%v", err)
+ if _, ok := groups[i]; !ok {
+ // if group "i" fail to load every key, this stops
+ // setPath routines to load group names as keys
+ groups[i] = nil
+ }
+ continue
+ }
+ groups[i] = append(groups[i], _key{fprint: k})
+ }
+ }
+
+ for i, v := range dummy.Patterns {
+ globs, err := filepath.Glob(filepath.Join(reposDir, i))
+ if err != nil {
+ log.Errorf("malformed pattern %q: %v", i, err)
+ continue
+ }
+
+ for _, path := range globs {
+ a.setPathInit(path, v.Init, groups)
+ a.setPathRead(path, v.Read, groups)
+ a.setPathWrite(path, v.Write, groups)
+ }
+ }
+ // what to do with dummy.Global?
+ fmt.Printf("%+v\n", a.paths)
+ return nil
+}
+
+func (a *Access) addToPath(path string, k _key, p Perm) {
+ a.pathsMu.Lock()
+ defer a.pathsMu.Unlock()
+
+ if keys, found := a.paths[path]; found {
+ if n := k.find(keys); n >= 0 {
+ a.paths[path][n].p.set(p)
+ return
+ }
+ }
+ k.p.set(p)
+ a.paths[path] = append(a.paths[path], k)
+}
+
+func (a *Access) setPathInit(path string, init []string, groups map[string][]_key) {
+ for _, key := range init {
+ if _, ok := groups[key]; ok {
+ // set permissions on groups key
+ for i := range groups[key] {
+ k := groups[key][i]
+
+ a.addToPath(path, k, Init)
+ }
+ continue
+ }
+ fp, err := loadKey(key)
+ if err != nil {
+ log.Errorf("%v", err)
+ continue
+ }
+ k := _key{fprint: fp}
+ a.addToPath(path, k, Init)
+ }
}
-func loadAccessControl(r io.Reader) (*AccessControl, error) {
- ac := &AccessControl{}
+func (a *Access) setPathRead(path string, read []string, groups map[string][]_key) {
+ for _, key := range read {
+ if _, ok := groups[key]; ok {
+ // set permissions on groups key
+ for i := range groups[key] {
+ k := groups[key][i]
+
+ a.addToPath(path, k, Read)
+ }
+ continue
+ }
+ fp, err := loadKey(key)
+ if err != nil {
+ log.Errorf("%v", err)
+ continue
+ }
+ k := _key{fprint: fp}
+ a.addToPath(path, k, Read)
+ }
+}
+
+func (a *Access) setPathWrite(path string, write []string, groups map[string][]_key) {
+ for _, key := range write {
+ if _, ok := groups[key]; ok {
+ // set permissions on groups key
+ for i := range groups[key] {
+ k := groups[key][i]
+
+ a.addToPath(path, k, Write)
+ }
+ continue
+ }
+ fp, err := loadKey(key)
+ if err != nil {
+ log.Errorf("%v", err)
+ continue
+ }
+ k := _key{fprint: fp}
+ a.addToPath(path, k, Write)
+ }
+}
+
+// Can checks whenever if key has access to path
+func (a *Access) Can(path, key string, p Perm) bool {
+ a.pathsMu.RLock()
+ defer a.pathsMu.RUnlock()
+ keys, found := a.paths[path]
+ if !found {
+ return false
+ }
+ for _, v := range keys {
+ if key == v.fprint && v.p.can(p) {
+ return true
+ }
+ }
+ return false
+}
- err := yaml.NewDecoder(r).Decode(ac)
+// Reset discards all access state and rebuild its state
+// from disk
+func (a *Access) Reset() error {
+ a.pathsMu.Lock()
+ for i := range a.paths {
+ delete(a.paths, i)
+ }
+ a.pathsMu.Unlock()
+ b, err := ioutil.ReadFile(accessFile)
+ if err != nil {
+ return err
+ }
+ if err = a.parse(b); err != nil {
+ return err
+ }
+ return nil
+}
+
+// New initializes access strucuture, if any error is
+// returned it will be I/O errors reading or parsing
+// the access file.
+func New(reposPath string) (*Access, error) {
+ reposDir = reposPath
+ accessFile = filepath.Join(reposPath, "hgkeeper", AccessFile)
+ keysDir = filepath.Join(reposPath, "hgkeeper", KeysDir)
+ b, err := ioutil.ReadFile(accessFile)
+ if err != nil {
+ return nil, err
+ }
+ a := new(Access)
+ a.pathsMu.Lock()
+ a.paths = make(map[string][]_key)
+ a.pathsMu.Unlock()
- return ac, err
+ if err = a.parse(b); err != nil {
+ return nil, err
+ }
+ return a, nil
}
+
+// loadKey reads repoPath/hgkeeper/keys/name and tries to
+// parse as an authorized keys format, and returns its fingerprint
+func loadKey(name string) (string, error) {
+ k, err := ioutil.ReadFile(filepath.Join(keysDir, name))
+ if err != nil {
+ return "", fmt.Errorf("loadKey %q: %v", name, err)
+ }
+ //
+ pub, _, _, _, err := ssh.ParseAuthorizedKey(k)
+ if err != nil {
+ return "", fmt.Errorf("loadKey %q: %v", name, err)
+ }
+ return ssh.FingerprintSHA256(pub), nil
+}
+
+// isPublic checks whenever u is or not the reserved
+// hgkeeper public group
+func isPublic(u []byte) bool {
+ // Check for length to be fast. Safe because "public" is ASCII.
+ return len(u) == len(publicBytes) && bytes.EqualFold(u, publicBytes)
+}
+
+// find looks for k in keys, if found returns the index in keys,
+// otherwise it returns -1
+func (k *_key) find(keys []_key) (n int) {
+ for n = range keys {
+ if keys[n].fprint == k.fprint {
+ return
+ }
+ }
+ return -1
+}
--- a/access/access_test.go Thu Apr 18 23:15:44 2019 +0000
+++ b/access/access_test.go Sat Apr 27 18:19:37 2019 -0300
@@ -1,44 +1,90 @@
-package main
+package access
import (
- "strings"
"testing"
- "github.com/stretchr/testify/assert"
+ _ "github.com/stretchr/testify/assert"
)
-func TestAccessControlLoadSimple(t *testing.T) {
- assert := assert.New(t)
- data := `
+var accessData = `
global:
init:
- - admin
+ - admins
read:
- - everyone
+ - public
+groups:
+ admins:
+ - grim
+ pidgin:
+ - grim
+ pidgin-gsoc:
+ - student1
patterns:
hgkeeper:
read:
- - admin
+ - admins
+ pidgin/gsoc/*:
+ write:
+ - pidgin
+ - pidgin-gsoc
+ pidgin/*:
+ write:
+ - pidgin
`
- ac, err := loadAccessControl(strings.NewReader(data))
- assert.Nil(err)
+var keysData = []string{
+ "",
+ "",
+}
+
+// func TestAccessControlLoadSimple(t *testing.T) {
+// assert := assert.New(t)
+// data := `
+// global:
+// init:
+// - admin
+// read:
+// - everyone
+// patterns:
+// hgkeeper:
+// read:
+// - admin
+// `
+
+// ac, err := loadAccessControl(strings.NewReader(data))
+// assert.Nil(err)
- assert.NotNil(ac)
- assert.Equal(
- acl{
- Init: []string{"admin"},
- Read: []string{"everyone"},
- Write: nil,
- },
- ac.Global,
- )
- assert.Equal(
- acl{
- Init: nil,
- Read: []string{"admin"},
- Write: nil,
- },
- ac.Patterns["hgkeeper"],
- )
+// assert.NotNil(ac)
+// assert.Equal(
+// acl{
+// Init: []string{"admin"},
+// Read: []string{"everyone"},
+// Write: nil,
+// },
+// ac.Global,
+// )
+// assert.Equal(
+// acl{
+// Init: nil,
+// Read: []string{"admin"},
+// Write: nil,
+// },
+// ac.Patterns["hgkeeper"],
+// )
+// }
+
+func TestIsPublic(t *testing.T) {
+ tests := []struct {
+ u string
+ expect bool
+ }{
+ {"public", true},
+ {"öffentlich", false},
+ {"上市", false},
+ }
+ for _, v := range tests {
+ if e := isPublic([]byte(v.u)); e != v.expect {
+ t.Fatalf("isPublic(%s): expected: %v got: %v", v.u, v.expect, e)
+ }
+ }
}
--- a/serve/command.go Thu Apr 18 23:15:44 2019 +0000
+++ b/serve/command.go Sat Apr 27 18:19:37 2019 -0300
@@ -10,9 +10,7 @@
}
func (c *Command) Run(reposPath string) error {
- s := ssh.NewServer()
-
- err := s.SetHostKeysPath(c.SSHHostKeysPath)
+ s, err := ssh.NewServer(c.SSHHostKeysPath, reposPath)
if err != nil {
return err
}
--- a/ssh/keys.go Thu Apr 18 23:15:44 2019 +0000
+++ b/ssh/keys.go Sat Apr 27 18:19:37 2019 -0300
@@ -9,7 +9,7 @@
"golang.org/x/crypto/ssh"
)
-func (s *Server) SetHostKeysPath(hostKeysPath string) error {
+func (s *Server) setHostKeysPath(hostKeysPath string) error {
files, err := ioutil.ReadDir(hostKeysPath)
if err != nil {
return err
--- a/ssh/server.go Thu Apr 18 23:15:44 2019 +0000
+++ b/ssh/server.go Sat Apr 27 18:19:37 2019 -0300
@@ -4,23 +4,32 @@
"fmt"
"net"
+ "bitbucket.org/rw_grim/hgkeeper/access"
+
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
type Server struct {
cfg *ssh.ServerConfig
+ a *access.Access
listener net.Listener
}
-func NewServer() *Server {
- s := &Server{}
+func NewServer(hostKeysPath, reposPath string) (*Server, error) {
+ s := new(Server)
s.cfg = &ssh.ServerConfig{
MaxAuthTries: 1,
PublicKeyCallback: s.publicKeyCallback,
}
-
- return s
+ var err error
+ if err = s.setHostKeysPath(hostKeysPath); err != nil {
+ return nil, err
+ }
+ if s.a, err = access.New(reposPath); err != nil {
+ return nil, err
+ }
+ return s, nil
}
func (s *Server) Listen(addr string) error {
@@ -54,6 +63,9 @@
}
func (s *Server) publicKeyCallback(meta ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
+ if !s.a.Can("", "", access.Read) {
+ return nil, fmt.Errorf("permission denied")
+ }
return nil, nil
}