--- 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 @@
"github.com/go-yaml/yaml"
log "github.com/sirupsen/logrus"
- "golang.org/x/crypto/ssh"
+ _ "golang.org/x/crypto/ssh" @@ -30,12 +27,6 @@
var publicBytes = []byte(Public)
-// set once at Init, after that it may stay read-only
groups map[string][]string
permissions [numPerms][]string
@@ -43,273 +34,110 @@
// Access represents a parsed access file.
- // keysMu controls access to the patterns map
- // keys holds all users indexed by key fingerprint
- // usersMu controls access to the patterns map
- // users holds its patterns permissions indexed
- users map[string]map[string]Perm
+ 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
-// It's caller's duty to control the access to a.
-func (a *Access) UnmarshalYAML(f func(interface{}) error) error {
- Init []string `yaml:"init"`
- Read []string `yaml:"read"`
- Write []string `yaml:"write"`
+func New(adminRepo string) *Access { + groups: map[string][]string{}, + patterns: map[string]Acl{}, + fingerprintIndex: map[string]string{},
- 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)
+// 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 { - 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, "")
- log.Errorf("malformed pattern %q: %v", pattern, err)
- perms := permissions{v.Init, v.Read, v.Write}
- a.addPattern(pattern, perms, globals, dummy.Groups)
-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]
- a.users[user][pattern] = perm
- // if user not seen, parse key and append to
- if err := a.addToKeys(user); err != nil {
- log.Errorf("addToUsers: %v", err)
- 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 {
- a.addToUsers(pattern, perm, globals[p]...)
- for _, userOrGroup := range yamlPerm {
- if isPublic([]byte(userOrGroup)) {
- if _, ok := g[userOrGroup]; ok {
- a.addToUsers(pattern, perm, g[userOrGroup]...)
- a.addToUsers(pattern, perm, userOrGroup)
-func (a *Access) addToKeys(user string) error {
- keyfile := filepath.Join(keysDir, user)
- f, err := ioutil.ReadFile(keyfile)
- return fmt.Errorf("failed to read key file %q: %v", keyfile, err)
- // if the file is empty, we have nothing to do
- return fmt.Errorf("key file %q is empty", keyfile)
- // loop while we have data in f. f is updated by ssh.ParseAuthorizedKeys
- pub, _, _, f, err = ssh.ParseAuthorizedKey(f)
- log.Errorf("failed parsing key in %q: %v", keyfile, err)
- fp := ssh.FingerprintSHA256(pub)
- log.Debugf("found key for %q: %s", user, fp)
- log.Infof("found %d keys for %s", nKeys, user)
+ a.patterns = data.Patterns -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
- 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 { + for _, groupUsers := range a.groups { + for _, name := range groupUsers { + // don't add groups to the users list + if _, found := a.groups[name]; found { - if _, found := g[name]; found {
- globals[p] = append(globals[p], g[name]...)
+ for _, acl := range a.patterns { + for _, name := range acl.Users() { + // don't add groups to the users list + if _, found := a.groups[name]; found { - globals[p] = append(globals[p], name)
-// Can reports whenever if key (fingerprint) is has access p to path
-func (a *Access) Can(key, path string, p Perm) bool {
- defer a.keysMu.RUnlock()
- user, found := a.keys[key]
- 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 {
+ slice := make([]string, len(users)) + for name, _ := range users {
-// Reset discards all access state and rebuild its state
-func (a *Access) Reset() error {
- defer a.keysMu.Unlock()
- defer a.usersMu.Unlock()
- for i := range a.users {
- for i := range a.keys {
- f, err := os.Open(accessFile)
-// New initializes access strucuture, if any error is
-// returned it will be I/O errors reading or parsing
-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 { - log.Errorf("accessFile: %q", accessFile)
- log.Errorf("keysDir: %q", keysDir)
- users: map[string]map[string]Perm{},
- keys: map[string]string{},
+ if err := a.loadFile(r); err != 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)
-// 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
-func (a *Access) CheckPermission(key ssh.PublicKey) (string, error) {
- defer a.keysMu.RUnlock()
- fp := ssh.FingerprintSHA256(key)
- return "", fmt.Errorf("access: check permission: key %q permission denied", fp)
-// 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) {
- defer a.usersMu.RUnlock()
- patterns, ok := a.users[username]
- for pattern, perm := range patterns {
- // ignoring error because pattern was validated before
- // its insertion in map
- if ok, _ = filepath.Match(pattern, path); !ok {
+ log.Infof("keys: %#v", a.fingerprintIndex)
--- 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 keysData = []string{
func TestAccessControlLoadSimple(t *testing.T) {
- ac, err := loadAccessControl(strings.NewReader(data))
+ err := a.load(strings.NewReader(data))
- Init: []string{"admin"},
- Read: []string{"everyone"},
+ Init: []string{"admins"}, + Read: []string{"public"},
+ "admins": []string{"root"},
- Read: []string{"admin"},
+ Read: []string{"admins"}, - ac.Patterns["hgkeeper"],
+ a.patterns["hgkeeper"], + []string{"root", "public"}, -func TestIsPublic(t *testing.T) {
- for _, v := range tests {
- assert.Equal(t, v.expect, isPublic([]byte(v.u)))
+func TestAccessFindUsersComplicated(t *testing.T) { + err := a.load(strings.NewReader(data)) + []string{"public", "foo", "foobar", "bar", "baz", "qux", "quux"}, --- 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 @@
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
@@ -27,12 +28,17 @@
PublicKeyCallback: s.publicKeyCallback,
if err = s.setHostKeysPath(hostKeysPath); err != nil {
- 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 { @@ -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 s.a.Global.CanRead(access.Public) { + username = access.Public @@ -118,14 +128,14 @@
- username := serverConn.Permissions.Extensions["username"]
+ // username := serverConn.Permissions.Extensions["username"] - r, _, _ := s.a.GetPermissions(username, cmd.path)
- log.Warnf("user %q does not have read access to %s", username, cmd.path)
+ // r, _, _ := s.a.GetPermissions(username, cmd.path) + // 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 {