prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

owner.go (2141B)


      1 package ssh
      2 
      3 import (
      4 	"bufio"
      5 	"bytes"
      6 	"os"
      7 	"strings"
      8 	"sync"
      9 	"time"
     10 
     11 	charmssh "github.com/charmbracelet/ssh"
     12 	gossh "golang.org/x/crypto/ssh"
     13 )
     14 
     15 // The owner's keys (env GIT_OWNER_KEYS, an authorized_keys file) may push,
     16 // create repos, and read private ones. Every other key, and keyboard-
     17 // interactive, stays a read-only guest.
     18 
     19 const defaultOwnerKeysPath = ".ssh/owner_keys"
     20 
     21 type ownerCtxKey struct{}
     22 
     23 var (
     24 	ownerMu      sync.Mutex
     25 	ownerKeys    [][]byte
     26 	ownerModTime time.Time
     27 )
     28 
     29 func ownerKeysPath() string {
     30 	if p := strings.TrimSpace(os.Getenv("GIT_OWNER_KEYS")); p != "" {
     31 		return p
     32 	}
     33 	return defaultOwnerKeysPath
     34 }
     35 
     36 // loadOwnerKeys re-reads the keys file when it changes, so adding a key
     37 // needs no restart.
     38 func loadOwnerKeys() [][]byte {
     39 	ownerMu.Lock()
     40 	defer ownerMu.Unlock()
     41 	info, err := os.Stat(ownerKeysPath())
     42 	if err != nil {
     43 		ownerKeys, ownerModTime = nil, time.Time{}
     44 		return nil
     45 	}
     46 	if info.ModTime().Equal(ownerModTime) {
     47 		return ownerKeys
     48 	}
     49 	data, err := os.ReadFile(ownerKeysPath())
     50 	if err != nil {
     51 		return ownerKeys
     52 	}
     53 	var keys [][]byte
     54 	sc := bufio.NewScanner(bytes.NewReader(data))
     55 	for sc.Scan() {
     56 		line := strings.TrimSpace(sc.Text())
     57 		if line == "" || strings.HasPrefix(line, "#") {
     58 			continue
     59 		}
     60 		pk, _, _, _, err := gossh.ParseAuthorizedKey([]byte(line))
     61 		if err != nil {
     62 			continue
     63 		}
     64 		keys = append(keys, pk.Marshal())
     65 	}
     66 	ownerKeys, ownerModTime = keys, info.ModTime()
     67 	return keys
     68 }
     69 
     70 func isOwnerKey(key charmssh.PublicKey) bool {
     71 	if key == nil {
     72 		return false
     73 	}
     74 	raw := key.Marshal()
     75 	for _, k := range loadOwnerKeys() {
     76 		if bytes.Equal(k, raw) {
     77 			return true
     78 		}
     79 	}
     80 	return false
     81 }
     82 
     83 // publicKeyAuth accepts only the owner's keys, so a client offering several
     84 // keys reaches the owner's one; guests fall through to keyboard-interactive,
     85 // which asks nothing.
     86 func publicKeyAuth(ctx charmssh.Context, key charmssh.PublicKey) bool {
     87 	if isOwnerKey(key) {
     88 		ctx.SetValue(ownerCtxKey{}, true)
     89 		return true
     90 	}
     91 	return false
     92 }
     93 
     94 func isOwner(sess charmssh.Session) bool {
     95 	v, _ := sess.Context().Value(ownerCtxKey{}).(bool)
     96 	return v && isOwnerKey(sess.PublicKey())
     97 }