grim/hgkeeper
Clone
Summary
Browse
Changes
Graph
Add an admin repos command line argument
2019-07-22, Gary Kramlich
ed523f1c967e
Add an admin repos command line argument
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"
)
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"
)
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
)
type
(
groups
map
[
string
][]
string
permissions
[
numPerms
][]
string
)
// 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
// 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.
func
(
a
*
Access
)
parse
(
r
io
.
Reader
)
error
{
return
yaml
.
NewDecoder
(
r
).
Decode
(
a
)
}
// 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"`
}
type
file
struct
{
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
}
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
)
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
))
{
continue
}
if
_
,
found
:=
g
[
name
];
found
{
globals
[
p
]
=
append
(
globals
[
p
],
g
[
name
]
...
)
continue
}
globals
[
p
]
=
append
(
globals
[
p
],
name
)
}
}
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
}
}
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
)
}
// 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
)
log
.
Errorf
(
"accessFile: %q"
,
accessFile
)
log
.
Errorf
(
"keysDir: %q"
,
keysDir
)
a
:=
&
Access
{
users
:
map
[
string
]
map
[
string
]
Perm
{},
keys
:
map
[
string
]
string
{},
}
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
)
}
// 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
}
// 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
}
if
perm
.
can
(
Read
)
{
read
=
true
}
if
perm
.
can
(
Write
)
{
write
=
true
}
if
perm
.
can
(
Init
)
{
init
=
true
}
return
}
return
}