+7
-1
cmd/knotserver/main.go
+7
-1
cmd/knotserver/main.go
···
46
46
l.Error("failed to setup server", "error", err)
47
47
return
48
48
}
49
-
50
49
addr := fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
51
50
51
+
imux := knotserver.Internal(ctx, db, e)
52
+
iaddr := fmt.Sprintf("%s:%d", c.Server.Host, c.Server.InternalPort)
53
+
54
+
l.Info("starting internal server", "address", iaddr)
55
+
go http.ListenAndServe(iaddr, imux)
56
+
52
57
l.Info("starting main server", "address", addr)
53
58
l.Error("server error", "error", http.ListenAndServe(addr, mux))
59
+
54
60
return
55
61
}
+44
-15
cmd/repoguard/main.go
+44
-15
cmd/repoguard/main.go
···
5
5
"flag"
6
6
"fmt"
7
7
"log"
8
+
"net/http"
9
+
"net/url"
8
10
"os"
9
11
"os/exec"
10
12
"path"
···
21
23
clientIP string
22
24
23
25
// Command line flags
24
-
allowedUser = flag.String("user", "", "Allowed git user")
25
-
baseDirFlag = flag.String("base-dir", "/home/git", "Base directory for git repositories")
26
-
logPathFlag = flag.String("log-path", "/var/log/git-wrapper.log", "Path to log file")
26
+
incomingUser = flag.String("user", "", "Allowed git user")
27
+
baseDirFlag = flag.String("base-dir", "/home/git", "Base directory for git repositories")
28
+
logPathFlag = flag.String("log-path", "/var/log/git-wrapper.log", "Path to log file")
29
+
endpoint = flag.String("internal-api", "http://localhost:5555", "Internal API endpoint")
27
30
)
28
31
29
32
func main() {
···
40
43
}
41
44
}
42
45
43
-
if *allowedUser == "" {
46
+
if *incomingUser == "" {
44
47
exitWithLog("access denied: no user specified")
45
48
}
46
49
47
50
sshCommand := os.Getenv("SSH_ORIGINAL_COMMAND")
48
51
49
52
logEvent("Connection attempt", map[string]interface{}{
50
-
"user": *allowedUser,
53
+
"user": *incomingUser,
51
54
"command": sshCommand,
52
55
"client": clientIP,
53
56
})
···
63
66
64
67
gitCommand := cmdParts[0]
65
68
66
-
// example.com/repo
67
-
handlePath := strings.Trim(cmdParts[1], "'")
68
-
repoName := handleToDid(handlePath)
69
+
// did:foo/repo-name or
70
+
// handle/repo-name
71
+
components := filepath.SplitList(cmdParts[2])
72
+
if len(components) != 2 {
73
+
exitWithLog("invalid repo format, needs <user>/<repo>")
74
+
}
75
+
76
+
didOrHandle := components[0]
77
+
did := resolveToDid(didOrHandle)
78
+
repoName := components[1]
79
+
qualifiedRepoName := filepath.Join(did, repoName)
69
80
70
81
validCommands := map[string]bool{
71
82
"git-receive-pack": true,
···
76
87
exitWithLog("access denied: invalid git command")
77
88
}
78
89
79
-
did := path.Dir(repoName)
80
90
if gitCommand != "git-upload-pack" {
81
-
if !isAllowedUser(*allowedUser, did) {
91
+
if !isPushPermitted(*incomingUser, qualifiedRepoName) {
82
92
exitWithLog("access denied: user not allowed")
83
93
}
84
94
}
85
95
86
-
fullPath := filepath.Join(*baseDirFlag, repoName)
96
+
fullPath := filepath.Join(*baseDirFlag, qualifiedRepoName)
87
97
fullPath = filepath.Clean(fullPath)
88
98
89
99
logEvent("Processing command", map[string]interface{}{
90
-
"user": *allowedUser,
100
+
"user": *incomingUser,
91
101
"command": gitCommand,
92
102
"repo": repoName,
93
103
"fullPath": fullPath,
···
104
114
}
105
115
106
116
logEvent("Command completed", map[string]interface{}{
107
-
"user": *allowedUser,
117
+
"user": *incomingUser,
108
118
"command": gitCommand,
109
119
"repo": repoName,
110
120
"success": true,
111
121
})
122
+
}
123
+
124
+
func resolveToDid(didOrHandle string) string {
125
+
ident, err := auth.ResolveIdent(context.Background(), didOrHandle)
126
+
if err != nil {
127
+
exitWithLog(fmt.Sprintf("error resolving handle: %v", err))
128
+
}
129
+
130
+
// did:plc:foobarbaz/repo
131
+
return ident.DID.String()
112
132
}
113
133
114
134
func handleToDid(handlePath string) string {
···
166
186
}
167
187
}
168
188
169
-
func isAllowedUser(user, did string) bool {
170
-
return user == did
189
+
func isPushPermitted(user, qualifiedRepoName string) bool {
190
+
url, _ := url.Parse(*endpoint + "/push-allowed/")
191
+
url.Query().Add(user, user)
192
+
url.Query().Add(user, qualifiedRepoName)
193
+
194
+
req, err := http.Get(url.String())
195
+
if err != nil {
196
+
exitWithLog(fmt.Sprintf("error verifying permissions: %v", err))
197
+
}
198
+
199
+
return req.StatusCode == http.StatusNoContent
171
200
}
+5
-4
knotserver/config/config.go
+5
-4
knotserver/config/config.go
···
13
13
}
14
14
15
15
type Server struct {
16
-
Host string `env:"HOST, default=0.0.0.0"`
17
-
Port int `env:"PORT, default=5555"`
18
-
Secret string `env:"SECRET, required"`
19
-
DBPath string `env:"DB_PATH, default=knotserver.db"`
16
+
Host string `env:"HOST, default=0.0.0.0"`
17
+
Port int `env:"PORT, default=5555"`
18
+
InternalPort int `env:"PORT, default=5444"`
19
+
Secret string `env:"SECRET, required"`
20
+
DBPath string `env:"DB_PATH, default=knotserver.db"`
20
21
// This disables signature verification so use with caution.
21
22
Dev bool `env:"DEV, default=false"`
22
23
}
+47
knotserver/internal.go
+47
knotserver/internal.go
···
1
+
package knotserver
2
+
3
+
import (
4
+
"context"
5
+
"net/http"
6
+
7
+
"github.com/go-chi/chi/v5"
8
+
"github.com/sotangled/tangled/knotserver/db"
9
+
"github.com/sotangled/tangled/rbac"
10
+
)
11
+
12
+
type InternalHandle struct {
13
+
db *db.DB
14
+
e *rbac.Enforcer
15
+
}
16
+
17
+
func (h *InternalHandle) PushAllowed(w http.ResponseWriter, r *http.Request) {
18
+
user := r.URL.Query().Get("user")
19
+
repo := r.URL.Query().Get("repo")
20
+
21
+
if user == "" || repo == "" {
22
+
w.WriteHeader(http.StatusBadRequest)
23
+
return
24
+
}
25
+
26
+
ok, err := h.e.IsPushAllowed(user, ThisServer, repo)
27
+
if err != nil || !ok {
28
+
w.WriteHeader(http.StatusForbidden)
29
+
return
30
+
}
31
+
32
+
w.WriteHeader(http.StatusNoContent)
33
+
return
34
+
}
35
+
36
+
func Internal(ctx context.Context, db *db.DB, e *rbac.Enforcer) http.Handler {
37
+
r := chi.NewRouter()
38
+
39
+
h := InternalHandle{
40
+
db,
41
+
e,
42
+
}
43
+
44
+
r.Get("/push-allowed", h.PushAllowed)
45
+
46
+
return r
47
+
}
+4
rbac/rbac.go
+4
rbac/rbac.go
···
131
131
return e.isRole(user, "server:member", domain)
132
132
}
133
133
134
+
func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
135
+
return e.E.Enforce(user, domain, repo, "repo:push")
136
+
}
137
+
134
138
// keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin
135
139
func keyMatch2Func(args ...interface{}) (interface{}, error) {
136
140
name1 := args[0].(string)