go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/recorder/create_invocation.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  	"strings"
    20  	"time"
    21  
    22  	"github.com/golang/protobuf/ptypes"
    23  	"google.golang.org/grpc/codes"
    24  	"google.golang.org/grpc/metadata"
    25  	"google.golang.org/protobuf/types/known/timestamppb"
    26  
    27  	"go.chromium.org/luci/common/clock"
    28  	"go.chromium.org/luci/common/errors"
    29  	"go.chromium.org/luci/grpc/appstatus"
    30  	"go.chromium.org/luci/grpc/prpc"
    31  	"go.chromium.org/luci/server/auth"
    32  	"go.chromium.org/luci/server/auth/realms"
    33  	"go.chromium.org/luci/server/span"
    34  
    35  	"go.chromium.org/luci/resultdb/internal"
    36  	"go.chromium.org/luci/resultdb/internal/invocations"
    37  	"go.chromium.org/luci/resultdb/internal/permissions"
    38  	"go.chromium.org/luci/resultdb/pbutil"
    39  	pb "go.chromium.org/luci/resultdb/proto/v1"
    40  )
    41  
    42  // TestMagicOverdueDeadlineUnixSecs is a magic value used by tests to set an
    43  // invocation's deadline in the past.
    44  const TestMagicOverdueDeadlineUnixSecs = 904924800
    45  
    46  // isValidCreateState returns false if invocations cannot be created in the
    47  // given state `s`.
    48  func isValidCreateState(s pb.Invocation_State) bool {
    49  	switch s {
    50  	default:
    51  		return false
    52  	case pb.Invocation_STATE_UNSPECIFIED:
    53  	case pb.Invocation_ACTIVE:
    54  	case pb.Invocation_FINALIZING:
    55  	}
    56  	return true
    57  }
    58  
    59  // validateInvocationDeadline returns a non-nil error if deadline is invalid.
    60  func validateInvocationDeadline(deadline *timestamppb.Timestamp, now time.Time) error {
    61  	internal.AssertUTC(now)
    62  	switch d, err := ptypes.Timestamp(deadline); {
    63  	case err != nil:
    64  		return err
    65  
    66  	case deadline.GetSeconds() == TestMagicOverdueDeadlineUnixSecs && deadline.GetNanos() == 0:
    67  		return nil
    68  
    69  	case d.Sub(now) < 10*time.Second:
    70  		return errors.Reason("must be at least 10 seconds in the future").Err()
    71  
    72  	case d.Sub(now) > maxInvocationDeadlineDuration:
    73  		return errors.Reason("must be before %dh in the future", int(maxInvocationDeadlineDuration.Hours())).Err()
    74  
    75  	default:
    76  		return nil
    77  	}
    78  }
    79  
    80  // validateCreateInvocationRequest returns an error if req is determined to be
    81  // invalid.
    82  // It also adds the invocations to be included into the newly
    83  // created invocation to the given IDSet.
    84  func validateCreateInvocationRequest(req *pb.CreateInvocationRequest, now time.Time, includedIDs invocations.IDSet) error {
    85  	if err := pbutil.ValidateInvocationID(req.InvocationId); err != nil {
    86  		return errors.Annotate(err, "invocation_id").Err()
    87  	}
    88  	if err := pbutil.ValidateRequestID(req.RequestId); err != nil {
    89  		return errors.Annotate(err, "request_id").Err()
    90  	}
    91  
    92  	inv := req.Invocation
    93  	if inv == nil {
    94  		return errors.Annotate(errors.Reason("unspecified").Err(), "invocation").Err()
    95  	}
    96  
    97  	if err := pbutil.ValidateStringPairs(inv.GetTags()); err != nil {
    98  		return errors.Annotate(err, "invocation: tags").Err()
    99  	}
   100  
   101  	if inv.Realm == "" {
   102  		return errors.Annotate(errors.Reason("unspecified").Err(), "invocation: realm").Err()
   103  	}
   104  
   105  	if err := realms.ValidateRealmName(inv.Realm, realms.GlobalScope); err != nil {
   106  		return errors.Annotate(err, "invocation: realm").Err()
   107  	}
   108  
   109  	if inv.GetDeadline() != nil {
   110  		if err := validateInvocationDeadline(inv.Deadline, now); err != nil {
   111  			return errors.Annotate(err, "invocation: deadline").Err()
   112  		}
   113  	}
   114  
   115  	if !isValidCreateState(inv.GetState()) {
   116  		return errors.Reason("invocation: state: cannot be created in the state %s", inv.GetState()).Err()
   117  	}
   118  
   119  	for i, bqExport := range inv.GetBigqueryExports() {
   120  		if err := pbutil.ValidateBigQueryExport(bqExport); err != nil {
   121  			return errors.Annotate(err, "bigquery_export[%d]", i).Err()
   122  		}
   123  	}
   124  
   125  	for i, incInvName := range inv.GetIncludedInvocations() {
   126  		incInvID, err := pbutil.ParseInvocationName(incInvName)
   127  		if err != nil {
   128  			return errors.Annotate(err, "included_invocations[%d]: invalid included invocation name %q", i, incInvName).Err()
   129  		}
   130  		if incInvID == req.InvocationId {
   131  			return errors.Reason("included_invocations[%d]: invocation cannot include itself", i).Err()
   132  		}
   133  		includedIDs.Add(invocations.ID(incInvID))
   134  	}
   135  
   136  	if err := pbutil.ValidateSourceSpec(inv.GetSourceSpec()); err != nil {
   137  		return errors.Annotate(err, "source_spec").Err()
   138  	}
   139  
   140  	if inv.GetBaselineId() != "" {
   141  		if err := pbutil.ValidateBaselineID(inv.GetBaselineId()); err != nil {
   142  			return errors.Annotate(err, "invocation: baseline_id").Err()
   143  		}
   144  	}
   145  
   146  	if err := pbutil.ValidateProperties(req.Invocation.GetProperties()); err != nil {
   147  		return errors.Annotate(err, "properties").Err()
   148  	}
   149  
   150  	return nil
   151  }
   152  
   153  func verifyCreateInvocationPermissions(ctx context.Context, in *pb.CreateInvocationRequest) error {
   154  	inv := in.Invocation
   155  	if inv == nil {
   156  		return appstatus.BadRequest(errors.Annotate(errors.Reason("unspecified").Err(), "invocation").Err())
   157  	}
   158  
   159  	realm := inv.Realm
   160  	if realm == "" {
   161  		return appstatus.BadRequest(errors.Annotate(errors.Reason("unspecified").Err(), "invocation: realm").Err())
   162  	}
   163  	if err := realms.ValidateRealmName(realm, realms.GlobalScope); err != nil {
   164  		return appstatus.BadRequest(errors.Annotate(err, "invocation: realm").Err())
   165  	}
   166  
   167  	switch allowed, err := auth.HasPermission(ctx, permCreateInvocation, realm, nil); {
   168  	case err != nil:
   169  		return err
   170  	case !allowed:
   171  		return appstatus.Errorf(codes.PermissionDenied, `creator does not have permission to create invocations in realm %q`, realm)
   172  	}
   173  
   174  	if !strings.HasPrefix(in.InvocationId, "u-") {
   175  		switch allowed, err := auth.HasPermission(ctx, permCreateWithReservedID, realm, nil); {
   176  		case err != nil:
   177  			return err
   178  		case !allowed:
   179  			return appstatus.Errorf(codes.PermissionDenied, `only invocations created by trusted systems may have id not starting with "u-"; please generate "u-{GUID}" or reach out to ResultDB owners`)
   180  		}
   181  	}
   182  
   183  	if len(inv.GetBigqueryExports()) > 0 {
   184  		switch allowed, err := auth.HasPermission(ctx, permExportToBigQuery, realm, nil); {
   185  		case err != nil:
   186  			return err
   187  		case !allowed:
   188  			return appstatus.Errorf(codes.PermissionDenied, `creator does not have permission to set bigquery exports in realm %q`, inv.GetRealm())
   189  		}
   190  	}
   191  
   192  	if inv.GetProducerResource() != "" {
   193  		switch allowed, err := auth.HasPermission(ctx, permSetProducerResource, realm, nil); {
   194  		case err != nil:
   195  			return err
   196  		case !allowed:
   197  			return appstatus.Errorf(codes.PermissionDenied, `only invocations created by trusted system may have a populated producer_resource field`)
   198  		}
   199  	}
   200  
   201  	if inv.BaselineId != "" {
   202  		switch allowed, err := auth.HasPermission(ctx, permPutBaseline, realm, nil); {
   203  		case err != nil:
   204  			return err
   205  		case !allowed:
   206  			return appstatus.Errorf(codes.PermissionDenied, `creator does not have permission to set baseline ids in realm %q`, inv.GetRealm())
   207  		}
   208  	}
   209  
   210  	return nil
   211  }
   212  
   213  // CreateInvocation implements pb.RecorderServer.
   214  func (s *recorderServer) CreateInvocation(ctx context.Context, in *pb.CreateInvocationRequest) (*pb.Invocation, error) {
   215  	now := clock.Now(ctx).UTC()
   216  
   217  	if err := verifyCreateInvocationPermissions(ctx, in); err != nil {
   218  		return nil, err
   219  	}
   220  
   221  	includedInvs := make(invocations.IDSet)
   222  	if err := validateCreateInvocationRequest(in, now, includedInvs); err != nil {
   223  		return nil, appstatus.BadRequest(err)
   224  	}
   225  
   226  	if err := permissions.VerifyInvocations(span.Single(ctx), includedInvs, permIncludeInvocation); err != nil {
   227  		return nil, err
   228  	}
   229  
   230  	invs, tokens, err := s.createInvocations(ctx, []*pb.CreateInvocationRequest{in}, in.RequestId, now, invocations.NewIDSet(invocations.ID(in.InvocationId)))
   231  	if err != nil {
   232  		return nil, err
   233  	}
   234  	if len(invs) != 1 || len(tokens) != 1 {
   235  		panic("createInvocations did not return either an error or a valid invocation/token pair")
   236  	}
   237  	md := metadata.MD{}
   238  	md.Set(pb.UpdateTokenMetadataKey, tokens...)
   239  	prpc.SetHeader(ctx, md)
   240  	return invs[0], nil
   241  }
   242  
   243  func invocationAlreadyExists(id invocations.ID) error {
   244  	return appstatus.Errorf(codes.AlreadyExists, "%s already exists", id.Name())
   245  }