go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/recorder/update_included_invocations.go (about)

     1  // Copyright 2019 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 recorder
    16  
    17  import (
    18  	"context"
    19  
    20  	"cloud.google.com/go/spanner"
    21  	"google.golang.org/grpc/codes"
    22  	"google.golang.org/protobuf/types/known/emptypb"
    23  
    24  	"go.chromium.org/luci/common/data/stringset"
    25  	"go.chromium.org/luci/common/errors"
    26  	"go.chromium.org/luci/grpc/appstatus"
    27  	"go.chromium.org/luci/resultdb/internal/invocations"
    28  	"go.chromium.org/luci/resultdb/internal/permissions"
    29  	"go.chromium.org/luci/resultdb/internal/spanutil"
    30  	"go.chromium.org/luci/resultdb/pbutil"
    31  	pb "go.chromium.org/luci/resultdb/proto/v1"
    32  	"go.chromium.org/luci/server/span"
    33  )
    34  
    35  // validateUpdateIncludedInvocationsRequest returns a non-nil error if req is
    36  // determined to be invalid.
    37  func validateUpdateIncludedInvocationsRequest(req *pb.UpdateIncludedInvocationsRequest) error {
    38  	if _, err := pbutil.ParseInvocationName(req.IncludingInvocation); err != nil {
    39  		return errors.Annotate(err, "including_invocation").Err()
    40  	}
    41  	for _, name := range req.AddInvocations {
    42  		if name == req.IncludingInvocation {
    43  			return errors.Reason("cannot include itself").Err()
    44  		}
    45  		if _, err := pbutil.ParseInvocationName(name); err != nil {
    46  			return errors.Annotate(err, "add_invocations: %q", name).Err()
    47  		}
    48  	}
    49  
    50  	for _, name := range req.RemoveInvocations {
    51  		if _, err := pbutil.ParseInvocationName(name); err != nil {
    52  			return errors.Annotate(err, "remove_invocations: %q", name).Err()
    53  		}
    54  	}
    55  
    56  	both := stringset.NewFromSlice(req.AddInvocations...).Intersect(stringset.NewFromSlice(req.RemoveInvocations...)).ToSortedSlice()
    57  	if len(both) > 0 {
    58  		return errors.Reason("cannot add and remove the same invocation(s) at the same time: %q", both).Err()
    59  	}
    60  	return nil
    61  }
    62  
    63  // UpdateIncludedInvocations implements pb.RecorderServer.
    64  func (s *recorderServer) UpdateIncludedInvocations(ctx context.Context, in *pb.UpdateIncludedInvocationsRequest) (*emptypb.Empty, error) {
    65  	if err := validateUpdateIncludedInvocationsRequest(in); err != nil {
    66  		return nil, appstatus.BadRequest(err)
    67  	}
    68  	including := invocations.MustParseName(in.IncludingInvocation)
    69  	add := invocations.MustParseNames(in.AddInvocations)
    70  	remove := invocations.MustParseNames(in.RemoveInvocations)
    71  
    72  	err := mutateInvocation(ctx, including, func(ctx context.Context) error {
    73  		// To include invocation A into invocation B, in addition to checking the
    74  		// update token for B in mutateInvocation below, verify that the caller has
    75  		// permission 'resultdb.invocation.include' on A's realm.
    76  		// Perform this check in the same transaction as the update to avoid
    77  		// TOC-TOU vulnerabilities.
    78  		if err := permissions.VerifyInvocations(ctx, add, permIncludeInvocation); err != nil {
    79  			return err
    80  		}
    81  
    82  		// Accumulate keys to remove in a single KeySet.
    83  		ks := spanner.KeySets()
    84  		for rInv := range remove {
    85  			ks = spanner.KeySets(invocations.InclusionKey(including, rInv), ks)
    86  		}
    87  		ms := make([]*spanner.Mutation, 1, 1+len(add))
    88  		ms[0] = spanner.Delete("IncludedInvocations", ks)
    89  
    90  		switch states, err := invocations.ReadStateBatch(ctx, add); {
    91  		case err != nil:
    92  			return err
    93  		// Ensure every included invocation exists.
    94  		case len(states) != len(add):
    95  			return appstatus.Errorf(codes.NotFound, "at least one of the included invocations does not exist")
    96  		}
    97  		for aInv := range add {
    98  			ms = append(ms, spanutil.InsertOrUpdateMap("IncludedInvocations", map[string]any{
    99  				"InvocationId":         including,
   100  				"IncludedInvocationId": aInv,
   101  			}))
   102  		}
   103  		span.BufferWrite(ctx, ms...)
   104  		return nil
   105  	})
   106  
   107  	return &emptypb.Empty{}, err
   108  }