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