1package config
2
3import (
4 "strings"
5
6 "github.com/charmbracelet/log"
7
8 gm "github.com/charmbracelet/wish/git"
9 "github.com/gliderlabs/ssh"
10 gossh "golang.org/x/crypto/ssh"
11)
12
13// Push registers Git push functionality for the given repo and key.
14func (cfg *Config) Push(repo string, pk ssh.PublicKey) {
15 go func() {
16 err := cfg.Reload()
17 if err != nil {
18 log.Error("error reloading after push", "err", err)
19 }
20 if cfg.Cfg.Callbacks != nil {
21 cfg.Cfg.Callbacks.Push(repo)
22 }
23 r, err := cfg.Source.GetRepo(repo)
24 if err != nil {
25 log.Error("error getting repo after push", "err", err)
26 return
27 }
28 err = r.UpdateServerInfo()
29 if err != nil {
30 log.Error("error updating server info after push", "err", err)
31 }
32 }()
33}
34
35// Fetch registers Git fetch functionality for the given repo and key.
36func (cfg *Config) Fetch(repo string, pk ssh.PublicKey) {
37 if cfg.Cfg.Callbacks != nil {
38 cfg.Cfg.Callbacks.Fetch(repo)
39 }
40}
41
42// AuthRepo grants repo authorization to the given key.
43func (cfg *Config) AuthRepo(repo string, pk ssh.PublicKey) gm.AccessLevel {
44 return cfg.accessForKey(repo, pk)
45}
46
47// PasswordHandler returns whether or not password access is allowed.
48func (cfg *Config) PasswordHandler(ctx ssh.Context, password string) bool {
49 return (cfg.AnonAccess != "no-access") && cfg.AllowKeyless
50}
51
52// KeyboardInteractiveHandler returns whether or not keyboard interactive is allowed.
53func (cfg *Config) KeyboardInteractiveHandler(ctx ssh.Context, _ gossh.KeyboardInteractiveChallenge) bool {
54 return (cfg.AnonAccess != "no-access") && cfg.AllowKeyless
55}
56
57// PublicKeyHandler returns whether or not the given public key may access the
58// repo.
59func (cfg *Config) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool {
60 return cfg.accessForKey("", pk) != gm.NoAccess
61}
62
63func (cfg *Config) anonAccessLevel() gm.AccessLevel {
64 switch cfg.AnonAccess {
65 case "no-access":
66 return gm.NoAccess
67 case "read-only":
68 return gm.ReadOnlyAccess
69 case "read-write":
70 return gm.ReadWriteAccess
71 case "admin-access":
72 return gm.AdminAccess
73 default:
74 return gm.NoAccess
75 }
76}
77
78// accessForKey returns the access level for the given repo.
79//
80// If repo doesn't exist, then access is based on user's admin privileges, or
81// config.AnonAccess.
82// If repo exists, and private, then admins and collabs are allowed access.
83// If repo exists, and not private, then access is based on config.AnonAccess.
84func (cfg *Config) accessForKey(repo string, pk ssh.PublicKey) gm.AccessLevel {
85 anon := cfg.anonAccessLevel()
86 private := cfg.isPrivate(repo)
87 // Find user
88 for _, user := range cfg.Users {
89 for _, k := range user.PublicKeys {
90 apk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(k)))
91 if err != nil {
92 log.Error("malformed authorized key", "key", k)
93 return gm.NoAccess
94 }
95 if ssh.KeysEqual(pk, apk) {
96 if user.Admin {
97 return gm.AdminAccess
98 }
99 u := user
100 if cfg.isCollab(repo, &u) {
101 if anon > gm.ReadWriteAccess {
102 return anon
103 }
104 return gm.ReadWriteAccess
105 }
106 if !private {
107 if anon > gm.ReadOnlyAccess {
108 return anon
109 }
110 return gm.ReadOnlyAccess
111 }
112 }
113 }
114 }
115 // Don't restrict access to private repos if no users are configured.
116 // Return anon access level.
117 if private && len(cfg.Users) > 0 {
118 return gm.NoAccess
119 }
120 return anon
121}
122
123func (cfg *Config) findRepo(repo string) *RepoConfig {
124 for _, r := range cfg.Repos {
125 if r.Repo == repo {
126 return &r
127 }
128 }
129 return nil
130}
131
132func (cfg *Config) isPrivate(repo string) bool {
133 if r := cfg.findRepo(repo); r != nil {
134 return r.Private
135 }
136 return false
137}
138
139func (cfg *Config) isCollab(repo string, user *User) bool {
140 if user != nil {
141 for _, r := range user.CollabRepos {
142 if r == repo {
143 return true
144 }
145 }
146 if r := cfg.findRepo(repo); r != nil {
147 for _, c := range r.Collabs {
148 if c == user.Name {
149 return true
150 }
151 }
152 }
153 }
154 return false
155}