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

     1  // Copyright 2023 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  	"fmt"
    20  
    21  	"google.golang.org/grpc/codes"
    22  	"google.golang.org/protobuf/types/known/emptypb"
    23  
    24  	"go.chromium.org/luci/common/errors"
    25  	"go.chromium.org/luci/grpc/appstatus"
    26  	"go.chromium.org/luci/grpc/grpcutil"
    27  	"go.chromium.org/luci/resultdb/internal/invocations"
    28  	"go.chromium.org/luci/resultdb/internal/services/baselineupdater"
    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  
    33  	"go.chromium.org/luci/server/auth"
    34  	"go.chromium.org/luci/server/auth/realms"
    35  	"go.chromium.org/luci/server/span"
    36  )
    37  
    38  func noPermissionsError(inv invocations.ID) string {
    39  	return fmt.Sprintf(`Caller does not have permission to mark invocations/%s submitted (or it might not exist)`, string(inv))
    40  }
    41  
    42  func validateMarkInvocationSubmittedPermissions(ctx context.Context, inv invocations.ID) error {
    43  	readCtx, cancel := span.ReadOnlyTransaction(ctx)
    44  	defer cancel()
    45  
    46  	invRealm, err := invocations.ReadRealm(readCtx, inv)
    47  	if err != nil {
    48  		// If the invocation does not exist, we mask the error with permission
    49  		// denied to avoid leaking resource existence.
    50  		if grpcutil.Code(err) == codes.NotFound {
    51  			return appstatus.Errorf(codes.PermissionDenied, noPermissionsError(inv))
    52  		} else {
    53  			return err
    54  		}
    55  	}
    56  
    57  	// Parse the realm to check using format <project>:@project.
    58  	project, _ := realms.Split(invRealm)
    59  	realm := realms.Join(project, realms.ProjectRealm)
    60  	if err := realms.ValidateRealmName(realm, realms.GlobalScope); err != nil {
    61  		return errors.Annotate(err, "invocation: realm").Err()
    62  	}
    63  
    64  	switch allowed, err := auth.HasPermission(ctx, permSetSubmittedInvocation, realm, nil); {
    65  	case err != nil:
    66  		return err
    67  	case !allowed:
    68  		return appstatus.Errorf(codes.PermissionDenied, noPermissionsError(inv))
    69  	}
    70  
    71  	return nil
    72  }
    73  
    74  func markInvocationSubmitted(ctx context.Context, invocation invocations.ID) {
    75  	values := map[string]any{
    76  		"InvocationId": invocation,
    77  		"Submitted":    true,
    78  	}
    79  
    80  	span.BufferWrite(ctx, spanutil.UpdateMap("Invocations", values))
    81  }
    82  
    83  // MarkInvocationSubmitted implements pb.RecorderServer.
    84  // This adds the test variants to the BaselineTestVariant table if they
    85  // don't exist, or updating the LastUpdated so that its retention is updated.
    86  // The invocation must already be finalized.
    87  func (s *recorderServer) MarkInvocationSubmitted(ctx context.Context, req *pb.MarkInvocationSubmittedRequest) (*emptypb.Empty, error) {
    88  	// Parsing the invocation name also validates its syntax.
    89  	invID, err := pbutil.ParseInvocationName(req.Invocation)
    90  	if err != nil {
    91  		return &emptypb.Empty{}, appstatus.Error(codes.InvalidArgument, errors.Annotate(err, "invocation").Err().Error())
    92  	}
    93  	inv := invocations.ID(invID)
    94  
    95  	if err = validateMarkInvocationSubmittedPermissions(ctx, inv); err != nil {
    96  		return &emptypb.Empty{}, err
    97  	}
    98  
    99  	_, err = span.ReadWriteTransaction(ctx, func(ctx context.Context) error {
   100  		submitted, err := invocations.ReadSubmitted(ctx, inv)
   101  		if err != nil {
   102  			return err
   103  		}
   104  		if submitted {
   105  			// Invocation already marked submitted
   106  			return nil
   107  		}
   108  
   109  		markInvocationSubmitted(ctx, inv)
   110  
   111  		state, err := invocations.ReadState(ctx, inv)
   112  		if err != nil {
   113  			return err
   114  		}
   115  		if state != pb.Invocation_FINALIZED {
   116  			// Finalizer will schedule the task if it has not been finalized yet.
   117  			return nil
   118  		}
   119  
   120  		// Add this request to the task queue.
   121  		baselineupdater.Schedule(ctx, invID)
   122  		return nil
   123  	})
   124  	if err != nil {
   125  		return &emptypb.Empty{}, err
   126  	}
   127  	return &emptypb.Empty{}, nil
   128  }