blob: 0fe465e90d98e2e7866572fc7338e578e35659f2 [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rpc
import (
"context"
"fmt"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/grpc/appstatus"
"go.chromium.org/luci/buildbucket/appengine/common"
"go.chromium.org/luci/buildbucket/appengine/internal/perm"
"go.chromium.org/luci/buildbucket/appengine/model"
"go.chromium.org/luci/buildbucket/bbperms"
pb "go.chromium.org/luci/buildbucket/proto"
"go.chromium.org/luci/buildbucket/protoutil"
)
// validateGet validates the given request.
func validateGet(req *pb.GetBuildRequest) error {
switch {
case req.GetId() != 0:
if req.Builder != nil || req.BuildNumber != 0 {
return errors.Reason("id is mutually exclusive with (builder and build_number)").Err()
}
case req.GetBuilder() != nil && req.BuildNumber != 0:
if err := protoutil.ValidateRequiredBuilderID(req.Builder); err != nil {
return errors.Annotate(err, "builder").Err()
}
default:
return errors.Reason("one of id or (builder and build_number) is required").Err()
}
return nil
}
func getBuildIDByBuildNumber(ctx context.Context, bldr *pb.BuilderID, nbr int32) (int64, error) {
addr := fmt.Sprintf("luci.%s.%s/%s/%d", bldr.Project, bldr.Bucket, bldr.Builder, nbr)
switch ents, err := model.SearchTagIndex(ctx, "build_address", addr); {
case model.TagIndexIncomplete.In(err):
// Shouldn't happen because build address is globally unique (exactly one entry in a complete index).
return 0, errors.Reason("unexpected incomplete index for build address %q", addr).Err()
case err != nil:
return 0, err
case len(ents) == 0:
return 0, perm.NotFoundErr(ctx)
case len(ents) == 1:
return ents[0].BuildID, nil
default:
// Shouldn't happen because build address is globally unique and created before the build.
return 0, errors.Reason("unexpected number of results for build address %q: %d", addr, len(ents)).Err()
}
}
// GetBuild handles a request to retrieve a build. Implements pb.BuildsServer.
func (*Builds) GetBuild(ctx context.Context, req *pb.GetBuildRequest) (*pb.Build, error) {
if err := validateGet(req); err != nil {
return nil, appstatus.BadRequest(err)
}
m, err := model.NewBuildMask("", req.Fields, req.Mask)
if err != nil {
return nil, appstatus.BadRequest(errors.Annotate(err, "invalid mask").Err())
}
if req.Id == 0 {
req.Id, err = getBuildIDByBuildNumber(ctx, req.Builder, req.BuildNumber)
if err != nil {
return nil, err
}
}
bld, err := common.GetBuild(ctx, req.Id)
if err != nil {
return nil, err
}
// User needs BuildsGet or BuildsGetLimited permission to call this endpoint.
readPerm, err := perm.GetFirstAvailablePerm(ctx, bld.Proto.Builder, bbperms.BuildsGet, bbperms.BuildsGetLimited)
if err != nil {
var readShadowedErr error
var shadowedBkt string
// Checks if the build is a led build.
entities, getInfraErr := common.GetBuildEntities(ctx, req.Id, model.BuildInfraKind)
if getInfraErr == nil {
infra := entities[0].(*model.BuildInfra)
shadowedBkt = infra.Proto.GetLed().GetShadowedBucket()
if shadowedBkt != "" && shadowedBkt != bld.Proto.Builder.Bucket {
// The build is a led build. Check the use permission from the shadowed
// bucket.
shadowedBldr := &pb.BuilderID{
Project: bld.Proto.Builder.Project,
Bucket: shadowedBkt,
Builder: bld.Proto.Builder.Builder,
}
readPerm, readShadowedErr = perm.GetFirstAvailablePerm(ctx, shadowedBldr, bbperms.BuildsGet, bbperms.BuildsGetLimited)
}
}
if getInfraErr != nil || shadowedBkt == "" || readShadowedErr != nil {
// Either there's error getting build infra, or the build is not a led
// build, or the user doesn't have read permission in the shadowed bucket
// either.
// Return the original error.
return nil, err
}
}
bp, err := bld.ToProto(ctx, m, func(b *pb.Build) error {
if readPerm == bbperms.BuildsGet {
return nil
}
return perm.RedactBuild(ctx, nil, b)
})
if err != nil {
return nil, err
}
return bp, nil
}