Gitfed
bastien-mrq/gitfed / internal / ssh / session.go
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
}