grim/hgkeeper

f82b7c397849
Move from our custom yaml access setup to casbin
package ssh
import (
"fmt"
"net"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
"bitbucket.org/rw_grim/hgkeeper/access"
"bitbucket.org/rw_grim/hgkeeper/commands"
)
type Server struct {
reposPath string
cfg *ssh.ServerConfig
listener net.Listener
}
func NewServer(hostKeysPath, reposPath, adminRepo string) (*Server, error) {
s := &Server{
reposPath: reposPath,
}
s.cfg = &ssh.ServerConfig{
MaxAuthTries: 1,
PublicKeyCallback: s.publicKeyCallback,
}
var err error
if err = s.setHostKeysPath(hostKeysPath); err != nil {
return nil, err
}
return s, nil
}
func (s *Server) Listen(addr string) error {
listener, err := net.Listen("tcp", addr)
if err != nil {
return err
}
s.listener = listener
log.Infof("listening for ssh connections on %s", addr)
for {
tcpConn, err := s.listener.Accept()
if err != nil {
log.Errorf("failed to accept ssh connection: %v", err)
continue
}
sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, s.cfg)
if err != nil {
log.Errorf("ssh handshake failed: %v", err)
continue
}
log.Infof("new ssh connection from %s(%s)", sshConn.RemoteAddr(), sshConn.ClientVersion())
go ssh.DiscardRequests(reqs)
go s.processSSHChannels(chans, sshConn)
}
}
func (s *Server) publicKeyCallback(meta ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
username, err := access.UsernameFromPubkey(key)
log.Infof("username: %q; err %v", username, err)
if err != nil {
return nil, err
}
log.Infof("returning username: %q", username)
return &ssh.Permissions{
Extensions: map[string]string{"username": username},
}, nil
}
func (s *Server) processSSHChannels(chans <-chan ssh.NewChannel, serverConn *ssh.ServerConn) {
for ch := range chans {
// we process each channel in a go routine as there's network traffic involved
go s.processSSHChannel(ch, serverConn)
}
}
func (s *Server) processSSHChannel(ch ssh.NewChannel, serverConn *ssh.ServerConn) {
if t := ch.ChannelType(); t != "session" {
ch.Reject(ssh.UnknownChannelType, fmt.Sprintf("unsupported channel type %s", t))
return
}
conn, requests, err := ch.Accept()
if err != nil {
log.Warnf("failed to accept connection: %v", err)
return
}
// now run through all of the requests but only handle shell requests
go func() {
for req := range requests {
switch req.Type {
case "exec":
// this is garbage, but payload is a pascal string where the
// first 4 bytes are the length of the string followed by the
// data. the string() method converts it, but includes the
// length, so we just strip it off ahead of time and everything
// works fine.
rawCmd := string(req.Payload[4:])
log.Infof("%s requested command %q", serverConn.RemoteAddr(), rawCmd)
cmd, err := commands.Find(rawCmd, s.reposPath)
if err != nil {
log.Warnf("failed to find command for %q, %v", rawCmd, err)
req.Reply(false, nil)
continue
}
username := serverConn.Permissions.Extensions["username"]
log.Infof("username in exec: %q", username)
if err := cmd.Run(conn, serverConn, username, req); err != nil {
log.Warnf("%s command %q failed: %v", serverConn.RemoteAddr(), rawCmd, err)
req.Reply(false, nil)
}
default:
log.Debugf("%s unsupported request: %q", serverConn.RemoteAddr(), req.Type)
if req.WantReply {
req.Reply(false, nil)
}
}
}
}()
}