go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/recorder/update_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 "time" 20 21 "cloud.google.com/go/spanner" 22 "google.golang.org/grpc/codes" 23 24 "go.chromium.org/luci/common/clock" 25 "go.chromium.org/luci/common/errors" 26 "go.chromium.org/luci/common/logging" 27 "go.chromium.org/luci/grpc/appstatus" 28 "go.chromium.org/luci/server/auth" 29 "go.chromium.org/luci/server/span" 30 31 "go.chromium.org/luci/resultdb/internal/invocations" 32 "go.chromium.org/luci/resultdb/internal/spanutil" 33 "go.chromium.org/luci/resultdb/pbutil" 34 pb "go.chromium.org/luci/resultdb/proto/v1" 35 ) 36 37 // validateUpdateInvocationRequest returns non-nil error if req is invalid. 38 func validateUpdateInvocationRequest(req *pb.UpdateInvocationRequest, now time.Time) error { 39 if err := pbutil.ValidateInvocationName(req.Invocation.GetName()); err != nil { 40 return errors.Annotate(err, "invocation: name").Err() 41 } 42 43 if len(req.UpdateMask.GetPaths()) == 0 { 44 return errors.Reason("update_mask: paths is empty").Err() 45 } 46 47 for _, path := range req.UpdateMask.GetPaths() { 48 switch path { 49 // The cases in this switch statement must be synchronized with a 50 // similar switch statement in UpdateInvocation implementation. 51 52 case "deadline": 53 if err := validateInvocationDeadline(req.Invocation.GetDeadline(), now); err != nil { 54 return errors.Annotate(err, "invocation: deadline").Err() 55 } 56 57 case "bigquery_exports": 58 for i, bqExport := range req.Invocation.GetBigqueryExports() { 59 if err := pbutil.ValidateBigQueryExport(bqExport); err != nil { 60 return errors.Annotate(err, "invocation: bigquery_exports[%d]", i).Err() 61 } 62 } 63 64 case "properties": 65 if err := pbutil.ValidateProperties(req.Invocation.Properties); err != nil { 66 return errors.Annotate(err, "invocation: properties").Err() 67 } 68 69 case "source_spec": 70 if err := pbutil.ValidateSourceSpec(req.Invocation.SourceSpec); err != nil { 71 return errors.Annotate(err, "invocation: source_spec").Err() 72 } 73 74 case "baseline_id": 75 if err := pbutil.ValidateBaselineID(req.Invocation.BaselineId); err != nil { 76 return errors.Annotate(err, "invocation: baseline_id").Err() 77 } 78 79 default: 80 return errors.Reason("update_mask: unsupported path %q", path).Err() 81 } 82 } 83 84 return nil 85 } 86 87 func validateUpdateBaselinePermissions(ctx context.Context, realm string) error { 88 switch allowed, err := auth.HasPermission(ctx, permPutBaseline, realm, nil); { 89 case err != nil: 90 return err 91 case !allowed: 92 return appstatus.Errorf(codes.PermissionDenied, `caller does not have permission to set baseline ids in realm %s`, realm) 93 } 94 return nil 95 } 96 97 // UpdateInvocation implements pb.RecorderServer. 98 func (s *recorderServer) UpdateInvocation(ctx context.Context, in *pb.UpdateInvocationRequest) (*pb.Invocation, error) { 99 if err := validateUpdateInvocationRequest(in, clock.Now(ctx).UTC()); err != nil { 100 return nil, appstatus.BadRequest(err) 101 } 102 103 invID := invocations.MustParseName(in.Invocation.Name) 104 105 var ret *pb.Invocation 106 err := mutateInvocation(ctx, invID, func(ctx context.Context) error { 107 var err error 108 if ret, err = invocations.Read(ctx, invID); err != nil { 109 return err 110 } 111 112 values := map[string]any{ 113 "InvocationId": invID, 114 } 115 116 for _, path := range in.UpdateMask.Paths { 117 switch path { 118 // The cases in this switch statement must be synchronized with a 119 // similar switch statement in validateUpdateInvocationRequest. 120 121 case "deadline": 122 deadlne := in.Invocation.Deadline 123 values["Deadline"] = deadlne 124 ret.Deadline = deadlne 125 126 case "bigquery_exports": 127 bqExports := in.Invocation.BigqueryExports 128 values["BigQueryExports"] = bqExports 129 ret.BigqueryExports = bqExports 130 131 case "properties": 132 values["Properties"] = spanutil.Compressed(pbutil.MustMarshal(in.Invocation.Properties)) 133 ret.Properties = in.Invocation.Properties 134 135 case "source_spec": 136 // Store any gerrit changes in normalised form. 137 pbutil.SortGerritChanges(in.Invocation.SourceSpec.GetSources().GetChangelists()) 138 values["InheritSources"] = spanner.NullBool{Valid: in.Invocation.SourceSpec != nil, Bool: in.Invocation.SourceSpec.GetInherit()} 139 values["Sources"] = spanutil.Compressed(pbutil.MustMarshal(in.Invocation.SourceSpec.GetSources())) 140 ret.SourceSpec = in.Invocation.SourceSpec 141 142 case "baseline_id": 143 if err := validateUpdateBaselinePermissions(ctx, ret.Realm); err != nil { 144 // log the error and silently skip setting baseline id 145 logging.Warningf(ctx, "Silently swallowing permission error %s", err) 146 continue 147 } 148 baselineID := in.Invocation.BaselineId 149 values["BaselineId"] = baselineID 150 ret.BaselineId = baselineID 151 152 default: 153 panic("impossible") 154 } 155 } 156 157 span.BufferWrite(ctx, spanutil.UpdateMap("Invocations", values)) 158 return nil 159 }) 160 if err != nil { 161 return nil, err 162 } 163 return ret, nil 164 }