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  }