go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/casviewer/handlers.go (about) 1 // Copyright 2020 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package casviewer 16 17 import ( 18 "context" 19 "fmt" 20 "html/template" 21 "net/http" 22 "os" 23 "strconv" 24 25 "github.com/bazelbuild/remote-apis-sdks/go/pkg/digest" 26 "github.com/dustin/go-humanize" 27 "github.com/julienschmidt/httprouter" 28 29 "go.chromium.org/luci/common/errors" 30 "go.chromium.org/luci/grpc/grpcutil" 31 "go.chromium.org/luci/server/auth" 32 "go.chromium.org/luci/server/auth/realms" 33 "go.chromium.org/luci/server/router" 34 "go.chromium.org/luci/server/templates" 35 ) 36 37 // Path to the templates dir from the executable. 38 const templatePath = "templates" 39 40 var permMintToken = realms.RegisterPermission("luci.serviceAccounts.mintToken") 41 42 // InstallHandlers install CAS Viewer handlers to the router. 43 func InstallHandlers(r *router.Router, cc *ClientCache, appVersion string) { 44 baseMW := router.NewMiddlewareChain( 45 templates.WithTemplates(getTemplateBundle(appVersion)), 46 ) 47 blobMW := baseMW.Extend( 48 checkPermission, 49 withClientCacheMW(cc), 50 ) 51 52 r.GET("/", baseMW, rootHandler) 53 r.GET("/projects/:project/instances/:instance/blobs/:hash/:size/tree", blobMW, treeHandler) 54 r.GET("/projects/:project/instances/:instance/blobs/:hash/:size", blobMW, getHandler) 55 } 56 57 // getTemplateBundles returns template Bundle with base args. 58 func getTemplateBundle(appVersion string) *templates.Bundle { 59 return &templates.Bundle{ 60 Loader: templates.FileSystemLoader(os.DirFS(templatePath)), 61 DefaultTemplate: "base", 62 DefaultArgs: func(c context.Context, e *templates.Extra) (templates.Args, error) { 63 return templates.Args{ 64 "AppVersion": appVersion, 65 "User": auth.CurrentUser(c), 66 }, nil 67 }, 68 FuncMap: template.FuncMap{ 69 "treeURL": treeURL, 70 "getURL": getURL, 71 "readableBytes": func(s int64) string { return humanize.Bytes(uint64(s)) }, 72 }, 73 } 74 } 75 76 // checkPermission checks if the user has permission to read the blob. 77 func checkPermission(c *router.Context, next router.Handler) { 78 switch ok, err := auth.HasPermission(c.Request.Context(), permMintToken, readOnlyRealm(c.Params), nil); { 79 case err != nil: 80 renderErrorPage(c.Request.Context(), c.Writer, err) 81 case !ok: 82 err = errors.New("permission denied", grpcutil.PermissionDeniedTag) 83 renderErrorPage(c.Request.Context(), c.Writer, err) 84 default: 85 next(c) 86 } 87 } 88 89 // rootHandler renders top page. 90 func rootHandler(c *router.Context) { 91 templates.MustRender(c.Request.Context(), c.Writer, "pages/index.html", nil) 92 } 93 94 func treeHandler(c *router.Context) { 95 inst := fullInstName(c.Params) 96 cl, err := GetClient(c.Request.Context(), inst) 97 if err != nil { 98 renderErrorPage(c.Request.Context(), c.Writer, err) 99 return 100 } 101 bd, err := blobDigest(c.Params) 102 if err != nil { 103 renderErrorPage(c.Request.Context(), c.Writer, err) 104 return 105 } 106 err = renderTree(c.Request.Context(), c.Writer, cl, bd, inst) 107 if err != nil { 108 renderErrorPage(c.Request.Context(), c.Writer, err) 109 } 110 } 111 112 func getHandler(c *router.Context) { 113 cl, err := GetClient(c.Request.Context(), fullInstName(c.Params)) 114 if err != nil { 115 renderErrorPage(c.Request.Context(), c.Writer, err) 116 return 117 } 118 bd, err := blobDigest(c.Params) 119 if err != nil { 120 renderErrorPage(c.Request.Context(), c.Writer, err) 121 return 122 } 123 err = returnBlob(c.Request.Context(), c.Writer, cl, bd, fileName(c.Request)) 124 if err != nil { 125 renderErrorPage(c.Request.Context(), c.Writer, err) 126 } 127 } 128 129 func readOnlyRealm(p httprouter.Params) string { 130 return fmt.Sprintf("@internal:%s/cas-read-only", p.ByName("project")) 131 } 132 133 // fullInstName constructs full instance name from the URL parameters. 134 func fullInstName(p httprouter.Params) string { 135 return fmt.Sprintf( 136 "projects/%s/instances/%s", p.ByName("project"), p.ByName("instance")) 137 } 138 139 // blobDigest constructs a Digest from the URL parameters. 140 func blobDigest(p httprouter.Params) (*digest.Digest, error) { 141 size, err := strconv.ParseInt(p.ByName("size"), 10, 64) 142 if err != nil { 143 err = errors.Annotate(err, "Digest size must be number").Tag(grpcutil.InvalidArgumentTag).Err() 144 return nil, err 145 } 146 147 return &digest.Digest{ 148 Hash: p.ByName("hash"), 149 Size: size, 150 }, nil 151 } 152 153 // fileName extracts file name from query params. 154 func fileName(r *http.Request) string { 155 names := r.URL.Query()["filename"] 156 if len(names) >= 1 { 157 return names[0] 158 } else { 159 return "" 160 } 161 }