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 if pk != nil {
90 for _, user := range cfg.Users {
91 for _, k := range user.PublicKeys {
92 apk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(k)))
93 if err != nil {
94 log.Printf("error: malformed authorized key: '%s'", k)
95 return gm.NoAccess
96 }
97 if ssh.KeysEqual(pk, apk) {
98 if user.Admin {
99 return gm.AdminAccess
100 }
101 u := user
102 if cfg.isCollab(repo, &u) {
103 if anon > gm.ReadWriteAccess {
104 return anon
105 }
106 return gm.ReadWriteAccess
107 }
108 if !private {
109 if anon > gm.ReadOnlyAccess {
110 return anon
111 }
112 return gm.ReadOnlyAccess
113 }
114 }
115 }
116 }
117 }
118 // Don't restrict access to private repos if no users are configured.
119 // Return anon access level.
120 if private && len(cfg.Users) > 0 {
121 return gm.NoAccess
122 }
123 return anon
124}
125
126func (cfg *Config) findRepo(repo string) *RepoConfig {
127 for _, r := range cfg.Repos {
128 if r.Repo == repo {
129 return &r
130 }
131 }
132 return nil
133}
134
135func (cfg *Config) isPrivate(repo string) bool {
136 if r := cfg.findRepo(repo); r != nil {
137 return r.Private
138 }
139 return false
140}
141
142func (cfg *Config) isCollab(repo string, user *User) bool {
143 if user != nil {
144 for _, r := range user.CollabRepos {
145 if r == repo {
146 return true
147 }
148 }
149 if r := cfg.findRepo(repo); r != nil {
150 for _, c := range r.Collabs {
151 if c == user.Name {
152 return true
153 }
154 }
155 }
156 }
157 return false
158}