• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

alkem-io / server / #9106

22 Jan 2025 04:03PM UTC coverage: 14.35%. First build
#9106

Pull #4814

travis-ci

Pull Request #4814: RoleSet on organizations + platform

84 of 4951 branches covered (1.7%)

Branch coverage included in aggregate %.

48 of 312 new or added lines in 27 files covered. (15.38%)

2292 of 11606 relevant lines covered (19.75%)

7.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

28.28
/src/core/bootstrap/bootstrap.service.ts
1
import {
1✔
2
  DEFAULT_HOST_ORG_DISPLAY_NAME,
1✔
3
  DEFAULT_HOST_ORG_NAMEID,
1✔
4
  DEFAULT_SPACE_DISPLAYNAME,
1✔
5
  DEFAULT_SPACE_NAMEID,
1✔
6
} from '@common/constants';
1✔
7
import { Profiling } from '@common/decorators';
1✔
8
import { LogContext } from '@common/enums';
1✔
9
import { AiPersonaEngine } from '@common/enums/ai.persona.engine';
1✔
10
import { RoleName } from '@common/enums/role.name';
1✔
11
import { SpaceLevel } from '@common/enums/space.level';
1✔
12
import { TemplateDefaultType } from '@common/enums/template.default.type';
1✔
13
import { TemplateType } from '@common/enums/template.type';
1✔
14
import { VirtualContributorBodyOfKnowledgeType } from '@common/enums/virtual.contributor.body.of.knowledge.type';
1✔
15
import { VirtualContributorDataAccessMode } from '@common/enums/virtual.contributor.data.access.mode';
16
import { VirtualContributorInteractionMode } from '@common/enums/virtual.contributor.interaction.mode';
17
import { VirtualContributorWellKnown } from '@common/enums/virtual.contributor.well.known';
18
import { EntityNotFoundException } from '@common/exceptions';
19
import { BootstrapException } from '@common/exceptions/bootstrap.exception';
20
import { ActorContext } from '@core/actor-context/actor.context';
1✔
21
import { ActorContextService } from '@core/actor-context/actor.context.service';
1✔
22
import { RoleSetService } from '@domain/access/role-set/role.set.service';
1✔
23
import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service';
1✔
24
import { LicenseService } from '@domain/common/license/license.service';
1✔
25
import { MessagingService } from '@domain/communication/messaging/messaging.service';
1✔
26
import { OrganizationService } from '@domain/community/organization/organization.service';
1✔
27
import { OrganizationAuthorizationService } from '@domain/community/organization/organization.service.authorization';
1✔
28
import { OrganizationLookupService } from '@domain/community/organization-lookup/organization.lookup.service';
1✔
29
import { UserService } from '@domain/community/user/user.service';
30
import { UserAuthorizationService } from '@domain/community/user/user.service.authorization';
1✔
31
import { UserLookupService } from '@domain/community/user-lookup/user.lookup.service';
1✔
32
import { IVirtualAssistant } from '@domain/community/virtual-assistant/virtual.assistant.interface';
1✔
33
import { VirtualAssistantService } from '@domain/community/virtual-assistant/virtual.assistant.service';
1✔
34
import { AccountService } from '@domain/space/account/account.service';
1✔
35
import { AccountAuthorizationService } from '@domain/space/account/account.service.authorization';
1✔
36
import { AccountLicenseService } from '@domain/space/account/account.service.license';
37
import { CreateSpaceOnAccountInput } from '@domain/space/account/dto/account.dto.create.space';
38
import { Space } from '@domain/space/space/space.entity';
1✔
39
import { SpaceService } from '@domain/space/space/space.service';
1✔
40
import { SpaceAuthorizationService } from '@domain/space/space/space.service.authorization';
1✔
41
import { CreateTemplateContentSpaceInput } from '@domain/template/template-content-space/dto/template.content.space.dto.create';
1✔
42
import { TemplateDefaultService } from '@domain/template/template-default/template.default.service';
1✔
43
import { TemplatesSetService } from '@domain/template/templates-set/templates.set.service';
1✔
44
import { Inject, Injectable, LoggerService } from '@nestjs/common';
1✔
45
import { ConfigService } from '@nestjs/config';
1✔
46
import { InjectRepository } from '@nestjs/typeorm';
47
import { LicensePlanService } from '@platform/licensing/credential-based/license-plan/license.plan.service';
48
import { LicensingFrameworkService } from '@platform/licensing/credential-based/licensing-framework/licensing.framework.service';
49
import { PlatformService } from '@platform/platform/platform.service';
1✔
50
import { PlatformAuthorizationService } from '@platform/platform/platform.service.authorization';
1✔
51
import { PlatformWellKnownVirtualContributorsService } from '@platform/platform.well.known.virtual.contributors/platform.well.known.virtual.contributors.service';
1✔
52
import { PlatformTemplatesService } from '@platform/platform-templates/platform.templates.service';
1✔
53
import { AiServerService } from '@services/ai-server/ai-server/ai.server.service';
1✔
54
import { AiServerAuthorizationService } from '@services/ai-server/ai-server/ai.server.service.authorization';
1✔
55
import { McpApiKeyService } from '@services/mcp-server/auth/mcp-api-key.service';
1✔
56
import { AdminAuthorizationService } from '@src/platform-admin/domain/authorization/admin.authorization.service';
1✔
57
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
1✔
58
import { Repository } from 'typeorm';
1✔
59
import { bootstrapTemplateSpaceContentCalloutsSpaceL0Tutorials } from './platform-template-definitions/default-templates/bootstrap.template.space.content.callouts.space.l0.tutorials';
1✔
60
import { bootstrapTemplateSpaceContentCalloutsVcKnowledgeBase } from './platform-template-definitions/default-templates/bootstrap.template.space.content.callouts.vc.knowledge.base';
1✔
61
import { bootstrapTemplateSpaceContentSpaceL0 } from './platform-template-definitions/default-templates/bootstrap.template.space.content.space.l0';
1✔
62
import { bootstrapTemplateSpaceContentSubspace } from './platform-template-definitions/default-templates/bootstrap.template.space.content.subspace';
1✔
63
import * as defaultLicensePlan from './platform-template-definitions/license-plan/license-plans.json';
1✔
64
import * as defaultRoles from './platform-template-definitions/user/users.json';
1✔
65

