github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/processors/command/impl.go (about) 1 /* 2 * Copyright (c) 2021-present unTill Pro, Ltd. 3 */ 4 5 package commandprocessor 6 7 import ( 8 "bytes" 9 "context" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "net/http" 14 "strconv" 15 "time" 16 17 "github.com/voedger/voedger/pkg/goutils/iterate" 18 "github.com/voedger/voedger/pkg/goutils/logger" 19 "golang.org/x/exp/maps" 20 21 "github.com/voedger/voedger/pkg/appdef" 22 "github.com/voedger/voedger/pkg/appparts" 23 "github.com/voedger/voedger/pkg/iauthnz" 24 "github.com/voedger/voedger/pkg/in10n" 25 "github.com/voedger/voedger/pkg/istructs" 26 "github.com/voedger/voedger/pkg/istructsmem" 27 "github.com/voedger/voedger/pkg/pipeline" 28 "github.com/voedger/voedger/pkg/processors" 29 "github.com/voedger/voedger/pkg/projectors" 30 "github.com/voedger/voedger/pkg/sys/authnz" 31 "github.com/voedger/voedger/pkg/sys/blobber" 32 "github.com/voedger/voedger/pkg/sys/builtin" 33 workspacemgmt "github.com/voedger/voedger/pkg/sys/workspace" 34 coreutils "github.com/voedger/voedger/pkg/utils" 35 ibus "github.com/voedger/voedger/staging/src/github.com/untillpro/airs-ibus" 36 ) 37 38 func (cm *implICommandMessage) Body() []byte { return cm.body } 39 func (cm *implICommandMessage) AppQName() istructs.AppQName { return cm.appQName } 40 func (cm *implICommandMessage) WSID() istructs.WSID { return cm.wsid } 41 func (cm *implICommandMessage) Sender() ibus.ISender { return cm.sender } 42 func (cm *implICommandMessage) PartitionID() istructs.PartitionID { return cm.partitionID } 43 func (cm *implICommandMessage) RequestCtx() context.Context { return cm.requestCtx } 44 func (cm *implICommandMessage) QName() appdef.QName { return cm.qName } 45 func (cm *implICommandMessage) Token() string { return cm.token } 46 func (cm *implICommandMessage) Host() string { return cm.host } 47 48 func NewCommandMessage(requestCtx context.Context, body []byte, appQName istructs.AppQName, wsid istructs.WSID, sender ibus.ISender, 49 partitionID istructs.PartitionID, qName appdef.QName, token string, host string) ICommandMessage { 50 return &implICommandMessage{ 51 body: body, 52 appQName: appQName, 53 wsid: wsid, 54 sender: sender, 55 partitionID: partitionID, 56 requestCtx: requestCtx, 57 qName: qName, 58 token: token, 59 host: host, 60 } 61 } 62 63 // used in projectors.newSyncBranch() 64 func (c *cmdWorkpiece) AppPartition() appparts.IAppPartition { 65 return c.appPart 66 } 67 68 // need for sync projectors which are using wsid.GetNextWSID() 69 func (c *cmdWorkpiece) Context() context.Context { 70 return c.cmdMes.RequestCtx() 71 } 72 73 // used in projectors.NewSyncActualizerFactoryFactory 74 func (c *cmdWorkpiece) Event() istructs.IPLogEvent { 75 return c.pLogEvent 76 } 77 78 // borrows app partition for command 79 func (c *cmdWorkpiece) borrow() (err error) { 80 if c.appPart, err = c.appParts.Borrow(c.cmdMes.AppQName(), c.cmdMes.PartitionID(), appparts.ProcessorKind_Command); err != nil { 81 if errors.Is(err, appparts.ErrNotFound) || errors.Is(err, appparts.ErrNotAvailableEngines) { // partition is not deployed yet -> ErrNotFound 82 return coreutils.NewHTTPError(http.StatusServiceUnavailable, err) 83 } 84 // notest 85 return err 86 } 87 c.appStructs = c.appPart.AppStructs() 88 return nil 89 } 90 91 // releases resources: 92 // - borrowed app partition 93 // - plog event 94 func (c *cmdWorkpiece) release() { 95 if ev := c.pLogEvent; ev != nil { 96 c.pLogEvent = nil 97 ev.Release() 98 } 99 if ap := c.appPart; ap != nil { 100 c.appStructs = nil 101 c.appPart = nil 102 ap.Release() 103 } 104 } 105 106 func borrowAppPart(_ context.Context, work interface{}) error { 107 return work.(*cmdWorkpiece).borrow() 108 } 109 110 func (ap *appPartition) getWorkspace(wsid istructs.WSID) *workspace { 111 ws, ok := ap.workspaces[wsid] 112 if !ok { 113 ws = &workspace{ 114 NextWLogOffset: istructs.FirstOffset, 115 idGenerator: istructsmem.NewIDGenerator(), 116 } 117 ap.workspaces[wsid] = ws 118 } 119 return ws 120 } 121 122 func (cmdProc *cmdProc) getAppPartition(ctx context.Context, work interface{}) (err error) { 123 cmd := work.(*cmdWorkpiece) 124 ap, ok := cmdProc.appPartitions[cmd.cmdMes.AppQName()] 125 if !ok { 126 if ap, err = cmdProc.recovery(ctx, cmd); err != nil { 127 return fmt.Errorf("partition %d recovery failed: %w", cmdProc.pNumber, err) 128 } 129 cmdProc.appPartitions[cmd.cmdMes.AppQName()] = ap 130 } 131 cmdProc.appPartition = ap 132 return nil 133 } 134 135 func getIWorkspace(_ context.Context, work interface{}) (err error) { 136 cmd := work.(*cmdWorkpiece) 137 if cmd.cmdMes.QName() != workspacemgmt.QNameCommandCreateWorkspace { 138 cmd.iWorkspace = cmd.appStructs.AppDef().WorkspaceByDescriptor(cmd.wsDesc.AsQName(authnz.Field_WSKind)) 139 } 140 return nil 141 } 142 143 func getICommand(_ context.Context, work interface{}) (err error) { 144 cmd := work.(*cmdWorkpiece) 145 var cmdType appdef.IType 146 if cmd.iWorkspace == nil { 147 // DummyWS or c.sys.CreateWorkspace 148 cmdType = cmd.appStructs.AppDef().Type(cmd.cmdMes.QName()) 149 } else { 150 if cmdType = cmd.iWorkspace.Type(cmd.cmdMes.QName()); cmdType.Kind() == appdef.TypeKind_null { 151 return fmt.Errorf("command %s does not exist in workspace %s", cmd.cmdMes.QName(), cmd.iWorkspace.QName()) 152 } 153 } 154 ok := false 155 cmd.iCommand, ok = cmdType.(appdef.ICommand) 156 if !ok { 157 return fmt.Errorf("%s is not a command", cmd.cmdMes.QName()) 158 } 159 return nil 160 } 161 162 func (cmdProc *cmdProc) getCmdResultBuilder(_ context.Context, work interface{}) (err error) { 163 cmd := work.(*cmdWorkpiece) 164 cmdResultType := cmd.iCommand.Result() 165 if cmdResultType != nil { 166 cmd.cmdResultBuilder = cmd.appStructs.ObjectBuilder(cmdResultType.QName()) 167 } 168 return nil 169 } 170 171 func (cmdProc *cmdProc) buildCommandArgs(_ context.Context, work interface{}) (err error) { 172 cmd := work.(*cmdWorkpiece) 173 hs := cmd.hostStateProvider.get(cmd.appStructs, cmd.cmdMes.WSID(), cmd.reb.CUDBuilder(), 174 cmd.principals, cmd.cmdMes.Token(), cmd.cmdResultBuilder, cmd.workspace.NextWLogOffset) 175 hs.ClearIntents() 176 cmd.eca = istructs.ExecCommandArgs{ 177 CommandPrepareArgs: istructs.CommandPrepareArgs{ 178 PrepareArgs: istructs.PrepareArgs{ 179 ArgumentObject: cmd.argsObject, 180 WSID: cmd.cmdMes.WSID(), 181 Workpiece: work, 182 Workspace: cmd.iWorkspace, 183 }, 184 ArgumentUnloggedObject: cmd.unloggedArgsObject, 185 }, 186 State: hs, 187 Intents: hs, 188 } 189 return 190 } 191 192 func updateIDGeneratorFromO(root istructs.IObject, types appdef.IWithTypes, idGen istructs.IIDGenerator) { 193 // new IDs only here because update is not allowed for ODocs in Args 194 idGen.UpdateOnSync(root.AsRecordID(appdef.SystemField_ID), types.Type(root.QName())) 195 root.Containers(func(container string) { 196 // order of containers here is the order in the schema 197 // but order in the request could be different 198 // that is not a problem because for ODocs/ORecords ID generator will bump next ID only if syncID is actually next 199 root.Children(container, func(c istructs.IObject) { 200 updateIDGeneratorFromO(c, types, idGen) 201 }) 202 }) 203 } 204 205 func (cmdProc *cmdProc) recovery(ctx context.Context, cmd *cmdWorkpiece) (*appPartition, error) { 206 ap := &appPartition{ 207 workspaces: map[istructs.WSID]*workspace{}, 208 nextPLogOffset: istructs.FirstOffset, 209 } 210 var lastPLogEvent istructs.IPLogEvent 211 cb := func(plogOffset istructs.Offset, event istructs.IPLogEvent) (err error) { 212 ws := ap.getWorkspace(event.Workspace()) 213 214 event.CUDs(func(rec istructs.ICUDRow) { 215 if rec.IsNew() { 216 t := cmd.appStructs.AppDef().Type(rec.QName()) 217 ws.idGenerator.UpdateOnSync(rec.ID(), t) 218 } 219 }) 220 ao := event.ArgumentObject() 221 if cmd.appStructs.AppDef().Type(ao.QName()).Kind() == appdef.TypeKind_ODoc { 222 updateIDGeneratorFromO(ao, cmd.appStructs.AppDef(), ws.idGenerator) 223 } 224 ws.NextWLogOffset = event.WLogOffset() + 1 225 ap.nextPLogOffset = plogOffset + 1 226 if lastPLogEvent != nil { 227 lastPLogEvent.Release() // TODO: eliminate if there will be a better solution, see https://github.com/voedger/voedger/issues/1348 228 } 229 lastPLogEvent = event 230 return nil 231 } 232 233 if err := cmd.appStructs.Events().ReadPLog(ctx, cmdProc.pNumber, istructs.FirstOffset, istructs.ReadToTheEnd, cb); err != nil { 234 return nil, err 235 } 236 237 if lastPLogEvent != nil { 238 // re-apply the last event 239 cmd.pLogEvent = lastPLogEvent 240 cmd.workspace = ap.getWorkspace(lastPLogEvent.Workspace()) 241 cmd.workspace.NextWLogOffset-- // cmdProc.storeOp will bump it 242 if err := cmdProc.storeOp.DoSync(ctx, cmd); err != nil { 243 return nil, err 244 } 245 cmd.pLogEvent = nil 246 cmd.workspace = nil 247 lastPLogEvent.Release() // TODO: eliminate if there will be a better solution, see https://github.com/voedger/voedger/issues/1348 248 } 249 250 worskapcesJSON, err := json.Marshal(ap.workspaces) 251 if err != nil { 252 // notest 253 return nil, err 254 } 255 logger.Info(fmt.Sprintf(`app "%s" partition %d recovered: nextPLogOffset %d, workspaces: %s`, cmd.cmdMes.AppQName(), cmdProc.pNumber, ap.nextPLogOffset, string(worskapcesJSON))) 256 return ap, nil 257 } 258 259 func getIDGenerator(_ context.Context, work interface{}) (err error) { 260 cmd := work.(*cmdWorkpiece) 261 cmd.idGenerator = &implIDGenerator{ 262 IIDGenerator: cmd.workspace.idGenerator, 263 generatedIDs: map[istructs.RecordID]istructs.RecordID{}, 264 } 265 return nil 266 } 267 268 func (cmdProc *cmdProc) putPLog(_ context.Context, work interface{}) (err error) { 269 cmd := work.(*cmdWorkpiece) 270 if cmd.pLogEvent, err = cmd.appStructs.Events().PutPlog(cmd.rawEvent, nil, cmd.idGenerator); err != nil { 271 cmd.appPartitionRestartScheduled = true 272 } else { 273 cmdProc.appPartition.nextPLogOffset++ 274 } 275 return 276 } 277 278 func getWSDesc(_ context.Context, work interface{}) (err error) { 279 cmd := work.(*cmdWorkpiece) 280 cmd.wsDesc, err = cmd.appStructs.Records().GetSingleton(cmd.cmdMes.WSID(), authnz.QNameCDocWorkspaceDescriptor) 281 return err 282 } 283 284 func checkWSInitialized(_ context.Context, work interface{}) (err error) { 285 cmd := work.(*cmdWorkpiece) 286 wsDesc := work.(*cmdWorkpiece).wsDesc 287 cmdQName := cmd.cmdMes.QName() 288 if cmdQName == workspacemgmt.QNameCommandCreateWorkspace || 289 cmdQName == workspacemgmt.QNameCommandCreateWorkspaceID || // happens on creating a child of an another workspace 290 cmdQName == builtin.QNameCommandInit { 291 return nil 292 } 293 if wsDesc.QName() != appdef.NullQName { 294 if cmdQName == blobber.QNameCommandUploadBLOBHelper { 295 return nil 296 } 297 if wsDesc.AsInt64(workspacemgmt.Field_InitCompletedAtMs) > 0 && len(wsDesc.AsString(workspacemgmt.Field_InitError)) == 0 { 298 cmd.wsInitialized = true 299 return nil 300 } 301 if cmdQName == istructs.QNameCommandCUD { 302 if iauthnz.IsSystemPrincipal(cmd.principals, cmd.cmdMes.WSID()) { 303 // system -> allow any CUD to upload template, see https://github.com/voedger/voedger/issues/648 304 return nil 305 } 306 } 307 } 308 return processors.ErrWSNotInited 309 } 310 311 func checkWSActive(_ context.Context, work interface{}) (err error) { 312 cmd := work.(*cmdWorkpiece) 313 if iauthnz.IsSystemPrincipal(cmd.principals, cmd.cmdMes.WSID()) { 314 // system -> allow to work in any case 315 return nil 316 } 317 if cmd.wsDesc.QName() == appdef.NullQName { 318 return nil 319 } 320 if cmd.wsDesc.AsInt32(authnz.Field_Status) == int32(authnz.WorkspaceStatus_Active) { 321 return nil 322 } 323 return processors.ErrWSInactive 324 } 325 326 func limitCallRate(_ context.Context, work interface{}) (err error) { 327 cmd := work.(*cmdWorkpiece) 328 if cmd.appStructs.IsFunctionRateLimitsExceeded(cmd.cmdMes.QName(), cmd.cmdMes.WSID()) { 329 return coreutils.NewHTTPErrorf(http.StatusTooManyRequests) 330 } 331 return nil 332 } 333 334 func (cmdProc *cmdProc) authenticate(_ context.Context, work interface{}) (err error) { 335 cmd := work.(*cmdWorkpiece) 336 req := iauthnz.AuthnRequest{ 337 Host: cmd.cmdMes.Host(), 338 RequestWSID: cmd.cmdMes.WSID(), 339 Token: cmd.cmdMes.Token(), 340 } 341 if cmd.principals, cmd.principalPayload, err = cmdProc.authenticator.Authenticate(cmd.cmdMes.RequestCtx(), cmd.appStructs, 342 cmd.appStructs.AppTokens(), req); err != nil { 343 return coreutils.NewHTTPError(http.StatusUnauthorized, err) 344 } 345 return 346 } 347 348 func (cmdProc *cmdProc) authorizeRequest(_ context.Context, work interface{}) (err error) { 349 cmd := work.(*cmdWorkpiece) 350 req := iauthnz.AuthzRequest{ 351 OperationKind: iauthnz.OperationKind_EXECUTE, 352 Resource: cmd.cmdMes.QName(), 353 } 354 ok, err := cmdProc.authorizer.Authorize(cmd.appStructs, cmd.principals, req) 355 if err != nil { 356 return err 357 } 358 if !ok { 359 return coreutils.NewHTTPErrorf(http.StatusForbidden) 360 } 361 return nil 362 } 363 364 func getResources(_ context.Context, work interface{}) (err error) { 365 cmd := work.(*cmdWorkpiece) 366 cmd.resources = cmd.appStructs.Resources() 367 return nil 368 } 369 370 func getExec(_ context.Context, work interface{}) (err error) { 371 cmd := work.(*cmdWorkpiece) 372 iResource := cmd.resources.QueryResource(cmd.cmdMes.QName()) 373 iCommandFunc := iResource.(istructs.ICommandFunction) 374 cmd.cmdExec = iCommandFunc.Exec 375 return nil 376 } 377 378 func unmarshalRequestBody(_ context.Context, work interface{}) (err error) { 379 cmd := work.(*cmdWorkpiece) 380 if cmd.iCommand.Param() != nil && cmd.iCommand.Param().QName() == istructs.QNameRaw { 381 cmd.requestData["args"] = map[string]interface{}{ 382 processors.Field_RawObject_Body: string(cmd.cmdMes.Body()), 383 } 384 } else if err = json.Unmarshal(cmd.cmdMes.Body(), &cmd.requestData); err != nil { 385 err = fmt.Errorf("failed to unmarshal request body: %w", err) 386 } 387 return 388 } 389 390 func (cmdProc *cmdProc) getWorkspace(_ context.Context, work interface{}) (err error) { 391 cmd := work.(*cmdWorkpiece) 392 cmd.workspace = cmdProc.appPartition.getWorkspace(cmd.cmdMes.WSID()) 393 return nil 394 } 395 396 func (cmdProc *cmdProc) getRawEventBuilder(_ context.Context, work interface{}) (err error) { 397 cmd := work.(*cmdWorkpiece) 398 grebp := istructs.GenericRawEventBuilderParams{ 399 HandlingPartition: cmd.cmdMes.PartitionID(), 400 Workspace: cmd.cmdMes.WSID(), 401 QName: cmd.cmdMes.QName(), 402 RegisteredAt: istructs.UnixMilli(cmdProc.now().UnixMilli()), 403 PLogOffset: cmdProc.appPartition.nextPLogOffset, 404 WLogOffset: cmd.workspace.NextWLogOffset, 405 } 406 407 switch cmd.cmdMes.QName() { 408 case builtin.QNameCommandInit: // nolint, kept to not to break existing events only 409 cmd.reb = cmd.appStructs.Events().GetSyncRawEventBuilder( 410 istructs.SyncRawEventBuilderParams{ 411 SyncedAt: istructs.UnixMilli(cmdProc.now().UnixMilli()), 412 GenericRawEventBuilderParams: grebp, 413 }, 414 ) 415 default: 416 cmd.reb = cmd.appStructs.Events().GetNewRawEventBuilder( 417 istructs.NewRawEventBuilderParams{ 418 GenericRawEventBuilderParams: grebp, 419 }, 420 ) 421 } 422 return nil 423 } 424 425 func getArgsObject(_ context.Context, work interface{}) (err error) { 426 cmd := work.(*cmdWorkpiece) 427 if cmd.iCommand.Param() == nil { 428 return nil 429 } 430 aob := cmd.reb.ArgumentObjectBuilder() 431 if argsIntf, exists := cmd.requestData["args"]; exists { 432 args, ok := argsIntf.(map[string]interface{}) 433 if !ok { 434 return errors.New(`"args" field must be an object`) 435 } 436 aob.FillFromJSON(args) 437 } 438 if cmd.argsObject, err = aob.Build(); err != nil { 439 err = fmt.Errorf("argument object build failed: %w", err) 440 } 441 return 442 } 443 444 func getUnloggedArgsObject(_ context.Context, work interface{}) (err error) { 445 cmd := work.(*cmdWorkpiece) 446 if cmd.iCommand.UnloggedParam() == nil { 447 return nil 448 } 449 auob := cmd.reb.ArgumentUnloggedObjectBuilder() 450 if unloggedArgsIntf, exists := cmd.requestData["unloggedArgs"]; exists { 451 unloggedArgs, ok := unloggedArgsIntf.(map[string]interface{}) 452 if !ok { 453 return errors.New(`"unloggedArgs" field must be an object`) 454 } 455 auob.FillFromJSON(unloggedArgs) 456 } 457 if cmd.unloggedArgsObject, err = auob.Build(); err != nil { 458 err = fmt.Errorf("unlogged argument object build failed: %w", err) 459 } 460 return 461 } 462 463 func (xp xPath) Errorf(mes string, args ...interface{}) error { 464 return fmt.Errorf(string(xp)+": "+mes, args...) 465 } 466 467 func (xp xPath) Error(err error) error { 468 return xp.Errorf("%w", err) 469 } 470 471 func execCommand(_ context.Context, work interface{}) (err error) { 472 cmd := work.(*cmdWorkpiece) 473 begin := time.Now() 474 err = cmd.cmdExec(cmd.eca) 475 work.(*cmdWorkpiece).metrics.increase(ExecSeconds, time.Since(begin).Seconds()) 476 return err 477 } 478 479 func buildRawEvent(_ context.Context, work interface{}) (err error) { 480 cmd := work.(*cmdWorkpiece) 481 cmd.rawEvent, err = cmd.reb.BuildRawEvent() 482 status := http.StatusBadRequest 483 if errors.Is(err, istructsmem.ErrRecordIDUniqueViolation) { 484 status = http.StatusConflict 485 } 486 err = coreutils.WrapSysError(err, status) 487 return 488 } 489 490 func validateCmdResult(ctx context.Context, work interface{}) (err error) { 491 cmd := work.(*cmdWorkpiece) 492 if cmd.cmdResultBuilder != nil { 493 cmdResult, err := cmd.cmdResultBuilder.Build() 494 if err != nil { 495 return err 496 } 497 cmd.cmdResult = cmdResult 498 } 499 return nil 500 } 501 502 func (cmdProc *cmdProc) eventValidators(ctx context.Context, work interface{}) (err error) { 503 cmd := work.(*cmdWorkpiece) 504 for _, appEventValidator := range cmd.appStructs.EventValidators() { 505 if err = appEventValidator(ctx, cmd.rawEvent, cmd.appStructs, cmd.cmdMes.WSID()); err != nil { 506 return coreutils.WrapSysError(err, http.StatusForbidden) 507 } 508 } 509 return nil 510 } 511 512 func (cmdProc *cmdProc) cudsValidators(ctx context.Context, work interface{}) (err error) { 513 cmd := work.(*cmdWorkpiece) 514 for _, appCUDValidator := range cmd.appStructs.CUDValidators() { 515 err = iterate.ForEachError(cmd.rawEvent.CUDs, func(rec istructs.ICUDRow) error { 516 if appCUDValidator.Match(rec, cmd.cmdMes.WSID(), cmd.cmdMes.QName()) { 517 if err := appCUDValidator.Validate(ctx, cmd.appStructs, rec, cmd.cmdMes.WSID(), cmd.cmdMes.QName()); err != nil { 518 return coreutils.WrapSysError(err, http.StatusForbidden) 519 } 520 } 521 return nil 522 }) 523 if err != nil { 524 return err 525 } 526 } 527 return nil 528 } 529 530 func (cmdProc *cmdProc) validateCUDsQNames(ctx context.Context, work interface{}) (err error) { 531 cmd := work.(*cmdWorkpiece) 532 if cmd.iWorkspace == nil { 533 // dummy or c.sys.CreateWorkspace 534 return nil 535 } 536 return iterate.ForEachError(cmd.rawEvent.CUDs, func(cud istructs.ICUDRow) error { 537 if cmd.iWorkspace.Type(cud.QName()) == appdef.NullType { 538 return coreutils.NewHTTPErrorf(http.StatusBadRequest, fmt.Errorf("doc %s mentioned in resulting CUDs does not exist in the workspace %s", 539 cud.QName(), cmd.wsDesc.AsQName(authnz.Field_WSKind))) 540 } 541 return nil 542 }) 543 } 544 545 func parseCUDs(_ context.Context, work interface{}) (err error) { 546 cmd := work.(*cmdWorkpiece) 547 cuds, _, err := cmd.requestData.AsObjects("cuds") 548 if err != nil { 549 return err 550 } 551 if len(cuds) > builtin.MaxCUDs { 552 return coreutils.NewHTTPErrorf(http.StatusBadRequest, "too many cuds: ", len(cuds), " is in the request, max is ", builtin.MaxCUDs) 553 } 554 for cudNumber, cudIntf := range cuds { 555 cudXPath := xPath("cuds[" + strconv.Itoa(cudNumber) + "]") 556 cudDataMap, ok := cudIntf.(map[string]interface{}) 557 if !ok { 558 return cudXPath.Errorf("not an object") 559 } 560 cudData := coreutils.MapObject(cudDataMap) 561 562 parsedCUD := parsedCUD{} 563 564 parsedCUD.fields, ok, err = cudData.AsObject("fields") 565 if err != nil { 566 return cudXPath.Error(err) 567 } 568 if !ok { 569 return cudXPath.Errorf(`"fields" missing`) 570 } 571 // sys.ID внутри -> create, снаружи -> update 572 isCreate := false 573 if parsedCUD.id, isCreate, err = parsedCUD.fields.AsInt64(appdef.SystemField_ID); err != nil { 574 return cudXPath.Error(err) 575 } 576 if isCreate { 577 parsedCUD.opKind = iauthnz.OperationKind_INSERT 578 qNameStr, _, err := parsedCUD.fields.AsString(appdef.SystemField_QName) 579 if err != nil { 580 return cudXPath.Error(err) 581 } 582 if parsedCUD.qName, err = appdef.ParseQName(qNameStr); err != nil { 583 return cudXPath.Error(err) 584 } 585 } else { 586 parsedCUD.opKind = iauthnz.OperationKind_UPDATE 587 if parsedCUD.id, ok, err = cudData.AsInt64(appdef.SystemField_ID); err != nil { 588 return cudXPath.Error(err) 589 } 590 if !ok { 591 return cudXPath.Errorf(`"sys.ID" missing`) 592 } 593 if parsedCUD.existingRecord, err = cmd.appStructs.Records().Get(cmd.cmdMes.WSID(), true, istructs.RecordID(parsedCUD.id)); err != nil { 594 return 595 } 596 if parsedCUD.qName = parsedCUD.existingRecord.QName(); parsedCUD.qName == appdef.NullQName { 597 return coreutils.NewHTTPError(http.StatusNotFound, cudXPath.Errorf("record with queried id %d does not exist", parsedCUD.id)) 598 } 599 } 600 opStr := "UPDATE" 601 if isCreate { 602 opStr = "INSERT" 603 } 604 parsedCUD.xPath = xPath(fmt.Sprintf("%s %s %s", cudXPath, opStr, parsedCUD.qName)) 605 606 cmd.parsedCUDs = append(cmd.parsedCUDs, parsedCUD) 607 } 608 return err 609 } 610 611 func checkArgsRefIntegrity(_ context.Context, work interface{}) (err error) { 612 cmd := work.(*cmdWorkpiece) 613 if cmd.argsObject != nil { 614 if err = builtin.CheckRefIntegrity(cmd.argsObject, cmd.appStructs, cmd.cmdMes.WSID()); err != nil { 615 return err 616 } 617 } 618 if cmd.unloggedArgsObject != nil { 619 return builtin.CheckRefIntegrity(cmd.unloggedArgsObject, cmd.appStructs, cmd.cmdMes.WSID()) 620 } 621 return nil 622 } 623 624 // not a validator due of https://github.com/voedger/voedger/issues/1125 625 func checkIsActiveInCUDs(_ context.Context, work interface{}) (err error) { 626 cmd := work.(*cmdWorkpiece) 627 for _, cud := range cmd.parsedCUDs { 628 if cud.opKind != iauthnz.OperationKind_UPDATE { 629 continue 630 } 631 hasOnlySystemFields := true 632 sysIsActiveUpdating := false 633 isActiveAndOtherFieldsMixedOnUpdate := false 634 for fieldName := range cud.fields { 635 if !appdef.IsSysField(fieldName) { 636 hasOnlySystemFields = false 637 } else if fieldName == appdef.SystemField_IsActive { 638 sysIsActiveUpdating = true 639 } 640 if isActiveAndOtherFieldsMixedOnUpdate = sysIsActiveUpdating && !hasOnlySystemFields; isActiveAndOtherFieldsMixedOnUpdate { 641 break 642 } 643 } 644 if isActiveAndOtherFieldsMixedOnUpdate { 645 return coreutils.NewHTTPError(http.StatusForbidden, errors.New("updating other fields is not allowed if sys.IsActive is updating")) 646 } 647 } 648 return nil 649 } 650 651 func (cmdProc *cmdProc) authorizeCUDs(_ context.Context, work interface{}) (err error) { 652 cmd := work.(*cmdWorkpiece) 653 for _, parsedCUD := range cmd.parsedCUDs { 654 req := iauthnz.AuthzRequest{ 655 OperationKind: parsedCUD.opKind, 656 Resource: parsedCUD.qName, 657 Fields: maps.Keys(parsedCUD.fields), 658 } 659 ok, err := cmdProc.authorizer.Authorize(cmd.appStructs, cmd.principals, req) 660 if err != nil { 661 return parsedCUD.xPath.Error(err) 662 } 663 if !ok { 664 return coreutils.NewHTTPError(http.StatusForbidden, parsedCUD.xPath.Errorf("operation forbidden")) 665 } 666 } 667 return 668 } 669 670 func (cmdProc *cmdProc) writeCUDs(_ context.Context, work interface{}) (err error) { 671 cmd := work.(*cmdWorkpiece) 672 for _, parsedCUD := range cmd.parsedCUDs { 673 var cud istructs.IRowWriter 674 if parsedCUD.opKind == iauthnz.OperationKind_INSERT { 675 cud = cmd.reb.CUDBuilder().Create(parsedCUD.qName) 676 cud.PutRecordID(appdef.SystemField_ID, istructs.RecordID(parsedCUD.id)) 677 } else { 678 cud = cmd.reb.CUDBuilder().Update(parsedCUD.existingRecord) 679 } 680 if err := coreutils.MapToObject(parsedCUD.fields, cud); err != nil { 681 return parsedCUD.xPath.Error(err) 682 } 683 } 684 return nil 685 } 686 687 func (osp *wrongArgsCatcher) OnErr(err error, _ interface{}, _ pipeline.IWorkpieceContext) (newErr error) { 688 return coreutils.WrapSysError(err, http.StatusBadRequest) 689 } 690 691 func (cmdProc *cmdProc) n10n(_ context.Context, work interface{}) (err error) { 692 cmd := work.(*cmdWorkpiece) 693 cmdProc.n10nBroker.Update(in10n.ProjectionKey{ 694 App: cmd.cmdMes.AppQName(), 695 Projection: projectors.PLogUpdatesQName, 696 WS: istructs.WSID(cmdProc.pNumber), 697 }, cmd.rawEvent.PLogOffset()) 698 logger.Verbose("updated plog event on offset ", cmd.rawEvent.PLogOffset(), ", pnumber ", cmdProc.pNumber) 699 return nil 700 } 701 702 func sendResponse(cmd *cmdWorkpiece, handlingError error) { 703 if handlingError != nil { 704 cmd.metrics.increase(ErrorsTotal, 1.0) 705 //if error occurred somewhere in syncProjectors we have to measure elapsed time 706 if !cmd.syncProjectorsStart.IsZero() { 707 cmd.metrics.increase(ProjectorsSeconds, time.Since(cmd.syncProjectorsStart).Seconds()) 708 } 709 coreutils.ReplyErr(cmd.cmdMes.Sender(), handlingError) 710 return 711 } 712 body := bytes.NewBufferString(fmt.Sprintf(`{"CurrentWLogOffset":%d`, cmd.pLogEvent.WLogOffset())) 713 if len(cmd.idGenerator.generatedIDs) > 0 { 714 body.WriteString(`,"NewIDs":{`) 715 for rawID, generatedID := range cmd.idGenerator.generatedIDs { 716 body.WriteString(fmt.Sprintf(`"%d":%d,`, rawID, generatedID)) 717 } 718 body.Truncate(body.Len() - 1) 719 body.WriteString("}") 720 if logger.IsVerbose() { 721 logger.Verbose("generated IDs:", cmd.idGenerator.generatedIDs) 722 } 723 } 724 if cmd.cmdResult != nil { 725 cmdResult := coreutils.ObjectToMap(cmd.cmdResult, cmd.appStructs.AppDef()) 726 cmdResultBytes, err := json.Marshal(cmdResult) 727 if err != nil { 728 // notest 729 logger.Error("failed to marshal response: " + err.Error()) 730 return 731 } 732 body.WriteString(`,"Result":`) 733 body.Write(cmdResultBytes) 734 } 735 body.WriteString("}") 736 coreutils.ReplyJSON(cmd.cmdMes.Sender(), http.StatusOK, body.String()) 737 } 738 739 func (idGen *implIDGenerator) NextID(rawID istructs.RecordID, t appdef.IType) (storageID istructs.RecordID, err error) { 740 storageID, err = idGen.IIDGenerator.NextID(rawID, t) 741 idGen.generatedIDs[rawID] = storageID 742 return 743 }