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 }