package ssh
import (
"fmt"
"log"
"time"
gossh "golang.org/x/crypto/ssh"
"git.neuromancer.ovh/bastien-mrq/gitfed/internal/acl"
"git.neuromancer.ovh/bastien-mrq/gitfed/internal/ca"
"git.neuromancer.ovh/bastien-mrq/gitfed/internal/gitexec"
"git.neuromancer.ovh/bastien-mrq/gitfed/internal/store"
)
type execRequest struct {
Command string
}
func (s *Server) handleSession(id Identity, channel gossh.Channel, requests <-chan *gossh.Request) {
defer channel.Close()
for req := range requests {
switch req.Type {
case "exec":
var payload execRequest
if err := gossh.Unmarshal(req.Payload, &payload); err != nil {
_ = req.Reply(false, nil)
continue
}
_ = req.Reply(true, nil)
status := s.runExec(id, payload.Command, channel)
_, _ = channel.SendRequest("exit-status", false, gossh.Marshal(&struct{ Status uint32 }{uint32(status)}))
return
case "env":
_ = req.Reply(true, nil)
default:
if req.WantReply {
_ = req.Reply(false, nil)
}
}
}
}
func (s *Server) runExec(id Identity, command string, channel gossh.Channel) int {
if command == "gitfed-cert" {
return s.issueCertOverSSH(id, channel)
}
verb, repoName, err := gitexec.ParseCommand(command)
if err != nil {
fmt.Fprintf(channel.Stderr(), "gitfed: %v\n", err)
return 1
}
required := store.RoleRead
if verb == gitexec.ReceivePack {
required = store.RoleWrite
}
if id.Principal == "" {
fmt.Fprintf(channel.Stderr(), "gitfed: no authenticated identity\n")
return 1
}
_, allowed, err := acl.Check(s.store, repoName, id.Principal, required)
if err != nil {
s.audit(string(verb), id.Principal, id.Domain, err.Error(), false)
fmt.Fprintf(channel.Stderr(), "gitfed: repo %q: %v\n", repoName, err)
return 1
}
if !allowed {
s.auditRepo(string(verb), id.Principal, repoName, "permission denied", false)
fmt.Fprintf(channel.Stderr(), "gitfed: %s: permission denied on %q\n", id.Principal, repoName)
return 1
}
s.auditRepo(string(verb), id.Principal, repoName, "", true)
repo, err := s.store.GetRepo(repoName)
if err != nil {
fmt.Fprintf(channel.Stderr(), "gitfed: repo %q not found\n", repoName)
return 1
}
if err := gitexec.Serve(verb, repo.Path, channel, channel, channel.Stderr()); err != nil {
log.Printf("gitfed: %s serving %s for %s: %v", verb, repoName, id.Principal, err)
return 1
}
return 0
}
// issueCertOverSSH lets a raw-key-authenticated local user obtain a signed
// certificate for the same key, per DESIGN.md §5.1 ("à la connexion/login
// initial"). Federated principals already hold a certificate and don't need
// this.
func (s *Server) issueCertOverSSH(id Identity, channel gossh.Channel) int {
if !id.Local {
fmt.Fprintf(channel.Stderr(), "gitfed: certificate issuance is only available to local users\n")
return 1
}
if id.PubKey == "" {
fmt.Fprintf(channel.Stderr(), "gitfed: no key associated with this session\n")
return 1
}
pub, _, _, _, err := gossh.ParseAuthorizedKey([]byte(id.PubKey))
if err != nil {
fmt.Fprintf(channel.Stderr(), "gitfed: stored key is corrupt: %v\n", err)
return 1
}
ttl := time.Duration(s.cfg.CertTTLHours) * time.Hour
cert, err := s.ca.IssueUserCert(ca.IssueParams{
Username: id.Username,
Domain: s.cfg.Domain,
UserKey: pub,
TTL: ttl,
})
if err != nil {
fmt.Fprintf(channel.Stderr(), "gitfed: issue certificate: %v\n", err)
return 1
}
s.audit("cert-issue", id.Principal, s.cfg.Domain, "", true)
_, err = channel.Write(gossh.MarshalAuthorizedKey(cert))
if err != nil {
log.Printf("gitfed: write cert to %s: %v", id.Principal, err)
return 1
}
return 0
}