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}