Monorepo for Tangled tangled.org

knotserver: remove all mentions of knotserver secret

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 0808c86a 08fa350b

verified
Changed files
+427 -1472
knotclient
knotserver
rbac
-37
knotclient/signer.go
··· 7 7 "encoding/hex" 8 8 "encoding/json" 9 9 "fmt" 10 - "io" 11 - "log" 12 10 "net/http" 13 11 "net/url" 14 12 "time" ··· 103 101 } 104 102 105 103 return s.client.Do(req) 106 - } 107 - 108 - func (s *SignedClient) RepoLanguages(ownerDid, repoName, ref string) (*types.RepoLanguageResponse, error) { 109 - const ( 110 - Method = "GET" 111 - ) 112 - endpoint := fmt.Sprintf("/%s/%s/languages/%s", ownerDid, repoName, url.PathEscape(ref)) 113 - 114 - req, err := s.newRequest(Method, endpoint, nil) 115 - if err != nil { 116 - return nil, err 117 - } 118 - 119 - resp, err := s.client.Do(req) 120 - if err != nil { 121 - return nil, err 122 - } 123 - 124 - var result types.RepoLanguageResponse 125 - if resp.StatusCode != http.StatusOK { 126 - log.Println("failed to calculate languages", resp.Status) 127 - return &types.RepoLanguageResponse{}, nil 128 - } 129 - 130 - body, err := io.ReadAll(resp.Body) 131 - if err != nil { 132 - return nil, err 133 - } 134 - 135 - err = json.Unmarshal(body, &result) 136 - if err != nil { 137 - return nil, err 138 - } 139 - 140 - return &result, nil 141 104 } 142 105 143 106 func (s *SignedClient) RepoForkAheadBehind(ownerDid, source, name, branch, hiddenRef string) (*http.Response, error) {
+35
knotclient/unsigned.go
··· 248 248 249 249 return &formatPatchResponse, nil 250 250 } 251 + 252 + func (s *UnsignedClient) RepoLanguages(ownerDid, repoName, ref string) (*types.RepoLanguageResponse, error) { 253 + const ( 254 + Method = "GET" 255 + ) 256 + endpoint := fmt.Sprintf("/%s/%s/languages/%s", ownerDid, repoName, url.PathEscape(ref)) 257 + 258 + req, err := s.newRequest(Method, endpoint, nil, nil) 259 + if err != nil { 260 + return nil, err 261 + } 262 + 263 + resp, err := s.client.Do(req) 264 + if err != nil { 265 + return nil, err 266 + } 267 + 268 + var result types.RepoLanguageResponse 269 + if resp.StatusCode != http.StatusOK { 270 + log.Println("failed to calculate languages", resp.Status) 271 + return &types.RepoLanguageResponse{}, nil 272 + } 273 + 274 + body, err := io.ReadAll(resp.Body) 275 + if err != nil { 276 + return nil, err 277 + } 278 + 279 + err = json.Unmarshal(body, &result) 280 + if err != nil { 281 + return nil, err 282 + } 283 + 284 + return &result, nil 285 + }
+236 -177
knotserver/handler.go
··· 1 1 package knotserver 2 2 3 3 import ( 4 + "compress/gzip" 4 5 "context" 6 + "crypto/sha256" 7 + "encoding/json" 8 + "errors" 5 9 "fmt" 6 - "log/slog" 10 + "log" 7 11 "net/http" 8 - "runtime/debug" 12 + "net/url" 13 + "path/filepath" 14 + "strconv" 15 + "strings" 16 + "sync" 17 + "time" 9 18 19 + securejoin "github.com/cyphar/filepath-securejoin" 20 + "github.com/gliderlabs/ssh" 10 21 "github.com/go-chi/chi/v5" 11 - "tangled.sh/tangled.sh/core/idresolver" 12 - "tangled.sh/tangled.sh/core/jetstream" 13 - "tangled.sh/tangled.sh/core/knotserver/config" 22 + "github.com/go-git/go-git/v5/plumbing" 23 + "github.com/go-git/go-git/v5/plumbing/object" 14 24 "tangled.sh/tangled.sh/core/knotserver/db" 15 - "tangled.sh/tangled.sh/core/knotserver/xrpc" 16 - tlog "tangled.sh/tangled.sh/core/log" 17 - "tangled.sh/tangled.sh/core/notifier" 18 - "tangled.sh/tangled.sh/core/rbac" 25 + "tangled.sh/tangled.sh/core/knotserver/git" 19 26 "tangled.sh/tangled.sh/core/types" 20 27 ) 21 28 22 - type Handle struct { 23 - c *config.Config 24 - db *db.DB 25 - jc *jetstream.JetstreamClient 26 - e *rbac.Enforcer 27 - l *slog.Logger 28 - n *notifier.Notifier 29 - resolver *idresolver.Resolver 29 + func (h *Handle) Index(w http.ResponseWriter, r *http.Request) { 30 + w.Write([]byte("This is a knot server. More info at https://tangled.sh")) 30 31 } 31 32 32 - func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger, n *notifier.Notifier) (http.Handler, error) { 33 - r := chi.NewRouter() 33 + func (h *Handle) Capabilities(w http.ResponseWriter, r *http.Request) { 34 + w.Header().Set("Content-Type", "application/json") 34 35 35 - h := Handle{ 36 - c: c, 37 - db: db, 38 - e: e, 39 - l: l, 40 - jc: jc, 41 - n: n, 42 - resolver: idresolver.DefaultResolver(), 36 + capabilities := map[string]any{ 37 + "pull_requests": map[string]any{ 38 + "format_patch": true, 39 + "patch_submissions": true, 40 + "branch_submissions": true, 41 + "fork_submissions": true, 42 + }, 43 + "xrpc": true, 43 44 } 44 45 45 - err := e.AddKnot(rbac.ThisServer) 46 + jsonData, err := json.Marshal(capabilities) 46 47 if err != nil { 47 - return nil, fmt.Errorf("failed to setup enforcer: %w", err) 48 + http.Error(w, "Failed to serialize JSON", http.StatusInternalServerError) 49 + return 48 50 } 49 51 50 - // configure owner 51 - if err = h.configureOwner(); err != nil { 52 - return nil, err 53 - } 54 - h.l.Info("owner set", "did", h.c.Server.Owner) 55 - h.jc.AddDid(h.c.Server.Owner) 52 + w.Write(jsonData) 53 + } 54 + 55 + func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) { 56 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 57 + l := h.l.With("path", path, "handler", "RepoIndex") 58 + ref := chi.URLParam(r, "ref") 59 + ref, _ = url.PathUnescape(ref) 56 60 57 - // configure known-dids in jetstream consumer 58 - dids, err := h.db.GetAllDids() 61 + gr, err := git.Open(path, ref) 59 62 if err != nil { 60 - return nil, fmt.Errorf("failed to get all dids: %w", err) 61 - } 62 - for _, d := range dids { 63 - jc.AddDid(d) 63 + plain, err2 := git.PlainOpen(path) 64 + if err2 != nil { 65 + l.Error("opening repo", "error", err2.Error()) 66 + notFound(w) 67 + return 68 + } 69 + branches, _ := plain.Branches() 70 + 71 + log.Println(err) 72 + 73 + if errors.Is(err, plumbing.ErrReferenceNotFound) { 74 + resp := types.RepoIndexResponse{ 75 + IsEmpty: true, 76 + Branches: branches, 77 + } 78 + writeJSON(w, resp) 79 + return 80 + } else { 81 + l.Error("opening repo", "error", err.Error()) 82 + notFound(w) 83 + return 84 + } 64 85 } 65 86 66 - err = h.jc.StartJetstream(ctx, h.processMessages) 67 - if err != nil { 68 - return nil, fmt.Errorf("failed to start jetstream: %w", err) 69 - } 87 + var ( 88 + commits []*object.Commit 89 + total int 90 + branches []types.Branch 91 + files []types.NiceTree 92 + tags []object.Tag 93 + ) 70 94 71 - r.Get("/", h.Index) 72 - r.Get("/capabilities", h.Capabilities) 73 - r.Get("/version", h.Version) 74 - r.Get("/owner", func(w http.ResponseWriter, r *http.Request) { 75 - w.Write([]byte(h.c.Server.Owner)) 76 - }) 77 - r.Route("/{did}", func(r chi.Router) { 78 - // Repo routes 79 - r.Route("/{name}", func(r chi.Router) { 80 - r.Route("/collaborator", func(r chi.Router) { 81 - r.Use(h.VerifySignature) 82 - r.Post("/add", h.AddRepoCollaborator) 83 - }) 95 + var wg sync.WaitGroup 96 + errorsCh := make(chan error, 5) 84 97 85 - r.Route("/languages", func(r chi.Router) { 86 - r.With(h.VerifySignature) 87 - r.Get("/", h.RepoLanguages) 88 - r.Get("/{ref}", h.RepoLanguages) 89 - }) 98 + wg.Add(1) 99 + go func() { 100 + defer wg.Done() 101 + cs, err := gr.Commits(0, 60) 102 + if err != nil { 103 + errorsCh <- fmt.Errorf("commits: %w", err) 104 + return 105 + } 106 + commits = cs 107 + }() 90 108 91 - r.Get("/", h.RepoIndex) 92 - r.Get("/info/refs", h.InfoRefs) 93 - r.Post("/git-upload-pack", h.UploadPack) 94 - r.Post("/git-receive-pack", h.ReceivePack) 95 - r.Get("/compare/{rev1}/{rev2}", h.Compare) // git diff-tree compare of two objects 109 + wg.Add(1) 110 + go func() { 111 + defer wg.Done() 112 + t, err := gr.TotalCommits() 113 + if err != nil { 114 + errorsCh <- fmt.Errorf("calculating total: %w", err) 115 + return 116 + } 117 + total = t 118 + }() 96 119 97 - r.With(h.VerifySignature).Post("/hidden-ref/{forkRef}/{remoteRef}", h.NewHiddenRef) 120 + wg.Add(1) 121 + go func() { 122 + defer wg.Done() 123 + bs, err := gr.Branches() 124 + if err != nil { 125 + errorsCh <- fmt.Errorf("fetching branches: %w", err) 126 + return 127 + } 128 + branches = bs 129 + }() 98 130 99 - r.Route("/merge", func(r chi.Router) { 100 - r.With(h.VerifySignature) 101 - r.Post("/", h.Merge) 102 - r.Post("/check", h.MergeCheck) 103 - }) 131 + wg.Add(1) 132 + go func() { 133 + defer wg.Done() 134 + ts, err := gr.Tags() 135 + if err != nil { 136 + errorsCh <- fmt.Errorf("fetching tags: %w", err) 137 + return 138 + } 139 + tags = ts 140 + }() 104 141 105 - r.Route("/tree/{ref}", func(r chi.Router) { 106 - r.Get("/", h.RepoIndex) 107 - r.Get("/*", h.RepoTree) 108 - }) 142 + wg.Add(1) 143 + go func() { 144 + defer wg.Done() 145 + fs, err := gr.FileTree(r.Context(), "") 146 + if err != nil { 147 + errorsCh <- fmt.Errorf("fetching filetree: %w", err) 148 + return 149 + } 150 + files = fs 151 + }() 109 152 110 - r.Route("/blob/{ref}", func(r chi.Router) { 111 - r.Get("/*", h.Blob) 112 - }) 153 + wg.Wait() 154 + close(errorsCh) 113 155 114 - r.Route("/raw/{ref}", func(r chi.Router) { 115 - r.Get("/*", h.BlobRaw) 116 - }) 156 + // show any errors 157 + for err := range errorsCh { 158 + l.Error("loading repo", "error", err.Error()) 159 + writeError(w, err.Error(), http.StatusInternalServerError) 160 + return 161 + } 117 162 118 - r.Get("/log/{ref}", h.Log) 119 - r.Get("/archive/{file}", h.Archive) 120 - r.Get("/commit/{ref}", h.Diff) 121 - r.Get("/tags", h.Tags) 122 - r.Route("/branches", func(r chi.Router) { 123 - r.Get("/", h.Branches) 124 - r.Get("/{branch}", h.Branch) 125 - r.Route("/default", func(r chi.Router) { 126 - r.Get("/", h.DefaultBranch) 127 - r.With(h.VerifySignature).Put("/", h.SetDefaultBranch) 128 - }) 129 - }) 130 - }) 131 - }) 163 + rtags := []*types.TagReference{} 164 + for _, tag := range tags { 165 + var target *object.Tag 166 + if tag.Target != plumbing.ZeroHash { 167 + target = &tag 168 + } 169 + tr := types.TagReference{ 170 + Tag: target, 171 + } 132 172 133 - // xrpc apis 134 - r.Mount("/xrpc", h.XrpcRouter()) 173 + tr.Reference = types.Reference{ 174 + Name: tag.Name, 175 + Hash: tag.Hash.String(), 176 + } 135 177 136 - // Create a new repository. 137 - r.Route("/repo", func(r chi.Router) { 138 - r.Use(h.VerifySignature) 139 - r.Delete("/", h.RemoveRepo) 140 - r.Route("/fork", func(r chi.Router) { 141 - r.Post("/", h.RepoFork) 142 - r.Post("/sync/*", h.RepoForkSync) 143 - r.Get("/sync/*", h.RepoForkAheadBehind) 144 - }) 145 - }) 178 + if tag.Message != "" { 179 + tr.Message = tag.Message 180 + } 146 181 147 - r.Route("/member", func(r chi.Router) { 148 - r.Use(h.VerifySignature) 149 - r.Put("/add", h.AddMember) 150 - }) 182 + rtags = append(rtags, &tr) 183 + } 151 184 152 - // Socket that streams git oplogs 153 - r.Get("/events", h.Events) 185 + var readmeContent string 186 + var readmeFile string 187 + for _, readme := range h.c.Repo.Readme { 188 + content, _ := gr.FileContent(readme) 189 + if len(content) > 0 { 190 + readmeContent = string(content) 191 + readmeFile = readme 192 + } 193 + } 154 194 155 - // Health check. Used for two-way verification with appview. 156 - r.With(h.VerifySignature).Get("/health", h.Health) 195 + if ref == "" { 196 + mainBranch, err := gr.FindMainBranch() 197 + if err != nil { 198 + writeError(w, err.Error(), http.StatusInternalServerError) 199 + l.Error("finding main branch", "error", err.Error()) 200 + return 201 + } 202 + ref = mainBranch 203 + } 157 204 158 - // All public keys on the knot. 159 - r.Get("/keys", h.Keys) 205 + resp := types.RepoIndexResponse{ 206 + IsEmpty: false, 207 + Ref: ref, 208 + Commits: commits, 209 + Description: getDescription(path), 210 + Readme: readmeContent, 211 + ReadmeFileName: readmeFile, 212 + Files: files, 213 + Branches: branches, 214 + Tags: rtags, 215 + TotalCommits: total, 216 + } 160 217 161 - return r, nil 218 + writeJSON(w, resp) 162 219 } 163 220 164 - func (h *Handle) XrpcRouter() http.Handler { 165 - logger := tlog.New("knots") 221 + func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) { 222 + treePath := chi.URLParam(r, "*") 223 + ref := chi.URLParam(r, "ref") 224 + ref, _ = url.PathUnescape(ref) 166 225 167 - serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String()) 226 + l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath) 168 227 169 - xrpc := &xrpc.Xrpc{ 170 - Config: h.c, 171 - Db: h.db, 172 - Ingester: h.jc, 173 - Enforcer: h.e, 174 - Logger: logger, 175 - Notifier: h.n, 176 - Resolver: h.resolver, 177 - ServiceAuth: serviceAuth, 228 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 229 + gr, err := git.Open(path, ref) 230 + if err != nil { 231 + notFound(w) 232 + return 178 233 } 179 - return xrpc.Router() 180 - } 181 234 182 - // version is set during build time. 183 - var version string 184 - 185 - func (h *Handle) Version(w http.ResponseWriter, r *http.Request) { 186 - if version == "" { 187 - info, ok := debug.ReadBuildInfo() 188 - if !ok { 189 - http.Error(w, "failed to read build info", http.StatusInternalServerError) 190 - return 191 - } 192 - 193 - var modVer string 194 - for _, mod := range info.Deps { 195 - if mod.Path == "tangled.sh/tangled.sh/knotserver" { 196 - version = mod.Version 197 - break 198 - } 199 - } 235 + files, err := gr.FileTree(r.Context(), treePath) 236 + if err != nil { 237 + writeError(w, err.Error(), http.StatusInternalServerError) 238 + l.Error("file tree", "error", err.Error()) 239 + return 240 + } 200 241 201 - if modVer == "" { 202 - version = "unknown" 203 - } 242 + resp := types.RepoTreeResponse{ 243 + Ref: ref, 244 + Parent: treePath, 245 + Description: getDescription(path), 246 + DotDot: filepath.Dir(treePath), 247 + Files: files, 204 248 } 205 249 206 - w.Header().Set("Content-Type", "text/plain; charset=utf-8") 207 - fmt.Fprintf(w, "knotserver/%s", version) 250 + writeJSON(w, resp) 208 251 } 209 252 210 - func (h *Handle) configureOwner() error { 211 - cfgOwner := h.c.Server.Owner 253 + func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) { 254 + treePath := chi.URLParam(r, "*") 255 + ref := chi.URLParam(r, "ref") 256 + ref, _ = url.PathUnescape(ref) 212 257 213 - rbacDomain := "thisserver" 258 + l := h.l.With("handler", "BlobRaw", "ref", ref, "treePath", treePath) 214 259 215 - existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain) 260 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 261 + gr, err := git.Open(path, ref) 216 262 if err != nil { 217 - return err 263 + notFound(w) 264 + return 218 265 } 219 266 220 - switch len(existing) { 221 - case 0: 222 - // no owner configured, continue 223 - case 1: 224 - // find existing owner 225 - existingOwner := existing[0] 267 + contents, err := gr.RawContent(treePath) 268 + if err != nil { 269 + writeError(w, err.Error(), http.StatusBadRequest) 270 + l.Error("file content", "error", err.Error()) 271 + return 272 + } 273 + 274 + mimeType := http.DetectContentType(contents) 275 + 276 + // exception for svg 277 + if filepath.Ext(treePath) == ".svg" { 278 + mimeType = "image/svg+xml" 279 + } 280 + 281 + contentHash := sha256.Sum256(contents) 282 + eTag := fmt.Sprintf("\"%x\"", contentHash) 226 283 227 - // no ownership change, this is okay 228 - if existingOwner == h.c.Server.Owner { 229 - break 284 + // allow image, video, and text/plain files to be served directly 285 + switch { 286 + case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"): 287 + if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag { 288 + w.WriteHeader(http.StatusNotModified) 289 + return 230 290 } 291 + w.Header().Set("ETag", eTag) 231 292 232 - // remove existing owner 233 - err = h.e.RemoveKnotOwner(rbacDomain, existingOwner) 234 - if err != nil { 235 - return nil 236 - } 293 + case strings.HasPrefix(mimeType, "text/plain"): 294 + w.Header().Set("Cache-Control", "public, no-cache") 295 + 237 296 default: 238 297 l.Error("attempted to serve disallowed file type", "mimetype", mimeType) 239 298 writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden)
+10 -29
knotserver/ingester.go
··· 8 8 "net/http" 9 9 "net/url" 10 10 "path/filepath" 11 - "slices" 12 11 "strings" 13 12 14 13 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 103 102 l = l.With("target_branch", record.TargetBranch) 104 103 105 104 if record.Source == nil { 106 - reason := "not a branch-based pull request" 107 - l.Info("ignoring pull record", "reason", reason) 108 - return fmt.Errorf("ignoring pull record: %s", reason) 105 + return fmt.Errorf("ignoring pull record: not a branch-based pull request") 109 106 } 110 107 111 108 if record.Source.Repo != nil { 112 - reason := "fork based pull" 113 - l.Info("ignoring pull record", "reason", reason) 114 - return fmt.Errorf("ignoring pull record: %s", reason) 115 - } 116 - 117 - allDids, err := h.db.GetAllDids() 118 - if err != nil { 119 - return err 120 - } 121 - 122 - // presently: we only process PRs from collaborators for pipelines 123 - if !slices.Contains(allDids, did) { 124 - reason := "not a known did" 125 - l.Info("rejecting pull record", "reason", reason) 126 - return fmt.Errorf("rejected pull record: %s, %s", reason, did) 109 + return fmt.Errorf("ignoring pull record: fork based pull") 127 110 } 128 111 129 112 repoAt, err := syntax.ParseATURI(record.TargetRepo) 130 113 if err != nil { 131 - return err 114 + return fmt.Errorf("failed to parse ATURI: %w", err) 132 115 } 133 116 134 117 // resolve this aturi to extract the repo record ··· 144 127 145 128 resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 146 129 if err != nil { 147 - return err 130 + return fmt.Errorf("failed to resolver repo: %w", err) 148 131 } 149 132 150 133 repo := resp.Value.Val.(*tangled.Repo) 151 134 152 135 if repo.Knot != h.c.Server.Hostname { 153 - reason := "not this knot" 154 - l.Info("rejecting pull record", "reason", reason) 155 - return fmt.Errorf("rejected pull record: %s", reason) 136 + return fmt.Errorf("rejected pull record: not this knot, %s != %s", repo.Knot, h.c.Server.Hostname) 156 137 } 157 138 158 139 didSlashRepo, err := securejoin.SecureJoin(repo.Owner, repo.Name) 159 140 if err != nil { 160 - return err 141 + return fmt.Errorf("failed to construct relative repo path: %w", err) 161 142 } 162 143 163 144 repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo) 164 145 if err != nil { 165 - return err 146 + return fmt.Errorf("failed to construct absolute repo path: %w", err) 166 147 } 167 148 168 149 gr, err := git.Open(repoPath, record.Source.Branch) 169 150 if err != nil { 170 - return err 151 + return fmt.Errorf("failed to open git repository: %w", err) 171 152 } 172 153 173 154 workflowDir, err := gr.FileTree(ctx, workflow.WorkflowDir) 174 155 if err != nil { 175 - return err 156 + return fmt.Errorf("failed to open workflow directory: %w", err) 176 157 } 177 158 178 159 var pipeline workflow.RawPipeline ··· 215 196 cp := compiler.Compile(compiler.Parse(pipeline)) 216 197 eventJson, err := json.Marshal(cp) 217 198 if err != nil { 218 - return err 199 + return fmt.Errorf("failed to marshal pipeline event: %w", err) 219 200 } 220 201 221 202 // do not run empty pipelines
-53
knotserver/middleware.go
··· 1 - package knotserver 2 - 3 - import ( 4 - "crypto/hmac" 5 - "crypto/sha256" 6 - "encoding/hex" 7 - "net/http" 8 - "time" 9 - ) 10 - 11 - func (h *Handle) VerifySignature(next http.Handler) http.Handler { 12 - if h.c.Server.Dev { 13 - return next 14 - } 15 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 - signature := r.Header.Get("X-Signature") 17 - if signature == "" || !h.verifyHMAC(signature, r) { 18 - writeError(w, "signature verification failed", http.StatusForbidden) 19 - return 20 - } 21 - next.ServeHTTP(w, r) 22 - }) 23 - } 24 - 25 - func (h *Handle) verifyHMAC(signature string, r *http.Request) bool { 26 - secret := h.c.Server.Secret 27 - timestamp := r.Header.Get("X-Timestamp") 28 - if timestamp == "" { 29 - return false 30 - } 31 - 32 - // Verify that the timestamp is not older than a minute 33 - reqTime, err := time.Parse(time.RFC3339, timestamp) 34 - if err != nil { 35 - return false 36 - } 37 - if time.Since(reqTime) > time.Minute { 38 - return false 39 - } 40 - 41 - message := r.Method + r.URL.Path + timestamp 42 - 43 - mac := hmac.New(sha256.New, []byte(secret)) 44 - mac.Write([]byte(message)) 45 - expectedMAC := mac.Sum(nil) 46 - 47 - signatureBytes, err := hex.DecodeString(signature) 48 - if err != nil { 49 - return false 50 - } 51 - 52 - return hmac.Equal(signatureBytes, expectedMAC) 53 - }
+138 -1176
knotserver/routes.go
··· 1 1 package knotserver 2 2 3 3 import ( 4 - "compress/gzip" 5 4 "context" 6 - "crypto/sha256" 7 - "encoding/json" 8 - "errors" 9 5 "fmt" 10 - "log" 6 + "log/slog" 11 7 "net/http" 12 - "net/url" 13 - "os" 14 - "path/filepath" 15 - "strconv" 16 - "strings" 17 - "sync" 18 - "time" 8 + "runtime/debug" 19 9 20 - securejoin "github.com/cyphar/filepath-securejoin" 21 - "github.com/gliderlabs/ssh" 22 10 "github.com/go-chi/chi/v5" 23 - "github.com/go-git/go-git/v5/plumbing" 24 - "github.com/go-git/go-git/v5/plumbing/object" 25 - "tangled.sh/tangled.sh/core/hook" 11 + "tangled.sh/tangled.sh/core/idresolver" 12 + "tangled.sh/tangled.sh/core/jetstream" 13 + "tangled.sh/tangled.sh/core/knotserver/config" 26 14 "tangled.sh/tangled.sh/core/knotserver/db" 27 - "tangled.sh/tangled.sh/core/knotserver/git" 28 - "tangled.sh/tangled.sh/core/patchutil" 15 + "tangled.sh/tangled.sh/core/knotserver/xrpc" 16 + tlog "tangled.sh/tangled.sh/core/log" 17 + "tangled.sh/tangled.sh/core/notifier" 29 18 "tangled.sh/tangled.sh/core/rbac" 30 - "tangled.sh/tangled.sh/core/types" 19 + "tangled.sh/tangled.sh/core/xrpc/serviceauth" 31 20 ) 32 21 33 - func (h *Handle) Index(w http.ResponseWriter, r *http.Request) { 34 - w.Write([]byte("This is a knot server. More info at https://tangled.sh")) 22 + type Handle struct { 23 + c *config.Config 24 + db *db.DB 25 + jc *jetstream.JetstreamClient 26 + e *rbac.Enforcer 27 + l *slog.Logger 28 + n *notifier.Notifier 29 + resolver *idresolver.Resolver 35 30 } 36 31 37 - func (h *Handle) Capabilities(w http.ResponseWriter, r *http.Request) { 38 - w.Header().Set("Content-Type", "application/json") 32 + func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger, n *notifier.Notifier) (http.Handler, error) { 33 + r := chi.NewRouter() 39 34 40 - capabilities := map[string]any{ 41 - "pull_requests": map[string]any{ 42 - "format_patch": true, 43 - "patch_submissions": true, 44 - "branch_submissions": true, 45 - "fork_submissions": true, 46 - }, 35 + h := Handle{ 36 + c: c, 37 + db: db, 38 + e: e, 39 + l: l, 40 + jc: jc, 41 + n: n, 42 + resolver: idresolver.DefaultResolver(), 47 43 } 48 44 49 - jsonData, err := json.Marshal(capabilities) 45 + err := e.AddKnot(rbac.ThisServer) 50 46 if err != nil { 51 - http.Error(w, "Failed to serialize JSON", http.StatusInternalServerError) 52 - return 47 + return nil, fmt.Errorf("failed to setup enforcer: %w", err) 53 48 } 54 49 55 - w.Write(jsonData) 56 - } 57 - 58 - func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) { 59 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 60 - l := h.l.With("path", path, "handler", "RepoIndex") 61 - ref := chi.URLParam(r, "ref") 62 - ref, _ = url.PathUnescape(ref) 63 - 64 - gr, err := git.Open(path, ref) 65 - if err != nil { 66 - plain, err2 := git.PlainOpen(path) 67 - if err2 != nil { 68 - l.Error("opening repo", "error", err2.Error()) 69 - notFound(w) 70 - return 71 - } 72 - branches, _ := plain.Branches() 73 - 74 - log.Println(err) 75 - 76 - if errors.Is(err, plumbing.ErrReferenceNotFound) { 77 - resp := types.RepoIndexResponse{ 78 - IsEmpty: true, 79 - Branches: branches, 80 - } 81 - writeJSON(w, resp) 82 - return 83 - } else { 84 - l.Error("opening repo", "error", err.Error()) 85 - notFound(w) 86 - return 87 - } 50 + // configure owner 51 + if err = h.configureOwner(); err != nil { 52 + return nil, err 88 53 } 89 - 90 - var ( 91 - commits []*object.Commit 92 - total int 93 - branches []types.Branch 94 - files []types.NiceTree 95 - tags []object.Tag 96 - ) 97 - 98 - var wg sync.WaitGroup 99 - errorsCh := make(chan error, 5) 100 - 101 - wg.Add(1) 102 - go func() { 103 - defer wg.Done() 104 - cs, err := gr.Commits(0, 60) 105 - if err != nil { 106 - errorsCh <- fmt.Errorf("commits: %w", err) 107 - return 108 - } 109 - commits = cs 110 - }() 111 - 112 - wg.Add(1) 113 - go func() { 114 - defer wg.Done() 115 - t, err := gr.TotalCommits() 116 - if err != nil { 117 - errorsCh <- fmt.Errorf("calculating total: %w", err) 118 - return 119 - } 120 - total = t 121 - }() 54 + h.l.Info("owner set", "did", h.c.Server.Owner) 55 + h.jc.AddDid(h.c.Server.Owner) 122 56 123 - wg.Add(1) 124 - go func() { 125 - defer wg.Done() 126 - bs, err := gr.Branches() 127 - if err != nil { 128 - errorsCh <- fmt.Errorf("fetching branches: %w", err) 129 - return 130 - } 131 - branches = bs 132 - }() 133 - 134 - wg.Add(1) 135 - go func() { 136 - defer wg.Done() 137 - ts, err := gr.Tags() 138 - if err != nil { 139 - errorsCh <- fmt.Errorf("fetching tags: %w", err) 140 - return 141 - } 142 - tags = ts 143 - }() 144 - 145 - wg.Add(1) 146 - go func() { 147 - defer wg.Done() 148 - fs, err := gr.FileTree(r.Context(), "") 149 - if err != nil { 150 - errorsCh <- fmt.Errorf("fetching filetree: %w", err) 151 - return 152 - } 153 - files = fs 154 - }() 155 - 156 - wg.Wait() 157 - close(errorsCh) 158 - 159 - // show any errors 160 - for err := range errorsCh { 161 - l.Error("loading repo", "error", err.Error()) 162 - writeError(w, err.Error(), http.StatusInternalServerError) 163 - return 164 - } 165 - 166 - rtags := []*types.TagReference{} 167 - for _, tag := range tags { 168 - var target *object.Tag 169 - if tag.Target != plumbing.ZeroHash { 170 - target = &tag 171 - } 172 - tr := types.TagReference{ 173 - Tag: target, 174 - } 175 - 176 - tr.Reference = types.Reference{ 177 - Name: tag.Name, 178 - Hash: tag.Hash.String(), 179 - } 180 - 181 - if tag.Message != "" { 182 - tr.Message = tag.Message 183 - } 184 - 185 - rtags = append(rtags, &tr) 186 - } 187 - 188 - var readmeContent string 189 - var readmeFile string 190 - for _, readme := range h.c.Repo.Readme { 191 - content, _ := gr.FileContent(readme) 192 - if len(content) > 0 { 193 - readmeContent = string(content) 194 - readmeFile = readme 195 - } 196 - } 197 - 198 - if ref == "" { 199 - mainBranch, err := gr.FindMainBranch() 200 - if err != nil { 201 - writeError(w, err.Error(), http.StatusInternalServerError) 202 - l.Error("finding main branch", "error", err.Error()) 203 - return 204 - } 205 - ref = mainBranch 206 - } 207 - 208 - resp := types.RepoIndexResponse{ 209 - IsEmpty: false, 210 - Ref: ref, 211 - Commits: commits, 212 - Description: getDescription(path), 213 - Readme: readmeContent, 214 - ReadmeFileName: readmeFile, 215 - Files: files, 216 - Branches: branches, 217 - Tags: rtags, 218 - TotalCommits: total, 219 - } 220 - 221 - writeJSON(w, resp) 222 - return 223 - } 224 - 225 - func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) { 226 - treePath := chi.URLParam(r, "*") 227 - ref := chi.URLParam(r, "ref") 228 - ref, _ = url.PathUnescape(ref) 229 - 230 - l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath) 231 - 232 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 233 - gr, err := git.Open(path, ref) 57 + // configure known-dids in jetstream consumer 58 + dids, err := h.db.GetAllDids() 234 59 if err != nil { 235 - notFound(w) 236 - return 60 + return nil, fmt.Errorf("failed to get all dids: %w", err) 237 61 } 238 - 239 - files, err := gr.FileTree(r.Context(), treePath) 240 - if err != nil { 241 - writeError(w, err.Error(), http.StatusInternalServerError) 242 - l.Error("file tree", "error", err.Error()) 243 - return 62 + for _, d := range dids { 63 + jc.AddDid(d) 244 64 } 245 65 246 - resp := types.RepoTreeResponse{ 247 - Ref: ref, 248 - Parent: treePath, 249 - Description: getDescription(path), 250 - DotDot: filepath.Dir(treePath), 251 - Files: files, 252 - } 253 - 254 - writeJSON(w, resp) 255 - return 256 - } 257 - 258 - func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) { 259 - treePath := chi.URLParam(r, "*") 260 - ref := chi.URLParam(r, "ref") 261 - ref, _ = url.PathUnescape(ref) 262 - 263 - l := h.l.With("handler", "BlobRaw", "ref", ref, "treePath", treePath) 264 - 265 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 266 - gr, err := git.Open(path, ref) 66 + err = h.jc.StartJetstream(ctx, h.processMessages) 267 67 if err != nil { 268 - notFound(w) 269 - return 68 + return nil, fmt.Errorf("failed to start jetstream: %w", err) 270 69 } 271 70 272 - contents, err := gr.RawContent(treePath) 273 - if err != nil { 274 - writeError(w, err.Error(), http.StatusBadRequest) 275 - l.Error("file content", "error", err.Error()) 276 - return 277 - } 71 + r.Get("/", h.Index) 72 + r.Get("/capabilities", h.Capabilities) 73 + r.Get("/version", h.Version) 74 + r.Get("/owner", func(w http.ResponseWriter, r *http.Request) { 75 + w.Write([]byte(h.c.Server.Owner)) 76 + }) 77 + r.Route("/{did}", func(r chi.Router) { 78 + // Repo routes 79 + r.Route("/{name}", func(r chi.Router) { 278 80 279 - mimeType := http.DetectContentType(contents) 81 + r.Route("/languages", func(r chi.Router) { 82 + r.Get("/", h.RepoLanguages) 83 + r.Get("/{ref}", h.RepoLanguages) 84 + }) 280 85 281 - // exception for svg 282 - if filepath.Ext(treePath) == ".svg" { 283 - mimeType = "image/svg+xml" 284 - } 86 + r.Get("/", h.RepoIndex) 87 + r.Get("/info/refs", h.InfoRefs) 88 + r.Post("/git-upload-pack", h.UploadPack) 89 + r.Post("/git-receive-pack", h.ReceivePack) 90 + r.Get("/compare/{rev1}/{rev2}", h.Compare) // git diff-tree compare of two objects 285 91 286 - contentHash := sha256.Sum256(contents) 287 - eTag := fmt.Sprintf("\"%x\"", contentHash) 92 + r.Route("/tree/{ref}", func(r chi.Router) { 93 + r.Get("/", h.RepoIndex) 94 + r.Get("/*", h.RepoTree) 95 + }) 288 96 289 - // allow image, video, and text/plain files to be served directly 290 - switch { 291 - case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"): 292 - if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag { 293 - w.WriteHeader(http.StatusNotModified) 294 - return 295 - } 296 - w.Header().Set("ETag", eTag) 97 + r.Route("/blob/{ref}", func(r chi.Router) { 98 + r.Get("/*", h.Blob) 99 + }) 297 100 298 - case strings.HasPrefix(mimeType, "text/plain"): 299 - w.Header().Set("Cache-Control", "public, no-cache") 300 - 301 - default: 302 - l.Error("attempted to serve disallowed file type", "mimetype", mimeType) 303 - writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden) 304 - return 305 - } 306 - 307 - w.Header().Set("Content-Type", mimeType) 308 - w.Write(contents) 309 - } 310 - 311 - func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) { 312 - treePath := chi.URLParam(r, "*") 313 - ref := chi.URLParam(r, "ref") 314 - ref, _ = url.PathUnescape(ref) 315 - 316 - l := h.l.With("handler", "Blob", "ref", ref, "treePath", treePath) 317 - 318 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 319 - gr, err := git.Open(path, ref) 320 - if err != nil { 321 - notFound(w) 322 - return 323 - } 324 - 325 - var isBinaryFile bool = false 326 - contents, err := gr.FileContent(treePath) 327 - if errors.Is(err, git.ErrBinaryFile) { 328 - isBinaryFile = true 329 - } else if errors.Is(err, object.ErrFileNotFound) { 330 - notFound(w) 331 - return 332 - } else if err != nil { 333 - writeError(w, err.Error(), http.StatusInternalServerError) 334 - return 335 - } 336 - 337 - bytes := []byte(contents) 338 - // safe := string(sanitize(bytes)) 339 - sizeHint := len(bytes) 340 - 341 - resp := types.RepoBlobResponse{ 342 - Ref: ref, 343 - Contents: string(bytes), 344 - Path: treePath, 345 - IsBinary: isBinaryFile, 346 - SizeHint: uint64(sizeHint), 347 - } 348 - 349 - h.showFile(resp, w, l) 350 - } 351 - 352 - func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) { 353 - name := chi.URLParam(r, "name") 354 - file := chi.URLParam(r, "file") 355 - 356 - l := h.l.With("handler", "Archive", "name", name, "file", file) 357 - 358 - // TODO: extend this to add more files compression (e.g.: xz) 359 - if !strings.HasSuffix(file, ".tar.gz") { 360 - notFound(w) 361 - return 362 - } 363 - 364 - ref := strings.TrimSuffix(file, ".tar.gz") 365 - 366 - unescapedRef, err := url.PathUnescape(ref) 367 - if err != nil { 368 - notFound(w) 369 - return 370 - } 371 - 372 - safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(unescapedRef).Short(), "/", "-") 373 - 374 - // This allows the browser to use a proper name for the file when 375 - // downloading 376 - filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename) 377 - setContentDisposition(w, filename) 378 - setGZipMIME(w) 379 - 380 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 381 - gr, err := git.Open(path, unescapedRef) 382 - if err != nil { 383 - notFound(w) 384 - return 385 - } 386 - 387 - gw := gzip.NewWriter(w) 388 - defer gw.Close() 389 - 390 - prefix := fmt.Sprintf("%s-%s", name, safeRefFilename) 391 - err = gr.WriteTar(gw, prefix) 392 - if err != nil { 393 - // once we start writing to the body we can't report error anymore 394 - // so we are only left with printing the error. 395 - l.Error("writing tar file", "error", err.Error()) 396 - return 397 - } 398 - 399 - err = gw.Flush() 400 - if err != nil { 401 - // once we start writing to the body we can't report error anymore 402 - // so we are only left with printing the error. 403 - l.Error("flushing?", "error", err.Error()) 404 - return 405 - } 406 - } 407 - 408 - func (h *Handle) Log(w http.ResponseWriter, r *http.Request) { 409 - ref := chi.URLParam(r, "ref") 410 - ref, _ = url.PathUnescape(ref) 411 - 412 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 413 - 414 - l := h.l.With("handler", "Log", "ref", ref, "path", path) 415 - 416 - gr, err := git.Open(path, ref) 417 - if err != nil { 418 - notFound(w) 419 - return 420 - } 421 - 422 - // Get page parameters 423 - page := 1 424 - pageSize := 30 425 - 426 - if pageParam := r.URL.Query().Get("page"); pageParam != "" { 427 - if p, err := strconv.Atoi(pageParam); err == nil && p > 0 { 428 - page = p 429 - } 430 - } 431 - 432 - if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" { 433 - if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 { 434 - pageSize = ps 435 - } 436 - } 437 - 438 - // convert to offset/limit 439 - offset := (page - 1) * pageSize 440 - limit := pageSize 441 - 442 - commits, err := gr.Commits(offset, limit) 443 - if err != nil { 444 - writeError(w, err.Error(), http.StatusInternalServerError) 445 - l.Error("fetching commits", "error", err.Error()) 446 - return 447 - } 448 - 449 - total := len(commits) 450 - 451 - resp := types.RepoLogResponse{ 452 - Commits: commits, 453 - Ref: ref, 454 - Description: getDescription(path), 455 - Log: true, 456 - Total: total, 457 - Page: page, 458 - PerPage: pageSize, 459 - } 460 - 461 - writeJSON(w, resp) 462 - return 463 - } 464 - 465 - func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) { 466 - ref := chi.URLParam(r, "ref") 467 - ref, _ = url.PathUnescape(ref) 468 - 469 - l := h.l.With("handler", "Diff", "ref", ref) 470 - 471 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 472 - gr, err := git.Open(path, ref) 473 - if err != nil { 474 - notFound(w) 475 - return 476 - } 477 - 478 - diff, err := gr.Diff() 479 - if err != nil { 480 - writeError(w, err.Error(), http.StatusInternalServerError) 481 - l.Error("getting diff", "error", err.Error()) 482 - return 483 - } 484 - 485 - resp := types.RepoCommitResponse{ 486 - Ref: ref, 487 - Diff: diff, 488 - } 489 - 490 - writeJSON(w, resp) 491 - return 492 - } 493 - 494 - func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) { 495 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 496 - l := h.l.With("handler", "Refs") 497 - 498 - gr, err := git.Open(path, "") 499 - if err != nil { 500 - notFound(w) 501 - return 502 - } 503 - 504 - tags, err := gr.Tags() 505 - if err != nil { 506 - // Non-fatal, we *should* have at least one branch to show. 507 - l.Warn("getting tags", "error", err.Error()) 508 - } 509 - 510 - rtags := []*types.TagReference{} 511 - for _, tag := range tags { 512 - var target *object.Tag 513 - if tag.Target != plumbing.ZeroHash { 514 - target = &tag 515 - } 516 - tr := types.TagReference{ 517 - Tag: target, 518 - } 519 - 520 - tr.Reference = types.Reference{ 521 - Name: tag.Name, 522 - Hash: tag.Hash.String(), 523 - } 524 - 525 - if tag.Message != "" { 526 - tr.Message = tag.Message 527 - } 528 - 529 - rtags = append(rtags, &tr) 530 - } 531 - 532 - resp := types.RepoTagsResponse{ 533 - Tags: rtags, 534 - } 535 - 536 - writeJSON(w, resp) 537 - return 538 - } 539 - 540 - func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) { 541 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 542 - 543 - gr, err := git.PlainOpen(path) 544 - if err != nil { 545 - notFound(w) 546 - return 547 - } 101 + r.Route("/raw/{ref}", func(r chi.Router) { 102 + r.Get("/*", h.BlobRaw) 103 + }) 548 104 549 - branches, _ := gr.Branches() 550 - 551 - resp := types.RepoBranchesResponse{ 552 - Branches: branches, 553 - } 554 - 555 - writeJSON(w, resp) 556 - return 557 - } 558 - 559 - func (h *Handle) Branch(w http.ResponseWriter, r *http.Request) { 560 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 561 - branchName := chi.URLParam(r, "branch") 562 - branchName, _ = url.PathUnescape(branchName) 563 - 564 - l := h.l.With("handler", "Branch") 565 - 566 - gr, err := git.PlainOpen(path) 567 - if err != nil { 568 - notFound(w) 569 - return 570 - } 571 - 572 - ref, err := gr.Branch(branchName) 573 - if err != nil { 574 - l.Error("getting branch", "error", err.Error()) 575 - writeError(w, err.Error(), http.StatusInternalServerError) 576 - return 577 - } 105 + r.Get("/log/{ref}", h.Log) 106 + r.Get("/archive/{file}", h.Archive) 107 + r.Get("/commit/{ref}", h.Diff) 108 + r.Get("/tags", h.Tags) 109 + r.Route("/branches", func(r chi.Router) { 110 + r.Get("/", h.Branches) 111 + r.Get("/{branch}", h.Branch) 112 + r.Get("/default", h.DefaultBranch) 113 + }) 114 + }) 115 + }) 578 116 579 - commit, err := gr.Commit(ref.Hash()) 580 - if err != nil { 581 - l.Error("getting commit object", "error", err.Error()) 582 - writeError(w, err.Error(), http.StatusInternalServerError) 583 - return 584 - } 117 + // xrpc apis 118 + r.Mount("/xrpc", h.XrpcRouter()) 585 119 586 - defaultBranch, err := gr.FindMainBranch() 587 - isDefault := false 588 - if err != nil { 589 - l.Error("getting default branch", "error", err.Error()) 590 - // do not quit though 591 - } else if defaultBranch == branchName { 592 - isDefault = true 593 - } 120 + // Socket that streams git oplogs 121 + r.Get("/events", h.Events) 594 122 595 - resp := types.RepoBranchResponse{ 596 - Branch: types.Branch{ 597 - Reference: types.Reference{ 598 - Name: ref.Name().Short(), 599 - Hash: ref.Hash().String(), 600 - }, 601 - Commit: commit, 602 - IsDefault: isDefault, 603 - }, 604 - } 123 + // All public keys on the knot. 124 + r.Get("/keys", h.Keys) 605 125 606 - writeJSON(w, resp) 607 - return 126 + return r, nil 608 127 } 609 128 610 - func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) { 611 - l := h.l.With("handler", "Keys") 612 - 613 - switch r.Method { 614 - case http.MethodGet: 615 - keys, err := h.db.GetAllPublicKeys() 616 - if err != nil { 617 - writeError(w, err.Error(), http.StatusInternalServerError) 618 - l.Error("getting public keys", "error", err.Error()) 619 - return 620 - } 621 - 622 - data := make([]map[string]any, 0) 623 - for _, key := range keys { 624 - j := key.JSON() 625 - data = append(data, j) 626 - } 627 - writeJSON(w, data) 628 - return 629 - 630 - case http.MethodPut: 631 - pk := db.PublicKey{} 632 - if err := json.NewDecoder(r.Body).Decode(&pk); err != nil { 633 - writeError(w, "invalid request body", http.StatusBadRequest) 634 - return 635 - } 129 + func (h *Handle) XrpcRouter() http.Handler { 130 + logger := tlog.New("knots") 636 131 637 - _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key)) 638 - if err != nil { 639 - writeError(w, "invalid pubkey", http.StatusBadRequest) 640 - } 132 + serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String()) 641 133 642 - if err := h.db.AddPublicKey(pk); err != nil { 643 - writeError(w, err.Error(), http.StatusInternalServerError) 644 - l.Error("adding public key", "error", err.Error()) 645 - return 646 - } 647 - 648 - w.WriteHeader(http.StatusNoContent) 649 - return 134 + xrpc := &xrpc.Xrpc{ 135 + Config: h.c, 136 + Db: h.db, 137 + Ingester: h.jc, 138 + Enforcer: h.e, 139 + Logger: logger, 140 + Notifier: h.n, 141 + Resolver: h.resolver, 142 + ServiceAuth: serviceAuth, 650 143 } 144 + return xrpc.Router() 651 145 } 652 146 653 - func (h *Handle) RepoForkAheadBehind(w http.ResponseWriter, r *http.Request) { 654 - l := h.l.With("handler", "RepoForkAheadBehind") 147 + // version is set during build time. 148 + var version string 655 149 656 - data := struct { 657 - Did string `json:"did"` 658 - Source string `json:"source"` 659 - Name string `json:"name,omitempty"` 660 - HiddenRef string `json:"hiddenref"` 661 - }{} 662 - 663 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 664 - writeError(w, "invalid request body", http.StatusBadRequest) 665 - return 666 - } 667 - 668 - did := data.Did 669 - source := data.Source 670 - 671 - if did == "" || source == "" { 672 - l.Error("invalid request body, empty did or name") 673 - w.WriteHeader(http.StatusBadRequest) 674 - return 675 - } 676 - 677 - var name string 678 - if data.Name != "" { 679 - name = data.Name 680 - } else { 681 - name = filepath.Base(source) 682 - } 683 - 684 - branch := chi.URLParam(r, "branch") 685 - branch, _ = url.PathUnescape(branch) 686 - 687 - relativeRepoPath := filepath.Join(did, name) 688 - repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 689 - 690 - gr, err := git.PlainOpen(repoPath) 691 - if err != nil { 692 - log.Println(err) 693 - notFound(w) 694 - return 695 - } 696 - 697 - forkCommit, err := gr.ResolveRevision(branch) 698 - if err != nil { 699 - l.Error("error resolving ref revision", "msg", err.Error()) 700 - writeError(w, fmt.Sprintf("error resolving revision %s", branch), http.StatusBadRequest) 701 - return 702 - } 703 - 704 - sourceCommit, err := gr.ResolveRevision(data.HiddenRef) 705 - if err != nil { 706 - l.Error("error resolving hidden ref revision", "msg", err.Error()) 707 - writeError(w, fmt.Sprintf("error resolving revision %s", data.HiddenRef), http.StatusBadRequest) 708 - return 709 - } 710 - 711 - status := types.UpToDate 712 - if forkCommit.Hash.String() != sourceCommit.Hash.String() { 713 - isAncestor, err := forkCommit.IsAncestor(sourceCommit) 714 - if err != nil { 715 - log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err) 150 + func (h *Handle) Version(w http.ResponseWriter, r *http.Request) { 151 + if version == "" { 152 + info, ok := debug.ReadBuildInfo() 153 + if !ok { 154 + http.Error(w, "failed to read build info", http.StatusInternalServerError) 716 155 return 717 156 } 718 157 719 - if isAncestor { 720 - status = types.FastForwardable 721 - } else { 722 - status = types.Conflict 158 + var modVer string 159 + for _, mod := range info.Deps { 160 + if mod.Path == "tangled.sh/tangled.sh/knotserver" { 161 + version = mod.Version 162 + break 163 + } 723 164 } 724 - } 725 165 726 - w.Header().Set("Content-Type", "application/json") 727 - json.NewEncoder(w).Encode(types.AncestorCheckResponse{Status: status}) 728 - } 729 - 730 - func (h *Handle) RepoLanguages(w http.ResponseWriter, r *http.Request) { 731 - repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 732 - ref := chi.URLParam(r, "ref") 733 - ref, _ = url.PathUnescape(ref) 734 - 735 - l := h.l.With("handler", "RepoLanguages") 736 - 737 - gr, err := git.Open(repoPath, ref) 738 - if err != nil { 739 - l.Error("opening repo", "error", err.Error()) 740 - notFound(w) 741 - return 742 - } 743 - 744 - ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second) 745 - defer cancel() 746 - 747 - sizes, err := gr.AnalyzeLanguages(ctx) 748 - if err != nil { 749 - l.Error("failed to analyze languages", "error", err.Error()) 750 - writeError(w, err.Error(), http.StatusNoContent) 751 - return 752 - } 753 - 754 - resp := types.RepoLanguageResponse{Languages: sizes} 755 - 756 - writeJSON(w, resp) 757 - } 758 - 759 - func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) { 760 - l := h.l.With("handler", "RepoForkSync") 761 - 762 - data := struct { 763 - Did string `json:"did"` 764 - Source string `json:"source"` 765 - Name string `json:"name,omitempty"` 766 - }{} 767 - 768 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 769 - writeError(w, "invalid request body", http.StatusBadRequest) 770 - return 771 - } 772 - 773 - did := data.Did 774 - source := data.Source 775 - 776 - if did == "" || source == "" { 777 - l.Error("invalid request body, empty did or name") 778 - w.WriteHeader(http.StatusBadRequest) 779 - return 780 - } 781 - 782 - var name string 783 - if data.Name != "" { 784 - name = data.Name 785 - } else { 786 - name = filepath.Base(source) 787 - } 788 - 789 - branch := chi.URLParam(r, "*") 790 - branch, _ = url.PathUnescape(branch) 791 - 792 - relativeRepoPath := filepath.Join(did, name) 793 - repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 794 - 795 - gr, err := git.Open(repoPath, branch) 796 - if err != nil { 797 - log.Println(err) 798 - notFound(w) 799 - return 800 - } 801 - 802 - err = gr.Sync() 803 - if err != nil { 804 - l.Error("error syncing repo fork", "error", err.Error()) 805 - writeError(w, err.Error(), http.StatusInternalServerError) 806 - return 807 - } 808 - 809 - w.WriteHeader(http.StatusNoContent) 810 - } 811 - 812 - func (h *Handle) RepoFork(w http.ResponseWriter, r *http.Request) { 813 - l := h.l.With("handler", "RepoFork") 814 - 815 - data := struct { 816 - Did string `json:"did"` 817 - Source string `json:"source"` 818 - Name string `json:"name,omitempty"` 819 - }{} 820 - 821 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 822 - writeError(w, "invalid request body", http.StatusBadRequest) 823 - return 824 - } 825 - 826 - did := data.Did 827 - source := data.Source 828 - 829 - if did == "" || source == "" { 830 - l.Error("invalid request body, empty did or name") 831 - w.WriteHeader(http.StatusBadRequest) 832 - return 833 - } 834 - 835 - var name string 836 - if data.Name != "" { 837 - name = data.Name 838 - } else { 839 - name = filepath.Base(source) 840 - } 841 - 842 - relativeRepoPath := filepath.Join(did, name) 843 - repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 844 - 845 - err := git.Fork(repoPath, source) 846 - if err != nil { 847 - l.Error("forking repo", "error", err.Error()) 848 - writeError(w, err.Error(), http.StatusInternalServerError) 849 - return 850 - } 851 - 852 - // add perms for this user to access the repo 853 - err = h.e.AddRepo(did, rbac.ThisServer, relativeRepoPath) 854 - if err != nil { 855 - l.Error("adding repo permissions", "error", err.Error()) 856 - writeError(w, err.Error(), http.StatusInternalServerError) 857 - return 858 - } 859 - 860 - hook.SetupRepo( 861 - hook.Config( 862 - hook.WithScanPath(h.c.Repo.ScanPath), 863 - hook.WithInternalApi(h.c.Server.InternalListenAddr), 864 - ), 865 - repoPath, 866 - ) 867 - 868 - w.WriteHeader(http.StatusNoContent) 869 - } 870 - 871 - func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) { 872 - l := h.l.With("handler", "RemoveRepo") 873 - 874 - data := struct { 875 - Did string `json:"did"` 876 - Name string `json:"name"` 877 - }{} 878 - 879 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 880 - writeError(w, "invalid request body", http.StatusBadRequest) 881 - return 882 - } 883 - 884 - did := data.Did 885 - name := data.Name 886 - 887 - if did == "" || name == "" { 888 - l.Error("invalid request body, empty did or name") 889 - w.WriteHeader(http.StatusBadRequest) 890 - return 891 - } 892 - 893 - relativeRepoPath := filepath.Join(did, name) 894 - repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 895 - err := os.RemoveAll(repoPath) 896 - if err != nil { 897 - l.Error("removing repo", "error", err.Error()) 898 - writeError(w, err.Error(), http.StatusInternalServerError) 899 - return 900 - } 901 - 902 - w.WriteHeader(http.StatusNoContent) 903 - 904 - } 905 - func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) { 906 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 907 - 908 - data := types.MergeRequest{} 909 - 910 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 911 - writeError(w, err.Error(), http.StatusBadRequest) 912 - h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err) 913 - return 914 - } 915 - 916 - mo := &git.MergeOptions{ 917 - AuthorName: data.AuthorName, 918 - AuthorEmail: data.AuthorEmail, 919 - CommitBody: data.CommitBody, 920 - CommitMessage: data.CommitMessage, 921 - } 922 - 923 - patch := data.Patch 924 - branch := data.Branch 925 - gr, err := git.Open(path, branch) 926 - if err != nil { 927 - notFound(w) 928 - return 929 - } 930 - 931 - mo.FormatPatch = patchutil.IsFormatPatch(patch) 932 - 933 - if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil { 934 - var mergeErr *git.ErrMerge 935 - if errors.As(err, &mergeErr) { 936 - conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 937 - for i, conflict := range mergeErr.Conflicts { 938 - conflicts[i] = types.ConflictInfo{ 939 - Filename: conflict.Filename, 940 - Reason: conflict.Reason, 941 - } 942 - } 943 - response := types.MergeCheckResponse{ 944 - IsConflicted: true, 945 - Conflicts: conflicts, 946 - Message: mergeErr.Message, 947 - } 948 - writeConflict(w, response) 949 - h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr) 950 - } else { 951 - writeError(w, err.Error(), http.StatusBadRequest) 952 - h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error()) 166 + if modVer == "" { 167 + version = "unknown" 953 168 } 954 - return 955 169 } 956 170 957 - w.WriteHeader(http.StatusOK) 171 + w.Header().Set("Content-Type", "text/plain; charset=utf-8") 172 + fmt.Fprintf(w, "knotserver/%s", version) 958 173 } 959 174 960 - func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) { 961 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 962 - 963 - var data struct { 964 - Patch string `json:"patch"` 965 - Branch string `json:"branch"` 966 - } 175 + func (h *Handle) configureOwner() error { 176 + cfgOwner := h.c.Server.Owner 967 177 968 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 969 - writeError(w, err.Error(), http.StatusBadRequest) 970 - h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err) 971 - return 972 - } 178 + rbacDomain := "thisserver" 973 179 974 - patch := data.Patch 975 - branch := data.Branch 976 - gr, err := git.Open(path, branch) 180 + existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain) 977 181 if err != nil { 978 - notFound(w) 979 - return 182 + return err 980 183 } 981 184 982 - err = gr.MergeCheck([]byte(patch), branch) 983 - if err == nil { 984 - response := types.MergeCheckResponse{ 985 - IsConflicted: false, 986 - } 987 - writeJSON(w, response) 988 - return 989 - } 185 + switch len(existing) { 186 + case 0: 187 + // no owner configured, continue 188 + case 1: 189 + // find existing owner 190 + existingOwner := existing[0] 990 191 991 - var mergeErr *git.ErrMerge 992 - if errors.As(err, &mergeErr) { 993 - conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts)) 994 - for i, conflict := range mergeErr.Conflicts { 995 - conflicts[i] = types.ConflictInfo{ 996 - Filename: conflict.Filename, 997 - Reason: conflict.Reason, 998 - } 999 - } 1000 - response := types.MergeCheckResponse{ 1001 - IsConflicted: true, 1002 - Conflicts: conflicts, 1003 - Message: mergeErr.Message, 192 + // no ownership change, this is okay 193 + if existingOwner == h.c.Server.Owner { 194 + break 1004 195 } 1005 - writeConflict(w, response) 1006 - h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error()) 1007 - return 1008 - } 1009 - writeError(w, err.Error(), http.StatusInternalServerError) 1010 - h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error()) 1011 - } 1012 196 1013 - func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) { 1014 - rev1 := chi.URLParam(r, "rev1") 1015 - rev1, _ = url.PathUnescape(rev1) 1016 - 1017 - rev2 := chi.URLParam(r, "rev2") 1018 - rev2, _ = url.PathUnescape(rev2) 1019 - 1020 - l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2) 1021 - 1022 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1023 - gr, err := git.PlainOpen(path) 1024 - if err != nil { 1025 - notFound(w) 1026 - return 1027 - } 1028 - 1029 - commit1, err := gr.ResolveRevision(rev1) 1030 - if err != nil { 1031 - l.Error("error resolving revision 1", "msg", err.Error()) 1032 - writeError(w, fmt.Sprintf("error resolving revision %s", rev1), http.StatusBadRequest) 1033 - return 1034 - } 1035 - 1036 - commit2, err := gr.ResolveRevision(rev2) 1037 - if err != nil { 1038 - l.Error("error resolving revision 2", "msg", err.Error()) 1039 - writeError(w, fmt.Sprintf("error resolving revision %s", rev2), http.StatusBadRequest) 1040 - return 1041 - } 1042 - 1043 - rawPatch, formatPatch, err := gr.FormatPatch(commit1, commit2) 1044 - if err != nil { 1045 - l.Error("error comparing revisions", "msg", err.Error()) 1046 - writeError(w, "error comparing revisions", http.StatusBadRequest) 1047 - return 1048 - } 1049 - 1050 - writeJSON(w, types.RepoFormatPatchResponse{ 1051 - Rev1: commit1.Hash.String(), 1052 - Rev2: commit2.Hash.String(), 1053 - FormatPatch: formatPatch, 1054 - Patch: rawPatch, 1055 - }) 1056 - return 1057 - } 1058 - 1059 - func (h *Handle) NewHiddenRef(w http.ResponseWriter, r *http.Request) { 1060 - l := h.l.With("handler", "NewHiddenRef") 1061 - 1062 - forkRef := chi.URLParam(r, "forkRef") 1063 - forkRef, _ = url.PathUnescape(forkRef) 1064 - 1065 - remoteRef := chi.URLParam(r, "remoteRef") 1066 - remoteRef, _ = url.PathUnescape(remoteRef) 1067 - 1068 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1069 - gr, err := git.PlainOpen(path) 1070 - if err != nil { 1071 - notFound(w) 1072 - return 1073 - } 1074 - 1075 - err = gr.TrackHiddenRemoteRef(forkRef, remoteRef) 1076 - if err != nil { 1077 - l.Error("error tracking hidden remote ref", "msg", err.Error()) 1078 - writeError(w, "error tracking hidden remote ref", http.StatusBadRequest) 1079 - return 1080 - } 1081 - 1082 - w.WriteHeader(http.StatusNoContent) 1083 - return 1084 - } 1085 - 1086 - func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) { 1087 - l := h.l.With("handler", "AddMember") 1088 - 1089 - data := struct { 1090 - Did string `json:"did"` 1091 - }{} 1092 - 1093 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1094 - writeError(w, "invalid request body", http.StatusBadRequest) 1095 - return 1096 - } 1097 - 1098 - did := data.Did 1099 - 1100 - if err := h.db.AddDid(did); err != nil { 1101 - l.Error("adding did", "error", err.Error()) 1102 - writeError(w, err.Error(), http.StatusInternalServerError) 1103 - return 1104 - } 1105 - h.jc.AddDid(did) 1106 - 1107 - if err := h.e.AddKnotMember(rbac.ThisServer, did); err != nil { 1108 - l.Error("adding member", "error", err.Error()) 1109 - writeError(w, err.Error(), http.StatusInternalServerError) 1110 - return 1111 - } 1112 - 1113 - if err := h.fetchAndAddKeys(r.Context(), did); err != nil { 1114 - l.Error("fetching and adding keys", "error", err.Error()) 1115 - writeError(w, err.Error(), http.StatusInternalServerError) 1116 - return 1117 - } 1118 - 1119 - w.WriteHeader(http.StatusNoContent) 1120 - } 1121 - 1122 - func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) { 1123 - l := h.l.With("handler", "AddRepoCollaborator") 1124 - 1125 - data := struct { 1126 - Did string `json:"did"` 1127 - }{} 1128 - 1129 - ownerDid := chi.URLParam(r, "did") 1130 - repo := chi.URLParam(r, "name") 1131 - 1132 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1133 - writeError(w, "invalid request body", http.StatusBadRequest) 1134 - return 1135 - } 1136 - 1137 - if err := h.db.AddDid(data.Did); err != nil { 1138 - l.Error("adding did", "error", err.Error()) 1139 - writeError(w, err.Error(), http.StatusInternalServerError) 1140 - return 1141 - } 1142 - h.jc.AddDid(data.Did) 1143 - 1144 - repoName, _ := securejoin.SecureJoin(ownerDid, repo) 1145 - if err := h.e.AddCollaborator(data.Did, rbac.ThisServer, repoName); err != nil { 1146 - l.Error("adding repo collaborator", "error", err.Error()) 1147 - writeError(w, err.Error(), http.StatusInternalServerError) 1148 - return 1149 - } 1150 - 1151 - if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil { 1152 - l.Error("fetching and adding keys", "error", err.Error()) 1153 - writeError(w, err.Error(), http.StatusInternalServerError) 1154 - return 1155 - } 1156 - 1157 - w.WriteHeader(http.StatusNoContent) 1158 - } 1159 - 1160 - func (h *Handle) DefaultBranch(w http.ResponseWriter, r *http.Request) { 1161 - l := h.l.With("handler", "DefaultBranch") 1162 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1163 - 1164 - gr, err := git.Open(path, "") 1165 - if err != nil { 1166 - notFound(w) 1167 - return 1168 - } 1169 - 1170 - branch, err := gr.FindMainBranch() 1171 - if err != nil { 1172 - writeError(w, err.Error(), http.StatusInternalServerError) 1173 - l.Error("getting default branch", "error", err.Error()) 1174 - return 1175 - } 1176 - 1177 - writeJSON(w, types.RepoDefaultBranchResponse{ 1178 - Branch: branch, 1179 - }) 1180 - } 1181 - 1182 - func (h *Handle) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 1183 - l := h.l.With("handler", "SetDefaultBranch") 1184 - path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 1185 - 1186 - data := struct { 1187 - Branch string `json:"branch"` 1188 - }{} 1189 - 1190 - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 1191 - writeError(w, err.Error(), http.StatusBadRequest) 1192 - return 1193 - } 1194 - 1195 - gr, err := git.PlainOpen(path) 1196 - if err != nil { 1197 - notFound(w) 1198 - return 1199 - } 1200 - 1201 - err = gr.SetDefaultBranch(data.Branch) 1202 - if err != nil { 1203 - writeError(w, err.Error(), http.StatusInternalServerError) 1204 - l.Error("setting default branch", "error", err.Error()) 1205 - return 1206 - } 1207 - 1208 - w.WriteHeader(http.StatusNoContent) 1209 - } 1210 - 1211 - func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { 1212 - w.Write([]byte("ok")) 1213 - } 1214 - 1215 - func validateRepoName(name string) error { 1216 - // check for path traversal attempts 1217 - if name == "." || name == ".." || 1218 - strings.Contains(name, "/") || strings.Contains(name, "\\") { 1219 - return fmt.Errorf("Repository name contains invalid path characters") 1220 - } 1221 - 1222 - // check for sequences that could be used for traversal when normalized 1223 - if strings.Contains(name, "./") || strings.Contains(name, "../") || 1224 - strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") { 1225 - return fmt.Errorf("Repository name contains invalid path sequence") 1226 - } 1227 - 1228 - // then continue with character validation 1229 - for _, char := range name { 1230 - if !((char >= 'a' && char <= 'z') || 1231 - (char >= 'A' && char <= 'Z') || 1232 - (char >= '0' && char <= '9') || 1233 - char == '-' || char == '_' || char == '.') { 1234 - return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores") 197 + // remove existing owner 198 + err = h.e.RemoveKnotOwner(rbacDomain, existingOwner) 199 + if err != nil { 200 + return nil 1235 201 } 1236 - } 1237 - 1238 - // additional check to prevent multiple sequential dots 1239 - if strings.Contains(name, "..") { 1240 - return fmt.Errorf("Repository name cannot contain sequential dots") 202 + default: 203 + return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath) 1241 204 } 1242 205 1243 - // if all checks pass 1244 - return nil 206 + return h.e.AddKnotOwner(rbacDomain, cfgOwner) 1245 207 }
+8
rbac/rbac.go
··· 277 277 return e.isInviteAllowed(user, intoSpindle(domain)) 278 278 } 279 279 280 + func (e *Enforcer) IsRepoCreateAllowed(user, domain string) (bool, error) { 281 + return e.E.Enforce(user, domain, domain, "repo:create") 282 + } 283 + 284 + func (e *Enforcer) IsRepoDeleteAllowed(user, domain string) (bool, error) { 285 + return e.E.Enforce(user, domain, domain, "repo:delete") 286 + } 287 + 280 288 func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) { 281 289 return e.E.Enforce(user, domain, repo, "repo:push") 282 290 }