grim/hgkeeper

d5886fd34dc1
Merged in access-control (pull request #4)

access: implement

Approved-by: Gary Kramlich
--- a/access/access.go Fri May 03 13:03:43 2019 -0500
+++ b/access/access.go Wed May 08 02:14:09 2019 +0000
@@ -1,10 +1,10 @@
package access
import (
- "bufio"
"bytes"
"fmt"
"io"
+ "io/ioutil"
"os"
"path/filepath"
"sync"
@@ -37,21 +37,23 @@
reposDir string
)
-type _key struct {
- fprint string
- p Perm
-}
+type (
+ groups map[string][]string
+ permissions [numPerms][]string
+)
// Access represents a parsed access file.
type Access struct {
- // pathsMu controls access to the patterns map
- pathsMu sync.RWMutex
+ // keysMu controls access to the patterns map
+ keysMu sync.RWMutex
+ // keys holds all users indexed by key fingerprint
+ keys map[string]string
- // paths holds all keys that to a path, indexed by
- // path.
- paths map[string][]_key
-
- global [numPerms][]_key
+ // 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
}
// parse calls inderectly UnmarshalYAML, it's caller's duty to control the access to a.
@@ -69,160 +71,127 @@
Write []string `yaml:"write"`
}
type file struct {
- Global acl `yaml:"global"`
- Groups map[string][]string `yaml:"groups"`
- Patterns map[string]acl `yaml:"patterns"`
+ Global acl `yaml:"global"`
+ Groups groups `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 name, v := range dummy.Groups {
- if isPublic([]byte(name)) {
- log.Errorf("access: %q group is reserved, ignored", name)
- continue
- }
- if _, exists := groups[name]; exists {
- log.Errorf("access: group %q registered twice", name)
+
+ 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
}
- for _, keyFile := range v {
- k, err := loadKeys(keyFile)
- if err != nil {
- log.Errorf("%v", err)
- if _, ok := groups[name]; !ok {
- // if group "name" fail to load every key, this stops
- // setPath routines to load group names as keys
- groups[name] = nil
- }
- continue
- }
- keys := make([]_key, 0, len(k))
- for _, v := range k {
- keys = append(keys, _key{fprint: v})
- }
- groups[name] = append(groups[name], keys...)
- }
- }
- gperms := [numPerms][]string{dummy.Global.Init, dummy.Global.Read, dummy.Global.Write}
- a.setGlobals(groups, gperms)
- 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 {
- perms := [numPerms][]string{v.Init, v.Read, v.Write}
- a.setPath(path, perms, groups)
- }
+ perms := permissions{v.Init, v.Read, v.Write}
+ a.addPattern(pattern, perms, globals, dummy.Groups)
}
return nil
}
-func (a *Access) addToPath(path string, p Perm, ks ..._key) {
- for _, k := range ks {
- if keys, found := a.paths[path]; found {
- if n := k.find(keys); n >= 0 {
- a.paths[path][n].p.set(p)
- return
- }
+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
}
- k.p.set(p)
- a.paths[path] = append(a.paths[path], k)
+ // 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] = make(map[string]Perm)
+ a.users[user][pattern] = perm
}
}
-func (a *Access) setPath(path string, perms [numPerms][]string, groups map[string][]_key) {
+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 defaults
- a.addToPath(path, perm, a.global[0]...)
+ // use global fallback
+ a.addToUsers(pattern, perm, globals[p]...)
continue
}
- for _, keyOrGroup := range yamlPerm {
- // if it's a group, just copy what we already loaded
- if _, ok := groups[keyOrGroup]; ok {
- // set permissions on groups key
- for i := range groups[keyOrGroup] {
- k := groups[keyOrGroup][i]
-
- a.addToPath(path, perm, k)
- }
+ for _, userOrGroup := range yamlPerm {
+ if isPublic([]byte(userOrGroup)) {
continue
}
- // this is likely to change, it's here just to remember later.
- if keyOrGroup == "public" {
- a.addToPath(path, perm, a.global[p]...)
+ if _, ok := g[userOrGroup]; ok {
+ a.addToUsers(pattern, perm, g[userOrGroup]...)
continue
}
- ks, err := loadKeys(keyOrGroup)
- if err != nil {
- log.Errorf("%v", err)
- continue
- }
- keys := make([]_key, 0, len(ks))
- for _, v := range ks {
- keys = append(keys, _key{fprint: v})
- }
- a.addToPath(path, perm, keys...)
+ a.addToUsers(pattern, perm, userOrGroup)
}
}
}
-func (a *Access) setGlobals(groups map[string][]_key, perms [numPerms][]string) {
- for p, keyOrGroup := range perms {
+func (a *Access) addToKeys(user string) error {
+ ks, err := loadKeys(user)
+ if err != nil {
+ return fmt.Errorf("addToKeys: %v", err)
+ }
+ for _, key := range ks {
+ a.keys[key] = user
+ }
+ 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 keyOrGroup {
- // if it's a group, just copy what we already loaded
- if _, found := groups[name]; found {
- keys := make([]_key, 0, len(groups[name])+1)
- for _, v := range groups[name] {
- keys = append(keys, v)
- }
- a.global[p] = append(a.global[p], keys...)
+ for _, name := range userOrGroup {
+ if isPublic([]byte(name)) {
+ continue
+ }
+ if _, found := g[name]; found {
+ globals[p] = append(globals[p], g[name]...)
continue
}
- // this is likely to change, it's here just to remember later.
- if name == "public" {
- a.global[p] = append(a.global[p], _key{fprint: "public", p: perm})
- continue
- }
- k, err := loadKeys(name)
- if err != nil {
- log.Errorf("%v", err)
- continue
- }
- keys := make([]_key, 0, len(k))
- for _, v := range k {
- keys = append(keys, _key{fprint: v, p: perm})
- }
- a.global[p] = append(a.global[p], keys...)
+ globals[p] = append(globals[p], name)
}
}
+ return globals
}
-// 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]
+// 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
}
- for _, v := range keys {
- if key == v.fprint && v.p.can(p) {
+ 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
}
}
@@ -232,10 +201,16 @@
// Reset discards all access state and rebuild its state
// from disk
func (a *Access) Reset() error {
- a.pathsMu.Lock()
- defer a.pathsMu.Unlock()
- for i := range a.paths {
- delete(a.paths, i)
+ 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 {
@@ -253,30 +228,24 @@
accessFile = filepath.Join(reposPath, "hgkeeper", AccessFile)
keysDir = filepath.Join(reposPath, "hgkeeper", KeysDir)
a := new(Access)
- a.paths = make(map[string][]_key)
+ a.users = make(map[string]map[string]Perm)
+ a.keys = make(map[string]string)
return a, a.Reset()
}
// loadKeys reads file trying to parse it as an authorized key
// format, returning the finger prints of all keys found
func loadKeys(file string) ([]string, error) {
- f, err := os.Open(filepath.Join(keysDir, file))
+ f, err := ioutil.ReadFile(filepath.Join(keysDir, file))
if err != nil {
return nil, fmt.Errorf("loadKeys %q: %v", file, err)
}
- defer f.Close()
- keys := make([]string, 0, 20)
- // according to sshd(8) each line of the file contains one key,
- // ignoring empty and lines starting with #
- bio := bufio.NewScanner(f)
- for line := 1; bio.Scan(); line++ {
- key := bio.Text()
- if key == "" || key[0] == '#' {
- continue
- }
- pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key))
+ keys := make([]string, 0, 5)
+ for len(f) > 0 {
+ var pub ssh.PublicKey
+ pub, _, _, f, err = ssh.ParseAuthorizedKey(f)
if err != nil {
- log.Errorf("loadKeys %q:%d: %v", file, line, err)
+ log.Errorf("loadKeys %s: %v", file, err)
continue
}
keys = append(keys, ssh.FingerprintSHA256(pub))
@@ -295,26 +264,44 @@
// 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 CheckPermission(key ssh.PublicKey) (string, error) {
- // stubbed out for now
- return "hg", nil
+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
}
// GetPermissions will look up the given username and find the permissions that
-// the user has on the given path. It returns 3 bools for read, write, and
-// init respectively.
-func GetPermissions(username, path string) (bool, bool, bool) {
- // stubbed for now
- return true, false, false
-}
+// 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
+ }
-// 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
+ if perm.can(Read) {
+ read = true
+ }
+ if perm.can(Write) {
+ write = true
}
+ if perm.can(Init) {
+ init = true
+ }
+ return
}
- return -1
+
+ return
}
--- a/ssh/server.go Fri May 03 13:03:43 2019 -0500
+++ b/ssh/server.go Wed May 08 02:14:09 2019 +0000
@@ -67,7 +67,7 @@
}
func (s *Server) publicKeyCallback(meta ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
- username, err := access.CheckPermission(key)
+ username, err := s.a.CheckPermission(key)
if err != nil {
return nil, err
}
@@ -121,7 +121,7 @@
username := serverConn.Permissions.Extensions["username"]
// now check permissions
- r, _, _ := access.GetPermissions(username, cmd.path)
+ 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)