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 }