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  }