1✔
66
@Injectable()
1✔
67
export class BootstrapService {
68
  constructor(
69
    private accountService: AccountService,
1✔
70
    private accountAuthorizationService: AccountAuthorizationService,
71
    private actorContextService: ActorContextService,
1✔
72
    private spaceService: SpaceService,
1✔
73
    private userService: UserService,
1✔
74
    private userLookupService: UserLookupService,
1✔
75
    private userAuthorizationService: UserAuthorizationService,
1✔
76
    private organizationService: OrganizationService,
1✔
77
    private organizationLookupService: OrganizationLookupService,
1✔
78
    private organizationAuthorizationService: OrganizationAuthorizationService,
1✔
79
    private spaceAuthorizationService: SpaceAuthorizationService,
1✔
80
    private adminAuthorizationService: AdminAuthorizationService,
1✔
81
    private configService: ConfigService,
1✔
82
    private platformService: PlatformService,
1✔
83
    private platformAuthorizationService: PlatformAuthorizationService,
1✔
84
    private authorizationPolicyService: AuthorizationPolicyService,
1✔
85
    @InjectRepository(Space)
1✔
86
    private spaceRepository: Repository<Space>,
87
    @Inject(WINSTON_MODULE_NEST_PROVIDER)
1✔
88
    private readonly logger: LoggerService,
89
    private aiServer: AiServerService,
1✔
90
    private aiServerAuthorizationService: AiServerAuthorizationService,
1✔
91
    private templatesSetService: TemplatesSetService,
1✔
92
    private templateDefaultService: TemplateDefaultService,
1✔
93
    private platformTemplatesService: PlatformTemplatesService,
1✔
94
    private accountLicenseService: AccountLicenseService,
1✔
95
    private licenseService: LicenseService,
1✔
96
    private licensingFrameworkService: LicensingFrameworkService,
1✔
97
    private licensePlanService: LicensePlanService,
1✔
98
    private readonly messagingService: MessagingService,
1✔
99
    private platformWellKnownVirtualContributorsService: PlatformWellKnownVirtualContributorsService,
1✔
100
    private roleSetService: RoleSetService,
101
    private readonly virtualAssistantService: VirtualAssistantService,
102
    private readonly mcpApiKeyService: McpApiKeyService
103
  ) {}
104

×
105
  async bootstrap() {
×
106
    // this.ingestService.ingest(); // todo remove later
107
    try {
×
108
      this.logger.verbose?.('Bootstrapping...', LogContext.BOOTSTRAP);
×
109

110
      Profiling.logger = this.logger;
111
      const profilingEnabled = this.configService.get(
112
        'monitoring.logging.profiling_enabled',
×
113
        { infer: true }
×
114
      );
115
      if (profilingEnabled) {
116
        Profiling.profilingEnabled = profilingEnabled;
×
117
      }
×
118

×
119
      const anonymousActorContext = this.actorContextService.createAnonymous();
×
120

×
121
      // Order matters:
×
122
      // 1. Infrastructure: Forum, Messaging
×
123
      // 2. Templates (needed for VC creation)
×
124
      // 3. Organization (created without admin first)
×
125
      // 4. Guidance VC (needs organization and templates)
126
      // 5. Users (including Admin) - will get guidance conversation created successfully
127
      // 6. Link Admin to Organization
128
      // 7. License plans
×
129
      // 8. Authorization policies
130
      // 9. Space
×
131

132
      await this.platformService.ensureForumCreated();
133
      await this.ensureMessagingCreated();
×
134
      await this.ensurePlatformTemplatesArePresent();
135

136
      // Create Org first (without admin if needed)
137
      await this.ensureOrganizationSingleton();
138

139
      // Create VC (needs Org)
×
140
      await this.ensureGuidanceChat();
141

×
142
      // Create Users (including Admin)
143
      await this.bootstrapUserProfiles();
144

145
      // Ensure Admin is linked to Org
×
146
      await this.ensureAdminUserLinkedToOrganization();
147

148
      await this.bootstrapLicensePlans();
×
149
      await this.ensureAuthorizationsPopulated();
150
      // Register the virtual-assistant MCP trust-anchor key from the shared
151
      // ASSISTANT_MCP_API_KEY secret so delegated MCP works on a fresh deploy
152
      // with no manual DB surgery (issue #1937).
153
      await this.ensureAssistantMcpApiKey();
154
      await this.ensureSpaceSingleton(anonymousActorContext);
155
      // reset auth as last in the actions
156
      // await this.ensureSpaceNamesInElastic();
157
    } catch (error: any) {
×
158
      this.logger.error(
×
159
        `Unable to complete bootstrap process: ${error}`,
160
        error?.stack,
161
        LogContext.BOOTSTRAP
162
      );
163
      throw new BootstrapException(error.message, { originalException: error });
164
    }
165
  }
166

167
  /**
×
168
   * Ensure the `virtual-assistant` actor's MCP API key exists, derived from the
×
169
   * shared `ASSISTANT_MCP_API_KEY` secret (issue #1937). The assistant-service
170
   * sends this same plaintext as its delegation bearer; the server stores only
171
   * its SHA-256 hash, bound to the virtual-assistant actor. Idempotent (a no-op
172
   * once the row matches). Skipped — with a warning, never failing bootstrap —
173
   * when the secret is unset, malformed, or the actor is absent.
174
   */
175
  private async ensureAssistantMcpApiKey(): Promise<void> {
176
    const plaintext = process.env.ASSISTANT_MCP_API_KEY?.trim();
177
    if (!plaintext) {
×
178
      this.logger.warn?.(
×
179
        'ASSISTANT_MCP_API_KEY is not set — skipping virtual-assistant MCP key bootstrap; delegated MCP (the Web AI Assistant) is unavailable until it is provisioned',
180
        LogContext.BOOTSTRAP
181
      );
182
      return;
183
    }
184
    // The MCP API-key strategy only engages `Authorization: Bearer mcp_…`
185
    // headers, so a key without the prefix would bootstrap a row that can
186
    // never authenticate: every asvc call 401s while bootstrap logs success.
187
    if (!plaintext.startsWith('mcp_')) {
×
188
      this.logger.warn?.(
×
189
        "ASSISTANT_MCP_API_KEY does not start with 'mcp_' — skipping virtual-assistant MCP key bootstrap; the MCP host only accepts 'Bearer mcp_…' keys, so this key could never authenticate. Provision a key in the format mcp_<base64url(32)>",
190
        LogContext.BOOTSTRAP
191
      );
192
      return;
193
    }
×
194

×
195
    let virtualAssistant: IVirtualAssistant;
196
    try {
197
      virtualAssistant =
198
        await this.virtualAssistantService.getSingletonOrFail();
199
    } catch (error) {
200
      if (error instanceof EntityNotFoundException) {
201
        this.logger.warn?.(
202
          'virtual-assistant actor not found — skipping MCP key bootstrap (the actor is created by migration; ensure migrations have run)',
203
          LogContext.BOOTSTRAP
204
        );
205
        return;
206
      }
207
      // A transient/DB error must NOT masquerade as "actor absent" and silently
×
208
      // disable delegated MCP — surface it to bootstrap's error handler.
×
209
      throw error;
210
    }
×
211

×
212
    await this.mcpApiKeyService.ensureActorKeyFromPlaintext(
213
      virtualAssistant.id,
214
      plaintext,
215
      [{ operations: ['read', 'tools'] }]
×
216
    );
×
217
    this.logger.verbose?.(
218
      `Ensured virtual-assistant MCP API key from ASSISTANT_MCP_API_KEY (actor ${virtualAssistant.id})`,
219
      LogContext.BOOTSTRAP
220
    );
221
  }
×
222

223
  /**
224
   * Ensures the platform Messaging exists.
225
   * Creates it if missing (should happen only on fresh deployments).
226
   */
227
  private async ensureMessagingCreated(): Promise<void> {
228
    const messaging = await this.platformService.ensureMessagingCreated();
229
    this.logger.verbose?.(
230
      `Platform Messaging ensured: ${messaging.id}`,
231
      LogContext.BOOTSTRAP
232
    );
233
  }
234

235
  private async ensurePlatformTemplatesArePresent() {
236
    let authResetNeeded = await this.ensureSpaceTemplateIsPresent(
237
      TemplateDefaultType.PLATFORM_SPACE,
238
      'space',
239
      bootstrapTemplateSpaceContentSpaceL0
240
    );
241
    authResetNeeded =
242
      (await this.ensureSpaceTemplateIsPresent(
243
        TemplateDefaultType.PLATFORM_SUBSPACE,
244
        'subspace',
×
245
        bootstrapTemplateSpaceContentSubspace
×
246
      )) || authResetNeeded;
×
247
    authResetNeeded =
248
      (await this.ensureSpaceTemplateIsPresent(
×
249
        TemplateDefaultType.PLATFORM_SPACE_TUTORIALS,
250
        'space-tutorials',
251
        bootstrapTemplateSpaceContentCalloutsSpaceL0Tutorials
252
      )) || authResetNeeded;
×
253
    authResetNeeded =
254
      (await this.ensureSpaceTemplateIsPresent(
255
        TemplateDefaultType.PLATFORM_SUBSPACE_KNOWLEDGE,
256
        'knowledge',
×
257
        bootstrapTemplateSpaceContentCalloutsVcKnowledgeBase
258
      )) || authResetNeeded;
259
    if (authResetNeeded) {
260
      this.logger.verbose?.(
261
        '=== Identified that template defaults had not been reset; resetting auth now ===',
×
262
        LogContext.BOOTSTRAP
×
263
      );
×
264
      const updatedAuthorizations =
265
        await this.platformAuthorizationService.applyAuthorizationPolicy();
266
      await this.authorizationPolicyService.saveAll(updatedAuthorizations);
267
    }
268
  }
×
269

270
  private async ensureSpaceTemplateIsPresent(
271
    templateDefaultType: TemplateDefaultType,
272
    nameID: string,
273
    spaceContentData: CreateTemplateContentSpaceInput
×
274
  ): Promise<boolean> {
275
    const templatesSet =
276
      await this.platformTemplatesService.getPlatformTemplatesSet();
277
    const templateDefault =
×
278
      await this.platformTemplatesService.getPlatformTemplateDefault(
×
279
        templateDefaultType
×
280
      );
281

282
    if (!templateDefault) {
283
      throw new BootstrapException(
284
        `Unable to load Template Default for ${templateDefaultType}`
×
285
      );
286
    }
287
    if (!templateDefault.template) {
288
      this.logger.verbose?.(
289
        `No template set for ${templateDefaultType}, setting it...`,
×
290
        LogContext.BOOTSTRAP
291
      );
×
292
      // No template set, so create one and then set it
×
293
      const template = await this.templatesSetService.createTemplate(
294
        templatesSet,
×
295
        {
296
          profileData: {
297
            displayName: `${nameID}-Template`,
×
298
          },
×
299
          type: TemplateType.SPACE,
300
          contentSpaceData: spaceContentData,
301
        }
302
      );
303
      // Set the default template
304
      templateDefault.template = template;
305
      await this.templateDefaultService.save(templateDefault);
×
306
      return true;
307
    }
308
    return false;
309
  }
310

311
  async bootstrapUserProfiles() {
312
    const bootstrapAuthorizationRolesJson = {
1✔
313
      ...defaultRoles,
×
314
    };
×
NEW
315

×
316
    this.logger.verbose?.(
317
      'Authorization bootstrap: default configuration being loaded',
318
      LogContext.BOOTSTRAP
×
319
    );
×
320

321
    const users = bootstrapAuthorizationRolesJson.users;
322
    if (!users) {
323
      this.logger.verbose?.(
324
        'No users section in the authorization bootstrap file!',
325
        LogContext.BOOTSTRAP
326
      );
327
    } else {
328
      await this.createUserProfiles(users);
329
    }
330
  }
331

×
332
  async bootstrapLicensePlans() {
333
    const bootstrapLicensePlans = {
334
      ...defaultLicensePlan,
×
335
    };
336

×
337
    const licensePlans = bootstrapLicensePlans.licensePlans;
338
    if (!licensePlans) {
×
339
      this.logger.verbose?.(
340
        'No licensePlans section in the license plans bootstrap file!',
341
        LogContext.BOOTSTRAP
×
342
      );
343
    } else {
×
344
      await this.createLicensePlans(licensePlans);
×
345
    }
×
346
  }
347

348
  async createLicensePlans(licensePlansData: any[]) {
349
    try {
350
      const licensing =
351
        await this.licensingFrameworkService.getDefaultLicensingOrFail();
×
352
      for (const licensePlanData of licensePlansData) {
353
        const planExists =
354
          await this.licensePlanService.licensePlanByNameExists(
355
            licensePlanData.name
356
          );
357
        if (!planExists) {
×
358
          await this.licensingFrameworkService.createLicensePlan({
359
            ...licensePlanData,
360
            licensingID: licensing.id,
361
          });
362
        }
363
      }
364
    } catch (error: any) {
365
      throw new BootstrapException(
366
        `Unable to create license plans ${error.message}`
367
      );
368
    }
369
  }
370

371
  async createUserProfiles(usersData: any[]) {
372
    try {
373
      for (const userData of usersData) {
374
        const userExists = await this.userLookupService.isRegisteredUser(
375
          userData.email
376
        );
377
        if (!userExists) {
378
          const user = await this.userService.createUser({
379
            email: userData.email,
380
            firstName: userData.firstName,
381
            lastName: userData.lastName,
382
            profileData: {
×
383
              displayName: `${userData.firstName} ${userData.lastName}`,
×
384
            },
×
385
          });
386

387
          // Once all is done, reset the user authorizations
388
          const userAuthorizations =
389
            await this.userAuthorizationService.applyAuthorizationPolicy(
390
              user.id
×
391
            );
392
          await this.authorizationPolicyService.saveAll(userAuthorizations);
×
393

394
          const account = await this.userService.getAccount(user);
395
          const accountAuthorizations =
396
            await this.accountAuthorizationService.applyAuthorizationPolicy(
×
397
              account
398
            );
×
399
          await this.authorizationPolicyService.saveAll(accountAuthorizations);
×
400

401
          const credentialsData = userData.credentials;
402
          for (const credentialData of credentialsData) {
403
            await this.adminAuthorizationService.grantCredentialToUser({
404
              userID: user.id,
×
405
              type: credentialData.type,
×
406
              resourceID: credentialData.resourceID,
407
            });
408
          }
409
          await this.userAuthorizationService.grantCredentialsAllUsersReceive(
×
410
            user.id
411
          );
×
412
        }
413
      }
414
    } catch (error: any) {
415
      throw new BootstrapException(
×
416
        `Unable to create profiles ${error.message}`
417
      );
×
418
    }
×
419
  }
420

421
  private async ensureAuthorizationsPopulated() {
422
    // For platform
423
    const platform = await this.platformService.getPlatformOrFail();
×
424
    const platformAuthorization =
×
425
      this.authorizationPolicyService.validateAuthorization(
426
        platform.authorization
427
      );
428
    const platformCredentialRules =
429
      this.authorizationPolicyService.getCredentialRules(platformAuthorization);
×
430
    // Assume that zero rules means that the policy has not been reset
431
    if (platformCredentialRules.length == 0) {
432
      this.logger.verbose?.(
433
        '=== Identified that platform authorization had not been reset; resetting now ===',
434
        LogContext.BOOTSTRAP
435
      );
436
      const updatedAuthorizations =
×
437
        await this.platformAuthorizationService.applyAuthorizationPolicy();
×
438
      await this.authorizationPolicyService.saveAll(updatedAuthorizations);
439
    }
440

441
    // Also do same for AI Server until it is moved out of the server
442
    const aiServer = await this.aiServer.getAiServerOrFail();
443
    const aiServerAuthorization =
444
      this.authorizationPolicyService.validateAuthorization(
445
        aiServer.authorization
×
446
      );
447
    const aiServerCredentialRules =
448
      this.authorizationPolicyService.getCredentialRules(aiServerAuthorization);
×
449
    // Assume that zero rules means that the policy has not been reset
×
450
    if (aiServerCredentialRules.length == 0) {
×
451
      this.logger.verbose?.(
452
        '=== Identified that AI Server authorization had not been reset; resetting now ===',
453
        LogContext.BOOTSTRAP
454
      );
455
      const authorizations =
456
        await this.aiServerAuthorizationService.applyAuthorizationPolicy();
457
      await this.authorizationPolicyService.saveAll(authorizations);
458
    }
459
  }
460

×
461
  private async ensureOrganizationSingleton(actorContext?: ActorContext) {
462
    // create a default host org
463
    let hostOrganization =
×
464
      await this.organizationLookupService.getOrganizationByNameId(
465
        DEFAULT_HOST_ORG_NAMEID
466
      );
×
467
    if (!hostOrganization) {
468
      // If actorContext is not provided, we create without an admin initially
×
469
      // The admin will be linked later
470
      hostOrganization = await this.organizationService.createOrganization(
471
        {
×
472
          nameID: DEFAULT_HOST_ORG_NAMEID,
473
          profileData: {
474
            displayName: DEFAULT_HOST_ORG_DISPLAY_NAME,
×
475
          },
×
476
        },
477
        actorContext
478
      );
479
      const orgAuthorizations =
480
        await this.organizationAuthorizationService.applyAuthorizationPolicy(
×
481
          hostOrganization
×
482
        );
483
      await this.authorizationPolicyService.saveAll(orgAuthorizations);
484

485
      const account =
486
        await this.organizationService.getAccount(hostOrganization);
×
487
      const accountAuthorizations =
×
488
        await this.accountAuthorizationService.applyAuthorizationPolicy(
489
          account
490
        );
491
      await this.authorizationPolicyService.saveAll(accountAuthorizations);
×
492

493
      const accountEntitlements =
494
        await this.accountLicenseService.applyLicensePolicy(account.id);
495
      await this.licenseService.saveAll(accountEntitlements);
×
496
    }
497
  }
498

499
  private async ensureAdminUserLinkedToOrganization() {
×
500
    const adminActorContext = await this.getAdminActorContext();
×
501
    const hostOrganization =
×
502
      await this.organizationLookupService.getOrganizationByNameIdOrFail(
×
503
        DEFAULT_HOST_ORG_NAMEID
504
      );
505

506
    const roleSet = await this.organizationService.getRoleSet(hostOrganization);
507

×
508
    // Assign Admin as Associate and Admin
509
    await this.roleSetService.assignActorToRole(
510
      roleSet,
511
      RoleName.ASSOCIATE,
512
      adminActorContext.actorID,
×
513
      adminActorContext,
×
514
      false
515
    );
516

517
    await this.roleSetService.assignActorToRole(
518
      roleSet,
519
      RoleName.ADMIN,
520
      adminActorContext.actorID,
521
      adminActorContext,
522
      false
523
    );
524

525
    this.logger.verbose?.(
526
      `Ensured Admin user linked to Organization: ${hostOrganization.id}`,
527
      LogContext.BOOTSTRAP
×
528
    );
529
  }
×
530

×
531
  private async getAdminActorContext(): Promise<ActorContext> {
532
    const adminUserEmail = 'admin@alkem.io';
533
    const adminUser = await this.userService.getUserByEmail(adminUserEmail, {
×
534
      relations: {
×
535
        credentials: true,
536
      },
×
537
    });
538
    if (!adminUser) {
539
      throw new BootstrapException(
540
        `Unable to load fixed admin user for creating organization: ${adminUserEmail}`
541
      );
×
542
    }
543
    const ctx = new ActorContext();
544
    ctx.actorID = adminUser.id;
×
545
    ctx.isAnonymous = false;
546
    ctx.credentials = (adminUser.credentials || []).map(c => ({
×
547
      type: c.type,
548
      resourceID: c.resourceID,
549
    }));
550
    ctx.authenticationID = adminUser.authenticationID ?? undefined;
551
    return ctx;
552
  }
553

554
  private async ensureSpaceSingleton(actorContext: ActorContext) {
555
    this.logger.verbose?.(
556
      '=== Ensuring at least one Account with a space is present ===',
557
      LogContext.BOOTSTRAP
×
558
    );
559
    const spaceCount = await this.spaceRepository.count();
560
    if (spaceCount == 0) {
561
      this.logger.verbose?.('...No space present...', LogContext.BOOTSTRAP);
×
562
      this.logger.verbose?.(
563
        '........creating on default organization',
564
        LogContext.BOOTSTRAP
×
565
      );
566
      const hostOrganization =
567
        await this.organizationLookupService.getOrganizationByNameIdOrFail(
568
          DEFAULT_HOST_ORG_NAMEID
569
        );
570

571
      const account =
572
        await this.organizationService.getAccount(hostOrganization);
573
      const spaceInput: CreateSpaceOnAccountInput = {
574
        accountID: account.id,
575
        nameID: DEFAULT_SPACE_NAMEID,
576
        about: {
577
          profileData: {
578
            displayName: DEFAULT_SPACE_DISPLAYNAME,
579
            tagline: 'An empty space to be populated',
580
          },
581
        },
×
582
        level: SpaceLevel.L0,
×
583
        levelZeroSpaceID: '',
584
        collaborationData: {
585
          calloutsSetData: {},
586
        },
587
      };
588

589
      const space = await this.accountService.createSpaceOnAccount(
590
        spaceInput,
591
        actorContext
592
      );
593
      const spaceAuthorizations =
594
        await this.spaceAuthorizationService.applyAuthorizationPolicy(space.id);
595
      await this.authorizationPolicyService.saveAll(spaceAuthorizations);
596

597
      const accountEntitlements =
598
        await this.accountLicenseService.applyLicensePolicy(account.id);
599
      await this.licenseService.saveAll(accountEntitlements);
600

601
      return this.spaceService.getSpaceOrFail(space.id);
602
    }
603
  }
604

605
  private async ensureGuidanceChat() {
606
    // Check if the CHAT_GUIDANCE well-known VC is configured
607
    const wellKnownVCId =
608
      await this.platformWellKnownVirtualContributorsService.getVirtualContributorID(
609
        VirtualContributorWellKnown.CHAT_GUIDANCE
610
      );
611

612
    if (!wellKnownVCId) {
613
      // Get admin account:
614
      const hostOrganization =
615
        await this.organizationLookupService.getOrganizationByNameIdOrFail(
616
          DEFAULT_HOST_ORG_NAMEID
617
        );
618
      const account =
619
        await this.organizationService.getAccount(hostOrganization);
620

621
      // Create the VC
622
      const vc = await this.accountService.createVirtualContributorOnAccount({
623
        accountID: account.id,
624
        aiPersona: {
625
          engine: AiPersonaEngine.GUIDANCE,
626
          prompt: [],
627
          externalConfig: undefined,
628
        },
629
        profileData: {
630
          displayName: 'Guidance',
631
          description: 'Guidance Virtual Contributor',
632
        },
633
        dataAccessMode: VirtualContributorDataAccessMode.NONE,
634
        bodyOfKnowledgeType: VirtualContributorBodyOfKnowledgeType.WEBSITE,
635
        interactionModes: [
636
          VirtualContributorInteractionMode.DISCUSSION_TAGGING,
637
        ],
638
        knowledgeBaseData: {
639
          profile: {
640
            displayName: 'Knowledge Base for Virtual Contributor',
641
          },
642
          calloutsSetData: {},
643
        },
644
      });
645

646
      // Apply authorization for the newly created VC via account auth reset
647
      // (the earlier account auth reset in ensureOrganizationSingleton ran before
648
      // this VC existed, so it was skipped)
649
      const accountAuthorizations =
650
        await this.accountAuthorizationService.applyAuthorizationPolicy(
651
          account
652
        );
653
      await this.authorizationPolicyService.saveAll(accountAuthorizations);
654

655
      // Register the VC as the CHAT_GUIDANCE well-known VC
656
      await this.platformWellKnownVirtualContributorsService.setMapping(
657
        VirtualContributorWellKnown.CHAT_GUIDANCE,
658
        vc.id
659
      );
660
    }
661
  }
662
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc