+7
-1
cmd/knotserver/main.go
+7
-1
cmd/knotserver/main.go
···
46
l.Error("failed to setup server", "error", err)
47
return
48
}
49
addr := fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
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
+
57
l.Info("starting main server", "address", addr)
58
l.Error("server error", "error", http.ListenAndServe(addr, mux))
59
+
60
return
61
}
+44
-15
cmd/repoguard/main.go
+44
-15
cmd/repoguard/main.go
···
5
"flag"
6
"fmt"
7
"log"
8
"os"
9
"os/exec"
10
"path"
···
21
clientIP string
22
23
// 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")
27
)
28
29
func main() {
···
40
}
41
}
42
43
-
if *allowedUser == "" {
44
exitWithLog("access denied: no user specified")
45
}
46
47
sshCommand := os.Getenv("SSH_ORIGINAL_COMMAND")
48
49
logEvent("Connection attempt", map[string]interface{}{
50
-
"user": *allowedUser,
51
"command": sshCommand,
52
"client": clientIP,
53
})
···
63
64
gitCommand := cmdParts[0]
65
66
-
// example.com/repo
67
-
handlePath := strings.Trim(cmdParts[1], "'")
68
-
repoName := handleToDid(handlePath)
69
70
validCommands := map[string]bool{
71
"git-receive-pack": true,
···
76
exitWithLog("access denied: invalid git command")
77
}
78
79
-
did := path.Dir(repoName)
80
if gitCommand != "git-upload-pack" {
81
-
if !isAllowedUser(*allowedUser, did) {
82
exitWithLog("access denied: user not allowed")
83
}
84
}
85
86
-
fullPath := filepath.Join(*baseDirFlag, repoName)
87
fullPath = filepath.Clean(fullPath)
88
89
logEvent("Processing command", map[string]interface{}{
90
-
"user": *allowedUser,
91
"command": gitCommand,
92
"repo": repoName,
93
"fullPath": fullPath,
···
104
}
105
106
logEvent("Command completed", map[string]interface{}{
107
-
"user": *allowedUser,
108
"command": gitCommand,
109
"repo": repoName,
110
"success": true,
111
})
112
}
113
114
func handleToDid(handlePath string) string {
···
166
}
167
}
168
169
-
func isAllowedUser(user, did string) bool {
170
-
return user == did
171
}
···
5
"flag"
6
"fmt"
7
"log"
8
+
"net/http"
9
+
"net/url"
10
"os"
11
"os/exec"
12
"path"
···
23
clientIP string
24
25
// Command line flags
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")
30
)
31
32
func main() {
···
43
}
44
}
45
46
+
if *incomingUser == "" {
47
exitWithLog("access denied: no user specified")
48
}
49
50
sshCommand := os.Getenv("SSH_ORIGINAL_COMMAND")
51
52
logEvent("Connection attempt", map[string]interface{}{
53
+
"user": *incomingUser,
54
"command": sshCommand,
55
"client": clientIP,
56
})
···
66
67
gitCommand := cmdParts[0]
68
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)
80
81
validCommands := map[string]bool{
82
"git-receive-pack": true,
···
87
exitWithLog("access denied: invalid git command")
88
}
89
90
if gitCommand != "git-upload-pack" {
91
+
if !isPushPermitted(*incomingUser, qualifiedRepoName) {
92
exitWithLog("access denied: user not allowed")
93
}
94
}
95
96
+
fullPath := filepath.Join(*baseDirFlag, qualifiedRepoName)
97
fullPath = filepath.Clean(fullPath)
98
99
logEvent("Processing command", map[string]interface{}{
100
+
"user": *incomingUser,
101
"command": gitCommand,
102
"repo": repoName,
103
"fullPath": fullPath,
···
114
}
115
116
logEvent("Command completed", map[string]interface{}{
117
+
"user": *incomingUser,
118
"command": gitCommand,
119
"repo": repoName,
120
"success": true,
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()
132
}
133
134
func handleToDid(handlePath string) string {
···
186
}
187
}
188
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
200
}
+5
-4
knotserver/config/config.go
+5
-4
knotserver/config/config.go
···
13
}
14
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"`
20
// This disables signature verification so use with caution.
21
Dev bool `env:"DEV, default=false"`
22
}
···
13
}
14
15
type Server struct {
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"`
21
// This disables signature verification so use with caution.
22
Dev bool `env:"DEV, default=false"`
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
return e.isRole(user, "server:member", domain)
132
}
133
134
+
func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
135
+
return e.E.Enforce(user, domain, repo, "repo:push")
136
+
}
137
+
138
// keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin
139
func keyMatch2Func(args ...interface{}) (interface{}, error) {
140
name1 := args[0].(string)