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