github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/sys/workspace/impl.go (about) 1 /* 2 * Copyright (c) 2022-present unTill Pro, Ltd. 3 */ 4 5 package workspace 6 7 import ( 8 "context" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "io/fs" 13 "net/http" 14 "path/filepath" 15 "strconv" 16 "strings" 17 18 "github.com/voedger/voedger/pkg/goutils/iterate" 19 "github.com/voedger/voedger/pkg/goutils/logger" 20 "github.com/voedger/voedger/pkg/utils/federation" 21 22 "github.com/voedger/voedger/pkg/appdef" 23 "github.com/voedger/voedger/pkg/extensionpoints" 24 "github.com/voedger/voedger/pkg/istructs" 25 "github.com/voedger/voedger/pkg/istructsmem" 26 "github.com/voedger/voedger/pkg/itokens" 27 payloads "github.com/voedger/voedger/pkg/itokens-payloads" 28 "github.com/voedger/voedger/pkg/state" 29 "github.com/voedger/voedger/pkg/sys/authnz" 30 "github.com/voedger/voedger/pkg/sys/blobber" 31 coreutils "github.com/voedger/voedger/pkg/utils" 32 ) 33 34 // Projector<A, InvokeCreateWorkspaceID> 35 // triggered by CDoc<ChildWorkspace> (not a singleton) 36 // targetApp/userProfileWSID 37 func invokeCreateWorkspaceIDProjector(federation federation.IFederation, tokensAPI itokens.ITokens) func(event istructs.IPLogEvent, s istructs.IState, intents istructs.IIntents) (err error) { 38 return func(event istructs.IPLogEvent, s istructs.IState, intents istructs.IIntents) (err error) { 39 return iterate.ForEachError(event.CUDs, func(rec istructs.ICUDRow) error { 40 if rec.QName() != authnz.QNameCDocChildWorkspace || !rec.IsNew() { 41 return nil 42 } 43 ownerWSID := event.Workspace() 44 wsName := rec.AsString(authnz.Field_WSName) 45 wsKind := rec.AsQName(authnz.Field_WSKind) 46 templateName := rec.AsString(field_TemplateName) 47 templateParams := rec.AsString(Field_TemplateParams) 48 appQName := s.App() 49 targetApp := appQName.String() 50 targetClusterID := istructs.MainClusterID // TODO: on https://github.com/voedger/voedger/commit/1e7ce3f2c546e9bf1332edb31a5beed5954bc476 was NullClusetrID! 51 wsidToCallCreateWSIDAt := coreutils.GetPseudoWSID(ownerWSID, wsName, targetClusterID) 52 return ApplyInvokeCreateWorkspaceID(federation, appQName, tokensAPI, wsName, wsKind, wsidToCallCreateWSIDAt, targetApp, 53 templateName, templateParams, rec, ownerWSID) 54 }) 55 } 56 } 57 58 // triggered by cdoc.registry.Login or by cdoc.sys.ChildWorkspace 59 // wsid - pseudoProfile: crc32(wsName) or crc32(login) 60 // sys/registry app 61 func ApplyInvokeCreateWorkspaceID(federation federation.IFederation, appQName istructs.AppQName, tokensAPI itokens.ITokens, 62 wsName string, wsKind appdef.QName, wsidToCallCreateWSIDAt istructs.WSID, targetApp string, templateName string, templateParams string, 63 ownerDoc istructs.ICUDRow, ownerWSID istructs.WSID) error { 64 // Call WS[$PseudoWSID].c.CreateWorkspaceID() 65 ownerApp := appQName.String() 66 ownerQName := ownerDoc.QName() 67 ownerID := ownerDoc.ID() 68 wsKindInitializationData := ownerDoc.AsString(authnz.Field_WSKindInitializationData) 69 createWSIDCmdURL := fmt.Sprintf("api/%s/%d/c.sys.CreateWorkspaceID", targetApp, wsidToCallCreateWSIDAt) 70 logger.Info("aproj.sys.InvokeCreateWorkspaceID: request to " + createWSIDCmdURL) 71 body := fmt.Sprintf(`{"args":{"OwnerWSID":%d,"OwnerQName2":"%s","OwnerID":%d,"OwnerApp":"%s","WSName":"%s","WSKind":"%s","WSKindInitializationData":%q,"TemplateName":"%s","TemplateParams":%q}}`, 72 ownerWSID, ownerQName.String(), ownerID, ownerApp, wsName, wsKind.String(), wsKindInitializationData, templateName, templateParams) 73 targetAppQName, err := istructs.ParseAppQName(targetApp) 74 if err != nil { 75 // parsed already by c.registry.CreateLogin 76 // notest 77 return err 78 } 79 systemPrincipalToken, err := payloads.GetSystemPrincipalToken(tokensAPI, targetAppQName) 80 if err != nil { 81 return fmt.Errorf("aproj.sys.InvokeCreateWorkspaceID: %w", err) 82 } 83 84 if _, err = federation.Func(createWSIDCmdURL, body, 85 coreutils.WithAuthorizeBy(systemPrincipalToken), 86 coreutils.WithDiscardResponse(), 87 coreutils.WithExpectedCode(http.StatusOK), 88 coreutils.WithExpectedCode(http.StatusConflict), 89 ); err != nil { 90 return fmt.Errorf("aproj.sys.InvokeCreateWorkspaceID: c.sys.CreateWorkspaceID failed: %w. Body:\n%s", err, body) 91 } 92 return nil 93 } 94 95 // c.sys.CreateWorkspaceID 96 // ChildWorkspace -> pseudoWSID(profileWSID+"/"+wsName, targetCluster) translated to AppWSID 97 // Login -> ((PseudoWSID->AppWSID).Base, targetCluster) 98 // targetApp 99 func execCmdCreateWorkspaceID(asp istructs.IAppStructsProvider, appQName istructs.AppQName) istructsmem.ExecCommandClosure { 100 return func(args istructs.ExecCommandArgs) (err error) { 101 // TODO: AuthZ: System,SystemToken in header 102 ownerWSID := args.ArgumentObject.AsInt64(Field_OwnerWSID) 103 wsName := args.ArgumentObject.AsString(authnz.Field_WSName) 104 // Check that ownerWSID + wsName does not exist yet: View<WorkspaceIDIdx> to deduplication 105 kb, err := args.State.KeyBuilder(state.View, QNameViewWorkspaceIDIdx) 106 if err != nil { 107 return err 108 } 109 kb.PutInt64(Field_OwnerWSID, ownerWSID) 110 kb.PutString(authnz.Field_WSName, wsName) 111 _, ok, err := args.State.CanExist(kb) 112 if err != nil { 113 return err 114 } 115 if ok { 116 return coreutils.NewHTTPErrorf(http.StatusConflict, fmt.Sprintf("workspace with name %s and ownerWSID %d already exists", wsName, ownerWSID)) 117 } 118 119 // ownerWSID := istructs.WSID(args.ArgumentObject.AsInt64(FldOwnerWSID)) 120 // Get new WSID from View<NextBaseWSID> 121 as, err := asp.AppStructs(appQName) 122 if err != nil { 123 return err 124 } 125 newWSID, err := GetNextWSID(args.Workpiece.(interface{ Context() context.Context }).Context(), as, args.WSID.ClusterID()) 126 if err != nil { 127 return err 128 } 129 130 // Create CDoc<WorkspaceID>{wsParams, WSID: $NewWSID} 131 kb, err = args.State.KeyBuilder(state.Record, QNameCDocWorkspaceID) 132 if err != nil { 133 return err 134 } 135 cdocWorkspaceID, err := args.Intents.NewValue(kb) 136 if err != nil { 137 return err 138 } 139 cdocWorkspaceID.PutRecordID(appdef.SystemField_ID, 1) 140 cdocWorkspaceID.PutInt64(Field_OwnerWSID, args.ArgumentObject.AsInt64(Field_OwnerWSID)) // CDoc<Login> -> pseudoWSID->AppWSID, CDoc<ChildWorkspace> -> owner profile WSID 141 cdocWorkspaceID.PutString(Field_OwnerQName2, args.ArgumentObject.AsString(Field_OwnerQName2)) // registry.Login or sys.UserProfile 142 cdocWorkspaceID.PutInt64(Field_OwnerID, args.ArgumentObject.AsInt64(Field_OwnerID)) // CDoc<Login>.ID or CDoc<ChildWorkspace>.ID 143 cdocWorkspaceID.PutString(Field_OwnerApp, args.ArgumentObject.AsString(Field_OwnerApp)) 144 cdocWorkspaceID.PutString(authnz.Field_WSName, args.ArgumentObject.AsString(authnz.Field_WSName)) // CDoc<Login> -> crc32(loginHash), CDoc<ChildWorkspace> -> wsName 145 cdocWorkspaceID.PutQName(authnz.Field_WSKind, args.ArgumentObject.AsQName(authnz.Field_WSKind)) // CDoc<Login> -> sys.DeviceProfile or sys.UserProfile, CDoc<ChildWorkspace> -> provided wsKind (e.g. air.Restaurant) 146 cdocWorkspaceID.PutString(authnz.Field_WSKindInitializationData, args.ArgumentObject.AsString(authnz.Field_WSKindInitializationData)) 147 cdocWorkspaceID.PutString(field_TemplateName, args.ArgumentObject.AsString(field_TemplateName)) 148 cdocWorkspaceID.PutString(Field_TemplateParams, args.ArgumentObject.AsString(Field_TemplateParams)) 149 cdocWorkspaceID.PutInt64(authnz.Field_WSID, int64(newWSID)) 150 151 return 152 } 153 } 154 155 // sp.sys.WorkspaceIDIdx 156 // triggered by cdoc.sys.WorkspaceID 157 // targetApp/appWS 158 func workspaceIDIdxProjector(event istructs.IPLogEvent, s istructs.IState, intents istructs.IIntents) (err error) { 159 return iterate.ForEachError(event.CUDs, func(rec istructs.ICUDRow) error { 160 if rec.QName() != QNameCDocWorkspaceID || !rec.IsNew() { // skip on update cdoc.sys.WorkspaceID on e.g. deactivate workspace 161 return nil 162 } 163 kb, err := s.KeyBuilder(state.View, QNameViewWorkspaceIDIdx) 164 if err != nil { 165 // notest 166 return nil 167 } 168 ownerWSID := rec.AsInt64(Field_OwnerWSID) 169 wsName := rec.AsString(authnz.Field_WSName) 170 wsid := rec.AsInt64(authnz.Field_WSID) 171 kb.PutInt64(Field_OwnerWSID, ownerWSID) 172 kb.PutString(authnz.Field_WSName, wsName) 173 wsIdxVB, err := intents.NewValue(kb) 174 if err != nil { 175 // notest 176 return nil 177 } 178 wsIdxVB.PutInt64(authnz.Field_WSID, wsid) 179 wsIdxVB.PutRecordID(field_IDOfCDocWorkspaceID, rec.ID()) 180 return nil 181 }) 182 } 183 184 // Projector<A, InvokeCreateWorkspace> 185 // triggered by CDoc<WorkspaceID> 186 // targetApp/appWS 187 func invokeCreateWorkspaceProjector(federation federation.IFederation, tokensAPI itokens.ITokens) func(event istructs.IPLogEvent, s istructs.IState, intents istructs.IIntents) (err error) { 188 return func(event istructs.IPLogEvent, s istructs.IState, intents istructs.IIntents) (err error) { 189 return iterate.ForEachError(event.CUDs, func(rec istructs.ICUDRow) error { 190 if rec.QName() != QNameCDocWorkspaceID || !rec.IsNew() { // skip on update cdoc.sys.WorkspaceID on e.g. deactivate workspace 191 return nil 192 } 193 194 newWSID := rec.AsInt64(authnz.Field_WSID) 195 wsName := rec.AsString(authnz.Field_WSName) 196 wsKind := rec.AsQName(authnz.Field_WSKind) 197 wsKindInitializationData := rec.AsString(authnz.Field_WSKindInitializationData) 198 templateName := rec.AsString(field_TemplateName) 199 ownerWSID := rec.AsInt64(Field_OwnerWSID) 200 ownerQName := rec.AsString(Field_OwnerQName2) 201 ownerID := rec.AsInt64(Field_OwnerID) 202 ownerApp := rec.AsString(Field_OwnerApp) 203 templateParams := rec.AsString(Field_TemplateParams) 204 body := fmt.Sprintf(`{"args":{"OwnerWSID":%d,"OwnerQName2":"%s","OwnerID":%d,"OwnerApp":"%s","WSName":"%s","WSKind":"%s","WSKindInitializationData":%q,"TemplateName":"%s","TemplateParams":%q}}`, 205 ownerWSID, ownerQName, ownerID, ownerApp, wsName, wsKind.String(), wsKindInitializationData, templateName, templateParams) 206 appQName := s.App() 207 createWSCmdURL := fmt.Sprintf("api/%s/%d/c.sys.CreateWorkspace", appQName.String(), newWSID) 208 logger.Info("aproj.sys.InvokeCreateWorkspace: request to " + createWSCmdURL) 209 systemPrincipalToken, err := payloads.GetSystemPrincipalToken(tokensAPI, appQName) 210 if err != nil { 211 return fmt.Errorf("aproj.sys.InvokeCreateWorkspace: %w", err) 212 } 213 if _, err = federation.Func(createWSCmdURL, body, coreutils.WithAuthorizeBy(systemPrincipalToken), coreutils.WithDiscardResponse()); err != nil { 214 return fmt.Errorf("aproj.sys.InvokeCreateWorkspace: c.sys.CreateWorkspace failed: %w", err) 215 } 216 return nil 217 }) 218 } 219 } 220 221 // c.sys.CreateWorkspace 222 // должно быть вызвано в целевом приложении, т.к. профиль пользователя находится в целевом приложении на схеме!!! 223 func execCmdCreateWorkspace(now coreutils.TimeFunc, asp istructs.IAppStructsProvider, appQName istructs.AppQName) istructsmem.ExecCommandClosure { 224 return func(args istructs.ExecCommandArgs) error { 225 // TODO: AuthZ: System, SystemToken in header 226 // Check that CDoc<sys.WorkspaceDescriptor> does not exist yet (IRecords.GetSingleton()) 227 wsKindInitializationDataStr := args.ArgumentObject.AsString(authnz.Field_WSKindInitializationData) 228 wsKind := args.ArgumentObject.AsQName(authnz.Field_WSKind) 229 newWSID := args.WSID 230 231 wsKindInitializationData := map[string]interface{}{} 232 233 e := func() error { 234 as, err := asp.AppStructs(appQName) 235 if err != nil { 236 return fmt.Errorf("failed to get appStructs for appQName %s: %w", appQName.String(), err) 237 } 238 wsKindType := as.AppDef().Type(wsKind) 239 if wsKindType.Kind() == appdef.TypeKind_null { 240 return fmt.Errorf("unknown workspace kind: %s", wsKind.String()) 241 } 242 if len(wsKindInitializationDataStr) == 0 { 243 return nil 244 } 245 // validate wsKindInitializationData 246 if err := json.Unmarshal([]byte(wsKindInitializationDataStr), &wsKindInitializationData); err != nil { 247 return fmt.Errorf("failed to unmarshal workspace initialization data: %w", err) 248 } 249 if err := validateWSKindInitializationData(as, wsKindInitializationData, wsKindType); err != nil { 250 return fmt.Errorf("failed to validate workspace initialization data: %w", err) 251 } 252 return nil 253 }() 254 255 // create CDoc<sys.WorkspaceDescriptor> (singleton) 256 kb, err := args.State.KeyBuilder(state.Record, authnz.QNameCDocWorkspaceDescriptor) 257 if err != nil { 258 return err 259 } 260 cdocWSDesc, err := args.Intents.NewValue(kb) 261 if err != nil { 262 return err 263 } 264 cdocWSDesc.PutRecordID(appdef.SystemField_ID, 1) 265 cdocWSDesc.PutInt64(Field_OwnerWSID, args.ArgumentObject.AsInt64(Field_OwnerWSID)) // CDoc<Login> -> pseudo WSID, CDoc<ChildWorkspace> -> owner profile WSID 266 cdocWSDesc.PutString(Field_OwnerQName2, args.ArgumentObject.AsString(Field_OwnerQName2)) // registry.Login or sys.UserProfile 267 cdocWSDesc.PutInt64(Field_OwnerID, args.ArgumentObject.AsInt64(Field_OwnerID)) // CDoc<Login>.ID or CDoc<ChildWorkspace>.ID 268 cdocWSDesc.PutString(authnz.Field_WSName, args.ArgumentObject.AsString(authnz.Field_WSName)) // CDoc<Login> -> "hardcoded", CDoc<ChildWorkspace> -> wsName 269 cdocWSDesc.PutQName(authnz.Field_WSKind, wsKind) // CDoc<Login> -> sys.DeviceProfile or sys.UserProfile, CDoc<ChildWorkspace> -> provided wsKind (e.g. air.Restaurant) 270 cdocWSDesc.PutString(Field_OwnerApp, args.ArgumentObject.AsString(Field_OwnerApp)) 271 cdocWSDesc.PutString(authnz.Field_WSKindInitializationData, wsKindInitializationDataStr) 272 cdocWSDesc.PutString(field_TemplateName, args.ArgumentObject.AsString(field_TemplateName)) 273 cdocWSDesc.PutString(Field_TemplateParams, args.ArgumentObject.AsString(Field_TemplateParams)) 274 cdocWSDesc.PutInt64(authnz.Field_WSID, int64(newWSID)) 275 cdocWSDesc.PutInt64(authnz.Field_CreatedAtMs, now().UnixMilli()) 276 cdocWSDesc.PutInt32(authnz.Field_Status, int32(authnz.WorkspaceStatus_Active)) 277 if e != nil { 278 cdocWSDesc.PutString(Field_CreateError, e.Error()) 279 logger.Info("c.sys.CreateWorkspace: ", e.Error()) 280 } else { 281 // if no error create CDoc{$wsKind} 282 kb, err := args.State.KeyBuilder(state.Record, wsKind) 283 if err != nil { 284 return err 285 } 286 cdocWSKind, err := args.Intents.NewValue(kb) 287 if err != nil { 288 return err 289 } 290 cdocWSKind.PutRecordID(appdef.SystemField_ID, 2) 291 return coreutils.MapToObject(wsKindInitializationData, cdocWSKind) // validated already in func() 292 } 293 return nil 294 } 295 } 296 297 // Projector<A, InitializeWorkspace> 298 // triggered by CDoc<WorkspaceDescriptor> 299 func initializeWorkspaceProjector(nowFunc coreutils.TimeFunc, federation federation.IFederation, ep extensionpoints.IExtensionPoint, 300 tokensAPI itokens.ITokens, wsPostInitFunc WSPostInitFunc) func(event istructs.IPLogEvent, s istructs.IState, intents istructs.IIntents) (err error) { 301 return func(event istructs.IPLogEvent, s istructs.IState, intents istructs.IIntents) (err error) { 302 return iterate.ForEachError(event.CUDs, func(rec istructs.ICUDRow) error { 303 if rec.QName() != authnz.QNameCDocWorkspaceDescriptor { 304 return nil 305 } 306 if rec.AsQName(authnz.Field_WSKind) == authnz.QNameCDoc_WorkspaceKind_AppWorkspace { 307 // AppWS -> self-initialized already 308 return nil 309 } 310 // If updated return. We do NOT react on update since we update record from projector 311 if !rec.IsNew() { 312 return nil 313 } 314 ownerUpdated := false 315 wsDescr := rec 316 newWSID := rec.AsInt64(authnz.Field_WSID) 317 newWSName := wsDescr.AsString(authnz.Field_WSName) 318 ownerApp := rec.AsString(Field_OwnerApp) 319 var wsError error 320 logPrefix := fmt.Sprintf("aproj.sys.InitializeWorkspace[%s:%d]>:", newWSName, newWSID) 321 info := func(args ...interface{}) { 322 logger.Info(logPrefix, args) 323 } 324 325 er := func(args ...interface{}) { 326 logger.Error(logPrefix, args) 327 } 328 defer func() { 329 if ownerUpdated { 330 if wsError != nil { 331 info("initialization completed with error:", wsError) 332 } else { 333 info("initialization completed") 334 } 335 } else { 336 info("initialization not completed because updateOwner() failed") 337 } 338 }() 339 340 info(workspace, newWSName, "init started") 341 342 targetAppQName := s.App() 343 344 systemPrincipalToken_TargetApp, err := payloads.GetSystemPrincipalToken(tokensAPI, targetAppQName) 345 if err != nil { 346 return fmt.Errorf("%s: %w", logPrefix, err) 347 } 348 ownerAppQName, err := istructs.ParseAppQName(ownerApp) 349 if err != nil { 350 // parsed already by c.registry.CreateLogin and InitChildWorkspace ????????? 351 // notest 352 return err 353 } 354 systemPrincipalToken_OwnerApp, err := payloads.GetSystemPrincipalToken(tokensAPI, ownerAppQName) 355 if err != nil { 356 return fmt.Errorf("%s: %w", logPrefix, err) 357 } 358 359 // If len(new.createError) > 0 -> UpdateOwner(wsParams, new.WSID, new.createError), return 360 createErrorStr := wsDescr.AsString(Field_CreateError) 361 if len(createErrorStr) > 0 { 362 wsError = errors.New(createErrorStr) 363 info("have new.createError, will just updateOwner():", createErrorStr) 364 ownerUpdated = updateOwner(rec, ownerApp, newWSID, wsError, systemPrincipalToken_OwnerApp, federation, info, er) 365 return nil 366 } 367 368 updateWSDescrURL := fmt.Sprintf("api/%s/%d/c.sys.CUD", targetAppQName.String(), event.Workspace()) 369 // if wsDecr.initStartedAtMs == 0 370 if wsDescr.AsInt64(Field_InitStartedAtMs) == 0 { 371 info("initStartedAtMs = 0. WS init was not started") 372 // WS[currentWS].c.sys.CUD(wsDescr.ID, initStartedAtMs) 373 body := fmt.Sprintf(`{"cuds": [{"sys.ID": %d,"fields": {"sys.QName": "%s","%s": %d}}]}`, 374 wsDescr.ID(), authnz.QNameCDocWorkspaceDescriptor, Field_InitStartedAtMs, nowFunc().UnixMilli()) 375 info("updating initStartedAtMs:", updateWSDescrURL) 376 377 if _, err := federation.Func(updateWSDescrURL, body, coreutils.WithAuthorizeBy(systemPrincipalToken_TargetApp), coreutils.WithDiscardResponse()); err != nil { 378 er("failed to update initStartedAtMs:", err) 379 return nil 380 } 381 382 wsKind := wsDescr.AsQName(authnz.Field_WSKind) 383 if wsError = buildWorkspace(wsDescr.AsString(field_TemplateName), ep, wsKind, federation, newWSID, 384 targetAppQName, newWSName, systemPrincipalToken_TargetApp); wsError != nil { 385 wsError = fmt.Errorf("workspace %s building: %w", wsDescr.AsString(field_TemplateName), wsError) 386 } 387 388 wsErrStr := "" 389 if wsError != nil { 390 wsErrStr = wsError.Error() 391 } 392 body = fmt.Sprintf(`{"cuds":[{"sys.ID":%d,"fields":{"sys.QName":"%s","%s":%q,"%s":%d}}]}`, 393 wsDescr.ID(), authnz.QNameCDocWorkspaceDescriptor, Field_InitError, wsErrStr, Field_InitCompletedAtMs, nowFunc().UnixMilli()) 394 if _, err = federation.Func(updateWSDescrURL, body, coreutils.WithAuthorizeBy(systemPrincipalToken_TargetApp), coreutils.WithDiscardResponse()); err != nil { 395 er("failed to update initError+initCompletedAtMs:", err) 396 return nil 397 } 398 } else if wsDescr.AsInt64(Field_InitCompletedAtMs) == 0 { 399 info("initCompletedAtMs = 0. WS data init was interrupted") 400 wsError = errors.New("workspace data initialization was interrupted") 401 body := fmt.Sprintf(`{"cuds":[{"fields":{"sys.QName":"%s","%s":%q,"%s":%d}}]}`, 402 authnz.QNameCDocWorkspaceDescriptor, Field_InitError, wsError.Error(), Field_InitCompletedAtMs, nowFunc().UnixMilli()) 403 if _, err = federation.Func(updateWSDescrURL, body, coreutils.WithAuthorizeBy(systemPrincipalToken_TargetApp), coreutils.WithDiscardResponse()); err != nil { 404 er("failed to update initError+initCompletedAtMs:", err) 405 return nil 406 } 407 } else { // initCompletedAtMs > 0 408 info("initStartedAtMs > 0 && initCompletedAtMs > 0") 409 if initError := wsDescr.AsString(Field_InitError); len(initError) > 0 { 410 wsError = errors.New(initError) 411 } 412 } 413 414 if wsError == nil && wsPostInitFunc != nil { 415 wsError = wsPostInitFunc(targetAppQName, wsDescr.AsQName(authnz.Field_WSKind), istructs.WSID(newWSID), federation, systemPrincipalToken_TargetApp) 416 } 417 418 ownerUpdated = updateOwner(rec, ownerApp, newWSID, wsError, systemPrincipalToken_OwnerApp, federation, info, er) 419 return nil 420 }) 421 } 422 } 423 424 func updateOwner(rec istructs.ICUDRow, ownerApp string, newWSID int64, err error, principalToken string, federation federation.IFederation, 425 infoLogger func(args ...interface{}), errorLogger func(args ...interface{})) (ok bool) { 426 ownerWSID := rec.AsInt64(Field_OwnerWSID) 427 ownerID := rec.AsInt64(Field_OwnerID) 428 errStr := "" 429 if err != nil { 430 errStr = err.Error() 431 } 432 433 updateOwnerURL := fmt.Sprintf("api/%s/%d/c.sys.CUD", ownerApp, ownerWSID) 434 ownerQName := rec.AsString(Field_OwnerQName2) 435 infoLogger(fmt.Sprintf("updating owner cdoc.%s at %s/%d: NewWSID=%d, WSError='%s'", ownerQName, 436 ownerApp, ownerWSID, newWSID, errStr)) 437 body := fmt.Sprintf(`{"cuds":[{"sys.ID":%d,"fields":{"%s":%d,"%s":%q}}]}`, 438 ownerID, authnz.Field_WSID, newWSID, authnz.Field_WSError, errStr) 439 if _, err = federation.Func(updateOwnerURL, body, coreutils.WithAuthorizeBy(principalToken), coreutils.WithDiscardResponse()); err != nil { 440 errorLogger("failed to updateOwner:", err) 441 } 442 return err == nil 443 } 444 445 func parseWSTemplateBLOBs(fsEntries []fs.DirEntry, blobIDs map[int64]map[string]struct{}, wsTemplateFS coreutils.EmbedFS) (blobs []blobber.StoredBLOB, err error) { 446 for _, ent := range fsEntries { 447 switch ent.Name() { 448 case "data.json", "provide.go": 449 default: 450 underscorePos := strings.Index(ent.Name(), "_") 451 if underscorePos < 0 { 452 return nil, fmt.Errorf("wrong blob file name format: %s", ent.Name()) 453 } 454 recordIDStr := ent.Name()[:underscorePos] 455 recordID, err := strconv.Atoi(recordIDStr) 456 if err != nil { 457 return nil, fmt.Errorf("wrong recordID in blob %s: %w", ent.Name(), err) 458 } 459 fieldName := strings.Replace(ent.Name()[underscorePos+1:], filepath.Ext(ent.Name()), "", -1) 460 if len(fieldName) == 0 { 461 return nil, fmt.Errorf("no fieldName in blob %s", ent.Name()) 462 } 463 fieldNames, ok := blobIDs[int64(recordID)] 464 if !ok { 465 fieldNames = map[string]struct{}{} 466 blobIDs[int64(recordID)] = fieldNames 467 } 468 if _, exists := fieldNames[fieldName]; exists { 469 return nil, fmt.Errorf("recordID %d: blob for field %s is met again: %s", recordID, fieldName, ent.Name()) 470 } 471 fieldNames[fieldName] = struct{}{} 472 blobContent, err := wsTemplateFS.ReadFile(ent.Name()) 473 if err != nil { 474 return nil, fmt.Errorf("failed to read blob %s content: %w", ent.Name(), err) 475 } 476 blobs = append(blobs, blobber.StoredBLOB{ 477 BLOB: coreutils.BLOB{ 478 FieldName: fieldName, 479 Content: blobContent, 480 Name: ent.Name(), 481 MimeType: filepath.Ext(ent.Name())[1:], // excluding dot 482 }, 483 RecordID: istructs.RecordID(recordID), 484 }) 485 } 486 } 487 return blobs, nil 488 } 489 490 func checkOrphanedBLOBs(blobIDs map[int64]map[string]struct{}, workspaceData []map[string]interface{}) error { 491 orphanedBLOBRecordIDs := map[int64]struct{}{} 492 for blobRecID := range blobIDs { 493 orphanedBLOBRecordIDs[blobRecID] = struct{}{} 494 } 495 496 for _, record := range workspaceData { 497 recIDIntf, ok := record[appdef.SystemField_ID] 498 if !ok { 499 return errors.New("record with missing sys.ID field is met") 500 } 501 recID := int64(recIDIntf.(float64)) 502 blobFields, ok := blobIDs[recID] 503 if !ok { 504 continue 505 } 506 delete(orphanedBLOBRecordIDs, recID) 507 for blobField := range blobFields { 508 if _, ok := record[blobField]; !ok { 509 return fmt.Errorf("have blob for an unknown field for recordID %d: %s", recID, blobField) 510 } 511 } 512 } 513 514 if len(orphanedBLOBRecordIDs) > 0 { 515 return fmt.Errorf("orphaned blobs met for ids %v", orphanedBLOBRecordIDs) 516 } 517 return nil 518 } 519 520 func ValidateTemplate(wsTemplateName string, ep extensionpoints.IExtensionPoint, wsKind appdef.QName) (wsBLOBs []blobber.StoredBLOB, wsData []map[string]interface{}, err error) { 521 if len(wsTemplateName) == 0 { 522 return nil, nil, nil 523 } 524 epWSTemplates := ep.ExtensionPoint(EPWSTemplates) 525 epWSKindTemplatesIntf, ok := epWSTemplates.Find(wsKind) 526 if !ok { 527 return nil, nil, fmt.Errorf("no templates for workspace kind %s", wsKind.String()) 528 } 529 epWSKindTemplates := epWSKindTemplatesIntf.(extensionpoints.IExtensionPoint) 530 wsTemplateFSIntf, ok := epWSKindTemplates.Find(wsTemplateName) 531 if !ok { 532 return nil, nil, fmt.Errorf("unknown workspace template name %s for workspace kind %s", wsTemplateName, wsKind.String()) 533 } 534 wsTemplateFS := wsTemplateFSIntf.(coreutils.EmbedFS) 535 fsEntries, err := wsTemplateFS.ReadDir(".") 536 if err != nil { 537 return nil, nil, fmt.Errorf("failed to read dir content: %w", err) 538 } 539 wsData = []map[string]interface{}{} 540 dataBytes, err := wsTemplateFS.ReadFile("data.json") 541 if err != nil { 542 return nil, nil, fmt.Errorf("failed to read data.json: %w", err) 543 } 544 if err := json.Unmarshal(dataBytes, &wsData); err != nil { 545 return nil, nil, fmt.Errorf("failed to unmarshal data.json: %w", err) 546 } 547 548 // check blob entries 549 // newBLOBID fieldName 550 blobIDs := map[int64]map[string]struct{}{} 551 wsBLOBs, err = parseWSTemplateBLOBs(fsEntries, blobIDs, wsTemplateFS) 552 if err != nil { 553 return nil, nil, err 554 } 555 if err := checkOrphanedBLOBs(blobIDs, wsData); err != nil { 556 return nil, nil, err 557 } 558 return wsBLOBs, wsData, nil 559 }