grim/hgkeeper

Parents ed523f1c967e
Children eb233ca9b428
Lots of work on access need to clean up commands before preceeding
--- a/access/access.go Mon Jul 22 22:12:09 2019 -0500
+++ b/access/access.go Tue Jul 23 15:30:21 2019 -0500
@@ -1,17 +1,14 @@
package access
import (
- "bytes"
- "fmt"
"io"
- "io/ioutil"
"os"
"path/filepath"
"sync"
"github.com/go-yaml/yaml"
log "github.com/sirupsen/logrus"
- "golang.org/x/crypto/ssh"
+ _ "golang.org/x/crypto/ssh"
)
const (
@@ -30,12 +27,6 @@
var publicBytes = []byte(Public)
-// set once at Init, after that it may stay read-only
-var (
- accessFile string
- keysDir string
-)
-
type (
groups map[string][]string
permissions [numPerms][]string
@@ -43,273 +34,110 @@
// Access represents a parsed access file.
type Access struct {
- // keysMu controls access to the patterns map
- keysMu sync.RWMutex
- // keys holds all users indexed by key fingerprint
- keys map[string]string
+ lock sync.RWMutex
+
+ repoPath string
- // usersMu controls access to the patterns map
- usersMu sync.RWMutex
- // users holds its patterns permissions indexed
- // by user name
- users map[string]map[string]Perm
-}
+ global Acl
+ groups map[string][]string
+ patterns map[string]Acl
-// parse calls inderectly UnmarshalYAML, it's caller's duty to control the access to a.
-func (a *Access) parse(r io.Reader) error {
- return yaml.NewDecoder(r).Decode(a)
+ // index ssh key fingerprint to users
+ fingerprintIndex map[string]string
}
-// UnmarshalYAML unmarshals access yaml into an internal access
-// representation.
-// It's caller's duty to control the access to a.
-func (a *Access) UnmarshalYAML(f func(interface{}) error) error {
- type acl struct {
- Init []string `yaml:"init"`
- Read []string `yaml:"read"`
- Write []string `yaml:"write"`
+func New(adminRepo string) *Access {
+ return &Access{
+ repoPath: adminRepo,
+ groups: map[string][]string{},
+ patterns: map[string]Acl{},
+ fingerprintIndex: map[string]string{},
}
- type file struct {
- Global acl `yaml:"global"`
- Groups groups `yaml:"groups"`
- Patterns map[string]acl `yaml:"patterns"`
+}
+
+func (a *Access) Reload() error {
+ path := filepath.Join(a.repoPath, AccessFile)
+ reader, err := os.Open(path)
+ if err != nil {
+ return err
}
- var dummy file
- err := f(&dummy)
- if err != nil {
+ 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 {
return err
}
- users := permissions{dummy.Global.Init, dummy.Global.Read, dummy.Global.Write}
- globals := parseGlobals(dummy.Groups, users)
- for pattern, v := range dummy.Patterns {
- // validate the pattern
- _, err := filepath.Match(pattern, "")
- if err != nil {
- log.Errorf("malformed pattern %q: %v", pattern, err)
- continue
- }
-
- perms := permissions{v.Init, v.Read, v.Write}
- a.addPattern(pattern, perms, globals, dummy.Groups)
- }
- return nil
-}
-
-func (a *Access) addToUsers(pattern string, p Perm, users ...string) {
- for _, user := range users {
- if _, found := a.users[user]; found {
- perm := a.users[user][pattern]
- perm.set(p)
- a.users[user][pattern] = perm
- continue
- }
- // if user not seen, parse key and append to
- // keys map
- if err := a.addToKeys(user); err != nil {
- log.Errorf("addToUsers: %v", err)
- continue
- }
- var perm Perm
- perm.set(p)
- a.users[user] = map[string]Perm{}
- a.users[user][pattern] = perm
- }
-}
-
-func (a *Access) addPattern(pattern string, perms, globals permissions, g groups) {
- for p, yamlPerm := range perms {
- // casting p is not required as of current Go tip (1.13)
- // see: http://golang.org/issues/19113
- perm := Perm(1 << uint32(p))
-
- if len(yamlPerm) == 0 {
- // use global fallback
- a.addToUsers(pattern, perm, globals[p]...)
- continue
- }
- for _, userOrGroup := range yamlPerm {
- if isPublic([]byte(userOrGroup)) {
- continue
- }
- if _, ok := g[userOrGroup]; ok {
- a.addToUsers(pattern, perm, g[userOrGroup]...)
- continue
- }
- a.addToUsers(pattern, perm, userOrGroup)
- }
- }
-}
-
-func (a *Access) addToKeys(user string) error {
- keyfile := filepath.Join(keysDir, user)
-
- f, err := ioutil.ReadFile(keyfile)
- if err != nil {
- return fmt.Errorf("failed to read key file %q: %v", keyfile, err)
- }
-
- // if the file is empty, we have nothing to do
- if len(f) == 0 {
- return fmt.Errorf("key file %q is empty", keyfile)
- }
-
- nKeys := 0
- // loop while we have data in f. f is updated by ssh.ParseAuthorizedKeys
- // while reading keys.
- for len(f) > 0 {
- var pub ssh.PublicKey
- pub, _, _, f, err = ssh.ParseAuthorizedKey(f)
- if err != nil {
- log.Errorf("failed parsing key in %q: %v", keyfile, err)
- continue
- }
-
- fp := ssh.FingerprintSHA256(pub)
-
- log.Debugf("found key for %q: %s", user, fp)
- a.keys[fp] = user
- nKeys++
- }
-
- log.Infof("found %d keys for %s", nKeys, user)
+ a.global = data.Global
+ a.groups = data.Groups
+ a.patterns = data.Patterns
return nil
}
-func parseGlobals(g groups, users permissions) (globals permissions) {
- for p, userOrGroup := range users {
- // casting p is not required as of current Go tip (1.13)
- // see: http://golang.org/issues/19113
- var perm Perm
- perm.set(Perm(1 << uint32(p)))
- for _, name := range userOrGroup {
- if isPublic([]byte(name)) {
+func (a *Access) findUsers() []string {
+ users := map[string]bool{}
+
+ for _, name := range a.global.Users() {
+ // don't add groups to the users list
+ if _, found := a.groups[name]; found {
+ continue
+ }
+
+ users[name] = true
+ }
+
+ for _, groupUsers := range a.groups {
+ for _, name := range groupUsers {
+ // don't add groups to the users list
+ if _, found := a.groups[name]; found {
continue
}
- if _, found := g[name]; found {
- globals[p] = append(globals[p], g[name]...)
+
+ users[name] = true
+ }
+ }
+
+ for _, acl := range a.patterns {
+ for _, name := range acl.Users() {
+ // don't add groups to the users list
+ if _, found := a.groups[name]; found {
continue
}
- globals[p] = append(globals[p], name)
+ users[name] = true
}
}
- return globals
-}
-// Can reports whenever if key (fingerprint) is has access p to path
-func (a *Access) Can(key, path string, p Perm) bool {
- a.keysMu.RLock()
- defer a.keysMu.RUnlock()
- user, found := a.keys[key]
- if !found {
- return false
- }
- a.usersMu.RLock()
- defer a.usersMu.RUnlock()
- patterns := a.users[user]
- for pattern, perm := range patterns {
- // ignoring error because pattern was validated before
- // its insertion in map
- if ok, _ := filepath.Match(pattern, path); !ok {
- continue
- }
- if perm.can(p) {
- return true
- }
+ slice := make([]string, len(users))
+ idx := 0
+ for name, _ := range users {
+ slice[idx] = name
+ idx++
}
- return false
-}
-// Reset discards all access state and rebuild its state
-// from disk
-func (a *Access) Reset() error {
- a.usersMu.Lock()
- a.keysMu.Lock()
- defer a.keysMu.Unlock()
- defer a.usersMu.Unlock()
-
- for i := range a.users {
- delete(a.users, i)
- }
- for i := range a.keys {
- delete(a.keys, i)
- }
- f, err := os.Open(accessFile)
- if err != nil {
- return err
- }
- defer f.Close()
- return a.parse(f)
+ return slice
}
-// New initializes access strucuture, if any error is
-// returned it will be I/O errors reading or parsing
-// the access file.
-func New(reposPath, adminRepo string) (*Access, error) {
- accessFile = filepath.Join(reposPath, adminRepo, AccessFile)
- keysDir = filepath.Join(reposPath, adminRepo, KeysDir)
+// 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()
- log.Errorf("accessFile: %q", accessFile)
- log.Errorf("keysDir: %q", keysDir)
-
- a := &Access{
- users: map[string]map[string]Perm{},
- keys: map[string]string{},
+ if err := a.loadFile(r); err != nil {
+ return err
}
- return a, a.Reset()
-}
-
-// 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)
-}
+ users := a.findUsers()
-// CheckPermission checks if we're supposed to allow the given ssh key. If the
-// key is not found error is returned. If it is found, the username it belongs
-// to is returned.
-func (a *Access) CheckPermission(key ssh.PublicKey) (string, error) {
- a.keysMu.RLock()
- defer a.keysMu.RUnlock()
- fp := ssh.FingerprintSHA256(key)
- u, ok := a.keys[fp]
- if !ok {
- return "", fmt.Errorf("access: check permission: key %q permission denied", fp)
- }
- return u, nil
-}
+ a.loadSshKeys(users)
-// GetPermissions will look up the given username and find the permissions that
-// the user has on the given path.
-func (a *Access) GetPermissions(username, path string) (read bool, write bool, init bool) {
- a.usersMu.RLock()
- defer a.usersMu.RUnlock()
- patterns, ok := a.users[username]
- if !ok {
- return
- }
- for pattern, perm := range patterns {
- // ignoring error because pattern was validated before
- // its insertion in map
- if ok, _ = filepath.Match(pattern, path); !ok {
- continue
- }
+ log.Infof("keys: %#v", a.fingerprintIndex)
- if perm.can(Read) {
- read = true
- }
- if perm.can(Write) {
- write = true
- }
- if perm.can(Init) {
- init = true
- }
- return
- }
-
- return
+ return nil
}
--- a/access/access_test.go Mon Jul 22 22:12:09 2019 -0500
+++ b/access/access_test.go Tue Jul 23 15:30:21 2019 -0500
@@ -7,84 +7,100 @@
"github.com/stretchr/testify/assert"
)
-// var accessData = `
-// global:
-// init:
-// - admins
-// read:
-// - public
-// groups:
-// admins:
-// - grim
-// pidgin:
-// - grim
-// pidgin-gsoc:
-// - student1
-// patterns:
-// hgkeeper:
-// read:
-// - admins
-// pidgin/gsoc/*:
-// write:
-// - pidgin
-// - pidgin-gsoc
-// pidgin/*:
-// write:
-// - pidgin
-// `
-
-// var keysData = []string{
-// "",
-// "",
-// }
-
func TestAccessControlLoadSimple(t *testing.T) {
data := `
global:
init:
- - admin
+ - admins
read:
- - everyone
+ - public
+groups:
+ admins:
+ - root
patterns:
hgkeeper:
read:
- - admin
+ - admins
`
- ac, err := loadAccessControl(strings.NewReader(data))
+ a := New("")
+ err := a.load(strings.NewReader(data))
assert.Nil(t, err)
- assert.NotNil(t, ac)
+ assert.NotNil(t, a)
assert.Equal(
t,
- acl{
- Init: []string{"admin"},
- Read: []string{"everyone"},
+ Acl{
+ Init: []string{"admins"},
+ Read: []string{"public"},
Write: nil,
},
- ac.Global,
+ a.global,
+ )
+ assert.Equal(
+ t,
+ map[string][]string{
+ "admins": []string{"root"},
+ },
+ a.groups,
)
assert.Equal(
t,
- acl{
+ Acl{
Init: nil,
- Read: []string{"admin"},
+ Read: []string{"admins"},
Write: nil,
},
- ac.Patterns["hgkeeper"],
+ a.patterns["hgkeeper"],
+ )
+
+ assert.ElementsMatch(
+ t,
+ []string{"root", "public"},
+ a.findUsers(),
)
}
-func TestIsPublic(t *testing.T) {
- tests := []struct {
- u string
- expect bool
- }{
- {"public", true},
- {"öffentlich", false},
- {"上市", false},
- }
- for _, v := range tests {
- assert.Equal(t, v.expect, isPublic([]byte(v.u)))
- }
+func TestAccessFindUsersComplicated(t *testing.T) {
+ data := `
+global:
+ init:
+ - admins
+ read:
+ - public
+groups:
+ admins:
+ - foo
+ - bar
+ devs:
+ - baz
+ - qux
+ managers:
+ - admins
+ - devs
+ - quux
+patterns:
+ hgkeeper/:
+ write:
+ - admins
+ read:
+ - admins
+ project1/:
+ read:
+ - foobar
+ - devs
+ - admins
+`
+
+ a := New("")
+ err := a.load(strings.NewReader(data))
+ assert.Nil(t, err)
+
+ assert.NotNil(t, a)
+
+ assert.ElementsMatch(
+ t,
+ []string{"public", "foo", "foobar", "bar", "baz", "qux", "quux"},
+ a.findUsers(),
+ )
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/access/acl.go Tue Jul 23 15:30:21 2019 -0500
@@ -0,0 +1,70 @@
+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/types.go Tue Jul 23 15:30:21 2019 -0500
@@ -0,0 +1,7 @@
+package access
+
+type file struct {
+ Global Acl `yaml:"global"`
+ Groups map[string][]string `yaml:"groups"`
+ Patterns map[string]Acl `yaml:"patterns"`
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/access/users.go Tue Jul 23 15:30:21 2019 -0500
@@ -0,0 +1,60 @@
+package access
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ log "github.com/sirupsen/logrus"
+ "golang.org/x/crypto/ssh"
+)
+
+func (a *Access) loadSshKeys(users []string) error {
+USERS:
+ for _, username := range users {
+ keyfile := filepath.Join(a.repoPath, "keys", username)
+ if _, err := os.Stat(keyfile); os.IsNotExist(err) {
+ log.Warnf("no keys found for user %s", username)
+ continue
+ }
+
+ buffer, err := ioutil.ReadFile(keyfile)
+ if err != nil {
+ log.Warnf("failed to read keyfile for user %s: %v", username, err)
+ continue
+ }
+
+ for len(buffer) > 0 {
+ var pubkey ssh.PublicKey
+ pubkey, _, _, buffer, err = ssh.ParseAuthorizedKey(buffer)
+ if err != nil {
+ log.Warnf("failed to parse key file for user %s: %v", username, err)
+ continue USERS
+ }
+
+ fingerprint := ssh.FingerprintSHA256(pubkey)
+ a.fingerprintIndex[fingerprint] = username
+ }
+ }
+
+ return nil
+}
+
+// UsernameFromFingerprint looks up a username from an SSH key's fingerprint
+// and returns the username if found, or err if not found.
+func (a *Access) UsernameFromFingerprint(fingerprint string) (string, error) {
+ a.lock.Lock()
+ defer a.lock.Lock()
+
+ username, found := a.fingerprintIndex[fingerprint]
+ if !found {
+ return "", fmt.Errorf("user not found")
+ }
+
+ return username, nil
+}
+
+func (a *Access) UsernameFromPubkey(pubkey ssh.PublicKey) (string, error) {
+ return a.UsernameFromFingerprint(ssh.FingerprintSHA256(pubkey))
+}
--- a/go.sum Mon Jul 22 22:12:09 2019 -0500
+++ b/go.sum Tue Jul 23 15:30:21 2019 -0500
@@ -12,6 +12,7 @@
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=
@@ -20,6 +21,7 @@
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
--- a/ssh/server.go Mon Jul 22 22:12:09 2019 -0500
+++ b/ssh/server.go Tue Jul 23 15:30:21 2019 -0500
@@ -3,6 +3,7 @@
import (
"fmt"
"net"
+ "path/filepath"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
@@ -27,12 +28,17 @@
PublicKeyCallback: s.publicKeyCallback,
}
var err error
+
if err = s.setHostKeysPath(hostKeysPath); err != nil {
return nil, err
}
- if s.a, err = access.New(s.reposPath, adminRepo); err != nil {
+
+ s.a = access.New(filepath.Join(s.reposPath, adminRepo))
+
+ if err = s.a.Reload(); err != nil {
return nil, err
}
+
return s, nil
}
@@ -67,9 +73,13 @@
}
func (s *Server) publicKeyCallback(meta ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
- username, err := s.a.CheckPermission(key)
+ username, err := s.a.UsernameFromPubkey(key)
if err != nil {
- return nil, err
+ if s.a.Global.CanRead(access.Public) {
+ username = access.Public
+ } else {
+ return nil, err
+ }
}
return &ssh.Permissions{
@@ -118,14 +128,14 @@
continue
}
- username := serverConn.Permissions.Extensions["username"]
+ // username := serverConn.Permissions.Extensions["username"]
// now check permissions
- r, _, _ := s.a.GetPermissions(username, cmd.path)
- if !r {
- log.Warnf("user %q does not have read access to %s", username, cmd.path)
- req.Reply(false, nil)
- }
+ // r, _, _ := s.a.GetPermissions(username, cmd.path)
+ // if !r {
+ // log.Warnf("user %q does not have read access to %s", username, cmd.path)
+ // req.Reply(false, nil)
+ // }
log.Warnf("running %#v\n", cmd)
if err := cmd.run(conn, serverConn, req); err != nil {