forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
1package state
2
3import (
4 "context"
5 "log"
6 "net/http"
7 "strconv"
8 "strings"
9 "time"
10
11 "slices"
12
13 "github.com/bluesky-social/indigo/atproto/identity"
14 "github.com/go-chi/chi/v5"
15 "go.opentelemetry.io/otel/attribute"
16 "tangled.sh/tangled.sh/core/appview/db"
17 "tangled.sh/tangled.sh/core/appview/middleware"
18)
19
20func knotRoleMiddleware(s *State, group string) middleware.Middleware {
21 return func(next http.Handler) http.Handler {
22 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23 ctx, span := s.t.TraceStart(r.Context(), "knotRoleMiddleware")
24 defer span.End()
25
26 // requires auth also
27 actor := s.auth.GetUser(r.WithContext(ctx))
28 if actor == nil {
29 // we need a logged in user
30 log.Printf("not logged in, redirecting")
31 http.Error(w, "Forbiden", http.StatusUnauthorized)
32 return
33 }
34 domain := chi.URLParam(r, "domain")
35 if domain == "" {
36 http.Error(w, "malformed url", http.StatusBadRequest)
37 return
38 }
39
40 ok, err := s.enforcer.E.HasGroupingPolicy(actor.Did, group, domain)
41 if err != nil || !ok {
42 // we need a logged in user
43 log.Printf("%s does not have perms of a %s in domain %s", actor.Did, group, domain)
44 http.Error(w, "Forbiden", http.StatusUnauthorized)
45 return
46 }
47
48 next.ServeHTTP(w, r.WithContext(ctx))
49 })
50 }
51}
52
53func KnotOwner(s *State) middleware.Middleware {
54 return knotRoleMiddleware(s, "server:owner")
55}
56
57func RepoPermissionMiddleware(s *State, requiredPerm string) middleware.Middleware {
58 return func(next http.Handler) http.Handler {
59 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
60 ctx, span := s.t.TraceStart(r.Context(), "RepoPermissionMiddleware")
61 defer span.End()
62
63 // requires auth also
64 actor := s.auth.GetUser(r.WithContext(ctx))
65 if actor == nil {
66 // we need a logged in user
67 log.Printf("not logged in, redirecting")
68 http.Error(w, "Forbiden", http.StatusUnauthorized)
69 return
70 }
71 f, err := s.fullyResolvedRepo(r.WithContext(ctx))
72 if err != nil {
73 http.Error(w, "malformed url", http.StatusBadRequest)
74 return
75 }
76
77 ok, err := s.enforcer.E.Enforce(actor.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
78 if err != nil || !ok {
79 // we need a logged in user
80 log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.OwnerSlashRepo())
81 http.Error(w, "Forbiden", http.StatusUnauthorized)
82 return
83 }
84
85 next.ServeHTTP(w, r.WithContext(ctx))
86 })
87 }
88}
89
90func StripLeadingAt(next http.Handler) http.Handler {
91 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
92 path := req.URL.EscapedPath()
93 if strings.HasPrefix(path, "/@") {
94 req.URL.RawPath = "/" + strings.TrimPrefix(path, "/@")
95 }
96 next.ServeHTTP(w, req)
97 })
98}
99
100func ResolveIdent(s *State) middleware.Middleware {
101 excluded := []string{"favicon.ico"}
102
103 return func(next http.Handler) http.Handler {
104 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
105 didOrHandle := chi.URLParam(req, "user")
106 if slices.Contains(excluded, didOrHandle) {
107 next.ServeHTTP(w, req)
108 return
109 }
110
111 ctx, span := s.t.TraceStart(req.Context(), "ResolveIdent")
112 defer span.End()
113
114 id, err := s.resolver.ResolveIdent(ctx, didOrHandle)
115 if err != nil {
116 // invalid did or handle
117 log.Println("failed to resolve did/handle:", err)
118 w.WriteHeader(http.StatusNotFound)
119 return
120 }
121
122 ctx = context.WithValue(ctx, "resolvedId", *id)
123
124 next.ServeHTTP(w, req.WithContext(ctx))
125 })
126 }
127}
128
129func ResolveRepo(s *State) middleware.Middleware {
130 return func(next http.Handler) http.Handler {
131 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
132 ctx, span := s.t.TraceStart(req.Context(), "ResolveRepo")
133 defer span.End()
134
135 repoName := chi.URLParam(req, "repo")
136 id, ok := ctx.Value("resolvedId").(identity.Identity)
137 if !ok {
138 log.Println("malformed middleware")
139 w.WriteHeader(http.StatusInternalServerError)
140 return
141 }
142
143 repo, err := db.GetRepo(ctx, s.db, id.DID.String(), repoName)
144 if err != nil {
145 // invalid did or handle
146 log.Println("failed to resolve repo")
147 w.WriteHeader(http.StatusNotFound)
148 return
149 }
150
151 ctx = context.WithValue(ctx, "knot", repo.Knot)
152 ctx = context.WithValue(ctx, "repoAt", repo.AtUri)
153 ctx = context.WithValue(ctx, "repoDescription", repo.Description)
154 ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339))
155 next.ServeHTTP(w, req.WithContext(ctx))
156 })
157 }
158}
159
160// middleware that is tacked on top of /{user}/{repo}/pulls/{pull}
161func ResolvePull(s *State) middleware.Middleware {
162 return func(next http.Handler) http.Handler {
163 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
164 ctx, span := s.t.TraceStart(r.Context(), "ResolvePull")
165 defer span.End()
166
167 f, err := s.fullyResolvedRepo(r.WithContext(ctx))
168 if err != nil {
169 log.Println("failed to fully resolve repo", err)
170 http.Error(w, "invalid repo url", http.StatusNotFound)
171 return
172 }
173
174 prId := chi.URLParam(r, "pull")
175 prIdInt, err := strconv.Atoi(prId)
176 if err != nil {
177 http.Error(w, "bad pr id", http.StatusBadRequest)
178 log.Println("failed to parse pr id", err)
179 return
180 }
181
182 pr, err := db.GetPull(ctx, s.db, f.RepoAt, prIdInt)
183 if err != nil {
184 log.Println("failed to get pull and comments", err)
185 return
186 }
187
188 span.SetAttributes(attribute.Int("pull.id", prIdInt))
189
190 ctx = context.WithValue(ctx, "pull", pr)
191
192 next.ServeHTTP(w, r.WithContext(ctx))
193 })
194 }
195}