From c3a8314c5a59103921b522d99fdf1a9f78b2f31c Mon Sep 17 00:00:00 2001 From: Neil Smyth <30729240+techsmyth@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:38:30 +0100 Subject: [PATCH 01/19] Entitlements + license services (#4593) * first pass at two new modules for TemplatesManager, TemplateDefault * added templates manager to space; removed the SpaceDefaults entity (module still present until move all data to be via defaults * added templatesManager to platform * moved creating of default innovatin flow input to space defaults * back out space type on Template; tidy up Template module to use switch statements * created template applier module * tidy up naming * updated set of default template types * fixed circular dependency; moved logic for creating collaboration input to space defaults * removed loading of defaults from files for collaboration content * removed code based addition of callouts, innovation flow states * tidy up naming * added loading of default templates at platform level in to bootstrap * removed option to create new innovation flow template * added in migration: * loading in templates on bootstrap * added field for collaboration templates on templatesSet; added lookup for templatesManager * added mutation to create template from collaboration; added logic to prevent template used as default to be deleted; fixed removal of template set on template manager * initial creation of license + entitlements modules * add license into account * updated account to have license service + use that in mutations checking limits, removing notion of soft limits * ensure data is loaded properly on account for license checking * added mutation to reset the license calculations on account, including new auth privilege to be able to do so * renamed Licensing module to LicensingFramework module; trigger license reset on Account after assigning / removing license * removed usage of LicenseEngine outside of license services on space or account * renamed entitlement to licenseEntitlement as entity; first pass at migration * fixed issues in migration * fixed issues related to auth reset; tidied up loader creator imports * fixed auth cascade for templates of type post * license reset running * reset licenses on space after adding / removing license plans * removed need for license check in community; added entitlement check in roleset when adding a VC * remove auth reset when assigning / removing license plans * added License to RoleSet * added license to collaboration * tidied up retrieval of license for whiteboard; added license to collaboration in migration * fix typo; fix space spec file * fix additional tests * moved tempaltesManager to last migration in the list * fixed retrieval of template when creating collaboration * added logging * fixed bootstrap setting of templates * refactored inputCreator to do the data loading closer to usage; fixed picking up of templates; fixed bootstrap usage of templates * added ability to retrieve limits on entitlements + current usage * updated field names on entitlements * updated field names on entitlements * fixed account mutaiton logic bug * ensure that licenses are reset when assigning beta tester or vc campaign role to a user * added reset all account licenses mutation * fixed bug on space entitlements; refactored code to reduce duplication * fixed url generation for templates inside of TempaltesManager * fixed bootstrap order to create forum earlier * ensure collaboration creation on template provides some defaults for callouts * fix deletion of templates of type post * ensure more data is defaulted inside of template service for collaboration; add setting of isTemplate field on Collaboration, and also on contained Callouts * ensure isTempalte is passed to Collaboration entity * fixed groups in bootstrap space template; updated signature for creating callout from collaboration * fixed missing field * fixed type on mutation to create from collaboration * fixed typo * fixed groups in bootstrap space template; updated signature for creating callout from collaboration * fixed missing field * fixed type on mutation to create from collaboration * fixed typo * reworked applying collaboraiton template to collaboration * improved error message in wrong type of ID passed in * fixed build * made migration last in the list * rename migration to be last * removed read check when looking up collaboration * track free / plus / premium space entitlements separately * updated migration order * removed duplicate migration * moved auth reset to mutation for applying the template to another collaboration * extend lookup of entitlement usage to cover new types * updaed license policy to reflect new entitlements; made license engine work with entitlements, not license privileges; removed license privilege (no longer relevant) * updated migration to not drop indexes already removed * fix for license reset on space * added license policy rule for free space credential * ensure license entitlements are reset as part of the bootstrap * fixed typo * extended reset all to include resetting licenses on accounts + AI server; moved migration to be last * Address pr comment * Address PR feedback * Address PR comment * Address PR comments * Address PR comments * Address PR comment Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Improved types & naming * Address PR comments * Fixed switch-case logic in entitlements * Converge entitlements schema * Remove unused AuthorizationPrivilege --------- Co-authored-by: Carlos Cano Co-authored-by: Valentin Yanakiev Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/app.module.ts | 2 + .../credential.rule.types.constants.ts | 2 - src/common/enums/alkemio.error.status.ts | 2 + src/common/enums/authorization.policy.type.ts | 1 + src/common/enums/authorization.privilege.ts | 2 +- .../enums/license.entitlement.data.type.ts | 10 + src/common/enums/license.entitlement.type.ts | 20 + src/common/enums/license.privilege.ts | 15 - src/common/enums/license.type.ts | 13 + .../forbidden.license.policy.exception.ts | 4 +- ...nse.entitlement.not.available.exception.ts | 12 + .../license.entitlement.not.supported.ts | 12 + src/core/bootstrap/bootstrap.module.ts | 6 +- src/core/bootstrap/bootstrap.service.ts | 21 +- .../creators/loader.creators/index.ts | 1 + .../loader.creators/license.loader.creator.ts | 36 + .../license-engine/license.engine.service.ts | 36 +- ...icense.policy.rule.credential.interface.ts | 6 +- .../license.policy.rule.credential.ts | 8 +- src/domain/access/role-set/role.set.entity.ts | 9 + .../access/role-set/role.set.interface.ts | 3 + src/domain/access/role-set/role.set.module.ts | 8 +- .../role-set/role.set.resolver.fields.ts | 19 + .../role-set/role.set.resolver.mutations.ts | 26 +- .../role.set.service.authorization.ts | 14 +- .../role-set/role.set.service.license.ts | 88 +++ .../access/role-set/role.set.service.ts | 24 +- .../collaboration/collaboration.entity.ts | 9 + .../collaboration/collaboration.interface.ts | 3 + .../collaboration/collaboration.module.ts | 12 +- .../collaboration.resolver.fields.ts | 20 +- .../collaboration.resolver.mutations.ts | 2 +- .../collaboration.service.authorization.ts | 104 ++- .../collaboration.service.license.ts | 96 +++ .../collaboration/collaboration.service.ts | 34 +- .../dto/license.entitlement.dto.create.ts | 9 + .../license.entitlement.entity.ts | 32 + .../license.entitlement.interface.ts | 35 + .../license.entitlement.module.ts | 16 + .../license.entitlement.resolver.fields.ts | 32 + .../license.entitlement.service.ts | 188 +++++ .../common/license/dto/license.dto.create.ts | 8 + src/domain/common/license/license.entity.ts | 18 + .../common/license/license.interface.ts | 15 + src/domain/common/license/license.module.ts | 25 + .../common/license/license.resolver.fields.ts | 20 + .../license/license.service.authorization.ts | 25 + src/domain/common/license/license.service.ts | 226 ++++++ .../common/whiteboard/whiteboard.module.ts | 4 +- .../common/whiteboard/whiteboard.service.ts | 26 +- .../community/community/community.module.ts | 2 - .../community.service.authorization.ts | 51 +- .../community/community/community.service.ts | 4 +- .../organization/organization.module.ts | 2 - src/domain/community/user/user.module.ts | 8 - .../space/account.host/account.host.module.ts | 6 +- .../account.host/account.host.service.ts | 71 +- src/domain/space/account/account.entity.ts | 15 +- src/domain/space/account/account.interface.ts | 5 + src/domain/space/account/account.module.ts | 10 +- .../space/account/account.resolver.fields.ts | 16 + .../account/account.resolver.mutations.ts | 171 +++-- .../account/account.service.authorization.ts | 75 +- .../space/account/account.service.license.ts | 170 +++++ src/domain/space/account/account.service.ts | 12 +- .../dto/account.dto.reset.authorization.ts | 4 +- .../account/dto/account.dto.reset.license.ts | 12 + .../space.defaults/space.defaults.service.ts | 2 +- .../space/sort.spaces.by.activity.spec.ts | 1 + src/domain/space/space/space.entity.ts | 9 + src/domain/space/space/space.interface.ts | 2 + src/domain/space/space/space.module.ts | 12 +- .../space/space/space.resolver.fields.ts | 23 +- .../space/space/space.resolver.mutations.ts | 2 +- .../space/space.service.authorization.ts | 32 +- .../space/space/space.service.license.ts | 177 +++++ src/domain/space/space/space.service.spec.ts | 3 + src/domain/space/space/space.service.ts | 142 ++-- ...mplate.applier.dto.update.collaboration.ts | 1 + .../template.applier.resolver.mutations.ts | 58 +- .../template.applier.service.ts | 48 +- .../template/template/template.service.ts | 2 +- .../dto/templates.manager.dto.create..ts | 5 + .../calendar/calendar.resolver.fields.ts | 1 - src/domain/timeline/event/event.entity.ts | 1 - .../1731077703010-allowEventsFromSubspaces.ts | 70 +- .../1731500015640-licenseEntitlements.ts | 648 ++++++++++++++++++ .../admin/licensing/admin.licensing.module.ts | 8 +- .../admin.licensing.resolver.mutations.ts | 120 ++-- .../licensing/admin.licensing.service.ts | 52 +- .../license-plan/license.plan.entity.ts | 6 +- .../license-plan/license.plan.interface.ts | 4 +- .../license.plan.resolver.mutations.ts | 12 +- .../license-policy/license.policy.service.ts | 6 +- ...sing.framework.dto.create.license.plan.ts} | 4 +- .../licensing.framework.entity.ts} | 9 +- .../licensing.framework.interface.ts} | 2 +- .../licensing.framework.module.ts | 29 + .../licensing.framework.resolver.fields.ts} | 22 +- ...licensing.framework.resolver.mutations.ts} | 18 +- ...ensing.framework.service.authorization.ts} | 12 +- .../licensing.framework.service.ts} | 42 +- src/platform/licensing/licensing.module.ts | 29 - .../platform.role/platform.role.module.ts | 2 + .../platform.role.resolver.mutations.ts | 19 +- src/platform/platform/platform.entity.ts | 6 +- src/platform/platform/platform.interface.ts | 4 +- src/platform/platform/platform.module.ts | 4 +- .../platform/platform.resolver.fields.ts | 10 +- .../platform.service.authorization.ts | 12 +- src/platform/platform/platform.service.ts | 10 +- .../conversion.resolver.mutations.ts | 11 +- .../api/conversion/conversion.service.ts | 14 +- .../api/registration/registration.service.ts | 4 +- src/services/api/roles/roles.service.spec.ts | 1 + .../auth-reset.payload.interface.ts | 4 +- src/services/auth-reset/event.type.ts | 6 - .../publisher/auth-reset.service.ts | 77 ++- src/services/auth-reset/reset.event.type.ts | 8 + .../subscriber/auth-reset.controller.ts | 110 ++- .../auth-reset.subscriber.module.ts | 4 + .../community.resolver.service.ts | 75 +- .../license.entitlement.usage.module.ts | 9 + .../license.entitlement.usage.service.ts | 108 +++ .../storage.aggregator.resolver.service.ts | 11 +- test/mocks/account.service.mock.ts | 2 +- 126 files changed, 3342 insertions(+), 776 deletions(-) create mode 100644 src/common/enums/license.entitlement.data.type.ts create mode 100644 src/common/enums/license.entitlement.type.ts delete mode 100644 src/common/enums/license.privilege.ts create mode 100644 src/common/enums/license.type.ts create mode 100644 src/common/exceptions/license.entitlement.not.available.exception.ts create mode 100644 src/common/exceptions/license.entitlement.not.supported.ts create mode 100644 src/core/dataloader/creators/loader.creators/license.loader.creator.ts create mode 100644 src/domain/access/role-set/role.set.service.license.ts create mode 100644 src/domain/collaboration/collaboration/collaboration.service.license.ts create mode 100644 src/domain/common/license-entitlement/dto/license.entitlement.dto.create.ts create mode 100644 src/domain/common/license-entitlement/license.entitlement.entity.ts create mode 100644 src/domain/common/license-entitlement/license.entitlement.interface.ts create mode 100644 src/domain/common/license-entitlement/license.entitlement.module.ts create mode 100644 src/domain/common/license-entitlement/license.entitlement.resolver.fields.ts create mode 100644 src/domain/common/license-entitlement/license.entitlement.service.ts create mode 100644 src/domain/common/license/dto/license.dto.create.ts create mode 100644 src/domain/common/license/license.entity.ts create mode 100644 src/domain/common/license/license.interface.ts create mode 100644 src/domain/common/license/license.module.ts create mode 100644 src/domain/common/license/license.resolver.fields.ts create mode 100644 src/domain/common/license/license.service.authorization.ts create mode 100644 src/domain/common/license/license.service.ts create mode 100644 src/domain/space/account/account.service.license.ts create mode 100644 src/domain/space/account/dto/account.dto.reset.license.ts create mode 100644 src/domain/space/space/space.service.license.ts create mode 100644 src/domain/template/templates-manager/dto/templates.manager.dto.create..ts create mode 100644 src/migrations/1731500015640-licenseEntitlements.ts rename src/platform/{licensing/dto/license.manager.dto.create.license.plan.ts => licensing-framework/dto/licensing.framework.dto.create.license.plan.ts} (65%) rename src/platform/{licensing/licensing.entity.ts => licensing-framework/licensing.framework.entity.ts} (76%) rename src/platform/{licensing/licensing.interface.ts => licensing-framework/licensing.framework.interface.ts} (84%) create mode 100644 src/platform/licensing-framework/licensing.framework.module.ts rename src/platform/{licensing/licensing.resolver.fields.ts => licensing-framework/licensing.framework.resolver.fields.ts} (55%) rename src/platform/{licensing/licensing.resolver.mutations.ts => licensing-framework/licensing.framework.resolver.mutations.ts} (61%) rename src/platform/{licensing/licensing.service.authorization.ts => licensing-framework/licensing.framework.service.authorization.ts} (89%) rename src/platform/{licensing/licensing.service.ts => licensing-framework/licensing.framework.service.ts} (72%) delete mode 100644 src/platform/licensing/licensing.module.ts delete mode 100644 src/services/auth-reset/event.type.ts create mode 100644 src/services/auth-reset/reset.event.type.ts create mode 100644 src/services/infrastructure/license-entitlement-usage/license.entitlement.usage.module.ts create mode 100644 src/services/infrastructure/license-entitlement-usage/license.entitlement.usage.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 6be663ca3f..9a619c29a7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -82,6 +82,7 @@ import { PlatformHubModule } from '@platform/platform.hub/platform.hub.module'; import { AdminContributorsModule } from '@platform/admin/avatars/admin.avatar.module'; import { InputCreatorModule } from '@services/api/input-creator/input.creator.module'; import { TemplateApplierModule } from '@domain/template/template-applier/template.applier.module'; +import { LoaderCreatorModule } from '@core/dataloader/creators/loader.creator.module'; import { Cipher, EncryptionModule } from '@hedger/nestjs-encryption'; import { AdminUsersModule } from '@platform/admin/users/admin.users.module'; @@ -247,6 +248,7 @@ import { AdminUsersModule } from '@platform/admin/users/admin.users.module'; }; }, }), + LoaderCreatorModule, ScalarsModule, AuthenticationModule, AuthorizationModule, diff --git a/src/common/constants/authorization/credential.rule.types.constants.ts b/src/common/constants/authorization/credential.rule.types.constants.ts index 6df59b5bee..54f9535db5 100644 --- a/src/common/constants/authorization/credential.rule.types.constants.ts +++ b/src/common/constants/authorization/credential.rule.types.constants.ts @@ -16,8 +16,6 @@ export const CREDENTIAL_RULE_TYPES_SPACE_COMMUNITY_APPLY_GLOBAL_REGISTERED = 'credentialRuleTypes-spaceCommunityApplyGlobalRegistered'; export const CREDENTIAL_RULE_TYPES_SPACE_COMMUNITY_JOIN_GLOBAL_REGISTERED = 'credentialRuleTypes-spaceCommunityJoinGlobalRegistered'; -export const CREDENTIAL_RULE_TYPES_ACCESS_VIRTUAL_CONTRIBUTORS = - 'credentialRuleTypes-accessVirtualContributors'; export const CREDENTIAL_RULE_TYPES_CALLOUT_SAVE_AS_TEMPLATE = 'credentialRuleTypes-calloutSaveAsTemplate'; export const CREDENTIAL_RULE_TYPES_CALLOUT_UPDATE_PUBLISHER_ADMINS = diff --git a/src/common/enums/alkemio.error.status.ts b/src/common/enums/alkemio.error.status.ts index 6977af0450..fadc368932 100644 --- a/src/common/enums/alkemio.error.status.ts +++ b/src/common/enums/alkemio.error.status.ts @@ -25,6 +25,8 @@ export enum AlkemioErrorStatus { NOT_ENABLED = 'NOT_ENABLED', USER_NOT_REGISTERED = 'USER_NOT_REGISTERED', LICENSE_NOT_FOUND = 'LICENSE_NOT_FOUND', + LICENSE_ENTITLEMENT_NOT_AVAILABLE = 'LICENSE_ENTITLEMENT_NOT_AVAILABLE', + LICENSE_ENTITLEMENT_NOT_SUPPORTED = 'LICENSE_ENTITLEMENT_NOT_SUPPORTED', MATRIX_ENTITY_NOT_FOUND_ERROR = 'MATRIX_ENTITY_NOT_FOUND_ERROR', BOOTSTRAP_FAILED = 'BOOTSTRAP_FAILED', NOTIFICATION_PAYLOAD_BUILDER_ERROR = 'NOTIFICATION_PAYLOAD_BUILDER_ERROR', diff --git a/src/common/enums/authorization.policy.type.ts b/src/common/enums/authorization.policy.type.ts index 7d3f2e2021..e5aad33921 100644 --- a/src/common/enums/authorization.policy.type.ts +++ b/src/common/enums/authorization.policy.type.ts @@ -54,6 +54,7 @@ export enum AuthorizationPolicyType { LIBRARY = 'library', IN_MEMORY = 'in-memory', LICENSING = 'licensing', + LICENSE = 'license', LICENSE_POLICY = 'license-policy', UNKNOWN = 'unknown', AI_PERSONA_SERVICE = 'ai-persona-service', diff --git a/src/common/enums/authorization.privilege.ts b/src/common/enums/authorization.privilege.ts index dc815a203e..3396955e01 100644 --- a/src/common/enums/authorization.privilege.ts +++ b/src/common/enums/authorization.privilege.ts @@ -8,6 +8,7 @@ export enum AuthorizationPrivilege { GRANT = 'grant', // allow the issuing / revoking of credentials of the same type within a given scope GRANT_GLOBAL_ADMINS = 'grant-global-admins', AUTHORIZATION_RESET = 'authorization-reset', + LICENSE_RESET = 'license-reset', PLATFORM_ADMIN = 'platform-admin', // To determine if the user should have access to the platform administration CONTRIBUTE = 'contribute', CREATE_CALLOUT = 'create-callout', @@ -40,7 +41,6 @@ export enum AuthorizationPrivilege { MOVE_POST = 'move-post', MOVE_CONTRIBUTION = 'move-contribution', ACCESS_INTERACTIVE_GUIDANCE = 'access-interactive-guidance', - ACCESS_VIRTUAL_CONTRIBUTOR = 'access-virtual-contributor', UPDATE_CONTENT = 'update-content', SAVE_AS_TEMPLATE = 'save-as-template', TRANSFER_RESOURCE = 'transfer-resource', diff --git a/src/common/enums/license.entitlement.data.type.ts b/src/common/enums/license.entitlement.data.type.ts new file mode 100644 index 0000000000..32cdc4a60c --- /dev/null +++ b/src/common/enums/license.entitlement.data.type.ts @@ -0,0 +1,10 @@ +import { registerEnumType } from '@nestjs/graphql'; + +export enum LicenseEntitlementDataType { + LIMIT = 'limit', + FLAG = 'flag', +} + +registerEnumType(LicenseEntitlementDataType, { + name: 'LicenseEntitlementDataType', +}); diff --git a/src/common/enums/license.entitlement.type.ts b/src/common/enums/license.entitlement.type.ts new file mode 100644 index 0000000000..474fef329b --- /dev/null +++ b/src/common/enums/license.entitlement.type.ts @@ -0,0 +1,20 @@ +import { registerEnumType } from '@nestjs/graphql'; + +export enum LicenseEntitlementType { + ACCOUNT_SPACE_FREE = 'account-space-free', + ACCOUNT_SPACE_PLUS = 'account-space-plus', + ACCOUNT_SPACE_PREMIUM = 'account-space-premium', + ACCOUNT_VIRTUAL_CONTRIBUTOR = 'account-virtual-contributor', + ACCOUNT_INNOVATION_PACK = 'account-innovation-pack', + ACCOUNT_INNOVATION_HUB = 'account-innovation-hub', + SPACE_FREE = 'space-free', + SPACE_PLUS = 'space-plus', + SPACE_PREMIUM = 'space-premium', + SPACE_FLAG_SAVE_AS_TEMPLATE = 'space-flag-save-as-template', + SPACE_FLAG_VIRTUAL_CONTRIBUTOR_ACCESS = 'space-flag-virtual-contributor-access', + SPACE_FLAG_WHITEBOARD_MULTI_USER = 'space-flag-whiteboard-multi-user', +} + +registerEnumType(LicenseEntitlementType, { + name: 'LicenseEntitlementType', +}); diff --git a/src/common/enums/license.privilege.ts b/src/common/enums/license.privilege.ts deleted file mode 100644 index 6d4e3ae70e..0000000000 --- a/src/common/enums/license.privilege.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { registerEnumType } from '@nestjs/graphql'; - -export enum LicensePrivilege { - SPACE_VIRTUAL_CONTRIBUTOR_ACCESS = 'space-virtual-contributor-access', - SPACE_WHITEBOARD_MULTI_USER = 'space-whiteboard-multi-user', - SPACE_SAVE_AS_TEMPLATE = 'space-save-as-template', - - ACCOUNT_CREATE_SPACE = 'account-create-space', - ACCOUNT_CREATE_VIRTUAL_CONTRIBUTOR = 'account-create-virtual-contributor', - ACCOUNT_CREATE_INNOVATION_PACK = 'account-create-innovation-pack', -} - -registerEnumType(LicensePrivilege, { - name: 'LicensePrivilege', -}); diff --git a/src/common/enums/license.type.ts b/src/common/enums/license.type.ts new file mode 100644 index 0000000000..39771d0027 --- /dev/null +++ b/src/common/enums/license.type.ts @@ -0,0 +1,13 @@ +import { registerEnumType } from '@nestjs/graphql'; + +export enum LicenseType { + ACCOUNT = 'account', + SPACE = 'space', + WHITEBOARD = 'whiteboard', + ROLESET = 'roleset', + COLLABORATION = 'collaboration', +} + +registerEnumType(LicenseType, { + name: 'LicenseType', +}); diff --git a/src/common/exceptions/forbidden.license.policy.exception.ts b/src/common/exceptions/forbidden.license.policy.exception.ts index 1563e960ec..f96c5cd65a 100644 --- a/src/common/exceptions/forbidden.license.policy.exception.ts +++ b/src/common/exceptions/forbidden.license.policy.exception.ts @@ -1,11 +1,11 @@ import { AlkemioErrorStatus, LogContext } from '@common/enums'; import { BaseException } from './base.exception'; -import { LicensePrivilege } from '@common/enums/license.privilege'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; export class ForbiddenLicensePolicyException extends BaseException { constructor( error: string, - public checkedPrivilege: LicensePrivilege, + public checkedEntitlement: LicenseEntitlementType, public licensePolicyId: string, public licenseId: string ) { diff --git a/src/common/exceptions/license.entitlement.not.available.exception.ts b/src/common/exceptions/license.entitlement.not.available.exception.ts new file mode 100644 index 0000000000..d2d4ef9795 --- /dev/null +++ b/src/common/exceptions/license.entitlement.not.available.exception.ts @@ -0,0 +1,12 @@ +import { LogContext, AlkemioErrorStatus } from '@common/enums'; +import { BaseException } from './base.exception'; + +export class LicenseEntitlementNotAvailableException extends BaseException { + constructor(error: string, context: LogContext, code?: AlkemioErrorStatus) { + super( + error, + context, + code ?? AlkemioErrorStatus.LICENSE_ENTITLEMENT_NOT_AVAILABLE + ); + } +} diff --git a/src/common/exceptions/license.entitlement.not.supported.ts b/src/common/exceptions/license.entitlement.not.supported.ts new file mode 100644 index 0000000000..c2541a4566 --- /dev/null +++ b/src/common/exceptions/license.entitlement.not.supported.ts @@ -0,0 +1,12 @@ +import { LogContext, AlkemioErrorStatus } from '@common/enums'; +import { BaseException } from './base.exception'; + +export class LicenseEntitlementNotSupportedException extends BaseException { + constructor(error: string, context: LogContext, code?: AlkemioErrorStatus) { + super( + error, + context, + code ?? AlkemioErrorStatus.LICENSE_ENTITLEMENT_NOT_SUPPORTED + ); + } +} diff --git a/src/core/bootstrap/bootstrap.module.ts b/src/core/bootstrap/bootstrap.module.ts index 2e91a8f094..1e2fb9036f 100644 --- a/src/core/bootstrap/bootstrap.module.ts +++ b/src/core/bootstrap/bootstrap.module.ts @@ -19,14 +19,16 @@ import { ContributorModule } from '@domain/community/contributor/contributor.mod import { TemplatesSetModule } from '@domain/template/templates-set/templates.set.module'; import { TemplatesManagerModule } from '@domain/template/templates-manager/templates.manager.module'; import { TemplateDefaultModule } from '@domain/template/template-default/template.default.module'; -import { LicensingModule } from '@platform/licensing/licensing.module'; +import { LicenseModule } from '@domain/common/license/license.module'; import { LicensePlanModule } from '@platform/license-plan/license.plan.module'; +import { LicensingFrameworkModule } from '@platform/licensing-framework/licensing.framework.module'; @Module({ imports: [ AiServerModule, AgentModule, AuthorizationPolicyModule, + LicenseModule, ContributorModule, SpaceModule, OrganizationModule, @@ -43,7 +45,7 @@ import { LicensePlanModule } from '@platform/license-plan/license.plan.module'; TemplatesSetModule, TemplatesManagerModule, TemplateDefaultModule, - LicensingModule, + LicensingFrameworkModule, LicensePlanModule, ], providers: [BootstrapService], diff --git a/src/core/bootstrap/bootstrap.service.ts b/src/core/bootstrap/bootstrap.service.ts index ddbd2e37d3..b5aca6edde 100644 --- a/src/core/bootstrap/bootstrap.service.ts +++ b/src/core/bootstrap/bootstrap.service.ts @@ -55,8 +55,10 @@ import { bootstrapSpaceCallouts } from './platform-template-definitions/space/bo import { bootstrapSpaceTutorialsInnovationFlowStates } from './platform-template-definitions/space-tutorials/bootstrap.space.tutorials.innovation.flow.states'; import { bootstrapSpaceTutorialsCalloutGroups } from './platform-template-definitions/space-tutorials/bootstrap.space.tutorials.callout.groups'; import { bootstrapSpaceTutorialsCallouts } from './platform-template-definitions/space-tutorials/bootstrap.space.tutorials.callouts'; -import { LicensingService } from '@platform/licensing/licensing.service'; +import { LicenseService } from '@domain/common/license/license.service'; +import { AccountLicenseService } from '@domain/space/account/account.service.license'; import { LicensePlanService } from '@platform/license-plan/license.plan.service'; +import { LicensingFrameworkService } from '@platform/licensing-framework/licensing.framework.service'; @Injectable() export class BootstrapService { @@ -84,7 +86,9 @@ export class BootstrapService { private templatesManagerService: TemplatesManagerService, private templatesSetService: TemplatesSetService, private templateDefaultService: TemplateDefaultService, - private licensingService: LicensingService, + private accountLicenseService: AccountLicenseService, + private licenseService: LicenseService, + private licensingFrameworkService: LicensingFrameworkService, private licensePlanService: LicensePlanService ) {} @@ -273,14 +277,15 @@ export class BootstrapService { async createLicensePlans(licensePlansData: any[]) { try { - const licensing = await this.licensingService.getDefaultLicensingOrFail(); + const licensing = + await this.licensingFrameworkService.getDefaultLicensingOrFail(); for (const licensePlanData of licensePlansData) { const planExists = await this.licensePlanService.licensePlanByNameExists( licensePlanData.name ); if (!planExists) { - await this.licensingService.createLicensePlan({ + await this.licensingFrameworkService.createLicensePlan({ ...licensePlanData, licensingID: licensing.id, }); @@ -454,6 +459,10 @@ export class BootstrapService { account ); await this.authorizationPolicyService.saveAll(accountAuthorizations); + + const accountEntitlements = + await this.accountLicenseService.applyLicensePolicy(account.id); + await this.licenseService.saveAll(accountEntitlements); } } @@ -508,6 +517,10 @@ export class BootstrapService { await this.spaceAuthorizationService.applyAuthorizationPolicy(space); await this.authorizationPolicyService.saveAll(spaceAuthorizations); + const accountEntitlements = + await this.accountLicenseService.applyLicensePolicy(account.id); + await this.licenseService.saveAll(accountEntitlements); + return this.spaceService.getSpaceOrFail(space.id); } } diff --git a/src/core/dataloader/creators/loader.creators/index.ts b/src/core/dataloader/creators/loader.creators/index.ts index 4aad4579fe..77e5c362b3 100644 --- a/src/core/dataloader/creators/loader.creators/index.ts +++ b/src/core/dataloader/creators/loader.creators/index.ts @@ -7,6 +7,7 @@ export * from './profile/profile.tagsets.loader.creator'; export * from './callout-framing/callout.framing.whiteboard.loader'; export * from './profile.loader.creator'; +export * from './license.loader.creator'; export * from './preferences.loader.creator'; export * from './agent.loader.creator'; export * from './authorization.loader.creator'; diff --git a/src/core/dataloader/creators/loader.creators/license.loader.creator.ts b/src/core/dataloader/creators/loader.creators/license.loader.creator.ts new file mode 100644 index 0000000000..67dd69d262 --- /dev/null +++ b/src/core/dataloader/creators/loader.creators/license.loader.creator.ts @@ -0,0 +1,36 @@ +import { EntityManager } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager } from '@nestjs/typeorm'; +import { DataLoaderInitError } from '@common/exceptions/data-loader'; +import { createTypedRelationDataLoader } from '../../utils'; +import { DataLoaderCreator, DataLoaderCreatorOptions } from '../base'; +import { ILicense } from '@domain/common/license/license.interface'; +import { License } from '@domain/common/license/license.entity'; + +@Injectable() +export class LicenseLoaderCreator implements DataLoaderCreator { + constructor(@InjectEntityManager() private manager: EntityManager) {} + + create( + options?: DataLoaderCreatorOptions< + ILicense, + { id: string; license?: License } + > + ) { + if (!options?.parentClassRef) { + throw new DataLoaderInitError( + `${this.constructor.name} requires the 'parentClassRef' to be provided.` + ); + } + + return createTypedRelationDataLoader( + this.manager, + options.parentClassRef, + { + license: true, + }, + this.constructor.name, + options + ); + } +} diff --git a/src/core/license-engine/license.engine.service.ts b/src/core/license-engine/license.engine.service.ts index 0e389a3450..a79c84518f 100644 --- a/src/core/license-engine/license.engine.service.ts +++ b/src/core/license-engine/license.engine.service.ts @@ -5,7 +5,6 @@ import { ForbiddenException, } from '@common/exceptions'; import { LogContext } from '@common/enums'; -import { LicensePrivilege } from '@common/enums/license.privilege'; import { ILicensePolicy } from '@platform/license-policy/license.policy.interface'; import { ForbiddenLicensePolicyException } from '@common/exceptions/forbidden.license.policy.exception'; import { EntityManager } from 'typeorm'; @@ -13,6 +12,7 @@ import { InjectEntityManager } from '@nestjs/typeorm'; import { LicensePolicy } from '@platform/license-policy'; import { IAgent, ICredential } from '@domain/agent'; import { ILicensePolicyCredentialRule } from './license.policy.rule.credential.interface'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; @Injectable() export class LicenseEngineService { @@ -23,31 +23,31 @@ export class LicenseEngineService { private entityManager: EntityManager ) {} - public async grantAccessOrFail( - privilegeRequired: LicensePrivilege, + public async grantEntitlementOrFail( + entitlementRequired: LicenseEntitlementType, agent: IAgent, msg: string, licensePolicy: ILicensePolicy | undefined ) { - const accessGranted = await this.isAccessGranted( - privilegeRequired, + const accessGranted = await this.isEntitlementGranted( + entitlementRequired, agent, licensePolicy ); if (accessGranted) return true; - const errorMsg = `License.engine: unable to grant '${privilegeRequired}' privilege: ${msg} license: ${agent.id}`; + const errorMsg = `License.engine: unable to grant '${entitlementRequired}' privilege: ${msg} license: ${agent.id}`; // If you get to here then no match was found throw new ForbiddenLicensePolicyException( errorMsg, - privilegeRequired, + entitlementRequired, licensePolicy?.id || 'no license policy', agent.id ); } - public async isAccessGranted( - privilegeRequired: LicensePrivilege, + public async isEntitlementGranted( + entitlementRequired: LicenseEntitlementType, agent: IAgent, licensePolicy?: ILicensePolicy | undefined ): Promise { @@ -60,9 +60,11 @@ export class LicenseEngineService { for (const credentialRule of credentialRules) { for (const credential of credentials) { if (credential.type === credentialRule.credentialType) { - if (credentialRule.grantedPrivileges.includes(privilegeRequired)) { + if ( + credentialRule.grantedEntitlements.includes(entitlementRequired) + ) { this.logger.verbose?.( - `[CredentialRule] Granted privilege '${privilegeRequired}' using rule '${credentialRule.name}'`, + `[CredentialRule] Granted privilege '${entitlementRequired}' using rule '${credentialRule.name}'`, LogContext.LICENSE ); return true; @@ -94,14 +96,14 @@ export class LicenseEngineService { return policy; } - public async getGrantedPrivileges( + public async getGrantedEntitlements( agent: IAgent, licensePolicy?: ILicensePolicy - ) { + ): Promise { const policy = await this.getLicensePolicyOrFail(licensePolicy); const credentials = await this.getCredentialsFromAgent(agent); - const grantedPrivileges: LicensePrivilege[] = []; + const grantedEntitlements: LicenseEntitlementType[] = []; const credentialRules = this.convertCredentialRulesStr( policy.credentialRulesStr @@ -109,14 +111,14 @@ export class LicenseEngineService { for (const rule of credentialRules) { for (const credential of credentials) { if (rule.credentialType === credential.type) { - for (const privilege of rule.grantedPrivileges) { - grantedPrivileges.push(privilege); + for (const entitlement of rule.grantedEntitlements) { + grantedEntitlements.push(entitlement); } } } } - const uniquePrivileges = grantedPrivileges.filter( + const uniquePrivileges = grantedEntitlements.filter( (item, i, ar) => ar.indexOf(item) === i ); diff --git a/src/core/license-engine/license.policy.rule.credential.interface.ts b/src/core/license-engine/license.policy.rule.credential.interface.ts index 3b26eee257..aa2690fbbf 100644 --- a/src/core/license-engine/license.policy.rule.credential.interface.ts +++ b/src/core/license-engine/license.policy.rule.credential.interface.ts @@ -1,5 +1,5 @@ import { LicenseCredential } from '@common/enums/license.credential'; -import { LicensePrivilege } from '@common/enums/license.privilege'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; import { Field, ObjectType } from '@nestjs/graphql'; @ObjectType('LicensePolicyCredentialRule') @@ -7,8 +7,8 @@ export abstract class ILicensePolicyCredentialRule { @Field(() => LicenseCredential) credentialType!: LicenseCredential; - @Field(() => [LicensePrivilege]) - grantedPrivileges!: LicensePrivilege[]; + @Field(() => [LicenseEntitlementType]) + grantedEntitlements!: LicenseEntitlementType[]; @Field(() => String, { nullable: true }) name!: string; diff --git a/src/core/license-engine/license.policy.rule.credential.ts b/src/core/license-engine/license.policy.rule.credential.ts index b5097306b7..1e93eaa6e1 100644 --- a/src/core/license-engine/license.policy.rule.credential.ts +++ b/src/core/license-engine/license.policy.rule.credential.ts @@ -1,21 +1,21 @@ import { LicenseCredential } from '@common/enums/license.credential'; -import { LicensePrivilege } from '@common/enums/license.privilege'; import { ILicensePolicyCredentialRule } from './license.policy.rule.credential.interface'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; export class LicensePolicyCredentialRule implements ILicensePolicyCredentialRule { credentialType: LicenseCredential; - grantedPrivileges: LicensePrivilege[]; + grantedEntitlements: LicenseEntitlementType[]; name: string; constructor( - grantedPrivileges: LicensePrivilege[], + grantedEntitlements: LicenseEntitlementType[], credentialType: LicenseCredential, name: string ) { this.credentialType = credentialType; - this.grantedPrivileges = grantedPrivileges; + this.grantedEntitlements = grantedEntitlements; this.name = name; } } diff --git a/src/domain/access/role-set/role.set.entity.ts b/src/domain/access/role-set/role.set.entity.ts index e20a85a739..bf7a9b8b30 100644 --- a/src/domain/access/role-set/role.set.entity.ts +++ b/src/domain/access/role-set/role.set.entity.ts @@ -16,12 +16,21 @@ import { Application } from '@domain/access/application/application.entity'; import { Invitation } from '@domain/access/invitation/invitation.entity'; import { CommunityRoleType } from '@common/enums/community.role'; import { ENUM_LENGTH } from '@common/constants/entity.field.length.constants'; +import { License } from '@domain/common/license/license.entity'; @Entity() export class RoleSet extends AuthorizableEntity implements IRoleSet, IGroupable { + @OneToOne(() => License, { + eager: false, + cascade: true, + onDelete: 'SET NULL', + }) + @JoinColumn() + license?: License; + @OneToOne(() => Form, { eager: false, cascade: true, diff --git a/src/domain/access/role-set/role.set.interface.ts b/src/domain/access/role-set/role.set.interface.ts index a5c80dd283..c9472fc787 100644 --- a/src/domain/access/role-set/role.set.interface.ts +++ b/src/domain/access/role-set/role.set.interface.ts @@ -6,6 +6,7 @@ import { IApplication } from '@domain/access/application/application.interface'; import { IInvitation } from '@domain/access/invitation/invitation.interface'; import { IRole } from '../role/role.interface'; import { CommunityRoleType } from '@common/enums/community.role'; +import { ILicense } from '@domain/common/license/license.interface'; @ObjectType('RoleSet') export abstract class IRoleSet extends IAuthorizable { @@ -25,4 +26,6 @@ export abstract class IRoleSet extends IAuthorizable { applicationForm?: IForm; parentRoleSet?: IRoleSet; + + license?: ILicense; } diff --git a/src/domain/access/role-set/role.set.module.ts b/src/domain/access/role-set/role.set.module.ts index dbaa7894f0..808f549e00 100644 --- a/src/domain/access/role-set/role.set.module.ts +++ b/src/domain/access/role-set/role.set.module.ts @@ -8,7 +8,6 @@ import { RoleSetResolverMutations } from './role.set.resolver.mutations'; import { RoleSetService } from './role.set.service'; import { RoleSetAuthorizationService } from './role.set.service.authorization'; import { FormModule } from '@domain/common/form/form.module'; -import { LicenseEngineModule } from '@core/license-engine/license.engine.module'; import { PlatformInvitationModule } from '@platform/invitation/platform.invitation.module'; import { InvitationModule } from '@domain/access/invitation/invitation.module'; import { ApplicationModule } from '@domain/access/application/application.module'; @@ -29,12 +28,14 @@ import { ActivityAdapterModule } from '@services/adapters/activity-adapter/activ import { LifecycleModule } from '@domain/common/lifecycle/lifecycle.module'; import { CommunityCommunicationModule } from '@domain/community/community-communication/community.communication.module'; import { RoleSetResolverFieldsPublic } from './role.set.resolver.fields.public'; +import { LicenseModule } from '@domain/common/license/license.module'; +import { RoleSetLicenseService } from './role.set.service.license'; @Module({ imports: [ AuthorizationModule, AuthorizationPolicyModule, - LicenseEngineModule, + LicenseModule, FormModule, AgentModule, UserModule, @@ -57,6 +58,7 @@ import { RoleSetResolverFieldsPublic } from './role.set.resolver.fields.public'; providers: [ RoleSetService, RoleSetAuthorizationService, + RoleSetLicenseService, RoleSetResolverMutations, RoleSetResolverFields, RoleSetResolverFieldsPublic, @@ -64,6 +66,6 @@ import { RoleSetResolverFieldsPublic } from './role.set.resolver.fields.public'; RoleSetServiceLifecycleApplication, RoleSetServiceLifecycleInvitation, ], - exports: [RoleSetService, RoleSetAuthorizationService], + exports: [RoleSetService, RoleSetAuthorizationService, RoleSetLicenseService], }) export class RoleSetModule {} diff --git a/src/domain/access/role-set/role.set.resolver.fields.ts b/src/domain/access/role-set/role.set.resolver.fields.ts index d650ad9e49..92a0982c6e 100644 --- a/src/domain/access/role-set/role.set.resolver.fields.ts +++ b/src/domain/access/role-set/role.set.resolver.fields.ts @@ -18,6 +18,11 @@ import { IVirtualContributor } from '@domain/community/virtual-contributor/virtu import { IInvitation } from '../invitation/invitation.interface'; import { IPlatformInvitation } from '@platform/invitation/platform.invitation.interface'; import { RoleSetMemberCredentials } from '@domain/community/user/dto/user.dto.role.set.member.credentials'; +import { ILicense } from '@domain/common/license/license.interface'; +import { RoleSet } from './role.set.entity'; +import { LicenseLoaderCreator } from '@core/dataloader/creators/loader.creators/license.loader.creator'; +import { ILoader } from '@core/dataloader/loader.interface'; +import { Loader } from '@core/dataloader/decorators/data.loader.decorator'; @Resolver(() => IRoleSet) export class RoleSetResolverFields { @@ -194,4 +199,18 @@ export class RoleSetResolverFields { const apps = await this.roleSetService.getApplications(roleSet); return apps || []; } + + @AuthorizationAgentPrivilege(AuthorizationPrivilege.READ) + @UseGuards(GraphqlGuard) + @ResolveField('license', () => ILicense, { + nullable: false, + description: 'The License operating on this RoleSet.', + }) + async license( + @Parent() roleSet: IRoleSet, + @Loader(LicenseLoaderCreator, { parentClassRef: RoleSet }) + loader: ILoader + ): Promise { + return loader.load(roleSet.id); + } } diff --git a/src/domain/access/role-set/role.set.resolver.mutations.ts b/src/domain/access/role-set/role.set.resolver.mutations.ts index cdb900bd62..49c0a6aa50 100644 --- a/src/domain/access/role-set/role.set.resolver.mutations.ts +++ b/src/domain/access/role-set/role.set.resolver.mutations.ts @@ -54,8 +54,8 @@ import { NotificationInputCommunityInvitation } from '@services/adapters/notific import { RoleSetAuthorizationService } from './role.set.service.authorization'; import { CommunityMembershipStatus } from '@common/enums/community.membership.status'; import { JoinAsEntryRoleOnRoleSetInput } from './dto/role.set.dto.entry.role.join'; -import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -import { LifecycleService } from '@domain/common/lifecycle/lifecycle.service'; +import { LicenseService } from '@domain/common/license/license.service'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; import { InvitationLifecycleEvent, InvitationLifecycleState, @@ -64,6 +64,8 @@ import { ApplicationLifecycleEvent, ApplicationLifecycleState, } from '../application/application.service.lifecycle'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { LifecycleService } from '@domain/common/lifecycle/lifecycle.service'; @Resolver() export class RoleSetResolverMutations { @@ -86,6 +88,7 @@ export class RoleSetResolverMutations { private contributorService: ContributorService, private platformInvitationAuthorizationService: PlatformInvitationAuthorizationService, private platformInvitationService: PlatformInvitationService, + private licenseService: LicenseService, private lifecycleService: LifecycleService, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService ) {} @@ -166,7 +169,14 @@ export class RoleSetResolverMutations { @Args('roleData') roleData: AssignRoleOnRoleSetToVirtualContributorInput ): Promise { const roleSet = await this.roleSetService.getRoleSetOrFail( - roleData.roleSetID + roleData.roleSetID, + { + relations: { + license: { + entitlements: true, + }, + }, + } ); let requiredPrivilege = AuthorizationPrivilege.GRANT; @@ -191,12 +201,10 @@ export class RoleSetResolverMutations { `assign virtual community role: ${roleSet.id}` ); - // Also require ACCESS_VIRTUAL_CONTRIBUTORS to assign a virtual contributor - this.authorizationService.grantAccessOrFail( - agentInfo, - roleSet.authorization, - AuthorizationPrivilege.ACCESS_VIRTUAL_CONTRIBUTOR, - `assign virtual community role VC privilege: ${roleSet.id}` + // Also require ACCOUNT_VIRTUAL_CONTRIBUTOR entitlement for the RoleSet + this.licenseService.isEntitlementEnabledOrFail( + roleSet.license, + LicenseEntitlementType.ACCOUNT_VIRTUAL_CONTRIBUTOR ); await this.roleSetService.assignVirtualToRole( diff --git a/src/domain/access/role-set/role.set.service.authorization.ts b/src/domain/access/role-set/role.set.service.authorization.ts index 5b19bc2654..4dc446df74 100644 --- a/src/domain/access/role-set/role.set.service.authorization.ts +++ b/src/domain/access/role-set/role.set.service.authorization.ts @@ -36,6 +36,7 @@ import { CommunityMembershipPolicy } from '@common/enums/community.membership.po import { CommunityRoleType } from '@common/enums/community.role'; import { IRoleSet } from './role.set.interface'; import { UUID } from '@domain/common/scalars/scalar.uuid'; +import { LicenseAuthorizationService } from '@domain/common/license/license.service.authorization'; @Injectable() export class RoleSetAuthorizationService { @@ -45,7 +46,8 @@ export class RoleSetAuthorizationService { private applicationAuthorizationService: ApplicationAuthorizationService, private invitationAuthorizationService: InvitationAuthorizationService, private virtualContributorService: VirtualContributorService, - private platformInvitationAuthorizationService: PlatformInvitationAuthorizationService + private platformInvitationAuthorizationService: PlatformInvitationAuthorizationService, + private licenseAuthorizationService: LicenseAuthorizationService ) {} async applyAuthorizationPolicy( @@ -61,13 +63,15 @@ export class RoleSetAuthorizationService { applications: true, invitations: true, platformInvitations: true, + license: true, }, }); if ( !roleSet.roles || !roleSet.applications || !roleSet.invitations || - !roleSet.platformInvitations + !roleSet.platformInvitations || + !roleSet.license ) { throw new RelationshipNotFoundException( `Unable to load child entities for roleSet authorization: ${roleSet.id} `, @@ -138,6 +142,12 @@ export class RoleSetAuthorizationService { ); updatedAuthorizations.push(platformInvitationAuthorization); } + const licenseAuthorization = + this.licenseAuthorizationService.applyAuthorizationPolicy( + roleSet.license, + roleSet.authorization + ); + updatedAuthorizations.push(...licenseAuthorization); return updatedAuthorizations; } diff --git a/src/domain/access/role-set/role.set.service.license.ts b/src/domain/access/role-set/role.set.service.license.ts new file mode 100644 index 0000000000..294d567ed2 --- /dev/null +++ b/src/domain/access/role-set/role.set.service.license.ts @@ -0,0 +1,88 @@ +import { Inject, Injectable, LoggerService } from '@nestjs/common'; +import { LogContext } from '@common/enums'; +import { + EntityNotInitializedException, + RelationshipNotFoundException, +} from '@common/exceptions'; +import { LicenseService } from '@domain/common/license/license.service'; +import { ILicense } from '@domain/common/license/license.interface'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { RoleSetService } from './role.set.service'; + +@Injectable() +export class RoleSetLicenseService { + constructor( + private licenseService: LicenseService, + private roleSetService: RoleSetService, + @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService + ) {} + + async applyLicensePolicy( + roleSetID: string, + parentLicense: ILicense + ): Promise { + const roleSet = await this.roleSetService.getRoleSetOrFail(roleSetID, { + relations: { + license: { + entitlements: true, + }, + }, + }); + if (!roleSet.license || !roleSet.license.entitlements) { + throw new RelationshipNotFoundException( + `Unable to load RoleSet with entities at start of license reset: ${roleSet.id} `, + LogContext.LICENSE + ); + } + const updatedLicenses: ILicense[] = []; + + // Ensure always applying from a clean state + roleSet.license = this.licenseService.reset(roleSet.license); + + roleSet.license = await this.extendLicensePolicy( + roleSet.license, + parentLicense + ); + + updatedLicenses.push(roleSet.license); + + return updatedLicenses; + } + + private async extendLicensePolicy( + license: ILicense | undefined, + parentLicense: ILicense + ): Promise { + if ( + !license || + !license.entitlements || + !parentLicense || + !parentLicense.entitlements + ) { + throw new EntityNotInitializedException( + 'License or parent License with entitlements not found for RoleSet', + LogContext.LICENSE + ); + } + const parentEntitlements = parentLicense.entitlements; + for (const entitlement of license.entitlements) { + switch (entitlement.type) { + case LicenseEntitlementType.SPACE_FLAG_VIRTUAL_CONTRIBUTOR_ACCESS: + this.licenseService.findAndCopyParentEntitlement( + entitlement, + parentEntitlements + ); + break; + + default: + throw new EntityNotInitializedException( + `Unknown entitlement type for RoleSet: ${entitlement.type}`, + LogContext.LICENSE + ); + } + } + + return license; + } +} diff --git a/src/domain/access/role-set/role.set.service.ts b/src/domain/access/role-set/role.set.service.ts index 66cfa3a63e..2a7ce8eff5 100644 --- a/src/domain/access/role-set/role.set.service.ts +++ b/src/domain/access/role-set/role.set.service.ts @@ -55,6 +55,10 @@ import { RoleSetEventsService } from './role.set.service.events'; import { AiServerAdapter } from '@services/adapters/ai-server-adapter/ai.server.adapter'; import { CommunityMembershipStatus } from '@common/enums/community.membership.status'; import { CommunityCommunicationService } from '@domain/community/community-communication/community.communication.service'; +import { LicenseService } from '@domain/common/license/license.service'; +import { LicenseType } from '@common/enums/license.type'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; +import { LicenseEntitlementDataType } from '@common/enums/license.entitlement.data.type'; @Injectable() export class RoleSetService { @@ -74,6 +78,7 @@ export class RoleSetService { private roleSetEventsService: RoleSetEventsService, private aiServerAdapter: AiServerAdapter, private communityCommunicationService: CommunityCommunicationService, + private licenseService: LicenseService, @InjectRepository(RoleSet) private roleSetRepository: Repository, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService @@ -101,6 +106,18 @@ export class RoleSetService { roleSetData.applicationForm ); + roleSet.license = await this.licenseService.createLicense({ + type: LicenseType.ROLESET, + entitlements: [ + { + type: LicenseEntitlementType.SPACE_FLAG_VIRTUAL_CONTRIBUTOR_ACCESS, + dataType: LicenseEntitlementDataType.FLAG, + limit: 0, + enabled: false, + }, + ], + }); + return roleSet; } @@ -120,7 +137,7 @@ export class RoleSetService { return roleSet; } - async removeRoleSet(roleSetID: string): Promise { + async removeRoleSetOrFail(roleSetID: string): Promise { // Note need to load it in with all contained entities so can remove fully const roleSet = await this.getRoleSetOrFail(roleSetID, { relations: { @@ -129,6 +146,7 @@ export class RoleSetService { invitations: true, platformInvitations: true, applicationForm: true, + license: true, }, }); if ( @@ -136,7 +154,8 @@ export class RoleSetService { !roleSet.applications || !roleSet.invitations || !roleSet.platformInvitations || - !roleSet.applicationForm + !roleSet.applicationForm || + !roleSet.license ) { throw new RelationshipNotFoundException( `Unable to load child entities for roleSet for deletion: ${roleSet.id} `, @@ -172,6 +191,7 @@ export class RoleSetService { } await this.formService.removeForm(roleSet.applicationForm); + await this.licenseService.removeLicenseOrFail(roleSet.license.id); await this.roleSetRepository.remove(roleSet as RoleSet); return true; diff --git a/src/domain/collaboration/collaboration/collaboration.entity.ts b/src/domain/collaboration/collaboration/collaboration.entity.ts index 0dde120ee1..9a49a48af3 100644 --- a/src/domain/collaboration/collaboration/collaboration.entity.ts +++ b/src/domain/collaboration/collaboration/collaboration.entity.ts @@ -5,6 +5,7 @@ import { ICollaboration } from '@domain/collaboration/collaboration/collaboratio import { TagsetTemplateSet } from '@domain/common/tagset-template-set'; import { Timeline } from '@domain/timeline/timeline/timeline.entity'; import { InnovationFlow } from '../innovation-flow/innovation.flow.entity'; +import { License } from '@domain/common/license/license.entity'; @Entity() export class Collaboration @@ -45,4 +46,12 @@ export class Collaboration @Column('text', { nullable: false }) groupsStr!: string; + + @OneToOne(() => License, { + eager: false, + cascade: true, + onDelete: 'SET NULL', + }) + @JoinColumn() + license?: License; } diff --git a/src/domain/collaboration/collaboration/collaboration.interface.ts b/src/domain/collaboration/collaboration/collaboration.interface.ts index 6113909c6d..48338b86f8 100644 --- a/src/domain/collaboration/collaboration/collaboration.interface.ts +++ b/src/domain/collaboration/collaboration/collaboration.interface.ts @@ -4,6 +4,7 @@ import { ICallout } from '@domain/collaboration/callout/callout.interface'; import { ITagsetTemplateSet } from '@domain/common/tagset-template-set'; import { ITimeline } from '@domain/timeline/timeline/timeline.interface'; import { IInnovationFlow } from '../innovation-flow/innovation.flow.interface'; +import { ILicense } from '@domain/common/license/license.interface'; @ObjectType('Collaboration') export abstract class ICollaboration extends IAuthorizable { @@ -17,6 +18,8 @@ export abstract class ICollaboration extends IAuthorizable { groupsStr!: string; + license?: ILicense; + @Field(() => Boolean, { nullable: false, description: 'Whether this Collaboration is a Template or not.', diff --git a/src/domain/collaboration/collaboration/collaboration.module.ts b/src/domain/collaboration/collaboration/collaboration.module.ts index e478afe5bd..7b10ddffa7 100644 --- a/src/domain/collaboration/collaboration/collaboration.module.ts +++ b/src/domain/collaboration/collaboration/collaboration.module.ts @@ -20,9 +20,10 @@ import { TimelineModule } from '@domain/timeline/timeline/timeline.module'; import { StorageAggregatorResolverModule } from '@services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.module'; import { InnovationFlowModule } from '../innovation-flow/innovation.flow.module'; import { CalloutGroupsModule } from '../callout-groups/callout.group.module'; -import { LicenseEngineModule } from '@core/license-engine/license.engine.module'; import { RoleSetModule } from '@domain/access/role-set/role.set.module'; import { TemporaryStorageModule } from '@services/infrastructure/temporary-storage/temporary.storage.module'; +import { LicenseModule } from '@domain/common/license/license.module'; +import { CollaborationLicenseService } from './collaboration.service.license'; @Module({ imports: [ @@ -42,7 +43,7 @@ import { TemporaryStorageModule } from '@services/infrastructure/temporary-stora TagsetTemplateSetModule, InnovationFlowModule, CalloutGroupsModule, - LicenseEngineModule, + LicenseModule, TemporaryStorageModule, TypeOrmModule.forFeature([Collaboration]), ], @@ -51,7 +52,12 @@ import { TemporaryStorageModule } from '@services/infrastructure/temporary-stora CollaborationAuthorizationService, CollaborationResolverMutations, CollaborationResolverFields, + CollaborationLicenseService, + ], + exports: [ + CollaborationService, + CollaborationAuthorizationService, + CollaborationLicenseService, ], - exports: [CollaborationService, CollaborationAuthorizationService], }) export class CollaborationModule {} diff --git a/src/domain/collaboration/collaboration/collaboration.resolver.fields.ts b/src/domain/collaboration/collaboration/collaboration.resolver.fields.ts index d5524de8f9..fb1e8494d7 100644 --- a/src/domain/collaboration/collaboration/collaboration.resolver.fields.ts +++ b/src/domain/collaboration/collaboration/collaboration.resolver.fields.ts @@ -14,12 +14,16 @@ import { ICallout } from '../callout/callout.interface'; import { CollaborationArgsCallouts } from './dto/collaboration.args.callouts'; import { AgentInfo } from '@core/authentication.agent.info/agent.info'; import { Loader } from '@core/dataloader/decorators'; -import { CollaborationTimelineLoaderCreator } from '@core/dataloader/creators'; +import { + CollaborationTimelineLoaderCreator, + LicenseLoaderCreator, +} from '@core/dataloader/creators'; import { ILoader } from '@core/dataloader/loader.interface'; import { ITagsetTemplate } from '@domain/common/tagset-template/tagset.template.interface'; import { ITimeline } from '@domain/timeline/timeline/timeline.interface'; import { IInnovationFlow } from '../innovation-flow/innovation.flow.interface'; import { ICalloutGroup } from '../callout-groups/callout.group.interface'; +import { ILicense } from '@domain/common/license/license.interface'; @Resolver(() => ICollaboration) export class CollaborationResolverFields { @@ -89,4 +93,18 @@ export class CollaborationResolverFields { await this.collaborationService.getTagsetTemplatesSet(collaboration.id); return tagsetTemplateSet.tagsetTemplates; } + + @AuthorizationAgentPrivilege(AuthorizationPrivilege.READ) + @UseGuards(GraphqlGuard) + @ResolveField('license', () => ILicense, { + nullable: false, + description: 'The License operating on this Collaboration.', + }) + async license( + @Parent() collaboration: ICollaboration, + @Loader(LicenseLoaderCreator, { parentClassRef: Collaboration }) + loader: ILoader + ): Promise { + return loader.load(collaboration.id); + } } diff --git a/src/domain/collaboration/collaboration/collaboration.resolver.mutations.ts b/src/domain/collaboration/collaboration/collaboration.resolver.mutations.ts index 380d3aa90f..25fc2b62b3 100644 --- a/src/domain/collaboration/collaboration/collaboration.resolver.mutations.ts +++ b/src/domain/collaboration/collaboration/collaboration.resolver.mutations.ts @@ -57,7 +57,7 @@ export class CollaborationResolverMutations { AuthorizationPrivilege.DELETE, `delete collaboration: ${collaboration.id}` ); - return this.collaborationService.deleteCollaboration(deleteData.ID); + return this.collaborationService.deleteCollaborationOrFail(deleteData.ID); } @UseGuards(GraphqlGuard) diff --git a/src/domain/collaboration/collaboration/collaboration.service.authorization.ts b/src/domain/collaboration/collaboration/collaboration.service.authorization.ts index 11808579a2..f3c1b3ec80 100644 --- a/src/domain/collaboration/collaboration/collaboration.service.authorization.ts +++ b/src/domain/collaboration/collaboration/collaboration.service.authorization.ts @@ -15,7 +15,6 @@ import { CREDENTIAL_RULE_COLLABORATION_CONTRIBUTORS, POLICY_RULE_COLLABORATION_CREATE, POLICY_RULE_CALLOUT_CONTRIBUTE, - POLICY_RULE_COLLABORATION_WHITEBOARD_CREATE, CREDENTIAL_RULE_TYPES_CALLOUT_SAVE_AS_TEMPLATE, POLICY_RULE_COLLABORATION_WHITEBOARD_CONTRIBUTORS_CREATE, } from '@common/constants'; @@ -23,31 +22,29 @@ import { CommunityRoleType } from '@common/enums/community.role'; import { TimelineAuthorizationService } from '@domain/timeline/timeline/timeline.service.authorization'; import { InnovationFlowAuthorizationService } from '../innovation-flow/innovation.flow.service.authorization'; import { RelationshipNotFoundException } from '@common/exceptions/relationship.not.found.exception'; -import { LicenseEngineService } from '@core/license-engine/license.engine.service'; -import { LicensePrivilege } from '@common/enums/license.privilege'; import { IAgent } from '@domain/agent/agent/agent.interface'; import { ISpaceSettings } from '@domain/space/space.settings/space.settings.interface'; import { IRoleSet } from '@domain/access/role-set'; import { RoleSetService } from '@domain/access/role-set/role.set.service'; +import { LicenseAuthorizationService } from '@domain/common/license/license.service.authorization'; @Injectable() export class CollaborationAuthorizationService { constructor( - private licenseEngineService: LicenseEngineService, private collaborationService: CollaborationService, private roleSetService: RoleSetService, private authorizationPolicyService: AuthorizationPolicyService, private timelineAuthorizationService: TimelineAuthorizationService, private calloutAuthorizationService: CalloutAuthorizationService, - private innovationFlowAuthorizationService: InnovationFlowAuthorizationService + private innovationFlowAuthorizationService: InnovationFlowAuthorizationService, + private licenseAuthorizationService: LicenseAuthorizationService ) {} public async applyAuthorizationPolicy( collaborationInput: ICollaboration, parentAuthorization: IAuthorizationPolicy, roleSet?: IRoleSet, - spaceSettings?: ISpaceSettings, - spaceAgent?: IAgent + spaceSettings?: ISpaceSettings ): Promise { const collaboration = await this.collaborationService.getCollaborationOrFail( @@ -59,6 +56,9 @@ export class CollaborationAuthorizationService { profile: true, }, timeline: true, + license: { + entitlements: true, + }, }, } ); @@ -79,10 +79,9 @@ export class CollaborationAuthorizationService { collaboration.authorization = await this.appendCredentialRules( collaboration.authorization, roleSet, - spaceSettings, - spaceAgent + spaceSettings ); - if (roleSet && spaceSettings && spaceAgent) { + if (roleSet && spaceSettings) { collaboration.authorization = await this.appendCredentialRulesForContributors( collaboration.authorization, @@ -92,8 +91,7 @@ export class CollaborationAuthorizationService { collaboration.authorization = await this.appendPrivilegeRules( collaboration.authorization, - spaceSettings, - spaceAgent + spaceSettings ); } updatedAuthorizations.push(collaboration.authorization); @@ -117,7 +115,9 @@ export class CollaborationAuthorizationService { if ( !collaboration.callouts || !collaboration.innovationFlow || - !collaboration.innovationFlow.profile + !collaboration.innovationFlow.profile || + !collaboration.license || + !collaboration.license.entitlements ) { throw new RelationshipNotFoundException( `Unable to load child entities for collaboration authorization children: ${collaboration.id}`, @@ -137,6 +137,13 @@ export class CollaborationAuthorizationService { updatedAuthorizations.push(...updatedCalloutAuthorizations); } + const licenseAuthorization = + this.licenseAuthorizationService.applyAuthorizationPolicy( + collaboration.license, + collaboration.authorization + ); + updatedAuthorizations.push(...licenseAuthorization); + // Extend with contributor rules + then send into apply const clonedAuthorization = this.authorizationPolicyService.cloneAuthorizationPolicy( @@ -216,31 +223,25 @@ export class CollaborationAuthorizationService { if (!roleSet || !spaceSettings || !spaceAgent) { return authorization; } - const saveAsTemplateEnabled = - await this.licenseEngineService.isAccessGranted( - LicensePrivilege.SPACE_SAVE_AS_TEMPLATE, - spaceAgent - ); - if (saveAsTemplateEnabled) { - const adminCriterias = await this.roleSetService.getCredentialsForRole( - roleSet, - CommunityRoleType.ADMIN, - spaceSettings + + const adminCriterias = await this.roleSetService.getCredentialsForRole( + roleSet, + CommunityRoleType.ADMIN, + spaceSettings + ); + adminCriterias.push({ + type: AuthorizationCredential.GLOBAL_ADMIN, + resourceID: '', + }); + const saveAsTemplateRule = + this.authorizationPolicyService.createCredentialRule( + [AuthorizationPrivilege.SAVE_AS_TEMPLATE], + adminCriterias, + CREDENTIAL_RULE_TYPES_CALLOUT_SAVE_AS_TEMPLATE ); - adminCriterias.push({ - type: AuthorizationCredential.GLOBAL_ADMIN, - resourceID: '', - }); - const saveAsTemplateRule = - this.authorizationPolicyService.createCredentialRule( - [AuthorizationPrivilege.SAVE_AS_TEMPLATE], - adminCriterias, - CREDENTIAL_RULE_TYPES_CALLOUT_SAVE_AS_TEMPLATE - ); - saveAsTemplateRule.cascade = false; - newRules.push(saveAsTemplateRule); - } + saveAsTemplateRule.cascade = false; + newRules.push(saveAsTemplateRule); return this.authorizationPolicyService.appendCredentialAuthorizationRules( authorization, @@ -284,8 +285,7 @@ export class CollaborationAuthorizationService { private async appendPrivilegeRules( authorization: IAuthorizationPolicy, - spaceSettings: ISpaceSettings, - spaceAgent: IAgent + spaceSettings: ISpaceSettings ): Promise { const privilegeRules: AuthorizationPolicyRulePrivilege[] = []; @@ -296,18 +296,6 @@ export class CollaborationAuthorizationService { ); privilegeRules.push(createPrivilege); - const whiteboardRtEnabled = await this.licenseEngineService.isAccessGranted( - LicensePrivilege.SPACE_WHITEBOARD_MULTI_USER, - spaceAgent - ); - if (whiteboardRtEnabled) { - const createWhiteboardRtPrivilege = new AuthorizationPolicyRulePrivilege( - [AuthorizationPrivilege.CREATE_WHITEBOARD_RT], // todo - AuthorizationPrivilege.CREATE, - POLICY_RULE_COLLABORATION_WHITEBOARD_CREATE - ); - privilegeRules.push(createWhiteboardRtPrivilege); - } const collaborationSettings = spaceSettings.collaboration; if (collaborationSettings.allowMembersToCreateCallouts) { const createCalloutPrivilege = new AuthorizationPolicyRulePrivilege( @@ -317,15 +305,13 @@ export class CollaborationAuthorizationService { ); privilegeRules.push(createCalloutPrivilege); - if (whiteboardRtEnabled) { - const createWhiteboardRtContributePrivilege = - new AuthorizationPolicyRulePrivilege( - [AuthorizationPrivilege.CREATE_WHITEBOARD_RT], - AuthorizationPrivilege.CONTRIBUTE, - POLICY_RULE_COLLABORATION_WHITEBOARD_CONTRIBUTORS_CREATE - ); - privilegeRules.push(createWhiteboardRtContributePrivilege); - } + const createWhiteboardRtContributePrivilege = + new AuthorizationPolicyRulePrivilege( + [AuthorizationPrivilege.CREATE_WHITEBOARD_RT], + AuthorizationPrivilege.CONTRIBUTE, + POLICY_RULE_COLLABORATION_WHITEBOARD_CONTRIBUTORS_CREATE + ); + privilegeRules.push(createWhiteboardRtContributePrivilege); } return this.authorizationPolicyService.appendPrivilegeAuthorizationRules( diff --git a/src/domain/collaboration/collaboration/collaboration.service.license.ts b/src/domain/collaboration/collaboration/collaboration.service.license.ts new file mode 100644 index 0000000000..334c1fd79c --- /dev/null +++ b/src/domain/collaboration/collaboration/collaboration.service.license.ts @@ -0,0 +1,96 @@ +import { Inject, Injectable, LoggerService } from '@nestjs/common'; +import { LogContext } from '@common/enums'; +import { + EntityNotInitializedException, + RelationshipNotFoundException, +} from '@common/exceptions'; +import { LicenseService } from '@domain/common/license/license.service'; +import { ILicense } from '@domain/common/license/license.interface'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { CollaborationService } from './collaboration.service'; + +@Injectable() +export class CollaborationLicenseService { + constructor( + private licenseService: LicenseService, + private collaborationService: CollaborationService, + @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService + ) {} + + async applyLicensePolicy( + collaborationID: string, + parentLicense: ILicense + ): Promise { + const collaboration = + await this.collaborationService.getCollaborationOrFail(collaborationID, { + relations: { + license: { + entitlements: true, + }, + }, + }); + if (!collaboration.license || !collaboration.license.entitlements) { + throw new RelationshipNotFoundException( + `Unable to load Collaboration with entities at start of license reset: ${collaboration.id} `, + LogContext.LICENSE + ); + } + const updatedLicenses: ILicense[] = []; + + // Ensure always applying from a clean state + collaboration.license = this.licenseService.reset(collaboration.license); + + collaboration.license = await this.extendLicensePolicy( + collaboration.license, + parentLicense + ); + + updatedLicenses.push(collaboration.license); + + return updatedLicenses; + } + + private async extendLicensePolicy( + license: ILicense | undefined, + parentLicense: ILicense + ): Promise { + if ( + !license || + !license.entitlements || + !parentLicense || + !parentLicense.entitlements + ) { + throw new EntityNotInitializedException( + 'License or parent License with entitlements not found for RoleSet', + LogContext.LICENSE + ); + } + const parentEntitlements = parentLicense.entitlements; + for (const entitlement of license.entitlements) { + switch (entitlement.type) { + case LicenseEntitlementType.SPACE_FLAG_SAVE_AS_TEMPLATE: + this.licenseService.findAndCopyParentEntitlement( + entitlement, + parentEntitlements + ); + break; + + case LicenseEntitlementType.SPACE_FLAG_WHITEBOARD_MULTI_USER: + this.licenseService.findAndCopyParentEntitlement( + entitlement, + parentEntitlements + ); + break; + + default: + throw new EntityNotInitializedException( + `Unknown entitlement type for Collaboration: ${entitlement.type}`, + LogContext.LICENSE + ); + } + } + + return license; + } +} diff --git a/src/domain/collaboration/collaboration/collaboration.service.ts b/src/domain/collaboration/collaboration/collaboration.service.ts index a57c9656ad..3ef8da6faf 100644 --- a/src/domain/collaboration/collaboration/collaboration.service.ts +++ b/src/domain/collaboration/collaboration/collaboration.service.ts @@ -54,6 +54,10 @@ import { Callout } from '@domain/collaboration/callout'; import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type'; import { CreateInnovationFlowInput } from '../innovation-flow/dto/innovation.flow.dto.create'; import { IRoleSet } from '@domain/access/role-set'; +import { LicenseService } from '@domain/common/license/license.service'; +import { LicenseType } from '@common/enums/license.type'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; +import { LicenseEntitlementDataType } from '@common/enums/license.entitlement.data.type'; @Injectable() export class CollaborationService { @@ -70,7 +74,8 @@ export class CollaborationService { @InjectEntityManager('default') private entityManager: EntityManager, private timelineService: TimelineService, - private calloutGroupsService: CalloutGroupsService + private calloutGroupsService: CalloutGroupsService, + private licenseService: LicenseService ) {} async createCollaboration( @@ -126,6 +131,24 @@ export class CollaborationService { groupTagsetTemplateInput ); + collaboration.license = await this.licenseService.createLicense({ + type: LicenseType.COLLABORATION, + entitlements: [ + { + type: LicenseEntitlementType.SPACE_FLAG_SAVE_AS_TEMPLATE, + dataType: LicenseEntitlementDataType.FLAG, + limit: 0, + enabled: false, + }, + { + type: LicenseEntitlementType.SPACE_FLAG_WHITEBOARD_MULTI_USER, + dataType: LicenseEntitlementDataType.FLAG, + limit: 0, + enabled: false, + }, + ], + }); + // save the tagset template so can use it in the innovation flow as a template for it's tags await this.tagsetTemplateSetService.save(collaboration.tagsetTemplateSet); @@ -331,9 +354,9 @@ export class CollaborationService { return []; } - public async deleteCollaboration( + public async deleteCollaborationOrFail( collaborationID: string - ): Promise { + ): Promise { // Note need to load it in with all contained entities so can remove fully const collaboration = await this.getCollaborationOrFail(collaborationID, { relations: { @@ -341,13 +364,15 @@ export class CollaborationService { timeline: true, innovationFlow: true, authorization: true, + license: true, }, }); if ( !collaboration.callouts || !collaboration.innovationFlow || - !collaboration.authorization + !collaboration.authorization || + !collaboration.license ) throw new RelationshipNotFoundException( `Unable to remove Collaboration: missing child entities ${collaboration.id} `, @@ -368,6 +393,7 @@ export class CollaborationService { await this.innovationFlowService.deleteInnovationFlow( collaboration.innovationFlow.id ); + await this.licenseService.removeLicenseOrFail(collaboration.license.id); return await this.collaborationRepository.remove( collaboration as Collaboration diff --git a/src/domain/common/license-entitlement/dto/license.entitlement.dto.create.ts b/src/domain/common/license-entitlement/dto/license.entitlement.dto.create.ts new file mode 100644 index 0000000000..3ecc9bc0da --- /dev/null +++ b/src/domain/common/license-entitlement/dto/license.entitlement.dto.create.ts @@ -0,0 +1,9 @@ +import { LicenseEntitlementDataType } from '@common/enums/license.entitlement.data.type'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; + +export class CreateLicenseEntitlementInput { + type!: LicenseEntitlementType; + dataType!: LicenseEntitlementDataType; + limit!: number; + enabled!: boolean; +} diff --git a/src/domain/common/license-entitlement/license.entitlement.entity.ts b/src/domain/common/license-entitlement/license.entitlement.entity.ts new file mode 100644 index 0000000000..9612d29972 --- /dev/null +++ b/src/domain/common/license-entitlement/license.entitlement.entity.ts @@ -0,0 +1,32 @@ +import { Column, Entity, ManyToOne } from 'typeorm'; +import { ILicenseEntitlement } from './license.entitlement.interface'; +import { BaseAlkemioEntity } from '../entity/base-entity'; +import { License } from '../license/license.entity'; +import { ENUM_LENGTH } from '@common/constants/entity.field.length.constants'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; +import { LicenseEntitlementDataType } from '@common/enums/license.entitlement.data.type'; + +@Entity() +export class LicenseEntitlement + extends BaseAlkemioEntity + implements ILicenseEntitlement +{ + @ManyToOne(() => License, license => license.entitlements, { + eager: false, + cascade: false, + onDelete: 'CASCADE', + }) + license?: License; + + @Column('varchar', { length: ENUM_LENGTH, nullable: false }) + type!: LicenseEntitlementType; + + @Column('varchar', { length: ENUM_LENGTH, nullable: false }) + dataType!: LicenseEntitlementDataType; + + @Column('int', { nullable: false }) + limit!: number; + + @Column('boolean', { nullable: false }) + enabled!: boolean; +} diff --git a/src/domain/common/license-entitlement/license.entitlement.interface.ts b/src/domain/common/license-entitlement/license.entitlement.interface.ts new file mode 100644 index 0000000000..8a6dc2a181 --- /dev/null +++ b/src/domain/common/license-entitlement/license.entitlement.interface.ts @@ -0,0 +1,35 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { IBaseAlkemio } from '../entity/base-entity'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; +import { LicenseEntitlementDataType } from '@common/enums/license.entitlement.data.type'; +import { ILicense } from '../license/license.interface'; + +@ObjectType('LicenseEntitlement') +export abstract class ILicenseEntitlement extends IBaseAlkemio { + @Field(() => LicenseEntitlementType, { + nullable: false, + description: + 'Type of the entitlement, e.g. Space, Whiteboard contributors etc.', + }) + type!: LicenseEntitlementType; + + @Field(() => LicenseEntitlementDataType, { + nullable: false, + description: 'Data type of the entitlement, e.g. Limit, Feature flag etc.', + }) + dataType!: LicenseEntitlementDataType; + + @Field(() => Number, { + nullable: false, + description: 'Limit of the entitlement', + }) + limit!: number; + + @Field(() => Boolean, { + nullable: false, + description: 'If the Entitlement is enabled', + }) + enabled!: boolean; + + license?: ILicense; +} diff --git a/src/domain/common/license-entitlement/license.entitlement.module.ts b/src/domain/common/license-entitlement/license.entitlement.module.ts new file mode 100644 index 0000000000..aa2730268b --- /dev/null +++ b/src/domain/common/license-entitlement/license.entitlement.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { LicenseEntitlement } from './license.entitlement.entity'; +import { LicenseEntitlementService } from './license.entitlement.service'; +import { LicenseEntitlementUsageModule } from '@services/infrastructure/license-entitlement-usage/license.entitlement.usage.module'; +import { LicenseEntitlementResolverFields } from './license.entitlement.resolver.fields'; + +@Module({ + imports: [ + LicenseEntitlementUsageModule, + TypeOrmModule.forFeature([LicenseEntitlement]), + ], + providers: [LicenseEntitlementService, LicenseEntitlementResolverFields], + exports: [LicenseEntitlementService], +}) +export class LicenseEntitlementModule {} diff --git a/src/domain/common/license-entitlement/license.entitlement.resolver.fields.ts b/src/domain/common/license-entitlement/license.entitlement.resolver.fields.ts new file mode 100644 index 0000000000..e8b3693f98 --- /dev/null +++ b/src/domain/common/license-entitlement/license.entitlement.resolver.fields.ts @@ -0,0 +1,32 @@ +import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; +import { ILicenseEntitlement } from './license.entitlement.interface'; +import { LicenseEntitlementService } from './license.entitlement.service'; + +@Resolver(() => ILicenseEntitlement) +export class LicenseEntitlementResolverFields { + constructor(private licenseEntitlementService: LicenseEntitlementService) {} + + @ResolveField('isAvailable', () => Boolean, { + nullable: false, + description: 'Whether the specified entitlement is available.', + }) + async isAvailable( + @Parent() licenseEntitlement: ILicenseEntitlement + ): Promise { + return await this.licenseEntitlementService.isEntitlementAvailable( + licenseEntitlement.id + ); + } + + @ResolveField('usage', () => Number, { + nullable: false, + description: 'The amount of the spcified entitlement used.', + }) + async usage( + @Parent() licenseEntitlement: ILicenseEntitlement + ): Promise { + return await this.licenseEntitlementService.getEntitlementUsage( + licenseEntitlement.id + ); + } +} diff --git a/src/domain/common/license-entitlement/license.entitlement.service.ts b/src/domain/common/license-entitlement/license.entitlement.service.ts new file mode 100644 index 0000000000..602e01568a --- /dev/null +++ b/src/domain/common/license-entitlement/license.entitlement.service.ts @@ -0,0 +1,188 @@ +import { Inject, Injectable, LoggerService } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { FindOneOptions, Repository } from 'typeorm'; +import { + EntityNotFoundException, + RelationshipNotFoundException, +} from '@common/exceptions'; +import { LogContext } from '@common/enums'; +import { CreateLicenseEntitlementInput } from './dto/license.entitlement.dto.create'; +import { LicenseEntitlement } from './license.entitlement.entity'; +import { ILicenseEntitlement } from './license.entitlement.interface'; +import { LicenseType } from '@common/enums/license.type'; +import { LicenseEntitlementUsageService } from '@services/infrastructure/license-entitlement-usage/license.entitlement.usage.service'; +import { ILicense } from '../license/license.interface'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { LicenseEntitlementDataType } from '@common/enums/license.entitlement.data.type'; +import { LicenseEntitlementNotSupportedException } from '@common/exceptions/license.entitlement.not.supported'; + +@Injectable() +export class LicenseEntitlementService { + constructor( + private licenseEntitlementUsageService: LicenseEntitlementUsageService, + @Inject(WINSTON_MODULE_NEST_PROVIDER) + private readonly logger: LoggerService, + @InjectRepository(LicenseEntitlement) + private entitlementRepository: Repository + ) {} + + public createEntitlement( + entitlementInput: CreateLicenseEntitlementInput + ): ILicenseEntitlement { + const entitlement = new LicenseEntitlement(); + entitlement.limit = entitlementInput.limit; + entitlement.enabled = entitlementInput.enabled; + entitlement.type = entitlementInput.type; + entitlement.dataType = entitlementInput.dataType; + + return entitlement; + } + + async getEntitlementOrFail( + entitlementID: string, + options?: FindOneOptions + ): Promise { + const entitlement = await this.entitlementRepository.findOne({ + where: { id: entitlementID }, + ...options, + }); + if (!entitlement) + throw new EntityNotFoundException( + `Not able to locate entitlement with the specified ID: ${entitlementID}`, + LogContext.SPACES + ); + return entitlement; + } + + async deleteEntitlementOrFail( + entitlementID: string + ): Promise { + const entitlement = await this.getEntitlementOrFail(entitlementID); + + const { id } = entitlement; + const result = await this.entitlementRepository.remove( + entitlement as LicenseEntitlement + ); + return { + ...result, + id, + }; + } + + public async saveEntitlement( + entitlement: ILicenseEntitlement + ): Promise { + return await this.entitlementRepository.save(entitlement); + } + + public reset(entitlement: ILicenseEntitlement): ILicenseEntitlement { + entitlement.limit = 0; + entitlement.enabled = false; + return entitlement; + } + + private async getLicenseAndEntitlementOrFail( + licenseEntitlementID: string + ): Promise<{ licenseEntitlement: ILicenseEntitlement; license: ILicense }> { + const licenseEntitlement = await this.getEntitlementOrFail( + licenseEntitlementID, + { + relations: { + license: true, + }, + } + ); + if (!licenseEntitlement || !licenseEntitlement.license) { + throw new RelationshipNotFoundException( + `Unable to load license for entitlement: ${licenseEntitlementID}`, + LogContext.LICENSE + ); + } + const license = licenseEntitlement.license; + + return { licenseEntitlement, license }; + } + + public async getEntitlementUsage( + licenseEntitlementID: string + ): Promise { + const { license, licenseEntitlement } = + await this.getLicenseAndEntitlementOrFail(licenseEntitlementID); + + if (licenseEntitlement.dataType === LicenseEntitlementDataType.FLAG) { + return -1; + } + return await this.getEntitlementUsageUsingEntities( + license, + licenseEntitlement + ); + } + + public async getEntitlementUsageUsingEntities( + license: ILicense, + licenseEntitlement: ILicenseEntitlement + ): Promise { + switch (license.type) { + case LicenseType.ACCOUNT: + return await this.licenseEntitlementUsageService.getEntitlementUsageForAccount( + license.id, + licenseEntitlement.type + ); + default: + throw new EntityNotFoundException( + `Unexpected License Type encountered: ${license.type}`, + LogContext.LICENSE + ); + } + } + + public async isEntitlementAvailable( + licenseEntitlementID: string + ): Promise { + const { license, licenseEntitlement } = + await this.getLicenseAndEntitlementOrFail(licenseEntitlementID); + return this.isEntitlementAvailableUsingEntities( + license, + licenseEntitlement + ); + } + + public async isEntitlementAvailableUsingEntities( + license: ILicense, + licenseEntitlement: ILicenseEntitlement + ): Promise { + if (licenseEntitlement.dataType === LicenseEntitlementDataType.FLAG) { + return licenseEntitlement.enabled; + } + + const entitlementLimit = licenseEntitlement.limit; + let entitlementsUsed = 999; + switch (license.type) { + case LicenseType.ACCOUNT: + entitlementsUsed = await this.getEntitlementUsageUsingEntities( + license, + licenseEntitlement + ); + break; + case LicenseType.SPACE: + case LicenseType.COLLABORATION: + case LicenseType.ROLESET: + case LicenseType.WHITEBOARD: + throw new LicenseEntitlementNotSupportedException( + `License Type ${license.type} is not supported for entitlement of type ${licenseEntitlement.type}`, + LogContext.LICENSE + ); + default: + throw new EntityNotFoundException( + `Unexpected License Type encountered when checking availability: ${license.type}`, + LogContext.LICENSE + ); + } + this.logger.verbose?.( + `Checking entitlement usage on license (${license.id} for entitlement ${licenseEntitlement.type}): ${entitlementsUsed} of ${entitlementLimit}`, + LogContext.LICENSE + ); + + return entitlementsUsed < entitlementLimit; + } +} diff --git a/src/domain/common/license/dto/license.dto.create.ts b/src/domain/common/license/dto/license.dto.create.ts new file mode 100644 index 0000000000..49c5b6d018 --- /dev/null +++ b/src/domain/common/license/dto/license.dto.create.ts @@ -0,0 +1,8 @@ +import { LicenseType } from '@common/enums/license.type'; +import { CreateLicenseEntitlementInput } from '@domain/common/license-entitlement/dto/license.entitlement.dto.create'; + +export class CreateLicenseInput { + type!: LicenseType; + + entitlements!: CreateLicenseEntitlementInput[]; +} diff --git a/src/domain/common/license/license.entity.ts b/src/domain/common/license/license.entity.ts new file mode 100644 index 0000000000..5cba309b3c --- /dev/null +++ b/src/domain/common/license/license.entity.ts @@ -0,0 +1,18 @@ +import { Column, Entity, OneToMany } from 'typeorm'; +import { ILicense } from './license.interface'; +import { ENUM_LENGTH } from '@common/constants'; +import { AuthorizableEntity } from '../entity/authorizable-entity'; +import { LicenseEntitlement } from '../license-entitlement/license.entitlement.entity'; +import { LicenseType } from '@common/enums/license.type'; + +@Entity() +export class License extends AuthorizableEntity implements ILicense { + @OneToMany(() => LicenseEntitlement, entitlement => entitlement.license, { + eager: false, + cascade: true, + }) + entitlements?: LicenseEntitlement[]; + + @Column('varchar', { length: ENUM_LENGTH, nullable: false }) + type!: LicenseType; +} diff --git a/src/domain/common/license/license.interface.ts b/src/domain/common/license/license.interface.ts new file mode 100644 index 0000000000..fba2d47c5b --- /dev/null +++ b/src/domain/common/license/license.interface.ts @@ -0,0 +1,15 @@ +import { LicenseType } from '@common/enums/license.type'; +import { Field, ObjectType } from '@nestjs/graphql'; +import { IAuthorizable } from '../entity/authorizable-entity'; +import { ILicenseEntitlement } from '../license-entitlement/license.entitlement.interface'; + +@ObjectType('License') +export abstract class ILicense extends IAuthorizable { + @Field(() => LicenseType, { + nullable: true, + description: 'The type of entity that this License is being used with.', + }) + type!: LicenseType; + + entitlements?: ILicenseEntitlement[]; +} diff --git a/src/domain/common/license/license.module.ts b/src/domain/common/license/license.module.ts new file mode 100644 index 0000000000..d1c080ade7 --- /dev/null +++ b/src/domain/common/license/license.module.ts @@ -0,0 +1,25 @@ +import { AuthorizationModule } from '@core/authorization/authorization.module'; +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { License } from './license.entity'; +import { LicenseResolverFields } from './license.resolver.fields'; +import { LicenseService } from './license.service'; +import { LicenseEntitlementModule } from '../license-entitlement/license.entitlement.module'; +import { LicenseAuthorizationService } from './license.service.authorization'; +import { AuthorizationPolicyModule } from '../authorization-policy/authorization.policy.module'; + +@Module({ + imports: [ + LicenseEntitlementModule, + AuthorizationModule, + AuthorizationPolicyModule, + TypeOrmModule.forFeature([License]), + ], + providers: [ + LicenseService, + LicenseResolverFields, + LicenseAuthorizationService, + ], + exports: [LicenseService, LicenseAuthorizationService], +}) +export class LicenseModule {} diff --git a/src/domain/common/license/license.resolver.fields.ts b/src/domain/common/license/license.resolver.fields.ts new file mode 100644 index 0000000000..ee5e1d99eb --- /dev/null +++ b/src/domain/common/license/license.resolver.fields.ts @@ -0,0 +1,20 @@ +import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; +import { ILicense } from './license.interface'; +import { LicenseService } from './license.service'; +import { ILicenseEntitlement } from '../license-entitlement/license.entitlement.interface'; + +@Resolver(() => ILicense) +export class LicenseResolverFields { + constructor(private licenseService: LicenseService) {} + + @ResolveField('entitlements', () => [ILicenseEntitlement], { + nullable: false, + description: + 'The set of Entitlements associated with the License applicable to this entity.', + }) + async entitlements( + @Parent() license: ILicense + ): Promise { + return await this.licenseService.getEntitlements(license); + } +} diff --git a/src/domain/common/license/license.service.authorization.ts b/src/domain/common/license/license.service.authorization.ts new file mode 100644 index 0000000000..60dc623116 --- /dev/null +++ b/src/domain/common/license/license.service.authorization.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; +import { IAuthorizationPolicy } from '@domain/common/authorization-policy/authorization.policy.interface'; +import { ILicense } from './license.interface'; + +@Injectable() +export class LicenseAuthorizationService { + constructor(private authorizationPolicyService: AuthorizationPolicyService) {} + + applyAuthorizationPolicy( + license: ILicense, + parentAuthorization: IAuthorizationPolicy | undefined + ): IAuthorizationPolicy[] { + const updatedAuthorizations: IAuthorizationPolicy[] = []; + + license.authorization = + this.authorizationPolicyService.inheritParentAuthorization( + license.authorization, + parentAuthorization + ); + updatedAuthorizations.push(license.authorization); + + return updatedAuthorizations; + } +} diff --git a/src/domain/common/license/license.service.ts b/src/domain/common/license/license.service.ts new file mode 100644 index 0000000000..845200ca25 --- /dev/null +++ b/src/domain/common/license/license.service.ts @@ -0,0 +1,226 @@ +import { Inject, Injectable, LoggerService } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { ILicense } from './license.interface'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { FindOneOptions, Repository } from 'typeorm'; +import { License } from './license.entity'; +import { CreateLicenseInput } from './dto/license.dto.create'; +import { AuthorizationPolicy } from '../authorization-policy/authorization.policy.entity'; +import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type'; +import { LicenseEntitlementService } from '../license-entitlement/license.entitlement.service'; +import { EntityNotFoundException } from '@common/exceptions/entity.not.found.exception'; +import { LogContext } from '@common/enums/logging.context'; +import { RelationshipNotFoundException } from '@common/exceptions/relationship.not.found.exception'; +import { AuthorizationPolicyService } from '../authorization-policy/authorization.policy.service'; +import { ILicenseEntitlement } from '../license-entitlement/license.entitlement.interface'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; +import { LicenseEntitlementNotAvailableException } from '@common/exceptions/license.entitlement.not.available.exception'; + +@Injectable() +export class LicenseService { + constructor( + private licenseEntitlementService: LicenseEntitlementService, + private authorizationPolicyService: AuthorizationPolicyService, + @InjectRepository(License) + private licenseRepository: Repository, + @Inject(WINSTON_MODULE_NEST_PROVIDER) + private readonly logger: LoggerService + ) {} + + async createLicense(licenseData: CreateLicenseInput): Promise { + const license: ILicense = License.create(licenseData); + license.authorization = new AuthorizationPolicy( + AuthorizationPolicyType.LICENSE + ); + license.entitlements = []; + + for (const entitlementData of licenseData.entitlements) { + const entitlement = + this.licenseEntitlementService.createEntitlement(entitlementData); + license.entitlements.push(entitlement); + } + + return license; + } + + async getLicenseOrFail( + licenseID: string, + options?: FindOneOptions + ): Promise { + const license = await this.licenseRepository.findOne({ + where: { id: licenseID }, + ...options, + }); + if (!license) + throw new EntityNotFoundException( + `Unable to find License with ID: ${licenseID}`, + LogContext.LICENSE + ); + return license; + } + + async removeLicenseOrFail(licenseID: string): Promise { + // Note need to load it in with all contained entities so can remove fully + const license = await this.getLicenseOrFail(licenseID, { + relations: { + entitlements: true, + }, + }); + const entitlements = this.getEntitlementsFromLicenseOrFail(license); + + for (const entitlement of entitlements) { + await this.licenseEntitlementService.deleteEntitlementOrFail( + entitlement.id + ); + } + + if (license.authorization) + await this.authorizationPolicyService.delete(license.authorization); + + const deletedLicense = await this.licenseRepository.remove( + license as License + ); + deletedLicense.id = license.id; + return deletedLicense; + } + + async save(license: ILicense): Promise { + return this.licenseRepository.save(license); + } + + async saveAll(licenses: ILicense[]): Promise { + this.logger.verbose?.( + `Saving ${licenses.length} licenses`, + LogContext.LICENSE + ); + await this.licenseRepository.save(licenses, { + chunk: 100, + }); + } + + public async getEntitlements( + licenseInput: ILicense + ): Promise { + let license = licenseInput; + if (!license.entitlements) { + license = await this.getLicenseOrFail(licenseInput.id, { + relations: { + entitlements: true, + }, + }); + } + return this.getEntitlementsFromLicenseOrFail(license); + } + + public reset(license: ILicense): ILicense { + const entitlements = this.getEntitlementsFromLicenseOrFail(license); + for (const entitlement of entitlements) { + this.licenseEntitlementService.reset(entitlement); + } + return license; + } + + public getEntitlementLimit( + license: ILicense | undefined, + entitlementType: LicenseEntitlementType + ): number { + const entitlements = this.getEntitlementsFromLicenseOrFail(license); + const entitlement = this.getEntitlementFromEntitlementsOrFail( + entitlements, + entitlementType + ); + return entitlement.limit; + } + + public async isEntitlementAvailable( + license: ILicense, + entitlementType: LicenseEntitlementType + ): Promise { + const entitlements = this.getEntitlementsFromLicenseOrFail(license); + const entitlement = this.getEntitlementFromEntitlementsOrFail( + entitlements, + entitlementType + ); + return await this.licenseEntitlementService.isEntitlementAvailableUsingEntities( + license, + entitlement + ); + } + + public isEntitlementEnabled( + license: ILicense | undefined, + entitlementType: LicenseEntitlementType + ): boolean { + const entitlements = this.getEntitlementsFromLicenseOrFail(license); + const entitlement = this.getEntitlementFromEntitlementsOrFail( + entitlements, + entitlementType + ); + return entitlement.enabled; + } + + public isEntitlementEnabledOrFail( + license: ILicense | undefined, + entitlementType: LicenseEntitlementType + ): void { + const enabled = this.isEntitlementEnabled(license, entitlementType); + if (!enabled) { + throw new LicenseEntitlementNotAvailableException( + `Entitlement ${entitlementType} is not available for License: ${license?.id}`, + LogContext.LICENSE + ); + } + } + + public findAndCopyParentEntitlement( + childEntitlement: ILicenseEntitlement, + parentEntitlements: ILicenseEntitlement[] + ): void { + const parentEntitlement = parentEntitlements.find( + e => e.type === childEntitlement.type + ); + if (!parentEntitlement) { + throw new RelationshipNotFoundException( + `Parent entitlement not found: ${childEntitlement.type}`, + LogContext.LICENSE + ); + } + childEntitlement.limit = parentEntitlement.limit; + childEntitlement.enabled = parentEntitlement.enabled; + childEntitlement.dataType = parentEntitlement.dataType; + } + + private getEntitlementsFromLicenseOrFail( + license: ILicense | undefined + ): ILicenseEntitlement[] | never { + if (!license) { + throw new RelationshipNotFoundException( + 'Unable to load Entitlements for License', + LogContext.LICENSE + ); + } + if (!license.entitlements) { + throw new RelationshipNotFoundException( + `Unable to load Entitlements for License: ${license.id}`, + LogContext.LICENSE + ); + } + return license.entitlements; + } + + private getEntitlementFromEntitlementsOrFail( + entitlements: ILicenseEntitlement[], + type: LicenseEntitlementType + ): ILicenseEntitlement { + const entitlement = entitlements.find( + entitlement => entitlement.type === type + ); + if (!entitlement) { + throw new RelationshipNotFoundException( + `Unable to find entitlement of type ${type} in Entitlements for License: ${JSON.stringify(entitlements)}`, + LogContext.LICENSE + ); + } + return entitlement; + } +} diff --git a/src/domain/common/whiteboard/whiteboard.module.ts b/src/domain/common/whiteboard/whiteboard.module.ts index d362d251c0..925607fe71 100644 --- a/src/domain/common/whiteboard/whiteboard.module.ts +++ b/src/domain/common/whiteboard/whiteboard.module.ts @@ -13,14 +13,14 @@ import { WhiteboardService } from './whiteboard.service'; import { WhiteboardAuthorizationService } from './whiteboard.service.authorization'; import { StorageBucketModule } from '@domain/storage/storage-bucket/storage.bucket.module'; import { ProfileDocumentsModule } from '@domain/profile-documents/profile.documents.module'; -import { LicenseEngineModule } from '@core/license-engine/license.engine.module'; +import { LicenseModule } from '../license/license.module'; @Module({ imports: [ EntityResolverModule, AuthorizationModule, AuthorizationPolicyModule, - LicenseEngineModule, + LicenseModule, VisualModule, ProfileModule, UserModule, diff --git a/src/domain/common/whiteboard/whiteboard.service.ts b/src/domain/common/whiteboard/whiteboard.service.ts index 0018c6c49a..9ef5f70412 100644 --- a/src/domain/common/whiteboard/whiteboard.service.ts +++ b/src/domain/common/whiteboard/whiteboard.service.ts @@ -22,9 +22,9 @@ import { Whiteboard } from './whiteboard.entity'; import { IWhiteboard } from './whiteboard.interface'; import { CreateWhiteboardInput } from './dto/whiteboard.dto.create'; import { UpdateWhiteboardInput } from './dto/whiteboard.dto.update'; -import { LicenseEngineService } from '@core/license-engine/license.engine.service'; -import { LicensePrivilege } from '@common/enums/license.privilege'; import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type'; +import { LicenseService } from '../license/license.service'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; @Injectable() export class WhiteboardService { @@ -32,10 +32,10 @@ export class WhiteboardService { @InjectRepository(Whiteboard) private whiteboardRepository: Repository, private authorizationPolicyService: AuthorizationPolicyService, - private licenseEngineService: LicenseEngineService, private profileService: ProfileService, private profileDocumentsService: ProfileDocumentsService, - private communityResolverService: CommunityResolverService + private communityResolverService: CommunityResolverService, + private licenseService: LicenseService ) {} async createWhiteboard( @@ -177,22 +177,16 @@ export class WhiteboardService { return this.save(whiteboard); } - async isMultiUser(whiteboardId: string): Promise { - const community = - await this.communityResolverService.getCommunityFromWhiteboardOrFail( + public async isMultiUser(whiteboardId: string): Promise { + const license = + await this.communityResolverService.getCollaborationLicenseFromWhiteboardOrFail( whiteboardId ); - const levelZeroSpaceAgent = - await this.communityResolverService.getLevelZeroSpaceAgentForCommunityOrFail( - community.id - ); - const enabled = await this.licenseEngineService.isAccessGranted( - LicensePrivilege.SPACE_WHITEBOARD_MULTI_USER, - levelZeroSpaceAgent + return this.licenseService.isEntitlementEnabled( + license, + LicenseEntitlementType.SPACE_FLAG_WHITEBOARD_MULTI_USER ); - - return enabled; } public async getProfile( diff --git a/src/domain/community/community/community.module.ts b/src/domain/community/community/community.module.ts index 2da41a5ac1..deb7b5e8b6 100644 --- a/src/domain/community/community/community.module.ts +++ b/src/domain/community/community/community.module.ts @@ -13,7 +13,6 @@ import { CommunityService } from './community.service'; import { CommunityAuthorizationService } from './community.service.authorization'; import { StorageAggregatorResolverModule } from '@services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.module'; import { CommunityGuidelinesModule } from '../community-guidelines/community.guidelines.module'; -import { LicenseEngineModule } from '@core/license-engine/license.engine.module'; import { EntityResolverModule } from '@services/infrastructure/entity-resolver/entity.resolver.module'; import { VirtualContributorModule } from '../virtual-contributor/virtual.contributor.module'; import { RoleSetModule } from '@domain/access/role-set/role.set.module'; @@ -28,7 +27,6 @@ import { RoleSetModule } from '@domain/access/role-set/role.set.module'; RoleSetModule, CommunicationModule, CommunityGuidelinesModule, - LicenseEngineModule, AgentModule, StorageAggregatorResolverModule, VirtualContributorModule, diff --git a/src/domain/community/community/community.service.authorization.ts b/src/domain/community/community/community.service.authorization.ts index a5f43b6eec..52761eb5d6 100644 --- a/src/domain/community/community/community.service.authorization.ts +++ b/src/domain/community/community/community.service.authorization.ts @@ -10,31 +10,19 @@ import { IAuthorizationPolicy } from '@domain/common/authorization-policy/author import { UserGroupAuthorizationService } from '../user-group/user-group.service.authorization'; import { CommunicationAuthorizationService } from '@domain/communication/communication/communication.service.authorization'; import { IAuthorizationPolicyRuleCredential } from '@core/authorization/authorization.policy.rule.credential.interface'; -import { - CREDENTIAL_RULE_TYPES_ACCESS_VIRTUAL_CONTRIBUTORS, - CREDENTIAL_RULE_TYPES_COMMUNITY_READ_GLOBAL_REGISTERED, -} from '@common/constants'; +import { CREDENTIAL_RULE_TYPES_COMMUNITY_READ_GLOBAL_REGISTERED } from '@common/constants'; import { RelationshipNotFoundException } from '@common/exceptions/relationship.not.found.exception'; import { CommunityGuidelinesAuthorizationService } from '../community-guidelines/community.guidelines.service.authorization'; -import { ICredentialDefinition } from '@domain/agent/credential/credential.definition.interface'; -import { CommunityRoleType } from '@common/enums/community.role'; -import { LicenseEngineService } from '@core/license-engine/license.engine.service'; -import { LicensePrivilege } from '@common/enums/license.privilege'; -import { IAgent } from '@domain/agent'; import { ISpaceSettings } from '@domain/space/space.settings/space.settings.interface'; import { RoleSetAuthorizationService } from '@domain/access/role-set/role.set.service.authorization'; -import { RoleSetService } from '@domain/access/role-set/role.set.service'; -import { IRoleSet } from '@domain/access/role-set'; @Injectable() export class CommunityAuthorizationService { constructor( - private licenseEngineService: LicenseEngineService, private communityService: CommunityService, private authorizationPolicyService: AuthorizationPolicyService, private userGroupAuthorizationService: UserGroupAuthorizationService, private communicationAuthorizationService: CommunicationAuthorizationService, - private roleSetService: RoleSetService, private roleSetAuthorizationService: RoleSetAuthorizationService, private communityGuidelinesAuthorizationService: CommunityGuidelinesAuthorizationService ) {} @@ -42,7 +30,6 @@ export class CommunityAuthorizationService { async applyAuthorizationPolicy( communityID: string, parentAuthorization: IAuthorizationPolicy, - levelZeroSpaceAgent: IAgent, spaceSettings: ISpaceSettings, spaceMembershipAllowed: boolean, isSubspace: boolean @@ -83,10 +70,7 @@ export class CommunityAuthorizationService { community.authorization = await this.extendAuthorizationPolicy( community.authorization, - parentAuthorization?.anonymousReadAccess, - levelZeroSpaceAgent, - community.roleSet, - spaceSettings + parentAuthorization?.anonymousReadAccess ); // always false @@ -135,10 +119,7 @@ export class CommunityAuthorizationService { private async extendAuthorizationPolicy( authorization: IAuthorizationPolicy | undefined, - allowGlobalRegisteredReadAccess: boolean | undefined, - levelZeroSpaceAgent: IAgent, - roleSet: IRoleSet, - spaceSettings: ISpaceSettings + allowGlobalRegisteredReadAccess: boolean | undefined ): Promise { const newRules: IAuthorizationPolicyRuleCredential[] = []; @@ -153,32 +134,6 @@ export class CommunityAuthorizationService { newRules.push(globalRegistered); } - const accessVirtualContributors = - await this.licenseEngineService.isAccessGranted( - LicensePrivilege.SPACE_VIRTUAL_CONTRIBUTOR_ACCESS, - levelZeroSpaceAgent - ); - if (accessVirtualContributors) { - const criterias: ICredentialDefinition[] = - await this.roleSetService.getCredentialsForRoleWithParents( - roleSet, - CommunityRoleType.ADMIN, - spaceSettings - ); - criterias.push({ - type: AuthorizationCredential.GLOBAL_ADMIN, - resourceID: '', - }); - const accessVCsRule = - this.authorizationPolicyService.createCredentialRule( - [AuthorizationPrivilege.ACCESS_VIRTUAL_CONTRIBUTOR], - criterias, - CREDENTIAL_RULE_TYPES_ACCESS_VIRTUAL_CONTRIBUTORS - ); - accessVCsRule.cascade = true; // TODO: ideally make this not cascade so it is more specific - newRules.push(accessVCsRule); - } - // const updatedAuthorization = this.authorizationPolicyService.appendCredentialAuthorizationRules( diff --git a/src/domain/community/community/community.service.ts b/src/domain/community/community/community.service.ts index 039c4bcd64..0ca9c74352 100644 --- a/src/domain/community/community/community.service.ts +++ b/src/domain/community/community/community.service.ts @@ -153,7 +153,7 @@ export class CommunityService { return community; } - async removeCommunity(communityID: string): Promise { + async removeCommunityOrFail(communityID: string): Promise { // Note need to load it in with all contained entities so can remove fully const community = await this.getCommunityOrFail(communityID, { relations: { @@ -192,7 +192,7 @@ export class CommunityService { community.communication.id ); - await this.roleSetService.removeRoleSet(community.roleSet.id); + await this.roleSetService.removeRoleSetOrFail(community.roleSet.id); await this.communityGuidelinesService.deleteCommunityGuidelines( community.guidelines.id diff --git a/src/domain/community/organization/organization.module.ts b/src/domain/community/organization/organization.module.ts index 151fd34ba3..084909997c 100644 --- a/src/domain/community/organization/organization.module.ts +++ b/src/domain/community/organization/organization.module.ts @@ -18,7 +18,6 @@ import { PreferenceModule } from '@domain/common/preference'; import { PreferenceSetModule } from '@domain/common/preference-set/preference.set.module'; import { PlatformAuthorizationPolicyModule } from '@src/platform/authorization/platform.authorization.policy.module'; import { EntityResolverModule } from '@services/infrastructure/entity-resolver/entity.resolver.module'; -import { OrganizationStorageAggregatorLoaderCreator } from '@core/dataloader/creators/loader.creators/community/organization.storage.aggregator.loader.creator'; import { StorageAggregatorModule } from '@domain/storage/storage-aggregator/storage.aggregator.module'; import { ContributorModule } from '../contributor/contributor.module'; import { OrganizationRoleModule } from '../organization-role/organization.role.module'; @@ -52,7 +51,6 @@ import { AvatarCreatorModule } from '@services/external/avatar-creator/avatar.cr OrganizationResolverQueries, OrganizationResolverMutations, OrganizationResolverFields, - OrganizationStorageAggregatorLoaderCreator, ], exports: [OrganizationService, OrganizationAuthorizationService], }) diff --git a/src/domain/community/user/user.module.ts b/src/domain/community/user/user.module.ts index f93e5a1cac..7e49d9e3dd 100644 --- a/src/domain/community/user/user.module.ts +++ b/src/domain/community/user/user.module.ts @@ -20,12 +20,7 @@ import { PlatformAuthorizationPolicyModule } from '@src/platform/authorization/p import { NotificationAdapterModule } from '@services/adapters/notification-adapter/notification.adapter.module'; import { EntityResolverModule } from '@services/infrastructure/entity-resolver/entity.resolver.module'; import { MessagingModule } from '@domain/communication/messaging/messaging.module'; -import { - AgentLoaderCreator, - ProfileLoaderCreator, -} from '@core/dataloader/creators/loader.creators'; import { StorageAggregatorModule } from '@domain/storage/storage-aggregator/storage.aggregator.module'; -import { UserStorageAggregatorLoaderCreator } from '@core/dataloader/creators/loader.creators/community/user.storage.aggregator.loader.creator'; import { DocumentModule } from '@domain/storage/document/document.module'; import { StorageBucketModule } from '@domain/storage/storage-bucket/storage.bucket.module'; import { ContributorModule } from '../contributor/contributor.module'; @@ -62,9 +57,6 @@ import { KratosModule } from '@services/infrastructure/kratos/kratos.module'; UserResolverMutations, UserResolverQueries, UserResolverFields, - AgentLoaderCreator, - ProfileLoaderCreator, - UserStorageAggregatorLoaderCreator, ], exports: [UserService, UserAuthorizationService], }) diff --git a/src/domain/space/account.host/account.host.module.ts b/src/domain/space/account.host/account.host.module.ts index 5762fbe8e9..3e530b96f2 100644 --- a/src/domain/space/account.host/account.host.module.ts +++ b/src/domain/space/account.host/account.host.module.ts @@ -2,16 +2,18 @@ import { Module } from '@nestjs/common'; import { AccountHostService } from './account.host.service'; import { AgentModule } from '@domain/agent/agent/agent.module'; import { LicenseIssuerModule } from '@platform/license-issuer/license.issuer.module'; -import { LicensingModule } from '@platform/licensing/licensing.module'; import { StorageAggregatorModule } from '@domain/storage/storage-aggregator/storage.aggregator.module'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Account } from '../account/account.entity'; +import { LicenseModule } from '@domain/common/license/license.module'; +import { LicensingFrameworkModule } from '@platform/licensing-framework/licensing.framework.module'; @Module({ imports: [ AgentModule, LicenseIssuerModule, - LicensingModule, + LicensingFrameworkModule, + LicenseModule, StorageAggregatorModule, TypeOrmModule.forFeature([Account]), ], diff --git a/src/domain/space/account.host/account.host.service.ts b/src/domain/space/account.host/account.host.service.ts index 03fa807d0a..60b4a60489 100644 --- a/src/domain/space/account.host/account.host.service.ts +++ b/src/domain/space/account.host/account.host.service.ts @@ -19,19 +19,24 @@ import { AuthorizationPolicy } from '@domain/common/authorization-policy/authori import { StorageAggregatorService } from '@domain/storage/storage-aggregator/storage.aggregator.service'; import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; import { EntityManager, FindOneOptions, Repository } from 'typeorm'; -import { LicensingService } from '@platform/licensing/licensing.service'; import { StorageAggregatorType } from '@common/enums/storage.aggregator.type'; import { AgentType } from '@common/enums/agent.type'; import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type'; import { AccountType } from '@common/enums/account.type'; import { IAgent } from '@domain/agent/agent/agent.interface'; +import { LicenseService } from '@domain/common/license/license.service'; +import { LicenseType } from '@common/enums/license.type'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; +import { LicenseEntitlementDataType } from '@common/enums/license.entitlement.data.type'; +import { LicensingFrameworkService } from '@platform/licensing-framework/licensing.framework.service'; @Injectable() export class AccountHostService { constructor( private agentService: AgentService, private licenseIssuerService: LicenseIssuerService, - private licensingService: LicensingService, + private licensingFrameworkService: LicensingFrameworkService, + private licenseService: LicenseService, private storageAggregatorService: StorageAggregatorService, @InjectEntityManager('default') private entityManager: EntityManager, @@ -55,6 +60,48 @@ export class AccountHostService { type: AgentType.ACCOUNT, }); + account.license = await this.licenseService.createLicense({ + type: LicenseType.ACCOUNT, + entitlements: [ + { + type: LicenseEntitlementType.ACCOUNT_SPACE_FREE, + dataType: LicenseEntitlementDataType.LIMIT, + limit: 0, + enabled: false, + }, + { + type: LicenseEntitlementType.ACCOUNT_SPACE_PLUS, + dataType: LicenseEntitlementDataType.LIMIT, + limit: 0, + enabled: false, + }, + { + type: LicenseEntitlementType.ACCOUNT_SPACE_PREMIUM, + dataType: LicenseEntitlementDataType.LIMIT, + limit: 0, + enabled: false, + }, + { + type: LicenseEntitlementType.ACCOUNT_VIRTUAL_CONTRIBUTOR, + dataType: LicenseEntitlementDataType.LIMIT, + limit: 0, + enabled: false, + }, + { + type: LicenseEntitlementType.ACCOUNT_INNOVATION_HUB, + dataType: LicenseEntitlementDataType.LIMIT, + limit: 0, + enabled: false, + }, + { + type: LicenseEntitlementType.ACCOUNT_INNOVATION_PACK, + dataType: LicenseEntitlementDataType.LIMIT, + limit: 0, + enabled: false, + }, + ], + }); + return await this.accountRepository.save(account); } @@ -65,7 +112,7 @@ export class AccountHostService { const account = await this.getAccount(accountID, options); if (!account) throw new EntityNotFoundException( - `Unable to find Account with ID: ${accountID}`, + `Unable to find Account on Host with ID: ${accountID}`, LogContext.ACCOUNT ); return account; @@ -88,11 +135,12 @@ export class AccountHostService { licensePlanID?: string ): Promise { const licensingFramework = - await this.licensingService.getDefaultLicensingOrFail(); + await this.licensingFrameworkService.getDefaultLicensingOrFail(); const licensePlansToAssign: ILicensePlan[] = []; - const licensePlans = await this.licensingService.getLicensePlans( - licensingFramework.id - ); + const licensePlans = + await this.licensingFrameworkService.getLicensePlansOrFail( + licensingFramework.id + ); for (const plan of licensePlans) { if (type === AccountType.USER && plan.assignToNewUserAccounts) { licensePlansToAssign.push(plan); @@ -108,10 +156,11 @@ export class AccountHostService { plan => plan.id === licensePlanID ); if (!licensePlanAlreadyAssigned) { - const additionalPlan = await this.licensingService.getLicensePlanOrFail( - licensingFramework.id, - licensePlanID - ); + const additionalPlan = + await this.licensingFrameworkService.getLicensePlanOrFail( + licensingFramework.id, + licensePlanID + ); licensePlansToAssign.push(additionalPlan); } diff --git a/src/domain/space/account/account.entity.ts b/src/domain/space/account/account.entity.ts index d8d2eddd38..a825cf1cc6 100644 --- a/src/domain/space/account/account.entity.ts +++ b/src/domain/space/account/account.entity.ts @@ -8,11 +8,16 @@ import { StorageAggregator } from '@domain/storage/storage-aggregator/storage.ag import { InnovationHub } from '@domain/innovation-hub/innovation.hub.entity'; import { InnovationPack } from '@library/innovation-pack/innovation.pack.entity'; import { AccountType } from '@common/enums/account.type'; +import { License } from '@domain/common/license/license.entity'; +import { ENUM_LENGTH } from '@common/constants'; @Entity() export class Account extends AuthorizableEntity implements IAccount { - @Column('varchar', { length: 128, nullable: true }) + @Column('varchar', { length: ENUM_LENGTH, nullable: true }) type!: AccountType; + @Column('varchar', { length: ENUM_LENGTH, nullable: true }) + externalSubscriptionID!: string; + @OneToMany(() => Space, space => space.account, { eager: false, cascade: false, // important: each space looks after saving itself! Same as space.subspaces field @@ -27,6 +32,14 @@ export class Account extends AuthorizableEntity implements IAccount { @JoinColumn() agent?: Agent; + @OneToOne(() => License, { + eager: false, + cascade: true, + onDelete: 'SET NULL', + }) + @JoinColumn() + license?: License; + @OneToOne(() => StorageAggregator, { eager: false, cascade: true, diff --git a/src/domain/space/account/account.interface.ts b/src/domain/space/account/account.interface.ts index cf8bfe59fa..b17c3fa95b 100644 --- a/src/domain/space/account/account.interface.ts +++ b/src/domain/space/account/account.interface.ts @@ -7,6 +7,7 @@ import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.a import { IInnovationHub } from '@domain/innovation-hub/innovation.hub.interface'; import { IInnovationPack } from '@library/innovation-pack/innovation.pack.interface'; import { AccountType } from '@common/enums/account.type'; +import { ILicense } from '@domain/common/license/license.interface'; @ObjectType('Account') export class IAccount extends IAuthorizable { @@ -17,9 +18,13 @@ export class IAccount extends IAuthorizable { type!: AccountType; agent?: IAgent; + spaces!: ISpace[]; virtualContributors!: IVirtualContributor[]; innovationHubs!: IInnovationHub[]; innovationPacks!: IInnovationPack[]; storageAggregator?: IStorageAggregator; + + license?: ILicense; + externalSubscriptionID!: string; } diff --git a/src/domain/space/account/account.module.ts b/src/domain/space/account/account.module.ts index 060bf7ec37..7924790496 100644 --- a/src/domain/space/account/account.module.ts +++ b/src/domain/space/account/account.module.ts @@ -13,7 +13,6 @@ import { PlatformAuthorizationPolicyModule } from '@platform/authorization/platf import { NameReporterModule } from '@services/external/elasticsearch/name-reporter/name.reporter.module'; import { AccountResolverQueries } from './account.resolver.queries'; import { ContributorModule } from '@domain/community/contributor/contributor.module'; -import { LicensingModule } from '@platform/licensing/licensing.module'; import { VirtualContributorModule } from '@domain/community/virtual-contributor/virtual.contributor.module'; import { LicenseIssuerModule } from '@platform/license-issuer/license.issuer.module'; import { AccountHostModule } from '../account.host/account.host.module'; @@ -24,6 +23,9 @@ import { InnovationHubModule } from '@domain/innovation-hub/innovation.hub.modul import { InnovationPackModule } from '@library/innovation-pack/innovation.pack.module'; import { NamingModule } from '@services/infrastructure/naming/naming.module'; import { TemporaryStorageModule } from '@services/infrastructure/temporary-storage/temporary.storage.module'; +import { LicenseModule } from '@domain/common/license/license.module'; +import { AccountLicenseService } from './account.service.license'; +import { LicensingFrameworkModule } from '@platform/licensing-framework/licensing.framework.module'; @Module({ imports: [ @@ -35,9 +37,10 @@ import { TemporaryStorageModule } from '@services/infrastructure/temporary-stora StorageAggregatorModule, TemporaryStorageModule, PlatformAuthorizationPolicyModule, - LicensingModule, + LicensingFrameworkModule, LicenseIssuerModule, LicenseEngineModule, + LicenseModule, SpaceModule, InnovationHubModule, InnovationPackModule, @@ -53,7 +56,8 @@ import { TemporaryStorageModule } from '@services/infrastructure/temporary-stora AccountResolverFields, AccountResolverMutations, AccountResolverQueries, + AccountLicenseService, ], - exports: [AccountService, AccountAuthorizationService], + exports: [AccountService, AccountAuthorizationService, AccountLicenseService], }) export class AccountModule {} diff --git a/src/domain/space/account/account.resolver.fields.ts b/src/domain/space/account/account.resolver.fields.ts index 6978f4659c..59da23fb94 100644 --- a/src/domain/space/account/account.resolver.fields.ts +++ b/src/domain/space/account/account.resolver.fields.ts @@ -27,6 +27,8 @@ import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.a import { ISpace } from '../space/space.interface'; import { IVirtualContributor } from '@domain/community/virtual-contributor/virtual.contributor.interface'; import { IAccountSubscription } from './account.license.subscription.interface'; +import { ILicense } from '@domain/common/license/license.interface'; +import { LicenseLoaderCreator } from '@core/dataloader/creators/loader.creators/license.loader.creator'; @Resolver(() => IAccount) export class AccountResolverFields { @@ -49,6 +51,20 @@ export class AccountResolverFields { return loader.load(account.id); } + @AuthorizationAgentPrivilege(AuthorizationPrivilege.READ) + @UseGuards(GraphqlGuard) + @ResolveField('license', () => ILicense, { + nullable: false, + description: 'The License operating on this Account.', + }) + async license( + @Parent() account: Account, + @Loader(LicenseLoaderCreator, { parentClassRef: Account }) + loader: ILoader + ): Promise { + return loader.load(account.id); + } + @ResolveField('host', () => IContributor, { nullable: true, description: 'The Account host.', diff --git a/src/domain/space/account/account.resolver.mutations.ts b/src/domain/space/account/account.resolver.mutations.ts index edda44bbd5..3cd23310ea 100644 --- a/src/domain/space/account/account.resolver.mutations.ts +++ b/src/domain/space/account/account.resolver.mutations.ts @@ -38,15 +38,18 @@ import { TransferAccountInnovationHubInput } from './dto/account.dto.transfer.in import { TransferAccountInnovationPackInput } from './dto/account.dto.transfer.innovation.pack'; import { TransferAccountVirtualContributorInput } from './dto/account.dto.transfer.virtual.contributor'; import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; -import { IAuthorizationPolicy } from '@domain/common/authorization-policy'; -import { INameable } from '@domain/common/entity/nameable-entity'; import { TemporaryStorageService } from '@services/infrastructure/temporary-storage/temporary.storage.service'; +import { LicenseService } from '@domain/common/license/license.service'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; +import { AccountLicenseResetInput } from './dto/account.dto.reset.license'; +import { AccountLicenseService } from './account.service.license'; @Resolver() export class AccountResolverMutations { constructor( private accountService: AccountService, private accountAuthorizationService: AccountAuthorizationService, + private accountLicenseService: AccountLicenseService, private authorizationService: AuthorizationService, private authorizationPolicyService: AuthorizationPolicyService, private virtualContributorService: VirtualContributorService, @@ -59,14 +62,10 @@ export class AccountResolverMutations { private spaceService: SpaceService, private spaceAuthorizationService: SpaceAuthorizationService, private notificationAdapter: NotificationAdapter, - private temporaryStorageService: TemporaryStorageService + private temporaryStorageService: TemporaryStorageService, + private licenseService: LicenseService ) {} - SOFT_LIMIT_SPACE = 3; - SOFT_LIMIT_INNOVATION_HUB = 0; - SOFT_LIMIT_INNOVATION_PACK = 3; - SOFT_LIMIT_VIRTUAL_CONTRIBUTOR = 3; - @UseGuards(GraphqlGuard) @Mutation(() => IAccount, { description: 'Creates a new Level Zero Space within the specified Account.', @@ -79,20 +78,18 @@ export class AccountResolverMutations { spaceData.accountID, { relations: { - spaces: true, + license: { + entitlements: true, + }, }, } ); this.validateSoftLicenseLimitOrFail( + account, agentInfo, - account.authorization, - 'Space', - account.id, AuthorizationPrivilege.CREATE_SPACE, - AuthorizationPrivilege.PLATFORM_ADMIN, - this.SOFT_LIMIT_SPACE, - account.spaces + LicenseEntitlementType.ACCOUNT_SPACE_FREE ); let space = await this.accountService.createSpaceOnAccount( @@ -132,38 +129,6 @@ export class AccountResolverMutations { return space; } - private validateSoftLicenseLimitOrFail( - agentInfo: AgentInfo, - authorization: IAuthorizationPolicy | undefined, - resourceType: string, - accountID: string, - hardPrivilege: AuthorizationPrivilege, - softPrivilege: AuthorizationPrivilege, - softLimit: number, - nameableResouces: INameable[] - ) { - this.authorizationService.grantAccessOrFail( - agentInfo, - authorization, - hardPrivilege, - `create ${resourceType} on account: ${accountID}` - ); - const isPlatformAdmin = this.authorizationService.isAccessGranted( - agentInfo, - authorization, - softPrivilege - ); - if (!isPlatformAdmin) { - const resourceCount = nameableResouces.length; - if (resourceCount >= softLimit) { - throw new ValidationException( - `Unable to create ${resourceType} on account: ${accountID}. Soft limit of ${softLimit} reached`, - LogContext.ACCOUNT - ); - } - } - } - @UseGuards(GraphqlGuard) @Mutation(() => IInnovationHub, { description: 'Create an Innovation Hub on the specified account', @@ -177,20 +142,18 @@ export class AccountResolverMutations { { relations: { storageAggregator: true, - innovationHubs: true, + license: { + entitlements: true, + }, }, } ); this.validateSoftLicenseLimitOrFail( + account, agentInfo, - account.authorization, - 'Innovation Hub', - account.id, - AuthorizationPrivilege.PLATFORM_ADMIN, // Hard requirement for now - AuthorizationPrivilege.PLATFORM_ADMIN, - this.SOFT_LIMIT_INNOVATION_HUB, - account.innovationHubs + AuthorizationPrivilege.CREATE_INNOVATION_HUB, + LicenseEntitlementType.ACCOUNT_INNOVATION_HUB ); let innovationHub = await this.innovationHubService.createInnovationHub( @@ -222,20 +185,18 @@ export class AccountResolverMutations { virtualContributorData.accountID, { relations: { - virtualContributors: true, + license: { + entitlements: true, + }, }, } ); this.validateSoftLicenseLimitOrFail( + account, agentInfo, - account.authorization, - 'Virtual Contributor', - account.id, AuthorizationPrivilege.CREATE_VIRTUAL_CONTRIBUTOR, - AuthorizationPrivilege.PLATFORM_ADMIN, - this.SOFT_LIMIT_VIRTUAL_CONTRIBUTOR, - account.virtualContributors + LicenseEntitlementType.ACCOUNT_VIRTUAL_CONTRIBUTOR ); const virtual = await this.accountService.createVirtualContributorOnAccount( @@ -284,20 +245,18 @@ export class AccountResolverMutations { innovationPackData.accountID, { relations: { - innovationPacks: true, + license: { + entitlements: true, + }, }, } ); this.validateSoftLicenseLimitOrFail( + account, agentInfo, - account.authorization, - 'Innovation Pack', - account.id, AuthorizationPrivilege.CREATE_INNOVATION_PACK, - AuthorizationPrivilege.PLATFORM_ADMIN, - this.SOFT_LIMIT_INNOVATION_PACK, - account.innovationPacks + LicenseEntitlementType.ACCOUNT_INNOVATION_PACK ); const innovationPack = @@ -345,6 +304,32 @@ export class AccountResolverMutations { return await this.accountService.getAccountOrFail(account.id); } + @UseGuards(GraphqlGuard) + @Mutation(() => IAccount, { + description: + 'Reset the License with Entitlements on the specified Account.', + }) + async licenseResetOnAccount( + @CurrentUser() agentInfo: AgentInfo, + @Args('resetData') + licenseResetData: AccountLicenseResetInput + ): Promise { + const account = await this.accountService.getAccountOrFail( + licenseResetData.accountID + ); + this.authorizationService.grantAccessOrFail( + agentInfo, + account.authorization, + AuthorizationPrivilege.LICENSE_RESET, + `reset license definition on Account: ${agentInfo.userID}` + ); + const accountLicenses = await this.accountLicenseService.applyLicensePolicy( + account.id + ); + await this.licenseService.saveAll(accountLicenses); + return await this.accountService.getAccountOrFail(account.id); + } + @UseGuards(GraphqlGuard) @Mutation(() => IInnovationHub, { description: 'Transfer the specified InnovationHub to another Account.', @@ -568,4 +553,52 @@ export class AccountResolverMutations { `transfer ${resourceName} to target Account: ${agentInfo.email}` ); } + + private validateSoftLicenseLimitOrFail( + account: IAccount, + agentInfo: AgentInfo, + authorizationPrivilege: AuthorizationPrivilege, + licenseType: LicenseEntitlementType + ) { + if (!account.authorization) { + throw new RelationshipNotFoundException( + `Unable to load authorization on account: ${account.id}`, + LogContext.ACCOUNT + ); + } + if (!account.license) { + throw new RelationshipNotFoundException( + `Unable to load license on account: ${account.id}`, + LogContext.ACCOUNT + ); + } + const authorization = account.authorization; + const license = account.license; + + this.authorizationService.grantAccessOrFail( + agentInfo, + authorization, + authorizationPrivilege, + `create ${licenseType} on account: ${account.id}` + ); + const isEntitleMentEnabled = this.licenseService.isEntitlementAvailable( + license, + licenseType + ); + const isPlatformAdmin = this.authorizationService.isAccessGranted( + agentInfo, + authorization, + AuthorizationPrivilege.PLATFORM_ADMIN + ); + if (!isPlatformAdmin && !isEntitleMentEnabled) { + const entitlementLimit = this.licenseService.getEntitlementLimit( + license, + licenseType + ); + throw new ValidationException( + `Unable to create ${licenseType} on account: ${account.id}. Entitlement limit of ${entitlementLimit} of type ${licenseType} reached`, + LogContext.ACCOUNT + ); + } + } } diff --git a/src/domain/space/account/account.service.authorization.ts b/src/domain/space/account/account.service.authorization.ts index deadbf1e82..72f7844e54 100644 --- a/src/domain/space/account/account.service.authorization.ts +++ b/src/domain/space/account/account.service.authorization.ts @@ -32,9 +32,7 @@ import { AccountHostService } from '../account.host/account.host.service'; import { StorageAggregatorAuthorizationService } from '@domain/storage/storage-aggregator/storage.aggregator.service.authorization'; import { InnovationPackAuthorizationService } from '@library/innovation-pack/innovation.pack.service.authorization'; import { InnovationHubAuthorizationService } from '@domain/innovation-hub/innovation.hub.service.authorization'; -import { LicenseEngineService } from '@core/license-engine/license.engine.service'; -import { LicensePrivilege } from '@common/enums/license.privilege'; -import { IAgent } from '@domain/agent/agent/agent.interface'; +import { LicenseAuthorizationService } from '@domain/common/license/license.service.authorization'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; @Injectable() @@ -42,7 +40,6 @@ export class AccountAuthorizationService { constructor( private authorizationPolicyService: AuthorizationPolicyService, private agentAuthorizationService: AgentAuthorizationService, - private licenseEngineService: LicenseEngineService, private platformAuthorizationService: PlatformAuthorizationPolicyService, private spaceAuthorizationService: SpaceAuthorizationService, private virtualContributorAuthorizationService: VirtualContributorAuthorizationService, @@ -51,6 +48,7 @@ export class AccountAuthorizationService { private innovationHubAuthorizationService: InnovationHubAuthorizationService, private accountService: AccountService, private accountHostService: AccountHostService, + private licenseAuthorizationService: LicenseAuthorizationService, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService ) {} @@ -67,10 +65,11 @@ export class AccountAuthorizationService { innovationPacks: true, innovationHubs: true, storageAggregator: true, + license: true, }, } ); - if (!account.storageAggregator || !account.agent) { + if (!account.storageAggregator || !account.agent || !account.license) { throw new RelationshipNotFoundException( `Unable to load Account with entities at start of auth reset: ${account.id} `, LogContext.ACCOUNT @@ -94,7 +93,6 @@ export class AccountAuthorizationService { account.authorization = await this.extendAuthorizationPolicy( account.authorization, - account.agent, hostCredentials ); @@ -137,7 +135,8 @@ export class AccountAuthorizationService { !account.virtualContributors || !account.innovationPacks || !account.storageAggregator || - !account.innovationHubs + !account.innovationHubs || + !account.license ) { throw new RelationshipNotFoundException( `Unable to load Account with entities at start of auth reset: ${account.id} `, @@ -166,6 +165,13 @@ export class AccountAuthorizationService { ); updatedAuthorizations.push(agentAuthorization); + const licenseAuthorizations = + this.licenseAuthorizationService.applyAuthorizationPolicy( + account.license, + account.authorization + ); + updatedAuthorizations.push(...licenseAuthorizations); + const storageAggregatorAuthorizations = await this.storageAggregatorAuthorizationService.applyAuthorizationPolicy( account.storageAggregator, @@ -205,7 +211,6 @@ export class AccountAuthorizationService { private async extendAuthorizationPolicy( authorization: IAuthorizationPolicy | undefined, - accountAgent: IAgent, hostCredentials: ICredentialDefinition[] ): Promise { if (!authorization) { @@ -225,6 +230,7 @@ export class AccountAuthorizationService { this.authorizationPolicyService.createCredentialRuleUsingTypesOnly( [ AuthorizationPrivilege.AUTHORIZATION_RESET, + AuthorizationPrivilege.LICENSE_RESET, AuthorizationPrivilege.PLATFORM_ADMIN, AuthorizationPrivilege.TRANSFER_RESOURCE, AuthorizationPrivilege.CREATE_SPACE, @@ -276,50 +282,31 @@ export class AccountAuthorizationService { accountHostManage.cascade = true; newRules.push(accountHostManage); - const createSpace = await this.licenseEngineService.isAccessGranted( - LicensePrivilege.ACCOUNT_CREATE_SPACE, - accountAgent + // If the user is a beta tester or part of VC campaign then can create the resources + const createSpace = this.authorizationPolicyService.createCredentialRule( + [AuthorizationPrivilege.CREATE_SPACE], + [...hostCredentials], + CREDENTIAL_RULE_PLATFORM_CREATE_SPACE ); - if (createSpace) { - // If the user is a beta tester or part of VC campaign then can create the resources - const createSpace = this.authorizationPolicyService.createCredentialRule( - [AuthorizationPrivilege.CREATE_SPACE], - [...hostCredentials], - CREDENTIAL_RULE_PLATFORM_CREATE_SPACE - ); - createSpace.cascade = false; - newRules.push(createSpace); - } + createSpace.cascade = false; + newRules.push(createSpace); - const createVirtualContributor = - await this.licenseEngineService.isAccessGranted( - LicensePrivilege.ACCOUNT_CREATE_VIRTUAL_CONTRIBUTOR, - accountAgent - ); - if (createVirtualContributor) { - const createVC = this.authorizationPolicyService.createCredentialRule( - [AuthorizationPrivilege.CREATE_VIRTUAL_CONTRIBUTOR], - [...hostCredentials], - CREDENTIAL_RULE_PLATFORM_CREATE_VC - ); - createVC.cascade = false; - newRules.push(createVC); - } + const createVC = this.authorizationPolicyService.createCredentialRule( + [AuthorizationPrivilege.CREATE_VIRTUAL_CONTRIBUTOR], + [...hostCredentials], + CREDENTIAL_RULE_PLATFORM_CREATE_VC + ); + createVC.cascade = false; + newRules.push(createVC); const createInnovationPack = - await this.licenseEngineService.isAccessGranted( - LicensePrivilege.ACCOUNT_CREATE_INNOVATION_PACK, - accountAgent - ); - if (createInnovationPack) { - const createVC = this.authorizationPolicyService.createCredentialRule( + this.authorizationPolicyService.createCredentialRule( [AuthorizationPrivilege.CREATE_INNOVATION_PACK], [...hostCredentials], CREDENTIAL_RULE_PLATFORM_CREATE_INNOVATION_PACK ); - createVC.cascade = false; - newRules.push(createVC); - } + createInnovationPack.cascade = false; + newRules.push(createInnovationPack); return this.authorizationPolicyService.appendCredentialAuthorizationRules( authorization, diff --git a/src/domain/space/account/account.service.license.ts b/src/domain/space/account/account.service.license.ts new file mode 100644 index 0000000000..67beac3401 --- /dev/null +++ b/src/domain/space/account/account.service.license.ts @@ -0,0 +1,170 @@ +import { Inject, Injectable, LoggerService } from '@nestjs/common'; +import { LogContext } from '@common/enums'; +import { AccountService } from './account.service'; +import { + EntityNotInitializedException, + RelationshipNotFoundException, +} from '@common/exceptions'; +import { IAgent } from '@domain/agent/agent/agent.interface'; +import { LicenseService } from '@domain/common/license/license.service'; +import { ILicense } from '@domain/common/license/license.interface'; +import { LicenseEngineService } from '@core/license-engine/license.engine.service'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; +import { IAccount } from './account.interface'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { SpaceLicenseService } from '../space/space.service.license'; + +@Injectable() +export class AccountLicenseService { + constructor( + private licenseService: LicenseService, + private accountService: AccountService, + private licenseEngineService: LicenseEngineService, + private spaceLicenseService: SpaceLicenseService, + @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService + ) {} + + async applyLicensePolicy(accountID: string): Promise { + const account = await this.accountService.getAccountOrFail(accountID, { + relations: { + agent: { + credentials: true, + }, + spaces: true, + license: { + entitlements: true, + }, + }, + }); + if ( + !account.spaces || + !account.agent || + !account.license || + !account.license.entitlements + ) { + throw new RelationshipNotFoundException( + `Unable to load Account with entities at start of license reset: ${account.id} `, + LogContext.ACCOUNT + ); + } + const updatedLicenses: ILicense[] = []; + + // Ensure always applying from a clean state + account.license = this.licenseService.reset(account.license); + + account.license = await this.extendLicensePolicy( + account.license, + account.agent, + account + ); + + updatedLicenses.push(account.license); + + for (const space of account.spaces) { + const spaceLicenses = await this.spaceLicenseService.applyLicensePolicy( + space.id + ); + updatedLicenses.push(...spaceLicenses); + } + + return updatedLicenses; + } + + private async extendLicensePolicy( + license: ILicense | undefined, + accountAgent: IAgent, + account: IAccount + ): Promise { + if (!license || !license.entitlements) { + throw new EntityNotInitializedException( + `License with entitielements not found for account with agent ${accountAgent.id}`, + LogContext.LICENSE + ); + } + for (const entitlement of license.entitlements) { + switch (entitlement.type) { + case LicenseEntitlementType.ACCOUNT_SPACE_FREE: + const createSpace = + await this.licenseEngineService.isEntitlementGranted( + LicenseEntitlementType.ACCOUNT_SPACE_FREE, + accountAgent + ); + if (createSpace) { + entitlement.limit = 3; + entitlement.enabled = true; + } + break; + case LicenseEntitlementType.ACCOUNT_SPACE_PLUS: + const createSpacePLus = + await this.licenseEngineService.isEntitlementGranted( + LicenseEntitlementType.ACCOUNT_SPACE_PLUS, + accountAgent + ); + if (createSpacePLus) { + entitlement.limit = 0; + entitlement.enabled = true; + } + break; + case LicenseEntitlementType.ACCOUNT_SPACE_PREMIUM: + const createSpacePremium = + await this.licenseEngineService.isEntitlementGranted( + LicenseEntitlementType.ACCOUNT_SPACE_PREMIUM, + accountAgent + ); + if (createSpacePremium) { + entitlement.limit = 0; + entitlement.enabled = true; + } + break; + case LicenseEntitlementType.ACCOUNT_VIRTUAL_CONTRIBUTOR: + const createVirtualContributor = + await this.licenseEngineService.isEntitlementGranted( + LicenseEntitlementType.ACCOUNT_VIRTUAL_CONTRIBUTOR, + accountAgent + ); + if (createVirtualContributor) { + entitlement.limit = 3; + entitlement.enabled = true; + } + break; + case LicenseEntitlementType.ACCOUNT_INNOVATION_HUB: + const createInnovationHub = + await this.licenseEngineService.isEntitlementGranted( + LicenseEntitlementType.ACCOUNT_INNOVATION_HUB, + accountAgent + ); + if (createInnovationHub) { + entitlement.limit = 1; + entitlement.enabled = true; + } + break; + case LicenseEntitlementType.ACCOUNT_INNOVATION_PACK: + const createInnovationPack = + await this.licenseEngineService.isEntitlementGranted( + LicenseEntitlementType.ACCOUNT_INNOVATION_PACK, + accountAgent + ); + if (createInnovationPack) { + entitlement.limit = 3; + entitlement.enabled = true; + } + break; + default: + throw new EntityNotInitializedException( + `Unknown entitlement type for license: ${entitlement.type}`, + LogContext.LICENSE + ); + } + } + + if (account.externalSubscriptionID) { + // TODO: get subscription details from the WingBack api + set the entitlements accordingly + this.logger.verbose?.( + `Invoking external subscription service for account ${account.id}`, + LogContext.ACCOUNT + ); + } + + return license; + } +} diff --git a/src/domain/space/account/account.service.ts b/src/domain/space/account/account.service.ts index 9a372fb294..fca2ef3347 100644 --- a/src/domain/space/account/account.service.ts +++ b/src/domain/space/account/account.service.ts @@ -36,6 +36,7 @@ import { AccountHostService } from '../account.host/account.host.service'; import { IAgent } from '@domain/agent/agent/agent.interface'; import { IAccountSubscription } from './account.license.subscription.interface'; import { LicenseCredential } from '@common/enums/license.credential'; +import { LicenseService } from '@domain/common/license/license.service'; @Injectable() export class AccountService { @@ -51,6 +52,7 @@ export class AccountService { private innovationPackService: InnovationPackService, private innovationPackAuthorizationService: InnovationPackAuthorizationService, private namingService: NamingService, + private licenseService: LicenseService, @InjectRepository(Account) private accountRepository: Repository, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService @@ -136,7 +138,7 @@ export class AccountService { return await this.accountRepository.save(account); } - async deleteAccount(accountInput: IAccount): Promise { + async deleteAccountOrFail(accountInput: IAccount): Promise { const accountID = accountInput.id; const account = await this.getAccountOrFail(accountID, { relations: { @@ -146,6 +148,7 @@ export class AccountService { innovationPacks: true, storageAggregator: true, innovationHubs: true, + license: true, }, }); @@ -155,7 +158,8 @@ export class AccountService { !account.virtualContributors || !account.storageAggregator || !account.innovationHubs || - !account.innovationPacks + !account.innovationPacks || + !account.license ) { throw new RelationshipNotFoundException( `Unable to load all entities for deletion of account ${account.id} `, @@ -167,6 +171,8 @@ export class AccountService { await this.storageAggregatorService.delete(account.storageAggregator.id); + await this.licenseService.removeLicenseOrFail(account.license.id); + for (const vc of account.virtualContributors) { await this.virtualContributorService.deleteVirtualContributor(vc.id); } @@ -179,7 +185,7 @@ export class AccountService { } for (const space of account.spaces) { - await this.spaceService.deleteSpace({ ID: space.id }); + await this.spaceService.deleteSpaceOrFail({ ID: space.id }); } const result = await this.accountRepository.remove(account as Account); diff --git a/src/domain/space/account/dto/account.dto.reset.authorization.ts b/src/domain/space/account/dto/account.dto.reset.authorization.ts index f6d675ef86..9580cc08f7 100644 --- a/src/domain/space/account/dto/account.dto.reset.authorization.ts +++ b/src/domain/space/account/dto/account.dto.reset.authorization.ts @@ -1,9 +1,9 @@ -import { UUID_NAMEID } from '@domain/common/scalars'; +import { UUID } from '@domain/common/scalars'; import { Field, InputType } from '@nestjs/graphql'; @InputType() export class AccountAuthorizationResetInput { - @Field(() => UUID_NAMEID, { + @Field(() => UUID, { nullable: false, description: 'The identifier of the Account whose Authorization Policy should be reset.', diff --git a/src/domain/space/account/dto/account.dto.reset.license.ts b/src/domain/space/account/dto/account.dto.reset.license.ts new file mode 100644 index 0000000000..6abe6ff089 --- /dev/null +++ b/src/domain/space/account/dto/account.dto.reset.license.ts @@ -0,0 +1,12 @@ +import { UUID } from '@domain/common/scalars'; +import { Field, InputType } from '@nestjs/graphql'; + +@InputType() +export class AccountLicenseResetInput { + @Field(() => UUID, { + nullable: false, + description: + 'The identifier of the Account whose License and Entitlements should be reset.', + }) + accountID!: string; +} diff --git a/src/domain/space/space.defaults/space.defaults.service.ts b/src/domain/space/space.defaults/space.defaults.service.ts index c10d7d7b27..70ab1912a2 100644 --- a/src/domain/space/space.defaults/space.defaults.service.ts +++ b/src/domain/space/space.defaults/space.defaults.service.ts @@ -20,10 +20,10 @@ import { CreateCollaborationOnSpaceInput } from '../space/dto/space.dto.create.c import { CreateCollaborationInput } from '@domain/collaboration/collaboration/dto/collaboration.dto.create'; import { TemplateService } from '@domain/template/template/template.service'; import { InputCreatorService } from '@services/api/input-creator/input.creator.service'; -import { PlatformService } from '@platform/platform/platform.service'; import { TemplatesManagerService } from '@domain/template/templates-manager/templates.manager.service'; import { TemplateDefaultType } from '@common/enums/template.default.type'; import { ValidationException } from '@common/exceptions'; +import { PlatformService } from '@platform/platform/platform.service'; import { CollaborationService } from '@domain/collaboration/collaboration/collaboration.service'; import { ITemplatesManager } from '@domain/template/templates-manager'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; diff --git a/src/domain/space/space/sort.spaces.by.activity.spec.ts b/src/domain/space/space/sort.spaces.by.activity.spec.ts index f3c21a17d9..d16a8d2cb7 100644 --- a/src/domain/space/space/sort.spaces.by.activity.spec.ts +++ b/src/domain/space/space/sort.spaces.by.activity.spec.ts @@ -42,6 +42,7 @@ const createTestSpace = (id: string): ISpace => { virtualContributors: [], innovationHubs: [], innovationPacks: [], + externalSubscriptionID: '', spaces: [], type: AccountType.ORGANIZATION, }, diff --git a/src/domain/space/space/space.entity.ts b/src/domain/space/space/space.entity.ts index bc75aa8015..50c132b221 100644 --- a/src/domain/space/space/space.entity.ts +++ b/src/domain/space/space/space.entity.ts @@ -20,6 +20,7 @@ import { Agent } from '@domain/agent/agent/agent.entity'; import { SpaceVisibility } from '@common/enums/space.visibility'; import { Profile } from '@domain/common/profile'; import { TemplatesManager } from '@domain/template/templates-manager'; +import { License } from '@domain/common/license/license.entity'; import { SpaceLevel } from '@common/enums/space.level'; @Entity() export class Space extends NameableEntity implements ISpace { @@ -118,6 +119,14 @@ export class Space extends NameableEntity implements ISpace { @JoinColumn() templatesManager?: TemplatesManager; + @OneToOne(() => License, { + eager: false, + cascade: true, + onDelete: 'SET NULL', + }) + @JoinColumn() + license?: License; + constructor() { super(); this.nameID = ''; diff --git a/src/domain/space/space/space.interface.ts b/src/domain/space/space/space.interface.ts index 43d1967c5e..10208e16c9 100644 --- a/src/domain/space/space/space.interface.ts +++ b/src/domain/space/space/space.interface.ts @@ -9,6 +9,7 @@ import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.a import { IAccount } from '../account/account.interface'; import { SpaceVisibility } from '@common/enums/space.visibility'; import { ITemplatesManager } from '@domain/template/templates-manager'; +import { ILicense } from '@domain/common/license/license.interface'; import { SpaceLevel } from '@common/enums/space.level'; @ObjectType('Space') @@ -55,4 +56,5 @@ export class ISpace extends INameable { levelZeroSpaceID!: string; templatesManager?: ITemplatesManager; + license?: ILicense; } diff --git a/src/domain/space/space/space.module.ts b/src/domain/space/space/space.module.ts index 2dcdb33ed1..a9420a9438 100644 --- a/src/domain/space/space/space.module.ts +++ b/src/domain/space/space/space.module.ts @@ -12,7 +12,6 @@ import { SpaceFilterModule } from '@services/infrastructure/space-filter/space.f import { SpaceResolverSubscriptions } from './space.resolver.subscriptions'; import { ActivityAdapterModule } from '@services/adapters/activity-adapter/activity.adapter.module'; import { ContributionReporterModule } from '@services/external/elasticsearch/contribution-reporter'; -import { LoaderCreatorModule } from '@core/dataloader/creators'; import { NameReporterModule } from '@services/external/elasticsearch/name-reporter/name.reporter.module'; import { ContextModule } from '@domain/context/context/context.module'; import { AgentModule } from '@domain/agent/agent/agent.module'; @@ -24,13 +23,15 @@ import { PlatformAuthorizationPolicyModule } from '@platform/authorization/platf import { NamingModule } from '@services/infrastructure/naming/naming.module'; import { SpaceSettingsModule } from '../space.settings/space.settings.module'; import { AccountHostModule } from '../account.host/account.host.module'; -import { LicensingModule } from '@platform/licensing/licensing.module'; import { LicenseEngineModule } from '@core/license-engine/license.engine.module'; import { LicenseIssuerModule } from '@platform/license-issuer/license.issuer.module'; import { InputCreatorModule } from '@services/api/input-creator/input.creator.module'; import { RoleSetModule } from '@domain/access/role-set/role.set.module'; import { TemplatesManagerModule } from '@domain/template/templates-manager/templates.manager.module'; import { SpaceDefaultsModule } from '../space.defaults/space.defaults.module'; +import { LicensingFrameworkModule } from '@platform/licensing-framework/licensing.framework.module'; +import { LicenseModule } from '@domain/common/license/license.module'; +import { SpaceLicenseService } from './space.service.license'; @Module({ imports: [ @@ -41,7 +42,7 @@ import { SpaceDefaultsModule } from '../space.defaults/space.defaults.module'; ContextModule, CommunityModule, ProfileModule, - LicensingModule, + LicensingFrameworkModule, LicenseIssuerModule, LicenseEngineModule, NamingModule, @@ -54,20 +55,21 @@ import { SpaceDefaultsModule } from '../space.defaults/space.defaults.module'; InputCreatorModule, SpaceFilterModule, ActivityAdapterModule, - LoaderCreatorModule, RoleSetModule, NameReporterModule, SpaceDefaultsModule, + LicenseModule, TypeOrmModule.forFeature([Space]), ], providers: [ SpaceService, SpaceAuthorizationService, + SpaceLicenseService, SpaceResolverFields, SpaceResolverQueries, SpaceResolverMutations, SpaceResolverSubscriptions, ], - exports: [SpaceService, SpaceAuthorizationService], + exports: [SpaceService, SpaceAuthorizationService, SpaceLicenseService], }) export class SpaceModule {} diff --git a/src/domain/space/space/space.resolver.fields.ts b/src/domain/space/space/space.resolver.fields.ts index 9807b6d106..734f52f07b 100644 --- a/src/domain/space/space/space.resolver.fields.ts +++ b/src/domain/space/space/space.resolver.fields.ts @@ -33,9 +33,10 @@ import { EntityNotFoundException } from '@common/exceptions/entity.not.found.exc import { ISpaceSettings } from '../space.settings/space.settings.interface'; import { IAccount } from '../account/account.interface'; import { IContributor } from '@domain/community/contributor/contributor.interface'; -import { LicensePrivilege } from '@common/enums/license.privilege'; import { ISpaceSubscription } from './space.license.subscription.interface'; import { ITemplatesManager } from '@domain/template/templates-manager'; +import { ILicense } from '@domain/common/license/license.interface'; +import { LicenseLoaderCreator } from '@core/dataloader/creators/loader.creators/license.loader.creator'; @Resolver(() => ISpace) export class SpaceResolverFields { @@ -105,7 +106,6 @@ export class SpaceResolverFields { return this.spaceService.activeSubscription(space); } - @AuthorizationAgentPrivilege(AuthorizationPrivilege.READ) @UseGuards(GraphqlGuard) @ResolveField('collaboration', () => ICollaboration, { nullable: false, @@ -119,15 +119,18 @@ export class SpaceResolverFields { return loader.load(space.id); } - @ResolveField('licensePrivileges', () => [LicensePrivilege], { - nullable: true, - description: - 'The privileges granted based on the License credentials held by this Space.', + @AuthorizationAgentPrivilege(AuthorizationPrivilege.READ) + @UseGuards(GraphqlGuard) + @ResolveField('license', () => ILicense, { + nullable: false, + description: 'The License operating on this Space.', }) - async licensePrivileges( - @Parent() space: ISpace - ): Promise { - return this.spaceService.getLicensePrivileges(space); + async license( + @Parent() space: ISpace, + @Loader(LicenseLoaderCreator, { parentClassRef: Space }) + loader: ILoader + ): Promise { + return loader.load(space.id); } @AuthorizationAgentPrivilege(AuthorizationPrivilege.READ) diff --git a/src/domain/space/space/space.resolver.mutations.ts b/src/domain/space/space/space.resolver.mutations.ts index 3a95dc52a7..268094737d 100644 --- a/src/domain/space/space/space.resolver.mutations.ts +++ b/src/domain/space/space/space.resolver.mutations.ts @@ -102,7 +102,7 @@ export class SpaceResolverMutations { AuthorizationPrivilege.DELETE, `deleteSpace: ${space.nameID}` ); - return await this.spaceService.deleteSpace(deleteData); + return await this.spaceService.deleteSpaceOrFail(deleteData); } @UseGuards(GraphqlGuard) diff --git a/src/domain/space/space/space.service.authorization.ts b/src/domain/space/space/space.service.authorization.ts index abc8aa8e0c..d1dff412d8 100644 --- a/src/domain/space/space/space.service.authorization.ts +++ b/src/domain/space/space/space.service.authorization.ts @@ -33,11 +33,12 @@ import { ICredentialDefinition } from '@domain/agent/credential/credential.defin import { SpaceSettingsService } from '../space.settings/space.settings.service'; import { SpaceLevel } from '@common/enums/space.level'; import { AgentAuthorizationService } from '@domain/agent/agent/agent.service.authorization'; -import { IAgent } from '@domain/agent/agent/agent.interface'; import { ISpaceSettings } from '../space.settings/space.settings.interface'; import { RoleSetService } from '@domain/access/role-set/role.set.service'; import { IRoleSet } from '@domain/access/role-set'; import { TemplatesManagerAuthorizationService } from '@domain/template/templates-manager/templates.manager.service.authorization'; +import { LicenseAuthorizationService } from '@domain/common/license/license.service.authorization'; +import { ILicense } from '@domain/common/license/license.interface'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; @Injectable() @@ -54,6 +55,7 @@ export class SpaceAuthorizationService { private templatesManagerAuthorizationService: TemplatesManagerAuthorizationService, private spaceService: SpaceService, private spaceSettingsService: SpaceSettingsService, + private licenseAuthorizationService: LicenseAuthorizationService, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService ) {} @@ -79,13 +81,18 @@ export class SpaceAuthorizationService { storageAggregator: true, subspaces: true, templatesManager: true, + license: { + entitlements: true, + }, }, }); if ( !space.authorization || !space.community || !space.community.roleSet || - !space.subspaces + !space.subspaces || + !space.license || + !space.license.entitlements ) { throw new RelationshipNotFoundException( `Unable to load Space with entities at start of auth reset: ${space.id} `, @@ -94,8 +101,7 @@ export class SpaceAuthorizationService { } // Get the root space agent for licensing related logic - const levelZeroSpaceAgent = - await this.spaceService.getLevelZeroSpaceAgent(space); + const spaceLicense = space.license; const updatedAuthorizations: IAuthorizationPolicy[] = []; @@ -205,7 +211,7 @@ export class SpaceAuthorizationService { // propagate authorization rules for child entities const childAuthorzations = await this.propagateAuthorizationToChildEntities( space, - levelZeroSpaceAgent, + spaceLicense, spaceSettings, spaceMembershipAllowed ); @@ -244,7 +250,7 @@ export class SpaceAuthorizationService { public async propagateAuthorizationToChildEntities( space: ISpace, - levelZeroSpaceAgent: IAgent, + spaceLicense: ILicense, spaceSettings: ISpaceSettings, spaceMembershipAllowed: boolean ): Promise { @@ -256,7 +262,8 @@ export class SpaceAuthorizationService { !space.community.roleSet || !space.context || !space.profile || - !space.storageAggregator + !space.storageAggregator || + !space.license ) { throw new RelationshipNotFoundException( `Unable to load entities on auth reset for space base ${space.id} `, @@ -272,7 +279,6 @@ export class SpaceAuthorizationService { await this.communityAuthorizationService.applyAuthorizationPolicy( space.community.id, space.authorization, - levelZeroSpaceAgent, spaceSettings, spaceMembershipAllowed, isSubspaceCommunity @@ -284,8 +290,7 @@ export class SpaceAuthorizationService { space.collaboration, space.authorization, space.community.roleSet, - spaceSettings, - levelZeroSpaceAgent + spaceSettings ); updatedAuthorizations.push(...collaborationAuthorizations); @@ -303,6 +308,13 @@ export class SpaceAuthorizationService { ); updatedAuthorizations.push(...storageAuthorizations); + const licenseAuthorizations = + this.licenseAuthorizationService.applyAuthorizationPolicy( + space.license, + space.authorization + ); + updatedAuthorizations.push(...licenseAuthorizations); + // Level zero space only entities if (space.level === SpaceLevel.SPACE) { if (!space.templatesManager) { diff --git a/src/domain/space/space/space.service.license.ts b/src/domain/space/space/space.service.license.ts new file mode 100644 index 0000000000..33092bff30 --- /dev/null +++ b/src/domain/space/space/space.service.license.ts @@ -0,0 +1,177 @@ +import { Inject, Injectable, LoggerService } from '@nestjs/common'; +import { LogContext } from '@common/enums'; +import { + EntityNotInitializedException, + RelationshipNotFoundException, +} from '@common/exceptions'; +import { IAgent } from '@domain/agent/agent/agent.interface'; +import { LicenseService } from '@domain/common/license/license.service'; +import { ILicense } from '@domain/common/license/license.interface'; +import { LicenseEngineService } from '@core/license-engine/license.engine.service'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { SpaceService } from './space.service'; +import { RoleSetLicenseService } from '@domain/access/role-set/role.set.service.license'; +import { CollaborationLicenseService } from '@domain/collaboration/collaboration/collaboration.service.license'; + +@Injectable() +export class SpaceLicenseService { + constructor( + private licenseService: LicenseService, + private spaceService: SpaceService, + private licenseEngineService: LicenseEngineService, + private roleSetLicenseService: RoleSetLicenseService, + private collaborationLicenseService: CollaborationLicenseService, + @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService + ) {} + + async applyLicensePolicy(spaceID: string): Promise { + const space = await this.spaceService.getSpaceOrFail(spaceID, { + relations: { + agent: { + credentials: true, + }, + subspaces: true, + license: { + entitlements: true, + }, + community: { + roleSet: true, + }, + collaboration: true, + }, + }); + if ( + !space.subspaces || + !space.agent || + !space.license || + !space.license.entitlements || + !space.community || + !space.community.roleSet || + !space.collaboration + ) { + throw new RelationshipNotFoundException( + `Unable to load Space with entities at start of license reset: ${space.id} `, + LogContext.ACCOUNT + ); + } + const updatedLicenses: ILicense[] = []; + + // Ensure always applying from a clean state + space.license = this.licenseService.reset(space.license); + + space.license = await this.extendLicensePolicy(space.license, space.agent); + + updatedLicenses.push(space.license); + + const roleSetLicenses = await this.roleSetLicenseService.applyLicensePolicy( + space.community.roleSet.id, + space.license + ); + updatedLicenses.push(...roleSetLicenses); + + const collaborationLicenses = + await this.collaborationLicenseService.applyLicensePolicy( + space.collaboration.id, + space.license + ); + updatedLicenses.push(...collaborationLicenses); + + for (const subspace of space.subspaces) { + const subspaceLicenses = await this.applyLicensePolicy(subspace.id); + updatedLicenses.push(...subspaceLicenses); + } + + return updatedLicenses; + } + + private async extendLicensePolicy( + license: ILicense | undefined, + levelZeroSpaceAgent: IAgent + ): Promise { + if (!license || !license.entitlements) { + throw new EntityNotInitializedException( + `License with entitlements not found for Space with agent ${levelZeroSpaceAgent.id}`, + LogContext.LICENSE + ); + } + for (const entitlement of license.entitlements) { + switch (entitlement.type) { + case LicenseEntitlementType.SPACE_FREE: + const spaceFree = + await this.licenseEngineService.isEntitlementGranted( + LicenseEntitlementType.SPACE_FREE, + levelZeroSpaceAgent + ); + if (spaceFree) { + entitlement.limit = 1; + entitlement.enabled = true; + } + break; + case LicenseEntitlementType.SPACE_PLUS: + const spacePlus = + await this.licenseEngineService.isEntitlementGranted( + LicenseEntitlementType.SPACE_PLUS, + levelZeroSpaceAgent + ); + if (spacePlus) { + entitlement.limit = 1; + entitlement.enabled = true; + } + break; + case LicenseEntitlementType.SPACE_PREMIUM: + const spacePremium = + await this.licenseEngineService.isEntitlementGranted( + LicenseEntitlementType.SPACE_PREMIUM, + levelZeroSpaceAgent + ); + if (spacePremium) { + entitlement.limit = 1; + entitlement.enabled = true; + } + break; + case LicenseEntitlementType.SPACE_FLAG_SAVE_AS_TEMPLATE: + const saveAsTemplate = + await this.licenseEngineService.isEntitlementGranted( + LicenseEntitlementType.SPACE_FLAG_SAVE_AS_TEMPLATE, + levelZeroSpaceAgent + ); + if (saveAsTemplate) { + entitlement.limit = 1; + entitlement.enabled = true; + } + break; + case LicenseEntitlementType.SPACE_FLAG_VIRTUAL_CONTRIBUTOR_ACCESS: + const createVirtualContributor = + await this.licenseEngineService.isEntitlementGranted( + LicenseEntitlementType.SPACE_FLAG_VIRTUAL_CONTRIBUTOR_ACCESS, + levelZeroSpaceAgent + ); + if (createVirtualContributor) { + entitlement.limit = 1; + entitlement.enabled = true; + } + break; + case LicenseEntitlementType.SPACE_FLAG_WHITEBOARD_MULTI_USER: + const createInnovationHub = + await this.licenseEngineService.isEntitlementGranted( + LicenseEntitlementType.SPACE_FLAG_WHITEBOARD_MULTI_USER, + levelZeroSpaceAgent + ); + if (createInnovationHub) { + entitlement.limit = 1; + entitlement.enabled = true; + } + break; + + default: + throw new EntityNotInitializedException( + `Unknown entitlement type for Space: ${entitlement.type}`, + LogContext.LICENSE + ); + } + } + + return license; + } +} diff --git a/src/domain/space/space/space.service.spec.ts b/src/domain/space/space/space.service.spec.ts index 0caa8f9ac9..56cf45eb05 100644 --- a/src/domain/space/space/space.service.spec.ts +++ b/src/domain/space/space/space.service.spec.ts @@ -271,6 +271,7 @@ const getSubspacesMock = ( innovationHubs: [], innovationPacks: [], spaces: [], + externalSubscriptionID: '', type: AccountType.ORGANIZATION, ...getEntityMock(), }, @@ -367,6 +368,7 @@ const getSubsubspacesMock = (subsubspaceId: string, count: number): Space[] => { innovationHubs: [], innovationPacks: [], spaces: [], + externalSubscriptionID: '', type: AccountType.ORGANIZATION, ...getEntityMock(), }, @@ -480,6 +482,7 @@ const getSpaceMock = ({ innovationHubs: [], innovationPacks: [], spaces: [], + externalSubscriptionID: '', type: AccountType.ORGANIZATION, ...getEntityMock(), }, diff --git a/src/domain/space/space/space.service.ts b/src/domain/space/space/space.service.ts index 5e32f5627b..36d87eb432 100644 --- a/src/domain/space/space/space.service.ts +++ b/src/domain/space/space/space.service.ts @@ -14,14 +14,7 @@ import { ICommunity } from '@domain/community/community'; import { IContext } from '@domain/context/context'; import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { - DeepPartial, - FindManyOptions, - FindOneOptions, - In, - Not, - Repository, -} from 'typeorm'; +import { FindManyOptions, FindOneOptions, In, Not, Repository } from 'typeorm'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { Space } from './space.entity'; import { ISpace } from './space.interface'; @@ -70,11 +63,9 @@ import { StorageAggregatorType } from '@common/enums/storage.aggregator.type'; import { AccountHostService } from '../account.host/account.host.service'; import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type'; import { LicenseCredential } from '@common/enums/license.credential'; -import { LicensePrivilege } from '@common/enums/license.privilege'; import { LicenseEngineService } from '@core/license-engine/license.engine.service'; import { ISpaceSubscription } from './space.license.subscription.interface'; import { IAccount } from '../account/account.interface'; -import { LicensingService } from '@platform/licensing/licensing.service'; import { LicensePlanType } from '@common/enums/license.plan.type'; import { TemplateType } from '@common/enums/template.type'; import { CreateCollaborationInput } from '@domain/collaboration/collaboration/dto/collaboration.dto.create'; @@ -86,7 +77,13 @@ import { TemplateDefaultType } from '@common/enums/template.default.type'; import { CreateTemplatesManagerInput } from '@domain/template/templates-manager/dto/templates.manager.dto.create'; import { ITemplatesManager } from '@domain/template/templates-manager'; import { Activity } from '@platform/activity'; +import { LicensingFrameworkService } from '@platform/licensing-framework/licensing.framework.service'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; +import { LicenseEntitlementDataType } from '@common/enums/license.entitlement.data.type'; +import { LicenseService } from '@domain/common/license/license.service'; +import { LicenseType } from '@common/enums/license.type'; import { getDiff, hasOnlyAllowedFields } from '@common/utils'; +import { ILicensePlan } from '@platform/license-plan/license.plan.interface'; const EXPLORE_SPACES_LIMIT = 30; const EXPLORE_SPACES_ACTIVITY_DAYS_OLD = 30; @@ -108,8 +105,9 @@ export class SpaceService { private storageAggregatorService: StorageAggregatorService, private templatesManagerService: TemplatesManagerService, private collaborationService: CollaborationService, - private licensingService: LicensingService, + private licensingFrameworkService: LicensingFrameworkService, private licenseEngineService: LicenseEngineService, + private licenseService: LicenseService, @InjectRepository(Space) private spaceRepository: Repository, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService @@ -165,6 +163,48 @@ export class SpaceService { ); space.storageAggregator = storageAggregator; + space.license = await this.licenseService.createLicense({ + type: LicenseType.SPACE, + entitlements: [ + { + type: LicenseEntitlementType.SPACE_FREE, + dataType: LicenseEntitlementDataType.FLAG, + limit: 0, + enabled: false, + }, + { + type: LicenseEntitlementType.SPACE_PLUS, + dataType: LicenseEntitlementDataType.FLAG, + limit: 0, + enabled: false, + }, + { + type: LicenseEntitlementType.SPACE_PREMIUM, + dataType: LicenseEntitlementDataType.FLAG, + limit: 0, + enabled: false, + }, + { + type: LicenseEntitlementType.SPACE_FLAG_SAVE_AS_TEMPLATE, + dataType: LicenseEntitlementDataType.FLAG, + limit: 0, + enabled: false, + }, + { + type: LicenseEntitlementType.SPACE_FLAG_VIRTUAL_CONTRIBUTOR_ACCESS, + dataType: LicenseEntitlementDataType.FLAG, + limit: 0, + enabled: false, + }, + { + type: LicenseEntitlementType.SPACE_FLAG_WHITEBOARD_MULTI_USER, + dataType: LicenseEntitlementDataType.FLAG, + limit: 0, + enabled: true, + }, + ], + }); + const roleSetRolesData = this.spaceDefaultsService.getRoleSetCommunityRoles( space.level ); @@ -218,7 +258,7 @@ export class SpaceService { space.levelZeroSpaceID = space.id; } - // Collaboration: + //// Collaboration let collaborationData: CreateCollaborationInput = spaceData.collaborationData; collaborationData.isTemplate = false; @@ -280,7 +320,9 @@ export class SpaceService { return await this.spaceRepository.save(space); } - async deleteSpace(deleteData: DeleteSpaceInput): Promise { + async deleteSpaceOrFail( + deleteData: DeleteSpaceInput + ): Promise { const space = await this.getSpaceOrFail(deleteData.ID, { relations: { subspaces: true, @@ -291,6 +333,7 @@ export class SpaceService { profile: true, storageAggregator: true, templatesManager: true, + license: true, }, }); @@ -302,7 +345,8 @@ export class SpaceService { !space.agent || !space.profile || !space.storageAggregator || - !space.authorization + !space.authorization || + !space.license ) { throw new RelationshipNotFoundException( `Unable to load entities to delete Space: ${space.id} `, @@ -319,14 +363,17 @@ export class SpaceService { } await this.contextService.removeContext(space.context.id); - await this.collaborationService.deleteCollaboration(space.collaboration.id); - await this.communityService.removeCommunity(space.community.id); + await this.collaborationService.deleteCollaborationOrFail( + space.collaboration.id + ); + await this.communityService.removeCommunityOrFail(space.community.id); await this.profileService.deleteProfile(space.profile.id); await this.agentService.deleteAgent(space.agent.id); + await this.licenseService.removeLicenseOrFail(space.license.id); await this.authorizationPolicyService.delete(space.authorization); if (space.level === SpaceLevel.SPACE) { - if (!space.templatesManager) { + if (!space.templatesManager || !space.templatesManager) { throw new RelationshipNotFoundException( `Unable to load entities to delete base subspace: ${space.id} `, LogContext.SPACES @@ -853,29 +900,6 @@ export class SpaceService { return sortedSubspaces; } - async getLicensePrivileges(space: ISpace): Promise { - let spaceAgent = space.agent; - if (!space.agent) { - const accountWithAgent = await this.getSpaceOrFail(space.id, { - relations: { - agent: { - credentials: true, - }, - }, - }); - spaceAgent = accountWithAgent.agent; - } - if (!spaceAgent) { - throw new EntityNotFoundException( - `Unable to find agent with credentials for Space: ${space.id}`, - LogContext.ACCOUNT - ); - } - const privileges = - await this.licenseEngineService.getGrantedPrivileges(spaceAgent); - return privileges; - } - async getSubscriptions(spaceInput: ISpace): Promise { const space = await this.getSpaceOrFail(spaceInput.id, { relations: { @@ -1046,21 +1070,6 @@ export class SpaceService { ); } - public async getLevelZeroSpaceAgent(space: ISpace): Promise { - const levelZeroSpace = await this.getSpaceOrFail(space.levelZeroSpaceID, { - relations: { - agent: true, - }, - }); - if (!levelZeroSpace.agent) { - throw new RelationshipNotFoundException( - `Agent not initialised on Level Zero Space: ${levelZeroSpace.id}`, - LogContext.SPACES - ); - } - return levelZeroSpace.agent; - } - public async assignUserToRoles(roleSet: IRoleSet, agentInfo: AgentInfo) { await this.roleSetService.assignUserToRole( roleSet, @@ -1279,13 +1288,24 @@ export class SpaceService { public async activeSubscription( space: ISpace ): Promise { - const licensingFramework = - await this.licensingService.getDefaultLicensingOrFail(); - const today = new Date(); - const plans = await this.licensingService.getLicensePlans( - licensingFramework.id - ); + let plans: ILicensePlan[] = []; + + try { + const licensingFramework = + await this.licensingFrameworkService.getDefaultLicensingOrFail(); + + plans = await this.licensingFrameworkService.getLicensePlansOrFail( + licensingFramework.id + ); + } catch (error) { + this.logger.error( + 'Failed to retrieve licensing framework', + error, + LogContext.LICENSE + ); + return undefined; + } return (await this.getSubscriptions(space)) .filter( diff --git a/src/domain/template/template-applier/dto/template.applier.dto.update.collaboration.ts b/src/domain/template/template-applier/dto/template.applier.dto.update.collaboration.ts index 84c9c24678..bbe1faf885 100644 --- a/src/domain/template/template-applier/dto/template.applier.dto.update.collaboration.ts +++ b/src/domain/template/template-applier/dto/template.applier.dto.update.collaboration.ts @@ -16,6 +16,7 @@ export class UpdateCollaborationFromTemplateInput { collaborationTemplateID!: string; @Field(() => Boolean, { + nullable: true, description: 'Add the Callouts from the Collaboration Template', }) addCallouts = false; diff --git a/src/domain/template/template-applier/template.applier.resolver.mutations.ts b/src/domain/template/template-applier/template.applier.resolver.mutations.ts index 032e9a748d..ae629a1ab8 100644 --- a/src/domain/template/template-applier/template.applier.resolver.mutations.ts +++ b/src/domain/template/template-applier/template.applier.resolver.mutations.ts @@ -10,15 +10,20 @@ import { UpdateCollaborationFromTemplateInput } from './dto/template.applier.dto import { TemplateApplierService } from './template.applier.service'; import { CollaborationService } from '@domain/collaboration/collaboration/collaboration.service'; import { ICollaboration } from '@domain/collaboration/collaboration'; -import { StorageAggregatorResolverService } from '@services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service'; +import { RelationshipNotFoundException } from '@common/exceptions'; +import { LogContext } from '@common/enums'; +import { CalloutAuthorizationService } from '@domain/collaboration/callout/callout.service.authorization'; +import { IAuthorizationPolicy } from '@domain/common/authorization-policy/authorization.policy.interface'; +import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; @Resolver() export class TemplateApplierResolverMutations { constructor( private authorizationService: AuthorizationService, private collaborationService: CollaborationService, + private calloutAuthorizationService: CalloutAuthorizationService, private templateApplierService: TemplateApplierService, - private storageAggregatorResolverService: StorageAggregatorResolverService, + private authorizationPolicyService: AuthorizationPolicyService, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService ) {} @@ -32,7 +37,7 @@ export class TemplateApplierResolverMutations { @Args('updateData') updateData: UpdateCollaborationFromTemplateInput ): Promise { - const collaboration = + let targetCollaboration = await this.collaborationService.getCollaborationOrFail( updateData.collaborationID, { @@ -56,21 +61,46 @@ export class TemplateApplierResolverMutations { await this.authorizationService.grantAccessOrFail( agentInfo, - collaboration.authorization, + targetCollaboration.authorization, AuthorizationPrivilege.UPDATE, - `update InnovationFlow states from template: ${collaboration.id}` + `update InnovationFlow states from template: ${targetCollaboration.id}` ); - const storageAggregator = - await this.storageAggregatorResolverService.getStorageAggregatorForCollaboration( - collaboration.id + targetCollaboration = + await this.templateApplierService.updateCollaborationFromTemplate( + updateData, + targetCollaboration, + agentInfo.userID ); - - return await this.templateApplierService.updateCollaborationFromTemplate( - updateData, - collaboration, - storageAggregator, - agentInfo.userID + // Reset the authorization policy to re-evaluate the access control rules. + targetCollaboration = + await this.collaborationService.getCollaborationOrFail( + targetCollaboration.id, + { + relations: { + callouts: true, + authorization: true, + }, + } + ); + if (!targetCollaboration.callouts) { + throw new RelationshipNotFoundException( + `Unable to retrieve callouts for collaboration: ${targetCollaboration.id}`, + LogContext.TEMPLATES + ); + } + const updatedAuthorizations: IAuthorizationPolicy[] = []; + for (const callout of targetCollaboration.callouts) { + const calloutAuthorizations = + await this.calloutAuthorizationService.applyAuthorizationPolicy( + callout.id, + targetCollaboration.authorization + ); + updatedAuthorizations.push(...calloutAuthorizations); + } + await this.authorizationPolicyService.saveAll(updatedAuthorizations); + return this.collaborationService.getCollaborationOrFail( + targetCollaboration.id ); } } diff --git a/src/domain/template/template-applier/template.applier.service.ts b/src/domain/template/template-applier/template.applier.service.ts index 2aa9294522..60730bbf0c 100644 --- a/src/domain/template/template-applier/template.applier.service.ts +++ b/src/domain/template/template-applier/template.applier.service.ts @@ -7,13 +7,8 @@ import { CollaborationService } from '@domain/collaboration/collaboration/collab import { RelationshipNotFoundException } from '@common/exceptions'; import { LogContext } from '@common/enums/logging.context'; import { InputCreatorService } from '@services/api/input-creator/input.creator.service'; -import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface'; -import { CalloutAuthorizationService } from '@domain/collaboration/callout/callout.service.authorization'; -import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; -import { IAuthorizationPolicy } from '@domain/common/authorization-policy'; -import { NamingService } from '@services/infrastructure/naming/naming.service'; import { TagsetReservedName } from '@common/enums/tagset.reserved.name'; -import { ICallout } from '@domain/collaboration/callout'; +import { StorageAggregatorResolverService } from '@services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service'; @Injectable() export class TemplateApplierService { @@ -22,15 +17,12 @@ export class TemplateApplierService { private innovationFlowService: InnovationFlowService, private collaborationService: CollaborationService, private inputCreatorService: InputCreatorService, - private calloutAuthorizationService: CalloutAuthorizationService, - private namingService: NamingService, - private authorizationPolicyService: AuthorizationPolicyService + private storageAggregatorResolverService: StorageAggregatorResolverService ) {} async updateCollaborationFromTemplate( updateData: UpdateCollaborationFromTemplateInput, targetCollaboration: ICollaboration, - storageAggregator: IStorageAggregator, userID: string ): Promise { const collaborationTemplate = await this.templateService.getCollaboration( @@ -64,6 +56,10 @@ export class TemplateApplierService { newStatesStr ); + const storageAggregator = + await this.storageAggregatorResolverService.getStorageAggregatorForCollaboration( + targetCollaboration.id + ); if (updateData.addCallouts) { const calloutsFromTemplate = await this.inputCreatorService.buildCreateCalloutInputsFromCallouts( @@ -80,14 +76,7 @@ export class TemplateApplierService { this.ensureCalloutsInValidGroupsAndStates(targetCollaboration); // Need to save before applying authorization policy to get the callout ids - const result = await this.collaborationService.save(targetCollaboration); - - await this.applyAuthorizationPolicyToNewCallouts( - targetCollaboration, - newCallouts - ); - - return result; + return await this.collaborationService.save(targetCollaboration); } else { this.ensureCalloutsInValidGroupsAndStates(targetCollaboration); return await this.collaborationService.save(targetCollaboration); @@ -118,27 +107,4 @@ export class TemplateApplierService { targetCollaboration.callouts ); } - private async applyAuthorizationPolicyToNewCallouts( - targetCollaboration: ICollaboration, - newCallouts: ICallout[] - ): Promise { - const authorizations: IAuthorizationPolicy[] = []; - - const { roleSet: communityPolicy, spaceSettings } = - await this.namingService.getRoleSetAndSettingsForCollaboration( - targetCollaboration.id - ); - - for (const callout of newCallouts) { - const calloutAuthorizations = - await this.calloutAuthorizationService.applyAuthorizationPolicy( - callout.id, - targetCollaboration.authorization, - communityPolicy, - spaceSettings - ); - authorizations.push(...calloutAuthorizations); - } - return await this.authorizationPolicyService.saveAll(authorizations); - } } diff --git a/src/domain/template/template/template.service.ts b/src/domain/template/template/template.service.ts index 382d1254e5..3fee69b61a 100644 --- a/src/domain/template/template/template.service.ts +++ b/src/domain/template/template/template.service.ts @@ -334,7 +334,7 @@ export class TemplateService { LogContext.TEMPLATES ); } - await this.collaborationService.deleteCollaboration( + await this.collaborationService.deleteCollaborationOrFail( template.collaboration.id ); break; diff --git a/src/domain/template/templates-manager/dto/templates.manager.dto.create..ts b/src/domain/template/templates-manager/dto/templates.manager.dto.create..ts new file mode 100644 index 0000000000..a5ae916571 --- /dev/null +++ b/src/domain/template/templates-manager/dto/templates.manager.dto.create..ts @@ -0,0 +1,5 @@ +import { CreateTemplateDefaultInput } from '@domain/template/template-default/dto/template.default.dto.create'; + +export class CreateTemplatesManagerInput { + templateDefaultsData!: CreateTemplateDefaultInput[]; +} diff --git a/src/domain/timeline/calendar/calendar.resolver.fields.ts b/src/domain/timeline/calendar/calendar.resolver.fields.ts index 335b9a57c2..bfb35a397d 100644 --- a/src/domain/timeline/calendar/calendar.resolver.fields.ts +++ b/src/domain/timeline/calendar/calendar.resolver.fields.ts @@ -9,7 +9,6 @@ import { import { AgentInfo } from '@core/authentication.agent.info/agent.info'; import { CalendarService } from './calendar.service'; import { UUID_NAMEID } from '@domain/common/scalars'; -import { SpaceLevel } from '@common/enums/space.level'; import { SpaceSettingsService } from '@domain/space/space.settings/space.settings.service'; import { ICalendarEvent } from '../event/event.interface'; import { ICalendar } from './calendar.interface'; diff --git a/src/domain/timeline/event/event.entity.ts b/src/domain/timeline/event/event.entity.ts index bc5b84892d..cf5ddd5558 100644 --- a/src/domain/timeline/event/event.entity.ts +++ b/src/domain/timeline/event/event.entity.ts @@ -12,7 +12,6 @@ export class CalendarEvent extends NameableEntity implements ICalendarEvent { type!: CalendarEventType; // toDo fix createdBy circular dependency https://app.zenhub.com/workspaces/alkemio-development-5ecb98b262ebd9f4aec4194c/issues/gh/alkem-io/server/4529 - // @Index('FK_6a30f26ca267009fcf514e0e726') // @OneToOne(() => User, { // eager: false, // cascade: true, diff --git a/src/migrations/1731077703010-allowEventsFromSubspaces.ts b/src/migrations/1731077703010-allowEventsFromSubspaces.ts index e8428c0750..f22e891cd6 100644 --- a/src/migrations/1731077703010-allowEventsFromSubspaces.ts +++ b/src/migrations/1731077703010-allowEventsFromSubspaces.ts @@ -1,31 +1,45 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; +import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AllowEventsFromSubspaces1731077703010 implements MigrationInterface { - name = 'AllowEventsFromSubspaces1731077703010' +export class AllowEventsFromSubspaces1731077703010 + implements MigrationInterface +{ + name = 'AllowEventsFromSubspaces1731077703010'; - public async up(queryRunner: QueryRunner): Promise { - // add setting to calendar event to allow events from subspaces - // defaults to false per requirements - await queryRunner.query(`ALTER TABLE \`calendar_event\` ADD \`visibleOnParentCalendar\` tinyint NOT NULL`); - // add setting to space to allow events from subspaces - const spaceSettings: { id: string, settingsStr: string }[] = await queryRunner.query(`SELECT id, settingsStr FROM alkemio.space;`); - // iterate over all spaces and update settings - for (const { id, settingsStr } of spaceSettings) { - const newSettings = addEventsFromSubspacesSetting(settingsStr); - await queryRunner.query(`UPDATE alkemio.space SET settingsStr = ? WHERE id = ?`, [newSettings, id]); - } + public async up(queryRunner: QueryRunner): Promise { + // add setting to calendar event to allow events from subspaces + // defaults to false per requirements + await queryRunner.query( + `ALTER TABLE \`calendar_event\` ADD \`visibleOnParentCalendar\` tinyint NOT NULL` + ); + // add setting to space to allow events from subspaces + const spaceSettings: { id: string; settingsStr: string }[] = + await queryRunner.query(`SELECT id, settingsStr FROM space;`); + // iterate over all spaces and update settings + for (const { id, settingsStr } of spaceSettings) { + const newSettings = addEventsFromSubspacesSetting(settingsStr); + await queryRunner.query(`UPDATE space SET settingsStr = ? WHERE id = ?`, [ + newSettings, + id, + ]); } + } - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE \`calendar_event\` DROP COLUMN \`visibleOnParentCalendar\``); + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`calendar_event\` DROP COLUMN \`visibleOnParentCalendar\`` + ); - const spaceSettings: { id: string, settingsStr: string }[] = await queryRunner.query(`SELECT id, settingsStr FROM alkemio.space;`); + const spaceSettings: { id: string; settingsStr: string }[] = + await queryRunner.query(`SELECT id, settingsStr FROM space;`); - for (const { id, settingsStr } of spaceSettings) { - const newSettings = removeEventsFromSubspacesSetting(settingsStr); - await queryRunner.query(`UPDATE alkemio.space SET settingsStr = ? WHERE id = ?`, [newSettings, id]); - } + for (const { id, settingsStr } of spaceSettings) { + const newSettings = removeEventsFromSubspacesSetting(settingsStr); + await queryRunner.query(`UPDATE space SET settingsStr = ? WHERE id = ?`, [ + newSettings, + id, + ]); } + } } const addEventsFromSubspacesSetting = (existingSettings: string): string => { @@ -43,7 +57,7 @@ const addEventsFromSubspacesSetting = (existingSettings: string): string => { settings.collaboration.allowEventsFromSubspaces = true; return JSON.stringify(settings); -} +}; const removeEventsFromSubspacesSetting = (existingSettings: string): string => { let settings: ISpaceSettings | undefined; @@ -58,18 +72,20 @@ const removeEventsFromSubspacesSetting = (existingSettings: string): string => { } const { allowEventsFromSubspaces, ...rest } = settings.collaboration; - (settings.collaboration as Omit) = rest; + (settings.collaboration as Omit< + ISpaceSettingsCollaboration, + 'allowEventsFromSubspaces' + >) = rest; return JSON.stringify(settings); -} +}; type ISpaceSettings = { // ...rest are not important for this migration collaboration: ISpaceSettingsCollaboration; -} - +}; type ISpaceSettingsCollaboration = { // ...rest are not important for this migration allowEventsFromSubspaces: boolean; -} +}; diff --git a/src/migrations/1731500015640-licenseEntitlements.ts b/src/migrations/1731500015640-licenseEntitlements.ts new file mode 100644 index 0000000000..39567afcb9 --- /dev/null +++ b/src/migrations/1731500015640-licenseEntitlements.ts @@ -0,0 +1,648 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { randomUUID } from 'crypto'; +import { query } from 'express'; + +export class LicenseEntitlements1731500015640 implements MigrationInterface { + name = 'LicenseEntitlements1731500015640'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`license_plan\` DROP FOREIGN KEY \`FK_3030904030f5d30f483b49905d1\`` + ); + await queryRunner.query( + `ALTER TABLE \`platform\` DROP FOREIGN KEY \`FK_425bbb4b951f7f4629710763fc0\`` + ); + await queryRunner.query( + `DROP INDEX \`REL_425bbb4b951f7f4629710763fc\` ON \`platform\`` + ); + await queryRunner.renameColumn( + 'license_plan', + 'licensingId', + 'licensingFrameworkId' + ); + await queryRunner.renameColumn( + 'platform', + 'licensingId', + 'licensingFrameworkId' + ); + + await queryRunner.renameTable('licensing', 'licensing_framework'); + + await queryRunner.query(`CREATE TABLE \`license_entitlement\` (\`id\` char(36) NOT NULL, + \`createdDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + \`updatedDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + \`version\` int NOT NULL, \`type\` varchar(128) NOT NULL, + \`dataType\` varchar(128) NOT NULL, + \`limit\` int NOT NULL, + \`enabled\` tinyint NOT NULL, + \`licenseId\` char(36) NULL, + PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query(`CREATE TABLE \`license\` (\`id\` char(36) NOT NULL, + \`createdDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + \`updatedDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + \`version\` int NOT NULL, + \`type\` varchar(128) NOT NULL, + \`authorizationId\` char(36) NULL, + UNIQUE INDEX \`REL_bfd01743815f0dd68ac1c5c45c\` (\`authorizationId\`), + PRIMARY KEY (\`id\`)) ENGINE=InnoDB`); + await queryRunner.query( + `ALTER TABLE \`space\` ADD \`licenseId\` char(36) NULL` + ); + await queryRunner.query( + `ALTER TABLE \`space\` ADD UNIQUE INDEX \`IDX_3ef80ef55ba1a1d45e625ea838\` (\`licenseId\`)` + ); + await queryRunner.query( + `ALTER TABLE \`account\` ADD \`externalSubscriptionID\` varchar(128) NULL` + ); + await queryRunner.query( + `ALTER TABLE \`account\` ADD \`licenseId\` char(36) NULL` + ); + await queryRunner.query( + `ALTER TABLE \`account\` ADD UNIQUE INDEX \`IDX_8339e62882f239dc00ff5866f8\` (\`licenseId\`)` + ); + await queryRunner.query( + `ALTER TABLE \`role_set\` ADD \`licenseId\` char(36) NULL` + ); + await queryRunner.query( + `ALTER TABLE \`role_set\` ADD UNIQUE INDEX \`IDX_77f80ef55ba1a1d45e625ea838\` (\`licenseId\`)` + ); + + await queryRunner.query( + `ALTER TABLE \`collaboration\` ADD \`licenseId\` char(36) NULL` + ); + await queryRunner.query( + `ALTER TABLE \`collaboration\` ADD UNIQUE INDEX \`IDX_99f80ef55ba1a1d45e625ea838\` (\`licenseId\`)` + ); + + // Create the license entries with default values + const accounts: { + id: string; + }[] = await queryRunner.query(`SELECT id FROM \`account\``); + for (const account of accounts) { + const licenseID = await this.createLicense(queryRunner, 'account'); + await queryRunner.query( + `UPDATE account SET licenseId = '${licenseID}' WHERE id = '${account.id}'` + ); + await this.createLicenseEntitlement( + queryRunner, + licenseID, + LicenseEntitlementType.ACCOUNT_SPACE_FREE, + LicenseEntitlementDataType.LIMIT + ); + await this.createLicenseEntitlement( + queryRunner, + licenseID, + LicenseEntitlementType.ACCOUNT_SPACE_PLUS, + LicenseEntitlementDataType.LIMIT + ); + await this.createLicenseEntitlement( + queryRunner, + licenseID, + LicenseEntitlementType.ACCOUNT_SPACE_PREMIUM, + LicenseEntitlementDataType.LIMIT + ); + await this.createLicenseEntitlement( + queryRunner, + licenseID, + LicenseEntitlementType.ACCOUNT_VIRTUAL_CONTRIBUTOR, + LicenseEntitlementDataType.LIMIT + ); + await this.createLicenseEntitlement( + queryRunner, + licenseID, + LicenseEntitlementType.ACCOUNT_INNOVATION_PACK, + LicenseEntitlementDataType.LIMIT + ); + await this.createLicenseEntitlement( + queryRunner, + licenseID, + LicenseEntitlementType.ACCOUNT_INNOVATION_HUB, + LicenseEntitlementDataType.LIMIT + ); + } + + const spaces: { + id: string; + }[] = await queryRunner.query(`SELECT id FROM \`space\``); + for (const space of spaces) { + const licenseID = await this.createLicense(queryRunner, 'space'); + await queryRunner.query( + `UPDATE space SET licenseId = '${licenseID}' WHERE id = '${space.id}'` + ); + await this.createLicenseEntitlement( + queryRunner, + licenseID, + LicenseEntitlementType.SPACE_FREE, + LicenseEntitlementDataType.FLAG + ); + await this.createLicenseEntitlement( + queryRunner, + licenseID, + LicenseEntitlementType.SPACE_PLUS, + LicenseEntitlementDataType.FLAG + ); + await this.createLicenseEntitlement( + queryRunner, + licenseID, + LicenseEntitlementType.SPACE_PREMIUM, + LicenseEntitlementDataType.FLAG + ); + await this.createLicenseEntitlement( + queryRunner, + licenseID, + LicenseEntitlementType.SPACE_FLAG_SAVE_AS_TEMPLATE, + LicenseEntitlementDataType.FLAG + ); + await this.createLicenseEntitlement( + queryRunner, + licenseID, + LicenseEntitlementType.SPACE_FLAG_VIRTUAL_CONTRIBUTOR_ACCESS, + LicenseEntitlementDataType.FLAG + ); + await this.createLicenseEntitlement( + queryRunner, + licenseID, + LicenseEntitlementType.SPACE_FLAG_WHITEBOARD_MULTI_USER, + LicenseEntitlementDataType.FLAG + ); + } + + const roleSets: { + id: string; + }[] = await queryRunner.query(`SELECT id FROM \`role_set\``); + for (const roleSet of roleSets) { + const licenseID = await this.createLicense(queryRunner, 'roleset'); + await queryRunner.query( + `UPDATE role_set SET licenseId = '${licenseID}' WHERE id = '${roleSet.id}'` + ); + await this.createLicenseEntitlement( + queryRunner, + licenseID, + LicenseEntitlementType.SPACE_FLAG_VIRTUAL_CONTRIBUTOR_ACCESS, + LicenseEntitlementDataType.FLAG + ); + } + + const collaborations: { + id: string; + }[] = await queryRunner.query(`SELECT id FROM \`collaboration\``); + for (const collaboration of collaborations) { + const licenseID = await this.createLicense(queryRunner, 'collaboration'); + await queryRunner.query( + `UPDATE collaboration SET licenseId = '${licenseID}' WHERE id = '${collaboration.id}'` + ); + await this.createLicenseEntitlement( + queryRunner, + licenseID, + LicenseEntitlementType.SPACE_FLAG_SAVE_AS_TEMPLATE, + LicenseEntitlementDataType.FLAG + ); + await this.createLicenseEntitlement( + queryRunner, + licenseID, + LicenseEntitlementType.SPACE_FLAG_WHITEBOARD_MULTI_USER, + LicenseEntitlementDataType.FLAG + ); + } + + const licensePolicies: { + id: string; + credentialRulesStr: string; + }[] = await queryRunner.query( + `SELECT id, credentialRulesStr FROM license_policy` + ); + for (const policy of licensePolicies) { + await queryRunner.query( + `UPDATE license_policy SET credentialRulesStr = '${JSON.stringify(licenseCredentialRules)}' WHERE id = '${policy.id}'` + ); + } + + await queryRunner.query( + `CREATE UNIQUE INDEX \`REL_3ef80ef55ba1a1d45e625ea838\` ON \`space\` (\`licenseId\`)` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`REL_8339e62882f239dc00ff5866f8\` ON \`account\` (\`licenseId\`)` + ); + await queryRunner.query( + `ALTER TABLE \`license_entitlement\` ADD CONSTRAINT \`FK_44e464f560f510b9fc5fa073397\` FOREIGN KEY (\`licenseId\`) REFERENCES \`license\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`license\` ADD CONSTRAINT \`FK_bfd01743815f0dd68ac1c5c45c0\` FOREIGN KEY (\`authorizationId\`) REFERENCES \`authorization_policy\`(\`id\`) ON DELETE SET NULL ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`space\` ADD CONSTRAINT \`FK_3ef80ef55ba1a1d45e625ea8389\` FOREIGN KEY (\`licenseId\`) REFERENCES \`license\`(\`id\`) ON DELETE SET NULL ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`account\` ADD CONSTRAINT \`FK_8339e62882f239dc00ff5866f8c\` FOREIGN KEY (\`licenseId\`) REFERENCES \`license\`(\`id\`) ON DELETE SET NULL ON UPDATE NO ACTION` + ); + + await this.convergeSchema(queryRunner); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`account\` DROP FOREIGN KEY \`FK_8339e62882f239dc00ff5866f8c\`` + ); + await queryRunner.query( + `ALTER TABLE \`space\` DROP FOREIGN KEY \`FK_3ef80ef55ba1a1d45e625ea8389\`` + ); + await queryRunner.query( + `ALTER TABLE \`license\` DROP FOREIGN KEY \`FK_bfd01743815f0dd68ac1c5c45c0\`` + ); + await queryRunner.query( + `ALTER TABLE \`license_entitlement\` DROP FOREIGN KEY \`FK_44e464f560f510b9fc5fa073397\`` + ); + await queryRunner.query( + `ALTER TABLE \`platform\` DROP FOREIGN KEY \`FK_36d8347a558f81ced8a621fe509\`` + ); + await queryRunner.query( + `ALTER TABLE \`license_plan\` DROP FOREIGN KEY \`FK_9f99adf29316d6aa1d0e8ecae54\`` + ); + await queryRunner.query( + `DROP INDEX \`REL_8339e62882f239dc00ff5866f8\` ON \`account\`` + ); + await queryRunner.query( + `DROP INDEX \`REL_3ef80ef55ba1a1d45e625ea838\` ON \`space\`` + ); + await queryRunner.query( + `DROP INDEX \`REL_36d8347a558f81ced8a621fe50\` ON \`platform\`` + ); + await queryRunner.query( + `ALTER TABLE \`platform\` DROP INDEX \`IDX_36d8347a558f81ced8a621fe50\`` + ); + await queryRunner.query( + `ALTER TABLE \`account\` DROP INDEX \`IDX_8339e62882f239dc00ff5866f8\`` + ); + await queryRunner.query( + `ALTER TABLE \`account\` DROP COLUMN \`licenseId\`` + ); + await queryRunner.query( + `ALTER TABLE \`account\` DROP COLUMN \`externalSubscriptionID\`` + ); + await queryRunner.query( + `ALTER TABLE \`space\` DROP INDEX \`IDX_3ef80ef55ba1a1d45e625ea838\`` + ); + await queryRunner.query(`ALTER TABLE \`space\` DROP COLUMN \`licenseId\``); + await queryRunner.query( + `DROP INDEX \`REL_bfd01743815f0dd68ac1c5c45c\` ON \`license\`` + ); + await queryRunner.query(`DROP TABLE \`license\``); + await queryRunner.query(`DROP TABLE \`license_entitlement\``); + + await queryRunner.query( + `ALTER TABLE \`platform\` CHANGE \`licensingFrameworkId\` \`licensingId\` char(36) NULL` + ); + await queryRunner.query( + `ALTER TABLE \`license_plan\` CHANGE \`licensingFrameworkId\` \`licensingId\` char(36) NULL` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`IDX_dea52ce918df6950019678fa35\` ON \`space\` (\`templatesManagerId\`)` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`REL_425bbb4b951f7f4629710763fc\` ON \`platform\` (\`licensingId\`)` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`IDX_81f92b22d30540102e9654e892\` ON \`platform\` (\`templatesManagerId\`)` + ); + await queryRunner.query( + `ALTER TABLE \`platform\` ADD CONSTRAINT \`FK_425bbb4b951f7f4629710763fc0\` FOREIGN KEY (\`licensingId\`) REFERENCES \`licensing\`(\`id\`) ON DELETE SET NULL ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`license_plan\` ADD CONSTRAINT \`FK_3030904030f5d30f483b49905d1\` FOREIGN KEY (\`licensingId\`) REFERENCES \`licensing\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + private async createAuthorizationPolicy( + queryRunner: QueryRunner, + policyType: string + ): Promise { + const authID = randomUUID(); + await queryRunner.query( + `INSERT INTO authorization_policy (id, version, credentialRules, verifiedCredentialRules, anonymousReadAccess, privilegeRules, type) VALUES + ('${authID}', + 1, '', '', 0, '', '${policyType}')` + ); + return authID; + } + + private async createLicense( + queryRunner: QueryRunner, + type: string + ): Promise { + const templateDefaultID = randomUUID(); + const templateDefaultAuthID = await this.createAuthorizationPolicy( + queryRunner, + 'license' + ); + await queryRunner.query( + `INSERT INTO license (id, version, type, authorizationId) VALUES + ( + '${templateDefaultID}', + 1, + '${type}', + '${templateDefaultAuthID}')` + ); + return templateDefaultID; + } + + private async createLicenseEntitlement( + queryRunner: QueryRunner, + licenseID: string, + entitlementType: LicenseEntitlementType, + entitlementDataType: LicenseEntitlementDataType + ): Promise { + const licenseEntitlementID = randomUUID(); + + await queryRunner.query( + `INSERT INTO license_entitlement (id, version, type, dataType, license_entitlement.limit, enabled, licenseId) VALUES + ( + '${licenseEntitlementID}', + 1, + '${entitlementType}', + '${entitlementDataType}', + 0, + 0, + '${licenseID}' + )` + ); + return licenseEntitlementID; + } + + private async convergeSchema(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`license_entitlement\` DROP FOREIGN KEY \`FK_44e464f560f510b9fc5fa073397\`` + ); + await queryRunner.query( + `DROP INDEX \`IDX_99f80ef55ba1a1d45e625ea838\` ON \`collaboration\`` + ); + await queryRunner.query( + `DROP INDEX \`FK_3030904030f5d30f483b49905d1\` ON \`license_plan\`` + ); + await queryRunner.query( + `DROP INDEX \`IDX_77f80ef55ba1a1d45e625ea838\` ON \`role_set\`` + ); + await queryRunner.query( + `DROP INDEX \`IDX_3ef80ef55ba1a1d45e625ea838\` ON \`space\`` + ); + await queryRunner.query( + `DROP INDEX \`IDX_8339e62882f239dc00ff5866f8\` ON \`account\`` + ); + await queryRunner.query( + `ALTER TABLE \`collaboration\` ADD UNIQUE INDEX \`IDX_aa5815c9577533141cbc4aebe9\` (\`licenseId\`)` + ); + await queryRunner.query( + `ALTER TABLE \`licensing_framework\` DROP FOREIGN KEY \`FK_29b5cd2c555b47f80942dfa4aa7\`` + ); + await queryRunner.query( + `ALTER TABLE \`licensing_framework\` DROP FOREIGN KEY \`FK_427ff5dfcabbc692ed6d71acaea\`` + ); + await queryRunner.query( + `ALTER TABLE \`licensing_framework\` ADD UNIQUE INDEX \`IDX_29b5cd2c555b47f80942dfa4aa\` (\`authorizationId\`)` + ); + await queryRunner.query( + `ALTER TABLE \`licensing_framework\` ADD UNIQUE INDEX \`IDX_427ff5dfcabbc692ed6d71acae\` (\`licensePolicyId\`)` + ); + await queryRunner.query( + `ALTER TABLE \`platform\` ADD UNIQUE INDEX \`IDX_36d8347a558f81ced8a621fe50\` (\`licensingFrameworkId\`)` + ); + await queryRunner.query( + `ALTER TABLE \`role_set\` ADD UNIQUE INDEX \`IDX_c25bfb0c837427dd54e250b240\` (\`licenseId\`)` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`REL_aa5815c9577533141cbc4aebe9\` ON \`collaboration\` (\`licenseId\`)` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`REL_29b5cd2c555b47f80942dfa4aa\` ON \`licensing_framework\` (\`authorizationId\`)` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`REL_427ff5dfcabbc692ed6d71acae\` ON \`licensing_framework\` (\`licensePolicyId\`)` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`REL_36d8347a558f81ced8a621fe50\` ON \`platform\` (\`licensingFrameworkId\`)` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`REL_c25bfb0c837427dd54e250b240\` ON \`role_set\` (\`licenseId\`)` + ); + await queryRunner.query( + `ALTER TABLE \`license_entitlement\` ADD CONSTRAINT \`FK_badab780c9f3e196d98ab324686\` FOREIGN KEY (\`licenseId\`) REFERENCES \`license\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`collaboration\` ADD CONSTRAINT \`FK_aa5815c9577533141cbc4aebe9f\` FOREIGN KEY (\`licenseId\`) REFERENCES \`license\`(\`id\`) ON DELETE SET NULL ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`license_plan\` ADD CONSTRAINT \`FK_9f99adf29316d6aa1d0e8ecae54\` FOREIGN KEY (\`licensingFrameworkId\`) REFERENCES \`licensing_framework\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`licensing_framework\` ADD CONSTRAINT \`FK_29b5cd2c555b47f80942dfa4aa7\` FOREIGN KEY (\`authorizationId\`) REFERENCES \`authorization_policy\`(\`id\`) ON DELETE SET NULL ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`licensing_framework\` ADD CONSTRAINT \`FK_427ff5dfcabbc692ed6d71acaea\` FOREIGN KEY (\`licensePolicyId\`) REFERENCES \`license_policy\`(\`id\`) ON DELETE SET NULL ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`platform\` ADD CONSTRAINT \`FK_36d8347a558f81ced8a621fe509\` FOREIGN KEY (\`licensingFrameworkId\`) REFERENCES \`licensing_framework\`(\`id\`) ON DELETE SET NULL ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`role_set\` ADD CONSTRAINT \`FK_c25bfb0c837427dd54e250b240e\` FOREIGN KEY (\`licenseId\`) REFERENCES \`license\`(\`id\`) ON DELETE SET NULL ON UPDATE NO ACTION` + ); + await queryRunner.query( + 'DROP INDEX `IDX_aa5815c9577533141cbc4aebe9` ON `collaboration`' + ); + await queryRunner.query( + 'DROP INDEX `IDX_29b5cd2c555b47f80942dfa4aa` ON `licensing_framework`' + ); + await queryRunner.query( + 'DROP INDEX `IDX_427ff5dfcabbc692ed6d71acae` ON `licensing_framework`' + ); + await queryRunner.query( + 'DROP INDEX `REL_0c6a4d0a6c13a3f5df6ac01509` ON `licensing_framework`' + ); + await queryRunner.query( + 'DROP INDEX `REL_a5dae5a376dd49c7c076893d40` ON `licensing_framework`' + ); + await queryRunner.query( + 'DROP INDEX `IDX_36d8347a558f81ced8a621fe50` ON `platform`' + ); + await queryRunner.query( + 'DROP INDEX `IDX_c25bfb0c837427dd54e250b240` ON `role_set`' + ); + } + + private async divergeSchema(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`role_set\` DROP FOREIGN KEY \`FK_c25bfb0c837427dd54e250b240e\`` + ); + await queryRunner.query( + `ALTER TABLE \`platform\` DROP FOREIGN KEY \`FK_36d8347a558f81ced8a621fe509\`` + ); + await queryRunner.query( + `ALTER TABLE \`licensing_framework\` DROP FOREIGN KEY \`FK_427ff5dfcabbc692ed6d71acaea\`` + ); + await queryRunner.query( + `ALTER TABLE \`licensing_framework\` DROP FOREIGN KEY \`FK_29b5cd2c555b47f80942dfa4aa7\`` + ); + await queryRunner.query( + `ALTER TABLE \`license_plan\` DROP FOREIGN KEY \`FK_9f99adf29316d6aa1d0e8ecae54\`` + ); + await queryRunner.query( + `ALTER TABLE \`collaboration\` DROP FOREIGN KEY \`FK_aa5815c9577533141cbc4aebe9f\`` + ); + await queryRunner.query( + `ALTER TABLE \`license_entitlement\` DROP FOREIGN KEY \`FK_badab780c9f3e196d98ab324686\`` + ); + await queryRunner.query( + `DROP INDEX \`REL_c25bfb0c837427dd54e250b240\` ON \`role_set\`` + ); + await queryRunner.query( + `DROP INDEX \`REL_36d8347a558f81ced8a621fe50\` ON \`platform\`` + ); + await queryRunner.query( + `DROP INDEX \`REL_427ff5dfcabbc692ed6d71acae\` ON \`licensing_framework\`` + ); + await queryRunner.query( + `DROP INDEX \`REL_29b5cd2c555b47f80942dfa4aa\` ON \`licensing_framework\`` + ); + await queryRunner.query( + `DROP INDEX \`REL_aa5815c9577533141cbc4aebe9\` ON \`collaboration\`` + ); + await queryRunner.query( + `ALTER TABLE \`role_set\` DROP INDEX \`IDX_c25bfb0c837427dd54e250b240\`` + ); + await queryRunner.query( + `ALTER TABLE \`platform\` DROP INDEX \`IDX_36d8347a558f81ced8a621fe50\`` + ); + await queryRunner.query( + `ALTER TABLE \`licensing_framework\` DROP INDEX \`IDX_427ff5dfcabbc692ed6d71acae\`` + ); + await queryRunner.query( + `ALTER TABLE \`licensing_framework\` DROP INDEX \`IDX_29b5cd2c555b47f80942dfa4aa\`` + ); + await queryRunner.query( + `ALTER TABLE \`licensing_framework\` ADD CONSTRAINT \`FK_427ff5dfcabbc692ed6d71acaea\` FOREIGN KEY (\`licensePolicyId\`) REFERENCES \`license_policy\`(\`id\`) ON DELETE SET NULL ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`licensing_framework\` ADD CONSTRAINT \`FK_29b5cd2c555b47f80942dfa4aa7\` FOREIGN KEY (\`authorizationId\`) REFERENCES \`authorization_policy\`(\`id\`) ON DELETE SET NULL ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`collaboration\` DROP INDEX \`IDX_aa5815c9577533141cbc4aebe9\`` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`IDX_8339e62882f239dc00ff5866f8\` ON \`account\` (\`licenseId\`)` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`IDX_3ef80ef55ba1a1d45e625ea838\` ON \`space\` (\`licenseId\`)` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`IDX_77f80ef55ba1a1d45e625ea838\` ON \`role_set\` (\`licenseId\`)` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`REL_0c6a4d0a6c13a3f5df6ac01509\` ON \`licensing_framework\` (\`authorizationId\`)` + ); + await queryRunner.query( + `CREATE INDEX \`FK_3030904030f5d30f483b49905d1\` ON \`license_plan\` (\`licensingFrameworkId\`)` + ); + await queryRunner.query( + `CREATE UNIQUE INDEX \`IDX_99f80ef55ba1a1d45e625ea838\` ON \`collaboration\` (\`licenseId\`)` + ); + await queryRunner.query( + `ALTER TABLE \`license_entitlement\` ADD CONSTRAINT \`FK_44e464f560f510b9fc5fa073397\` FOREIGN KEY (\`licenseId\`) REFERENCES \`license\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + 'CREATE UNIQUE INDEX `IDX_c25bfb0c837427dd54e250b240` ON `role_set` (`licenseId`)' + ); + await queryRunner.query( + 'CREATE UNIQUE INDEX `IDX_36d8347a558f81ced8a621fe50` ON `platform` (`licensingFrameworkId`)' + ); + await queryRunner.query( + 'CREATE UNIQUE INDEX `REL_a5dae5a376dd49c7c076893d40` ON `licensing_framework` (`licensePolicyId`)' + ); + await queryRunner.query( + 'CREATE UNIQUE INDEX `REL_0c6a4d0a6c13a3f5df6ac01509` ON `licensing_framework` (`authorizationId`)' + ); + await queryRunner.query( + 'CREATE UNIQUE INDEX `IDX_427ff5dfcabbc692ed6d71acae` ON `licensing_framework` (`licensePolicyId`)' + ); + await queryRunner.query( + 'CREATE UNIQUE INDEX `IDX_29b5cd2c555b47f80942dfa4aa` ON `licensing_framework` (`authorizationId`)' + ); + await queryRunner.query( + 'CREATE UNIQUE INDEX `IDX_aa5815c9577533141cbc4aebe9` ON `collaboration` (`licenseId`)' + ); + + await this.divergeSchema(queryRunner); + } +} + +enum LicenseEntitlementType { + ACCOUNT_SPACE_FREE = 'account-space-free', + ACCOUNT_SPACE_PLUS = 'account-space-plus', + ACCOUNT_SPACE_PREMIUM = 'account-space-premium', + ACCOUNT_VIRTUAL_CONTRIBUTOR = 'account-virtual-contributor', + ACCOUNT_INNOVATION_PACK = 'account-innovation-pack', + ACCOUNT_INNOVATION_HUB = 'account-innovation-hub', + SPACE_FREE = 'space-free', + SPACE_PLUS = 'space-plus', + SPACE_PREMIUM = 'space-premium', + SPACE_FLAG_SAVE_AS_TEMPLATE = 'space-flag-save-as-template', + SPACE_FLAG_VIRTUAL_CONTRIBUTOR_ACCESS = 'space-flag-virtual-contributor-access', + SPACE_FLAG_WHITEBOARD_MULTI_USER = 'space-flag-whiteboard-multi-user', +} + +enum LicenseEntitlementDataType { + LIMIT = 'limit', + FLAG = 'flag', +} + +const licenseCredentialRules = [ + { + credentialType: 'space-feature-virtual-contributors', + grantedEntitlements: [ + LicenseEntitlementType.SPACE_FLAG_VIRTUAL_CONTRIBUTOR_ACCESS, + ], + name: 'Space Virtual Contributors', + }, + { + credentialType: 'space-feature-whiteboard-multi-user', + grantedEntitlements: [ + LicenseEntitlementType.SPACE_FLAG_WHITEBOARD_MULTI_USER, + ], + name: 'Space Multi-user whiteboards', + }, + { + credentialType: 'space-feature-save-as-template', + grantedEntitlements: [LicenseEntitlementType.SPACE_FLAG_SAVE_AS_TEMPLATE], + name: 'Space Save As Templatet', + }, + { + credentialType: 'space-license-free', + grantedEntitlements: [LicenseEntitlementType.SPACE_FREE], + name: 'Space License Free', + }, + { + credentialType: 'space-license-plus', + grantedEntitlements: [ + LicenseEntitlementType.SPACE_PLUS, + LicenseEntitlementType.SPACE_FLAG_WHITEBOARD_MULTI_USER, + LicenseEntitlementType.SPACE_FLAG_SAVE_AS_TEMPLATE, + ], + name: 'Space License Plus', + }, + { + credentialType: 'space-license-premium', + grantedEntitlements: [ + LicenseEntitlementType.SPACE_PREMIUM, + LicenseEntitlementType.SPACE_FLAG_WHITEBOARD_MULTI_USER, + LicenseEntitlementType.SPACE_FLAG_SAVE_AS_TEMPLATE, + ], + name: 'Space License Premium', + }, + { + credentialType: 'account-license-plus', + grantedEntitlements: [ + LicenseEntitlementType.ACCOUNT_SPACE_FREE, + LicenseEntitlementType.ACCOUNT_VIRTUAL_CONTRIBUTOR, + LicenseEntitlementType.ACCOUNT_INNOVATION_HUB, + LicenseEntitlementType.ACCOUNT_INNOVATION_PACK, + ], + name: 'Account License Plus', + }, +]; diff --git a/src/platform/admin/licensing/admin.licensing.module.ts b/src/platform/admin/licensing/admin.licensing.module.ts index 225a0424a5..2ed1f86889 100644 --- a/src/platform/admin/licensing/admin.licensing.module.ts +++ b/src/platform/admin/licensing/admin.licensing.module.ts @@ -1,23 +1,23 @@ import { AuthorizationModule } from '@core/authorization/authorization.module'; -import { AuthorizationPolicyModule } from '@domain/common/authorization-policy/authorization.policy.module'; import { Module } from '@nestjs/common'; import { AdminLicensingResolverMutations } from './admin.licensing.resolver.mutations'; import { AdminLicensingService } from './admin.licensing.service'; -import { LicensingModule } from '@platform/licensing/licensing.module'; import { LicenseIssuerModule } from '@platform/license-issuer/license.issuer.module'; import { SpaceModule } from '@domain/space/space/space.module'; import { AccountModule } from '@domain/space/account/account.module'; import { AccountHostModule } from '@domain/space/account.host/account.host.module'; +import { LicensingFrameworkModule } from '@platform/licensing-framework/licensing.framework.module'; +import { LicenseModule } from '@domain/common/license/license.module'; @Module({ imports: [ AccountModule, AccountHostModule, SpaceModule, - LicensingModule, + LicensingFrameworkModule, + LicenseModule, LicenseIssuerModule, AuthorizationModule, - AuthorizationPolicyModule, ], providers: [AdminLicensingResolverMutations, AdminLicensingService], exports: [], diff --git a/src/platform/admin/licensing/admin.licensing.resolver.mutations.ts b/src/platform/admin/licensing/admin.licensing.resolver.mutations.ts index cadd9c5489..b51bccc0d3 100644 --- a/src/platform/admin/licensing/admin.licensing.resolver.mutations.ts +++ b/src/platform/admin/licensing/admin.licensing.resolver.mutations.ts @@ -6,26 +6,30 @@ import { AgentInfo } from '@core/authentication.agent.info/agent.info'; import { AuthorizationService } from '@core/authorization/authorization.service'; import { AuthorizationPrivilege } from '@common/enums/authorization.privilege'; import { AssignLicensePlanToSpace } from './dto/admin.licensing.dto.assign.license.plan.to.space'; -import { LicensingService } from '@platform/licensing/licensing.service'; -import { ILicensing } from '@platform/licensing/licensing.interface'; import { AdminLicensingService } from './admin.licensing.service'; import { RevokeLicensePlanFromSpace } from './dto/admin.licensing.dto.revoke.license.plan.from.space'; -import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; import { ISpace } from '@domain/space/space/space.interface'; -import { SpaceAuthorizationService } from '@domain/space/space/space.service.authorization'; import { IAccount } from '@domain/space/account/account.interface'; import { AssignLicensePlanToAccount } from './dto/admin.licensing.dto.assign.license.plan.to.account'; -import { AccountAuthorizationService } from '@domain/space/account/account.service.authorization'; import { RevokeLicensePlanFromAccount } from './dto/admin.licensing.dto.revoke.license.plan.from.account'; +import { AccountLicenseService } from '@domain/space/account/account.service.license'; +import { LicenseService } from '@domain/common/license/license.service'; +import { LicensingFrameworkService } from '@platform/licensing-framework/licensing.framework.service'; +import { ILicensingFramework } from '@platform/licensing-framework/licensing.framework.interface'; +import { SpaceLicenseService } from '@domain/space/space/space.service.license'; +import { SpaceService } from '@domain/space/space/space.service'; +import { AccountService } from '@domain/space/account/account.service'; @Resolver() export class AdminLicensingResolverMutations { constructor( private authorizationService: AuthorizationService, - private authorizationPolicyService: AuthorizationPolicyService, - private spaceAuthorizationService: SpaceAuthorizationService, - private accountAuthorizationService: AccountAuthorizationService, - private licensingService: LicensingService, + private spaceService: SpaceService, + private spaceLicenseService: SpaceLicenseService, + private accountService: AccountService, + private accountLicenseService: AccountLicenseService, + private licensingFrameworkService: LicensingFrameworkService, + private licenseService: LicenseService, private adminLicensingService: AdminLicensingService ) {} @@ -38,13 +42,14 @@ export class AdminLicensingResolverMutations { @CurrentUser() agentInfo: AgentInfo, @Args('planData') planData: AssignLicensePlanToAccount ): Promise { - let licensing: ILicensing | undefined; + let licensing: ILicensingFramework | undefined; if (planData.licensingID) { - licensing = await this.licensingService.getLicensingOrFail( + licensing = await this.licensingFrameworkService.getLicensingOrFail( planData.licensingID ); } else { - licensing = await this.licensingService.getDefaultLicensingOrFail(); + licensing = + await this.licensingFrameworkService.getDefaultLicensingOrFail(); } this.authorizationService.grantAccessOrFail( @@ -58,12 +63,13 @@ export class AdminLicensingResolverMutations { planData, licensing.id ); - // Need to trigger an authorization reset as some license credentials are used in auth policy e.g. VCs feature flag - const updatedAuthorizations = - await this.accountAuthorizationService.applyAuthorizationPolicy(account); - await this.authorizationPolicyService.saveAll(updatedAuthorizations); - return account; + const updatedLicenses = await this.accountLicenseService.applyLicensePolicy( + account.id + ); + await this.licenseService.saveAll(updatedLicenses); + + return this.accountService.getAccountOrFail(account.id); } @UseGuards(GraphqlGuard) @@ -75,13 +81,14 @@ export class AdminLicensingResolverMutations { @CurrentUser() agentInfo: AgentInfo, @Args('planData') planData: AssignLicensePlanToSpace ): Promise { - let licensing: ILicensing | undefined; + let licensing: ILicensingFramework | undefined; if (planData.licensingID) { - licensing = await this.licensingService.getLicensingOrFail( + licensing = await this.licensingFrameworkService.getLicensingOrFail( planData.licensingID ); } else { - licensing = await this.licensingService.getDefaultLicensingOrFail(); + licensing = + await this.licensingFrameworkService.getDefaultLicensingOrFail(); } this.authorizationService.grantAccessOrFail( @@ -95,12 +102,13 @@ export class AdminLicensingResolverMutations { planData, licensing.id ); - // Need to trigger an authorization reset as some license credentials are used in auth policy e.g. VCs feature flag - const updatedAuthorizations = - await this.spaceAuthorizationService.applyAuthorizationPolicy(space); - await this.authorizationPolicyService.saveAll(updatedAuthorizations); - return space; + const updatedLicenses = await this.spaceLicenseService.applyLicensePolicy( + space.id + ); + await this.licenseService.saveAll(updatedLicenses); + + return this.spaceService.getSpaceOrFail(space.id); } @UseGuards(GraphqlGuard) @@ -112,13 +120,14 @@ export class AdminLicensingResolverMutations { @CurrentUser() agentInfo: AgentInfo, @Args('planData') planData: RevokeLicensePlanFromAccount ): Promise { - let licensing: ILicensing | undefined; + let licensing: ILicensingFramework | undefined; if (planData.licensingID) { - licensing = await this.licensingService.getLicensingOrFail( + licensing = await this.licensingFrameworkService.getLicensingOrFail( planData.licensingID ); } else { - licensing = await this.licensingService.getDefaultLicensingOrFail(); + licensing = + await this.licensingFrameworkService.getDefaultLicensingOrFail(); } this.authorizationService.grantAccessOrFail( @@ -133,11 +142,13 @@ export class AdminLicensingResolverMutations { planData, licensing.id ); - // Need to trigger an authorization reset as some license credentials are used in auth policy e.g. VCs feature flag - const updatedAuthorizations = - await this.accountAuthorizationService.applyAuthorizationPolicy(account); - await this.authorizationPolicyService.saveAll(updatedAuthorizations); - return account; + + const updatedLicenses = await this.accountLicenseService.applyLicensePolicy( + account.id + ); + await this.licenseService.saveAll(updatedLicenses); + + return this.accountService.getAccountOrFail(account.id); } @UseGuards(GraphqlGuard) @@ -149,13 +160,14 @@ export class AdminLicensingResolverMutations { @CurrentUser() agentInfo: AgentInfo, @Args('planData') planData: RevokeLicensePlanFromSpace ): Promise { - let licensing: ILicensing | undefined; + let licensing: ILicensingFramework | undefined; if (planData.licensingID) { - licensing = await this.licensingService.getLicensingOrFail( + licensing = await this.licensingFrameworkService.getLicensingOrFail( planData.licensingID ); } else { - licensing = await this.licensingService.getDefaultLicensingOrFail(); + licensing = + await this.licensingFrameworkService.getDefaultLicensingOrFail(); } this.authorizationService.grantAccessOrFail( @@ -169,9 +181,37 @@ export class AdminLicensingResolverMutations { planData, licensing.id ); - const updatedAuthorizations = - await this.spaceAuthorizationService.applyAuthorizationPolicy(space); - await this.authorizationPolicyService.saveAll(updatedAuthorizations); - return space; + + const updatedLicenses = await this.spaceLicenseService.applyLicensePolicy( + space.id + ); + await this.licenseService.saveAll(updatedLicenses); + return this.spaceService.getSpaceOrFail(space.id); + } + + @UseGuards(GraphqlGuard) + @Mutation(() => ISpace, { + description: 'Reset all license plans on Accounts', + }) + @Profiling.api + async resetLicenseOnAccounts( + @CurrentUser() agentInfo: AgentInfo + ): Promise { + const licensing = + await this.licensingFrameworkService.getDefaultLicensingOrFail(); + + this.authorizationService.grantAccessOrFail( + agentInfo, + licensing.authorization, + AuthorizationPrivilege.GRANT, + 'reset licenses on accounts' + ); + + const accounts = await this.adminLicensingService.getAllAccounts(); + for (const account of accounts) { + const updatedLicenses = + await this.accountLicenseService.applyLicensePolicy(account.id); + await this.licenseService.saveAll(updatedLicenses); + } } } diff --git a/src/platform/admin/licensing/admin.licensing.service.ts b/src/platform/admin/licensing/admin.licensing.service.ts index d6a2b3e2af..7e5bab4606 100644 --- a/src/platform/admin/licensing/admin.licensing.service.ts +++ b/src/platform/admin/licensing/admin.licensing.service.ts @@ -3,7 +3,6 @@ import { EntityNotInitializedException } from '@common/exceptions/entity.not.ini import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { AssignLicensePlanToSpace } from './dto/admin.licensing.dto.assign.license.plan.to.space'; -import { LicensingService } from '@platform/licensing/licensing.service'; import { LicenseIssuerService } from '@platform/license-issuer/license.issuer.service'; import { RevokeLicensePlanFromSpace } from './dto/admin.licensing.dto.revoke.license.plan.from.space'; import { SpaceService } from '@domain/space/space/space.service'; @@ -14,14 +13,19 @@ import { RevokeLicensePlanFromAccount } from './dto/admin.licensing.dto.revoke.l import { IAccount } from '@domain/space/account/account.interface'; import { LicensePlanType } from '@common/enums/license.plan.type'; import { ValidationException } from '@common/exceptions'; +import { LicensingFrameworkService } from '@platform/licensing-framework/licensing.framework.service'; +import { EntityManager } from 'typeorm'; +import { InjectEntityManager } from '@nestjs/typeorm'; @Injectable() export class AdminLicensingService { constructor( private accountHostService: AccountHostService, - private licensingService: LicensingService, + private licensingFrameworkService: LicensingFrameworkService, private licenseIssuerService: LicenseIssuerService, private spaceService: SpaceService, + @InjectEntityManager('default') + private entityManager: EntityManager, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService ) {} @@ -29,10 +33,11 @@ export class AdminLicensingService { licensePlanData: AssignLicensePlanToSpace, licensingID: string ): Promise { - const licensePlan = await this.licensingService.getLicensePlanOrFail( - licensingID, - licensePlanData.licensePlanID - ); + const licensePlan = + await this.licensingFrameworkService.getLicensePlanOrFail( + licensingID, + licensePlanData.licensePlanID + ); const isLicensePlanTypeForSpaces = licensePlan.type === LicensePlanType.SPACE_FEATURE_FLAG || licensePlan.type === LicensePlanType.SPACE_PLAN; @@ -71,10 +76,11 @@ export class AdminLicensingService { licensePlanData: RevokeLicensePlanFromSpace, licensingID: string ): Promise { - const licensePlan = await this.licensingService.getLicensePlanOrFail( - licensingID, - licensePlanData.licensePlanID - ); + const licensePlan = + await this.licensingFrameworkService.getLicensePlanOrFail( + licensingID, + licensePlanData.licensePlanID + ); const isLicensePlanTypeForSpaces = licensePlan.type === LicensePlanType.SPACE_FEATURE_FLAG || licensePlan.type === LicensePlanType.SPACE_PLAN; @@ -113,10 +119,11 @@ export class AdminLicensingService { licensePlanData: AssignLicensePlanToAccount, licensingID: string ): Promise { - const licensePlan = await this.licensingService.getLicensePlanOrFail( - licensingID, - licensePlanData.licensePlanID - ); + const licensePlan = + await this.licensingFrameworkService.getLicensePlanOrFail( + licensingID, + licensePlanData.licensePlanID + ); const isLicensePlanTypeForAccounts = licensePlan.type === LicensePlanType.ACCOUNT_PLAN || licensePlan.type === LicensePlanType.ACCOUNT_FEATURE_FLAG; @@ -154,10 +161,11 @@ export class AdminLicensingService { licensePlanData: RevokeLicensePlanFromAccount, licensingID: string ): Promise { - const licensePlan = await this.licensingService.getLicensePlanOrFail( - licensingID, - licensePlanData.licensePlanID - ); + const licensePlan = + await this.licensingFrameworkService.getLicensePlanOrFail( + licensingID, + licensePlanData.licensePlanID + ); const isLicensePlanTypeForAccounts = licensePlan.type === LicensePlanType.ACCOUNT_PLAN || licensePlan.type === LicensePlanType.ACCOUNT_FEATURE_FLAG; @@ -191,4 +199,12 @@ export class AdminLicensingService { return account; } + + public async getAllAccounts(): Promise { + return this.entityManager.find(IAccount, { + relations: { + license: true, + }, + }); + } } diff --git a/src/platform/license-plan/license.plan.entity.ts b/src/platform/license-plan/license.plan.entity.ts index 2674045725..7330fc6582 100644 --- a/src/platform/license-plan/license.plan.entity.ts +++ b/src/platform/license-plan/license.plan.entity.ts @@ -1,19 +1,19 @@ import { Column, Entity, ManyToOne } from 'typeorm'; import { ILicensePlan } from './license.plan.interface'; import { BaseAlkemioEntity } from '@domain/common/entity/base-entity'; -import { Licensing } from '@platform/licensing/licensing.entity'; import { LicenseCredential } from '@common/enums/license.credential'; import { LicensePlanType } from '@common/enums/license.plan.type'; import { ENUM_LENGTH } from '@common/constants'; +import { LicensingFramework } from '@platform/licensing-framework/licensing.framework.entity'; @Entity() export class LicensePlan extends BaseAlkemioEntity implements ILicensePlan { - @ManyToOne(() => Licensing, licensing => licensing.plans, { + @ManyToOne(() => LicensingFramework, licensing => licensing.plans, { eager: false, cascade: false, onDelete: 'CASCADE', }) - licensing?: Licensing; + licensingFramework?: LicensingFramework; @Column('text', { nullable: false }) name!: string; diff --git a/src/platform/license-plan/license.plan.interface.ts b/src/platform/license-plan/license.plan.interface.ts index bf4098de70..5ad77486a6 100644 --- a/src/platform/license-plan/license.plan.interface.ts +++ b/src/platform/license-plan/license.plan.interface.ts @@ -1,12 +1,12 @@ import { Field, ObjectType } from '@nestjs/graphql'; import { IBaseAlkemio } from '@domain/common/entity/base-entity'; -import { ILicensing } from '@platform/licensing/licensing.interface'; import { LicenseCredential } from '@common/enums/license.credential'; import { LicensePlanType } from '@common/enums/license.plan.type'; +import { ILicensingFramework } from '@platform/licensing-framework/licensing.framework.interface'; @ObjectType('LicensePlan') export abstract class ILicensePlan extends IBaseAlkemio { - licensing?: ILicensing; + licensingFramework?: ILicensingFramework; @Field(() => String, { description: 'The name of the License Plan', diff --git a/src/platform/license-plan/license.plan.resolver.mutations.ts b/src/platform/license-plan/license.plan.resolver.mutations.ts index 48b209ae2c..c0f80102cb 100644 --- a/src/platform/license-plan/license.plan.resolver.mutations.ts +++ b/src/platform/license-plan/license.plan.resolver.mutations.ts @@ -31,13 +31,13 @@ export class LicensePlanResolverMutations { deleteData.ID, { relations: { - licensing: { + licensingFramework: { authorization: true, }, }, } ); - if (!licensePlan.licensing) { + if (!licensePlan.licensingFramework) { throw new EntityNotFoundException( `Unable to find Licensing for LicensePlan with ID: ${deleteData.ID}`, LogContext.LICENSE @@ -45,7 +45,7 @@ export class LicensePlanResolverMutations { } await this.authorizationService.grantAccessOrFail( agentInfo, - licensePlan.licensing.authorization, + licensePlan.licensingFramework.authorization, AuthorizationPrivilege.DELETE, `deleteLicensePlan: ${licensePlan.id}` ); @@ -65,13 +65,13 @@ export class LicensePlanResolverMutations { updateData.ID, { relations: { - licensing: { + licensingFramework: { authorization: true, }, }, } ); - if (!licensePlan.licensing) { + if (!licensePlan.licensingFramework) { throw new EntityNotFoundException( `Unable to find Licensing for LicensePlan with ID: ${updateData.ID}`, LogContext.LICENSE @@ -79,7 +79,7 @@ export class LicensePlanResolverMutations { } await this.authorizationService.grantAccessOrFail( agentInfo, - licensePlan.licensing.authorization, + licensePlan.licensingFramework.authorization, AuthorizationPrivilege.UPDATE, `update LicensePlan: ${licensePlan.id}` ); diff --git a/src/platform/license-policy/license.policy.service.ts b/src/platform/license-policy/license.policy.service.ts index 6fb344ad42..14500ff600 100644 --- a/src/platform/license-policy/license.policy.service.ts +++ b/src/platform/license-policy/license.policy.service.ts @@ -6,10 +6,10 @@ import { ILicensePolicy } from './license.policy.interface'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { LicensePolicy } from './license.policy.entity'; import { LicenseEngineService } from '@core/license-engine/license.engine.service'; -import { LicensePrivilege } from '@common/enums/license.privilege'; import { LogContext } from '@common/enums/logging.context'; import { ILicensePolicyCredentialRule } from '@core/license-engine'; import { LicenseCredential } from '@common/enums/license.credential'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; @Injectable() export class LicensePolicyService { @@ -22,12 +22,12 @@ export class LicensePolicyService { ) {} createCredentialRule( - grantedPrivileges: LicensePrivilege[], + grantedEntitlements: LicenseEntitlementType[], credentialType: LicenseCredential, name: string ): ILicensePolicyCredentialRule { return { - grantedPrivileges, + grantedEntitlements: grantedEntitlements, credentialType, name, }; diff --git a/src/platform/licensing/dto/license.manager.dto.create.license.plan.ts b/src/platform/licensing-framework/dto/licensing.framework.dto.create.license.plan.ts similarity index 65% rename from src/platform/licensing/dto/license.manager.dto.create.license.plan.ts rename to src/platform/licensing-framework/dto/licensing.framework.dto.create.license.plan.ts index 34fd33cda0..71b07be9df 100644 --- a/src/platform/licensing/dto/license.manager.dto.create.license.plan.ts +++ b/src/platform/licensing-framework/dto/licensing.framework.dto.create.license.plan.ts @@ -3,7 +3,7 @@ import { Field, InputType } from '@nestjs/graphql'; import { CreateLicensePlanInput } from '@platform/license-plan'; @InputType() -export class CreateLicensePlanOnLicensingInput extends CreateLicensePlanInput { +export class CreateLicensePlanOnLicensingFrameworkInput extends CreateLicensePlanInput { @Field(() => UUID, { nullable: false }) - licensingID!: string; + licensingFrameworkID!: string; } diff --git a/src/platform/licensing/licensing.entity.ts b/src/platform/licensing-framework/licensing.framework.entity.ts similarity index 76% rename from src/platform/licensing/licensing.entity.ts rename to src/platform/licensing-framework/licensing.framework.entity.ts index b00b2c7085..e626b156d6 100644 --- a/src/platform/licensing/licensing.entity.ts +++ b/src/platform/licensing-framework/licensing.framework.entity.ts @@ -1,12 +1,15 @@ import { AuthorizableEntity } from '@domain/common/entity/authorizable-entity'; import { Entity, JoinColumn, OneToMany, OneToOne } from 'typeorm'; -import { ILicensing } from './licensing.interface'; +import { ILicensingFramework } from './licensing.framework.interface'; import { LicensePolicy } from '@platform/license-policy/license.policy.entity'; import { LicensePlan } from '@platform/license-plan/license.plan.entity'; @Entity() -export class Licensing extends AuthorizableEntity implements ILicensing { - @OneToMany(() => LicensePlan, licensePlan => licensePlan.licensing, { +export class LicensingFramework + extends AuthorizableEntity + implements ILicensingFramework +{ + @OneToMany(() => LicensePlan, licensePlan => licensePlan.licensingFramework, { eager: true, cascade: true, }) diff --git a/src/platform/licensing/licensing.interface.ts b/src/platform/licensing-framework/licensing.framework.interface.ts similarity index 84% rename from src/platform/licensing/licensing.interface.ts rename to src/platform/licensing-framework/licensing.framework.interface.ts index 581ffd1cd0..04707d8a20 100644 --- a/src/platform/licensing/licensing.interface.ts +++ b/src/platform/licensing-framework/licensing.framework.interface.ts @@ -4,7 +4,7 @@ import { ILicensePlan } from '@platform/license-plan/license.plan.interface'; import { ILicensePolicy } from '@platform/license-policy/license.policy.interface'; @ObjectType('Licensing') -export abstract class ILicensing extends IAuthorizable { +export abstract class ILicensingFramework extends IAuthorizable { plans!: ILicensePlan[]; licensePolicy!: ILicensePolicy; diff --git a/src/platform/licensing-framework/licensing.framework.module.ts b/src/platform/licensing-framework/licensing.framework.module.ts new file mode 100644 index 0000000000..9d5ba45975 --- /dev/null +++ b/src/platform/licensing-framework/licensing.framework.module.ts @@ -0,0 +1,29 @@ +import { AuthorizationModule } from '@core/authorization/authorization.module'; +import { AuthorizationPolicyModule } from '@domain/common/authorization-policy/authorization.policy.module'; +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { LicensingFramework } from './licensing.framework.entity'; +import { LicensingFrameworkResolverFields } from './licensing.framework.resolver.fields'; +import { LicensingFrameworkResolverMutations } from './licensing.framework.resolver.mutations'; +import { LicensingFrameworkService } from './licensing.framework.service'; +import { LicensingFrameworkAuthorizationService } from './licensing.framework.service.authorization'; +import { LicensePlanModule } from '@platform/license-plan/license.plan.module'; +import { LicensePolicyModule } from '@platform/license-policy/license.policy.module'; + +@Module({ + imports: [ + LicensePlanModule, + LicensePolicyModule, + AuthorizationModule, + AuthorizationPolicyModule, + TypeOrmModule.forFeature([LicensingFramework]), + ], + providers: [ + LicensingFrameworkResolverMutations, + LicensingFrameworkResolverFields, + LicensingFrameworkService, + LicensingFrameworkAuthorizationService, + ], + exports: [LicensingFrameworkService, LicensingFrameworkAuthorizationService], +}) +export class LicensingFrameworkModule {} diff --git a/src/platform/licensing/licensing.resolver.fields.ts b/src/platform/licensing-framework/licensing.framework.resolver.fields.ts similarity index 55% rename from src/platform/licensing/licensing.resolver.fields.ts rename to src/platform/licensing-framework/licensing.framework.resolver.fields.ts index 732baa38ac..691f1bd084 100644 --- a/src/platform/licensing/licensing.resolver.fields.ts +++ b/src/platform/licensing-framework/licensing.framework.resolver.fields.ts @@ -3,14 +3,14 @@ import { GraphqlGuard } from '@core/authorization'; import { UseGuards } from '@nestjs/common'; import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; import { AuthorizationAgentPrivilege } from '@src/common/decorators'; -import { ILicensing } from './licensing.interface'; -import { LicensingService } from './licensing.service'; +import { ILicensingFramework } from './licensing.framework.interface'; +import { LicensingFrameworkService } from './licensing.framework.service'; import { ILicensePlan } from '@platform/license-plan/license.plan.interface'; import { ILicensePolicy } from '@platform/license-policy'; -@Resolver(() => ILicensing) -export class LicensingResolverFields { - constructor(private licensingService: LicensingService) {} +@Resolver(() => ILicensingFramework) +export class LicensingFrameworkResolverFields { + constructor(private licensingFrameworkService: LicensingFrameworkService) {} @AuthorizationAgentPrivilege(AuthorizationPrivilege.READ) @ResolveField('plans', () => [ILicensePlan], { @@ -18,15 +18,19 @@ export class LicensingResolverFields { description: 'The License Plans in use on the platform.', }) @UseGuards(GraphqlGuard) - async plans(@Parent() licensing: ILicensing): Promise { - return await this.licensingService.getLicensePlans(licensing.id); + async plans( + @Parent() licensing: ILicensingFramework + ): Promise { + return await this.licensingFrameworkService.getLicensePlansOrFail( + licensing.id + ); } @ResolveField('policy', () => ILicensePolicy, { nullable: false, description: 'The LicensePolicy in use by the Licensing setup.', }) - policy(@Parent() licensing: ILicensing): Promise { - return this.licensingService.getLicensePolicy(licensing.id); + policy(@Parent() licensing: ILicensingFramework): Promise { + return this.licensingFrameworkService.getLicensePolicy(licensing.id); } } diff --git a/src/platform/licensing/licensing.resolver.mutations.ts b/src/platform/licensing-framework/licensing.framework.resolver.mutations.ts similarity index 61% rename from src/platform/licensing/licensing.resolver.mutations.ts rename to src/platform/licensing-framework/licensing.framework.resolver.mutations.ts index 9bc9d6b2fd..2717b5ac23 100644 --- a/src/platform/licensing/licensing.resolver.mutations.ts +++ b/src/platform/licensing-framework/licensing.framework.resolver.mutations.ts @@ -5,15 +5,15 @@ import { GraphqlGuard } from '@core/authorization'; import { AgentInfo } from '@core/authentication.agent.info/agent.info'; import { AuthorizationService } from '@core/authorization/authorization.service'; import { AuthorizationPrivilege } from '@common/enums/authorization.privilege'; -import { LicensingService } from './licensing.service'; -import { CreateLicensePlanOnLicensingInput } from './dto/license.manager.dto.create.license.plan'; +import { LicensingFrameworkService } from './licensing.framework.service'; +import { CreateLicensePlanOnLicensingFrameworkInput } from './dto/licensing.framework.dto.create.license.plan'; import { ILicensePlan } from '@platform/license-plan/license.plan.interface'; @Resolver() -export class LicensingResolverMutations { +export class LicensingFrameworkResolverMutations { constructor( private authorizationService: AuthorizationService, - private licensingService: LicensingService + private licensingFrameworkService: LicensingFrameworkService ) {} @UseGuards(GraphqlGuard) @@ -23,19 +23,19 @@ export class LicensingResolverMutations { @Profiling.api async createLicensePlan( @CurrentUser() agentInfo: AgentInfo, - @Args('planData') planData: CreateLicensePlanOnLicensingInput + @Args('planData') planData: CreateLicensePlanOnLicensingFrameworkInput ): Promise { - const licensing = await this.licensingService.getLicensingOrFail( - planData.licensingID + const licensing = await this.licensingFrameworkService.getLicensingOrFail( + planData.licensingFrameworkID ); this.authorizationService.grantAccessOrFail( agentInfo, licensing.authorization, AuthorizationPrivilege.CREATE, - `create licensePlan on licensing: ${licensing.id}` + `create licensePlan on licensing framework: ${licensing.id}` ); - return await this.licensingService.createLicensePlan(planData); + return await this.licensingFrameworkService.createLicensePlan(planData); } } diff --git a/src/platform/licensing/licensing.service.authorization.ts b/src/platform/licensing-framework/licensing.framework.service.authorization.ts similarity index 89% rename from src/platform/licensing/licensing.service.authorization.ts rename to src/platform/licensing-framework/licensing.framework.service.authorization.ts index 8b90195748..7c6afaab7e 100644 --- a/src/platform/licensing/licensing.service.authorization.ts +++ b/src/platform/licensing-framework/licensing.framework.service.authorization.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; -import { LicensingService } from './licensing.service'; -import { ILicensing } from './licensing.interface'; +import { LicensingFrameworkService } from './licensing.framework.service'; +import { ILicensingFramework } from './licensing.framework.interface'; import { IAuthorizationPolicy } from '@domain/common/authorization-policy'; import { EntityNotInitializedException, @@ -17,20 +17,20 @@ import { IAuthorizationPolicyRuleCredential } from '@core/authorization/authoriz import { CREDENTIAL_RULE_LICENSE_MANAGER } from '@common/constants/authorization/credential.rule.constants'; @Injectable() -export class LicensingAuthorizationService { +export class LicensingFrameworkAuthorizationService { constructor( private authorizationPolicyService: AuthorizationPolicyService, - private licensingService: LicensingService, + private licensingFrameworkService: LicensingFrameworkService, private licensePolicyAuthorizationService: LicensePolicyAuthorizationService ) {} async applyAuthorizationPolicy( - licensingInput: ILicensing, + licensingInput: ILicensingFramework, parentAuthorization: IAuthorizationPolicy | undefined ): Promise { let licensing = licensingInput; if (!licensing.licensePolicy) { - licensing = await this.licensingService.getLicensingOrFail( + licensing = await this.licensingFrameworkService.getLicensingOrFail( licensingInput.id, { relations: { diff --git a/src/platform/licensing/licensing.service.ts b/src/platform/licensing-framework/licensing.framework.service.ts similarity index 72% rename from src/platform/licensing/licensing.service.ts rename to src/platform/licensing-framework/licensing.framework.service.ts index 7fee0e7922..53174259d7 100644 --- a/src/platform/licensing/licensing.service.ts +++ b/src/platform/licensing-framework/licensing.framework.service.ts @@ -5,27 +5,27 @@ import { Inject, Injectable, LoggerService } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { FindOneOptions, Repository } from 'typeorm'; -import { CreateLicensePlanOnLicensingInput } from './dto/license.manager.dto.create.license.plan'; -import { Licensing } from './licensing.entity'; -import { ILicensing } from './licensing.interface'; +import { CreateLicensePlanOnLicensingFrameworkInput } from './dto/licensing.framework.dto.create.license.plan'; +import { LicensingFramework } from './licensing.framework.entity'; +import { ILicensingFramework } from './licensing.framework.interface'; import { ILicensePlan } from '@platform/license-plan/license.plan.interface'; import { LicensePlanService } from '@platform/license-plan/license.plan.service'; import { ILicensePolicy } from '@platform/license-policy/license.policy.interface'; @Injectable() -export class LicensingService { +export class LicensingFrameworkService { constructor( private licensePlanService: LicensePlanService, - @InjectRepository(Licensing) - private licensingRepository: Repository, + @InjectRepository(LicensingFramework) + private licensingFrameworkRepository: Repository, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService ) {} async getLicensingOrFail( licensingID: string, - options?: FindOneOptions - ): Promise { - const licensing = await this.licensingRepository.findOne({ + options?: FindOneOptions + ): Promise { + const licensing = await this.licensingFrameworkRepository.findOne({ where: { id: licensingID }, ...options, }); @@ -40,9 +40,9 @@ export class LicensingService { } async getDefaultLicensingOrFail( - options?: FindOneOptions - ): Promise { - const licensingFrameworks = await this.licensingRepository.find({ + options?: FindOneOptions + ): Promise { + const licensingFrameworks = await this.licensingFrameworkRepository.find({ ...options, }); @@ -55,11 +55,15 @@ export class LicensingService { return licensingFrameworks[0]; } - public async save(licensing: ILicensing): Promise { - return this.licensingRepository.save(licensing); + public async save( + licensing: ILicensingFramework + ): Promise { + return this.licensingFrameworkRepository.save(licensing); } - public async getLicensePlans(licensingID: string): Promise { + public async getLicensePlansOrFail( + licensingID: string + ): Promise { const licensing = await this.getLicensingOrFail(licensingID, { relations: { plans: true, @@ -78,7 +82,7 @@ export class LicensingService { licensingID: string, planID: string ): Promise { - const licensePlans = await this.getLicensePlans(licensingID); + const licensePlans = await this.getLicensePlansOrFail(licensingID); const plan = licensePlans.find(plan => plan.id === planID); if (!plan) { throw new EntityNotFoundException( @@ -91,10 +95,10 @@ export class LicensingService { } public async createLicensePlan( - licensePlanData: CreateLicensePlanOnLicensingInput + licensePlanData: CreateLicensePlanOnLicensingFrameworkInput ): Promise { const licensing = await this.getLicensingOrFail( - licensePlanData.licensingID, + licensePlanData.licensingFrameworkID, { relations: { plans: true, @@ -110,7 +114,7 @@ export class LicensingService { const licensePlan = await this.licensePlanService.createLicensePlan(licensePlanData); licensing.plans.push(licensePlan); - await this.licensingRepository.save(licensing); + await this.licensingFrameworkRepository.save(licensing); return licensePlan; } diff --git a/src/platform/licensing/licensing.module.ts b/src/platform/licensing/licensing.module.ts deleted file mode 100644 index 483af3a118..0000000000 --- a/src/platform/licensing/licensing.module.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AuthorizationModule } from '@core/authorization/authorization.module'; -import { AuthorizationPolicyModule } from '@domain/common/authorization-policy/authorization.policy.module'; -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { Licensing } from './licensing.entity'; -import { LicensingResolverFields } from './licensing.resolver.fields'; -import { LicensingResolverMutations } from './licensing.resolver.mutations'; -import { LicensingService } from './licensing.service'; -import { LicensingAuthorizationService } from './licensing.service.authorization'; -import { LicensePlanModule } from '@platform/license-plan/license.plan.module'; -import { LicensePolicyModule } from '@platform/license-policy/license.policy.module'; - -@Module({ - imports: [ - LicensePlanModule, - LicensePolicyModule, - AuthorizationModule, - AuthorizationPolicyModule, - TypeOrmModule.forFeature([Licensing]), - ], - providers: [ - LicensingResolverMutations, - LicensingResolverFields, - LicensingService, - LicensingAuthorizationService, - ], - exports: [LicensingService, LicensingAuthorizationService], -}) -export class LicensingModule {} diff --git a/src/platform/platform.role/platform.role.module.ts b/src/platform/platform.role/platform.role.module.ts index 039e7f0be0..f519d1e055 100644 --- a/src/platform/platform.role/platform.role.module.ts +++ b/src/platform/platform.role/platform.role.module.ts @@ -11,10 +11,12 @@ import { AgentModule } from '@domain/agent/agent/agent.module'; import { PlatformInvitationModule } from '@platform/invitation/platform.invitation.module'; import { AccountModule } from '@domain/space/account/account.module'; import { AuthorizationPolicyModule } from '@domain/common/authorization-policy/authorization.policy.module'; +import { LicenseModule } from '@domain/common/license/license.module'; @Module({ imports: [ AccountModule, + LicenseModule, AuthorizationModule, AuthorizationPolicyModule, PlatformModule, diff --git a/src/platform/platform.role/platform.role.resolver.mutations.ts b/src/platform/platform.role/platform.role.resolver.mutations.ts index 413b3abdac..fb37bd499f 100644 --- a/src/platform/platform.role/platform.role.resolver.mutations.ts +++ b/src/platform/platform.role/platform.role.resolver.mutations.ts @@ -18,17 +18,19 @@ import { IPlatformInvitation } from '@platform/invitation/platform.invitation.in import { PlatformRoleService } from './platform.role.service'; import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; import { AccountService } from '@domain/space/account/account.service'; -import { AccountAuthorizationService } from '@domain/space/account/account.service.authorization'; +import { AccountLicenseService } from '@domain/space/account/account.service.license'; +import { LicenseService } from '@domain/common/license/license.service'; @Resolver() export class PlatformRoleResolverMutations { constructor( private accountService: AccountService, - private accountAuthorizationService: AccountAuthorizationService, + private accountLicenseService: AccountLicenseService, private authorizationService: AuthorizationService, private authorizationPolicyService: AuthorizationPolicyService, private notificationAdapter: NotificationAdapter, private platformRoleService: PlatformRoleService, + private licenseService: LicenseService, private platformAuthorizationPolicyService: PlatformAuthorizationPolicyService ) {} @@ -62,7 +64,7 @@ export class PlatformRoleResolverMutations { membershipData.role === PlatformRole.BETA_TESTER || membershipData.role === PlatformRole.VC_CAMPAIGN ) { - await this.resetAuthorizationForUserAccount(user); + await this.resetLicenseForUserAccount(user); } this.notifyPlatformGlobalRoleChange( @@ -102,7 +104,7 @@ export class PlatformRoleResolverMutations { membershipData.role === PlatformRole.BETA_TESTER || membershipData.role === PlatformRole.VC_CAMPAIGN ) { - await this.resetAuthorizationForUserAccount(user); + await this.resetLicenseForUserAccount(user); } this.notifyPlatformGlobalRoleChange( @@ -114,11 +116,12 @@ export class PlatformRoleResolverMutations { return user; } - private async resetAuthorizationForUserAccount(user: IUser) { + private async resetLicenseForUserAccount(user: IUser) { const account = await this.accountService.getAccountOrFail(user.accountID); - const authorizations = - await this.accountAuthorizationService.applyAuthorizationPolicy(account); - await this.authorizationPolicyService.saveAll(authorizations); + const licenses = await this.accountLicenseService.applyLicensePolicy( + account.id + ); + await this.licenseService.saveAll(licenses); } @UseGuards(GraphqlGuard) diff --git a/src/platform/platform/platform.entity.ts b/src/platform/platform/platform.entity.ts index 155159157a..5ac831e1ff 100644 --- a/src/platform/platform/platform.entity.ts +++ b/src/platform/platform/platform.entity.ts @@ -3,10 +3,10 @@ import { Library } from '@library/library/library.entity'; import { Entity, JoinColumn, OneToMany, OneToOne } from 'typeorm'; import { IPlatform } from './platform.interface'; import { StorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.entity'; -import { Licensing } from '@platform/licensing/licensing.entity'; import { Forum } from '@platform/forum/forum.entity'; import { PlatformInvitation } from '@platform/invitation/platform.invitation.entity'; import { TemplatesManager } from '@domain/template/templates-manager/templates.manager.entity'; +import { LicensingFramework } from '@platform/licensing-framework/licensing.framework.entity'; @Entity() export class Platform extends AuthorizableEntity implements IPlatform { @@ -42,13 +42,13 @@ export class Platform extends AuthorizableEntity implements IPlatform { @JoinColumn() storageAggregator!: StorageAggregator; - @OneToOne(() => Licensing, { + @OneToOne(() => LicensingFramework, { eager: false, cascade: true, onDelete: 'SET NULL', }) @JoinColumn() - licensing?: Licensing; + licensingFramework?: LicensingFramework; @OneToMany( () => PlatformInvitation, diff --git a/src/platform/platform/platform.interface.ts b/src/platform/platform/platform.interface.ts index 73b487536d..0b8b70dcc2 100644 --- a/src/platform/platform/platform.interface.ts +++ b/src/platform/platform/platform.interface.ts @@ -6,7 +6,7 @@ import { ObjectType } from '@nestjs/graphql'; import { IConfig } from '@platform/configuration/config/config.interface'; import { IForum } from '@platform/forum'; import { IPlatformInvitation } from '@platform/invitation'; -import { ILicensing } from '@platform/licensing/licensing.interface'; +import { ILicensingFramework } from '@platform/licensing-framework/licensing.framework.interface'; import { IMetadata } from '@platform/metadata/metadata.interface'; @ObjectType('Platform') @@ -16,7 +16,7 @@ export abstract class IPlatform extends IAuthorizable { configuration?: IConfig; metadata?: IMetadata; storageAggregator!: IStorageAggregator; - licensing?: ILicensing; + licensingFramework?: ILicensingFramework; platformInvitations!: IPlatformInvitation[]; templatesManager?: ITemplatesManager; } diff --git a/src/platform/platform/platform.module.ts b/src/platform/platform/platform.module.ts index fec66a1df1..eea3920b82 100644 --- a/src/platform/platform/platform.module.ts +++ b/src/platform/platform/platform.module.ts @@ -13,10 +13,10 @@ import { PlatformAuthorizationService } from './platform.service.authorization'; import { KonfigModule } from '@platform/configuration/config/config.module'; import { MetadataModule } from '@platform/metadata/metadata.module'; import { StorageAggregatorModule } from '@domain/storage/storage-aggregator/storage.aggregator.module'; -import { LicensingModule } from '@platform/licensing/licensing.module'; import { ForumModule } from '@platform/forum/forum.module'; import { PlatformInvitationModule } from '@platform/invitation/platform.invitation.module'; import { TemplatesManagerModule } from '@domain/template/templates-manager/templates.manager.module'; +import { LicensingFrameworkModule } from '@platform/licensing-framework/licensing.framework.module'; @Module({ imports: [ @@ -28,7 +28,7 @@ import { TemplatesManagerModule } from '@domain/template/templates-manager/templ StorageAggregatorModule, KonfigModule, MetadataModule, - LicensingModule, + LicensingFrameworkModule, PlatformInvitationModule, TemplatesManagerModule, TypeOrmModule.forFeature([Platform]), diff --git a/src/platform/platform/platform.resolver.fields.ts b/src/platform/platform/platform.resolver.fields.ts index 5e45ceb010..76201f9c0f 100644 --- a/src/platform/platform/platform.resolver.fields.ts +++ b/src/platform/platform/platform.resolver.fields.ts @@ -11,9 +11,9 @@ import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.a import { GraphqlGuard } from '@core/authorization'; import { UseGuards } from '@nestjs/common'; import { ReleaseDiscussionOutput } from './dto/release.discussion.dto'; -import { ILicensing } from '@platform/licensing/licensing.interface'; import { IForum } from '@platform/forum'; import { ITemplatesManager } from '@domain/template/templates-manager/templates.manager.interface'; +import { ILicensingFramework } from '@platform/licensing-framework/licensing.framework.interface'; @Resolver(() => IPlatform) export class PlatformResolverFields { @@ -60,12 +60,14 @@ export class PlatformResolverFields { return this.platformService.getStorageAggregator(platform); } - @ResolveField('licensing', () => ILicensing, { + @ResolveField('licensingFramework', () => ILicensingFramework, { nullable: false, description: 'The Licensing in use by the platform.', }) - licensing(@Parent() platform: IPlatform): Promise { - return this.platformService.getLicensing(platform); + licensingFramework( + @Parent() platform: IPlatform + ): Promise { + return this.platformService.getLicensingFramework(platform); } @ResolveField(() => IConfig, { diff --git a/src/platform/platform/platform.service.authorization.ts b/src/platform/platform/platform.service.authorization.ts index 4586cbe22b..4c205f90dc 100644 --- a/src/platform/platform/platform.service.authorization.ts +++ b/src/platform/platform/platform.service.authorization.ts @@ -22,11 +22,11 @@ import { } from '@common/constants'; import { StorageAggregatorAuthorizationService } from '@domain/storage/storage-aggregator/storage.aggregator.service.authorization'; import { RelationshipNotFoundException } from '@common/exceptions/relationship.not.found.exception'; -import { LicensingAuthorizationService } from '@platform/licensing/licensing.service.authorization'; import { ForumAuthorizationService } from '@platform/forum/forum.service.authorization'; import { PlatformInvitationAuthorizationService } from '@platform/invitation/platform.invitation.service.authorization'; import { LibraryAuthorizationService } from '@library/library/library.service.authorization'; import { TemplatesManagerAuthorizationService } from '@domain/template/templates-manager/templates.manager.service.authorization'; +import { LicensingFrameworkAuthorizationService } from '@platform/licensing-framework/licensing.framework.service.authorization'; @Injectable() export class PlatformAuthorizationService { @@ -38,7 +38,7 @@ export class PlatformAuthorizationService { private storageAggregatorAuthorizationService: StorageAggregatorAuthorizationService, private platformInvitationAuthorizationService: PlatformInvitationAuthorizationService, private libraryAuthorizationService: LibraryAuthorizationService, - private licensingAuthorizationService: LicensingAuthorizationService, + private licensingFrameworkAuthorizationService: LicensingFrameworkAuthorizationService, private templatesManagerAuthorizationService: TemplatesManagerAuthorizationService ) {} @@ -50,7 +50,7 @@ export class PlatformAuthorizationService { forum: true, library: true, storageAggregator: true, - licensing: true, + licensingFramework: true, templatesManager: true, }, }); @@ -61,7 +61,7 @@ export class PlatformAuthorizationService { !platform.library || !platform.forum || !platform.storageAggregator || - !platform.licensing || + !platform.licensingFramework || !platform.templatesManager ) throw new RelationshipNotFoundException( @@ -137,8 +137,8 @@ export class PlatformAuthorizationService { updatedAuthorizations.push(...storageAuthorizations); const platformLicensingAuthorizations = - await this.licensingAuthorizationService.applyAuthorizationPolicy( - platform.licensing, + await this.licensingFrameworkAuthorizationService.applyAuthorizationPolicy( + platform.licensingFramework, platform.authorization ); updatedAuthorizations.push(...platformLicensingAuthorizations); diff --git a/src/platform/platform/platform.service.ts b/src/platform/platform/platform.service.ts index 3560b14af8..15e407982a 100644 --- a/src/platform/platform/platform.service.ts +++ b/src/platform/platform/platform.service.ts @@ -15,12 +15,12 @@ import { IPlatform } from './platform.interface'; import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface'; import { IAuthorizationPolicy } from '@domain/common/authorization-policy'; import { ReleaseDiscussionOutput } from './dto/release.discussion.dto'; -import { ILicensing } from '@platform/licensing/licensing.interface'; import { ForumService } from '@platform/forum/forum.service'; import { IForum } from '@platform/forum/forum.interface'; import { ForumDiscussionCategory } from '@common/enums/forum.discussion.category'; import { Discussion } from '@platform/forum-discussion/discussion.entity'; import { ITemplatesManager } from '@domain/template/templates-manager/templates.manager.interface'; +import { ILicensingFramework } from '@platform/licensing-framework/licensing.framework.interface'; @Injectable() export class PlatformService { @@ -132,13 +132,15 @@ export class PlatformService { return storageAggregator; } - async getLicensing(platformInput: IPlatform): Promise { + async getLicensingFramework( + platformInput: IPlatform + ): Promise { const platform = await this.getPlatformOrFail({ relations: { - licensing: true, + licensingFramework: true, }, }); - const licensing = platform.licensing; + const licensing = platform.licensingFramework; if (!licensing) { throw new EntityNotFoundException( diff --git a/src/services/api/conversion/conversion.resolver.mutations.ts b/src/services/api/conversion/conversion.resolver.mutations.ts index b7bddf534f..fa5d259d7f 100644 --- a/src/services/api/conversion/conversion.resolver.mutations.ts +++ b/src/services/api/conversion/conversion.resolver.mutations.ts @@ -54,7 +54,7 @@ export class ConversionResolverMutations { AuthorizationPrivilege.PLATFORM_ADMIN, `convert challenge to space: ${agentInfo.email}` ); - let space = await this.conversionService.convertChallengeToSpace( + let space = await this.conversionService.convertChallengeToSpaceOrFail( convertChallengeToSpaceData, agentInfo ); @@ -83,10 +83,11 @@ export class ConversionResolverMutations { AuthorizationPrivilege.CREATE, `convert opportunity to challenge: ${agentInfo.email}` ); - let subspace = await this.conversionService.convertOpportunityToChallenge( - convertOpportunityToChallengeData.subsubspaceID, - agentInfo - ); + let subspace = + await this.conversionService.convertOpportunityToChallengeOrFail( + convertOpportunityToChallengeData.subsubspaceID, + agentInfo + ); subspace = await this.spaceService.save(subspace); const subspaceAuthorizations = await this.spaceAuthorizationService.applyAuthorizationPolicy(subspace); diff --git a/src/services/api/conversion/conversion.service.ts b/src/services/api/conversion/conversion.service.ts index c56b5cd568..eeda65be1f 100644 --- a/src/services/api/conversion/conversion.service.ts +++ b/src/services/api/conversion/conversion.service.ts @@ -38,10 +38,10 @@ export class ConversionService { @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService ) {} - async convertChallengeToSpace( + async convertChallengeToSpaceOrFail( conversionData: ConvertSubspaceToSpaceInput, agentInfo: AgentInfo - ): Promise { + ): Promise { // TODO: needs to create a new ACCOUNT etc. NOT TRUE! const subspace = await this.spaceService.getSpaceOrFail( conversionData.subspaceID, @@ -208,19 +208,19 @@ export class ConversionService { // Now migrate all the child subsubspaces... const subsubspaces = await this.spaceService.getSubspaces(updatedSubspace); for (const subsubspace of subsubspaces) { - await this.convertOpportunityToChallenge(subsubspace.id, agentInfo); + await this.convertOpportunityToChallengeOrFail(subsubspace.id, agentInfo); } // Finally delete the Challenge - await this.spaceService.deleteSpace({ + await this.spaceService.deleteSpaceOrFail({ ID: updatedSubspace.id, }); return space; } - async convertOpportunityToChallenge( + async convertOpportunityToChallengeOrFail( subsubspaceID: string, agentInfo: AgentInfo - ): Promise { + ): Promise { const subsubspace = await this.spaceService.getSpaceOrFail(subsubspaceID, { relations: { parentSpace: { @@ -394,7 +394,7 @@ export class ConversionService { // Save both + then re-assign the roles await this.spaceService.save(subspace); const updatedOpportunity = await this.spaceService.save(subsubspace); - await this.spaceService.deleteSpace({ ID: updatedOpportunity.id }); + await this.spaceService.deleteSpaceOrFail({ ID: updatedOpportunity.id }); // Assign users to roles in new challenge await this.assignContributors( diff --git a/src/services/api/registration/registration.service.ts b/src/services/api/registration/registration.service.ts index 13a4e2f8f3..93318b656a 100644 --- a/src/services/api/registration/registration.service.ts +++ b/src/services/api/registration/registration.service.ts @@ -183,7 +183,7 @@ export class RegistrationService { const account = await this.userService.getAccount(user); user = await this.userService.deleteUser(deleteData); - await this.accountService.deleteAccount(account); + await this.accountService.deleteAccountOrFail(account); return user; } @@ -206,7 +206,7 @@ export class RegistrationService { organization = await this.organizationService.deleteOrganization(deleteData); - await this.accountService.deleteAccount(account); + await this.accountService.deleteAccountOrFail(account); organization.id = organizationID; return organization; } diff --git a/src/services/api/roles/roles.service.spec.ts b/src/services/api/roles/roles.service.spec.ts index bbd1454f7d..8fc48c3628 100644 --- a/src/services/api/roles/roles.service.spec.ts +++ b/src/services/api/roles/roles.service.spec.ts @@ -246,6 +246,7 @@ const getSpaceRoleResultMock = ({ virtualContributors: [], innovationHubs: [], innovationPacks: [], + externalSubscriptionID: '', spaces: [], type: AccountType.ORGANIZATION, }, diff --git a/src/services/auth-reset/auth-reset.payload.interface.ts b/src/services/auth-reset/auth-reset.payload.interface.ts index 0a953b13d2..1edbdb33ab 100644 --- a/src/services/auth-reset/auth-reset.payload.interface.ts +++ b/src/services/auth-reset/auth-reset.payload.interface.ts @@ -1,4 +1,4 @@ -import { AUTH_RESET_EVENT_TYPE } from './event.type'; +import { RESET_EVENT_TYPE } from './reset.event.type'; /** * The payload type of the event @@ -7,7 +7,7 @@ export interface AuthResetEventPayload { /** * the type of the event */ - type: AUTH_RESET_EVENT_TYPE; + type: RESET_EVENT_TYPE; /** * the uuid of the entity */ diff --git a/src/services/auth-reset/event.type.ts b/src/services/auth-reset/event.type.ts deleted file mode 100644 index 511c5244e8..0000000000 --- a/src/services/auth-reset/event.type.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum AUTH_RESET_EVENT_TYPE { - ACCOUNT = 'account-reset', - USER = 'user-reset', - ORGANIZATION = 'organization-reset', - PLATFORM = 'platform-reset', -} diff --git a/src/services/auth-reset/publisher/auth-reset.service.ts b/src/services/auth-reset/publisher/auth-reset.service.ts index ba1bf0aa24..1befb40b7f 100644 --- a/src/services/auth-reset/publisher/auth-reset.service.ts +++ b/src/services/auth-reset/publisher/auth-reset.service.ts @@ -8,7 +8,7 @@ import { Organization } from '@domain/community/organization'; import { AUTH_RESET_SERVICE } from '@common/constants'; import { AlkemioErrorStatus, LogContext } from '@common/enums'; import { TaskService } from '@services/task/task.service'; -import { AUTH_RESET_EVENT_TYPE } from '../event.type'; +import { RESET_EVENT_TYPE } from '../reset.event.type'; import { AuthResetEventPayload } from '../auth-reset.payload.interface'; import { BaseException } from '@common/exceptions/base.exception'; import { Account } from '@domain/space/account/account.entity'; @@ -28,10 +28,13 @@ export class AuthResetService { const task = taskId ? { id: taskId } : await this.taskService.create(); try { - await this.publishAllAccountsReset(task.id); - await this.publishAllOrganizationsReset(task.id); - await this.publishAllUsersReset(task.id); - await this.publishPlatformReset(); + await this.publishAuthorizationResetAllAccounts(task.id); + await this.publishAuthorizationResetAllOrganizations(task.id); + await this.publishAuthorizationResetAllUsers(task.id); + await this.publishAuthorizationResetPlatform(); + await this.publishAuthorizationResetAiServer(); + // And reset licenses + await this.publishLicenseResetAllAccounts(task.id); } catch (error) { throw new BaseException( `Error while initializing authorization reset: ${error}`, @@ -43,7 +46,7 @@ export class AuthResetService { return task.id; } - public async publishAllAccountsReset(taskId?: string) { + public async publishAuthorizationResetAllAccounts(taskId?: string) { const accounts = await this.manager.find(Account, { select: { id: true }, }); @@ -54,15 +57,42 @@ export class AuthResetService { accounts.forEach(({ id }) => this.authResetQueue.emit( - AUTH_RESET_EVENT_TYPE.ACCOUNT, - { id, type: AUTH_RESET_EVENT_TYPE.ACCOUNT, task: task.id } + RESET_EVENT_TYPE.AUTHORIZATION_RESET_ACCOUNT, + { + id, + type: RESET_EVENT_TYPE.AUTHORIZATION_RESET_ACCOUNT, + task: task.id, + } ) ); return task.id; } - public async publishAllUsersReset(taskId?: string) { + public async publishLicenseResetAllAccounts(taskId?: string) { + const accounts = await this.manager.find(Account, { + select: { id: true }, + }); + + const task = taskId + ? { id: taskId } + : await this.taskService.create(accounts.length); + + accounts.forEach(({ id }) => + this.authResetQueue.emit( + RESET_EVENT_TYPE.LICENSE_RESET_ACCOUNT, + { + id, + type: RESET_EVENT_TYPE.LICENSE_RESET_ACCOUNT, + task: task.id, + } + ) + ); + + return task.id; + } + + public async publishAuthorizationResetAllUsers(taskId?: string) { const users = await this.manager.find(User, { select: { id: true }, }); @@ -73,10 +103,10 @@ export class AuthResetService { users.forEach(({ id }) => this.authResetQueue.emit( - AUTH_RESET_EVENT_TYPE.USER, + RESET_EVENT_TYPE.AUTHORIZATION_RESET_USER, { id, - type: AUTH_RESET_EVENT_TYPE.USER, + type: RESET_EVENT_TYPE.AUTHORIZATION_RESET_USER, task: task.id, } ) @@ -85,7 +115,7 @@ export class AuthResetService { return task.id; } - public async publishAllOrganizationsReset(taskId?: string) { + public async publishAuthorizationResetAllOrganizations(taskId?: string) { const organizations = await this.manager.find(Organization, { select: { id: true }, }); @@ -96,16 +126,31 @@ export class AuthResetService { organizations.forEach(({ id }) => this.authResetQueue.emit( - AUTH_RESET_EVENT_TYPE.ORGANIZATION, - { id, type: AUTH_RESET_EVENT_TYPE.ORGANIZATION, task: task.id } + RESET_EVENT_TYPE.AUTHORIZATION_RESET_ORGANIZATION, + { + id, + type: RESET_EVENT_TYPE.AUTHORIZATION_RESET_ORGANIZATION, + task: task.id, + } ) ); return task.id; } - public async publishPlatformReset() { + public async publishAuthorizationResetPlatform() { + // does not need a task + this.authResetQueue.emit( + RESET_EVENT_TYPE.AUTHORIZATION_RESET_PLATFORM, + {} + ); + } + + public async publishAuthorizationResetAiServer() { // does not need a task - this.authResetQueue.emit(AUTH_RESET_EVENT_TYPE.PLATFORM, {}); + this.authResetQueue.emit( + RESET_EVENT_TYPE.AUTHORIZATION_RESET_AI_SERVER, + {} + ); } } diff --git a/src/services/auth-reset/reset.event.type.ts b/src/services/auth-reset/reset.event.type.ts new file mode 100644 index 0000000000..73d0b156a2 --- /dev/null +++ b/src/services/auth-reset/reset.event.type.ts @@ -0,0 +1,8 @@ +export enum RESET_EVENT_TYPE { + AUTHORIZATION_RESET_ACCOUNT = 'auth-reset-account', + AUTHORIZATION_RESET_USER = 'auth-reset-user', + AUTHORIZATION_RESET_ORGANIZATION = 'auth-reset-organization', + AUTHORIZATION_RESET_PLATFORM = 'auth-reset-platform', + AUTHORIZATION_RESET_AI_SERVER = 'auth-reset-ai-server', + LICENSE_RESET_ACCOUNT = 'license-reset-account', +} diff --git a/src/services/auth-reset/subscriber/auth-reset.controller.ts b/src/services/auth-reset/subscriber/auth-reset.controller.ts index 69dc9b8ef6..702884ee14 100644 --- a/src/services/auth-reset/subscriber/auth-reset.controller.ts +++ b/src/services/auth-reset/subscriber/auth-reset.controller.ts @@ -14,12 +14,15 @@ import { OrganizationService } from '@domain/community/organization/organization import { OrganizationAuthorizationService } from '@domain/community/organization/organization.service.authorization'; import { UserService } from '@domain/community/user/user.service'; import { UserAuthorizationService } from '@domain/community/user/user.service.authorization'; -import { AUTH_RESET_EVENT_TYPE } from '../event.type'; +import { RESET_EVENT_TYPE } from '../reset.event.type'; import { TaskService } from '@services/task/task.service'; import { AuthResetEventPayload } from '../auth-reset.payload.interface'; import { AccountAuthorizationService } from '@domain/space/account/account.service.authorization'; import { AccountService } from '@domain/space/account/account.service'; import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; +import { AccountLicenseService } from '@domain/space/account/account.service.license'; +import { LicenseService } from '@domain/common/license/license.service'; +import { AiServerAuthorizationService } from '@services/ai-server/ai-server/ai.server.service.authorization'; const MAX_RETRIES = 5; const RETRY_HEADER = 'x-retry-count'; @@ -30,16 +33,19 @@ export class AuthResetController { private readonly logger: LoggerService, private accountService: AccountService, private authorizationPolicyService: AuthorizationPolicyService, + private licenseService: LicenseService, private accountAuthorizationService: AccountAuthorizationService, + private accountLicenseService: AccountLicenseService, private platformAuthorizationService: PlatformAuthorizationService, private organizationAuthorizationService: OrganizationAuthorizationService, private userAuthorizationService: UserAuthorizationService, private organizationService: OrganizationService, + private aiServerAuthorizationService: AiServerAuthorizationService, private userService: UserService, private taskService: TaskService ) {} - @EventPattern(AUTH_RESET_EVENT_TYPE.ACCOUNT, Transport.RMQ) + @EventPattern(RESET_EVENT_TYPE.AUTHORIZATION_RESET_ACCOUNT, Transport.RMQ) public async authResetAccount( @Payload() payload: AuthResetEventPayload, @Ctx() context: RmqContext @@ -88,7 +94,54 @@ export class AuthResetController { } } - @EventPattern(AUTH_RESET_EVENT_TYPE.PLATFORM, Transport.RMQ) + @EventPattern(RESET_EVENT_TYPE.LICENSE_RESET_ACCOUNT, Transport.RMQ) + public async licenseResetAccount( + @Payload() payload: AuthResetEventPayload, + @Ctx() context: RmqContext + ) { + this.logger.verbose?.( + `Starting reset of license for account with id ${payload.id}.`, + LogContext.AUTH_POLICY + ); + const channel: Channel = context.getChannelRef(); + const originalMsg = context.getMessage() as Message; + + const retryCount = originalMsg.properties.headers?.[RETRY_HEADER] ?? 0; + + try { + const account = await this.accountService.getAccountOrFail(payload.id); + const updatedLicenses = + await this.accountLicenseService.applyLicensePolicy(account.id); + await this.licenseService.saveAll(updatedLicenses); + + const message = `Finished resetting license for account with id ${payload.id}.`; + this.logger.verbose?.(message, LogContext.AUTH_POLICY); + this.taskService.updateTaskResults(payload.task, message); + channel.ack(originalMsg); + } catch (error: any) { + if (retryCount >= MAX_RETRIES) { + const message = `Resetting license for account with id ${payload.id} failed! Max retries reached. Rejecting message.`; + this.logger.error(message, error?.stack, LogContext.AUTH); + this.taskService.updateTaskErrors(payload.task, message); + + channel.reject(originalMsg, false); // Reject and don't requeue + } else { + this.logger.warn( + `Processing license reset for account with id ${ + payload.id + } failed. Retrying (${retryCount + 1}/${MAX_RETRIES})`, + LogContext.AUTH + ); + channel.publish('', MessagingQueue.AUTH_RESET, originalMsg.content, { + headers: { [RETRY_HEADER]: retryCount + 1 }, + persistent: true, // Make the message durable + }); + channel.ack(originalMsg); // Acknowledge the original message + } + } + } + + @EventPattern(RESET_EVENT_TYPE.AUTHORIZATION_RESET_PLATFORM, Transport.RMQ) public async authResetPlatform(@Ctx() context: RmqContext) { this.logger.verbose?.( 'Starting reset of authorization for platform', @@ -132,7 +185,51 @@ export class AuthResetController { } } - @EventPattern(AUTH_RESET_EVENT_TYPE.USER, Transport.RMQ) + @EventPattern(RESET_EVENT_TYPE.AUTHORIZATION_RESET_AI_SERVER, Transport.RMQ) + public async authResetAiServer(@Ctx() context: RmqContext) { + this.logger.verbose?.( + 'Starting reset of authorization for AI Server', + LogContext.AUTH_POLICY + ); + const channel: Channel = context.getChannelRef(); + const originalMsg = context.getMessage() as Message; + + const retryCount = originalMsg.properties.headers?.[RETRY_HEADER] ?? 0; + + try { + const authorizations = + await this.aiServerAuthorizationService.applyAuthorizationPolicy(); + await this.authorizationPolicyService.saveAll(authorizations); + this.logger.verbose?.( + 'Finished resetting authorization for AI Server.', + LogContext.AUTH_POLICY + ); + channel.ack(originalMsg); + } catch (error: any) { + if (retryCount >= MAX_RETRIES) { + this.logger.error( + 'Resetting authorization for AI Server failed! Max retries reached. Rejecting message.', + error?.stack, + LogContext.AUTH + ); + channel.reject(originalMsg, false); // Reject and don't requeue + } else { + this.logger.warn( + `Processing authorization reset for AI Server failed. Retrying (${ + retryCount + 1 + }/${MAX_RETRIES})`, + LogContext.AUTH + ); + channel.publish('', MessagingQueue.AUTH_RESET, originalMsg.content, { + headers: { [RETRY_HEADER]: retryCount + 1 }, + persistent: true, // Make the message durable + }); + channel.ack(originalMsg); // Acknowledge the original message + } + } + } + + @EventPattern(RESET_EVENT_TYPE.AUTHORIZATION_RESET_USER, Transport.RMQ) public async authResetUser( @Payload() payload: AuthResetEventPayload, @Ctx() context: RmqContext @@ -182,7 +279,10 @@ export class AuthResetController { } } - @EventPattern(AUTH_RESET_EVENT_TYPE.ORGANIZATION, Transport.RMQ) + @EventPattern( + RESET_EVENT_TYPE.AUTHORIZATION_RESET_ORGANIZATION, + Transport.RMQ + ) public async authResetOrganization( @Payload() payload: AuthResetEventPayload, @Ctx() context: RmqContext diff --git a/src/services/auth-reset/subscriber/auth-reset.subscriber.module.ts b/src/services/auth-reset/subscriber/auth-reset.subscriber.module.ts index 97d069a490..b6bd51f4fa 100644 --- a/src/services/auth-reset/subscriber/auth-reset.subscriber.module.ts +++ b/src/services/auth-reset/subscriber/auth-reset.subscriber.module.ts @@ -7,6 +7,8 @@ import { OrganizationModule } from '@domain/community/organization/organization. import { TaskModule } from '@services/task/task.module'; import { AccountModule } from '@domain/space/account/account.module'; import { AuthorizationPolicyModule } from '@domain/common/authorization-policy/authorization.policy.module'; +import { AiServerModule } from '@services/ai-server/ai-server/ai.server.module'; +import { LicenseModule } from '@domain/common/license/license.module'; @Global() @Module({ @@ -18,6 +20,8 @@ import { AuthorizationPolicyModule } from '@domain/common/authorization-policy/a PlatformModule, OrganizationModule, TaskModule, + AiServerModule, + LicenseModule, ], controllers: [AuthResetController], }) diff --git a/src/services/infrastructure/entity-resolver/community.resolver.service.ts b/src/services/infrastructure/entity-resolver/community.resolver.service.ts index 8c0690cfb7..706e333721 100644 --- a/src/services/infrastructure/entity-resolver/community.resolver.service.ts +++ b/src/services/infrastructure/entity-resolver/community.resolver.service.ts @@ -9,9 +9,10 @@ import { Space } from '@domain/space/space/space.entity'; import { ISpace } from '@domain/space/space/space.interface'; import { RoomType } from '@common/enums/room.type'; import { VirtualContributor } from '@domain/community/virtual-contributor/virtual.contributor.entity'; -import { IAgent } from '@domain/agent'; import { IAccount } from '@domain/space/account/account.interface'; import { ICommunication } from '@domain/communication/communication/communication.interface'; +import { ILicense } from '@domain/common/license/license.interface'; +import { Collaboration } from '@domain/collaboration/collaboration'; @Injectable() export class CommunityResolverService { @@ -99,29 +100,6 @@ export class CommunityResolverService { return space.levelZeroSpaceID; } - public async getLevelZeroSpaceAgentForCommunityOrFail( - communityID: string - ): Promise { - const levelZeroSpaceID = - await this.getLevelZeroSpaceIdForCommunity(communityID); - const levelZeroSpace = await this.entityManager.findOne(Space, { - where: { - id: levelZeroSpaceID, - }, - relations: { - agent: true, - }, - }); - - if (!levelZeroSpace || !levelZeroSpace.agent) { - throw new EntityNotFoundException( - `Unable to find Space for given community id: ${communityID}`, - LogContext.COMMUNITY - ); - } - return levelZeroSpace.agent; - } - private async getAccountForCommunityOrFail( communityID: string ): Promise { @@ -280,6 +258,55 @@ export class CommunityResolverService { return community; } + public async getCollaborationLicenseFromWhiteboardOrFail( + whiteboardId: string + ): Promise { + // check for whitebaord in contributions + let collaboration = await this.entityManager.findOne(Collaboration, { + where: { + callouts: { + contributions: { + whiteboard: { + id: whiteboardId, + }, + }, + }, + }, + relations: { + license: { + entitlements: true, + }, + }, + }); + // check for whiteboard in framing + if (!collaboration) { + collaboration = await this.entityManager.findOne(Collaboration, { + where: { + callouts: { + framing: { + whiteboard: { + id: whiteboardId, + }, + }, + }, + }, + relations: { + license: { + entitlements: true, + }, + }, + }); + } + if (!collaboration || !collaboration.license) { + throw new EntityNotFoundException( + `Unable to find Collaboration with License for whiteboard: ${whiteboardId}`, + LogContext.COLLABORATION + ); + } + + return collaboration.license; + } + public async getCommunityFromCalendarEventOrFail( callendarEventId: string ): Promise { diff --git a/src/services/infrastructure/license-entitlement-usage/license.entitlement.usage.module.ts b/src/services/infrastructure/license-entitlement-usage/license.entitlement.usage.module.ts new file mode 100644 index 0000000000..eb1b3f1308 --- /dev/null +++ b/src/services/infrastructure/license-entitlement-usage/license.entitlement.usage.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { LicenseEntitlementUsageService } from './license.entitlement.usage.service'; + +@Module({ + imports: [], + providers: [LicenseEntitlementUsageService], + exports: [LicenseEntitlementUsageService], +}) +export class LicenseEntitlementUsageModule {} diff --git a/src/services/infrastructure/license-entitlement-usage/license.entitlement.usage.service.ts b/src/services/infrastructure/license-entitlement-usage/license.entitlement.usage.service.ts new file mode 100644 index 0000000000..196b830ad1 --- /dev/null +++ b/src/services/infrastructure/license-entitlement-usage/license.entitlement.usage.service.ts @@ -0,0 +1,108 @@ +import { EntityManager } from 'typeorm'; +import { InjectEntityManager } from '@nestjs/typeorm'; +import { Inject, LoggerService } from '@nestjs/common'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { + EntityNotFoundException, + RelationshipNotFoundException, +} from '@common/exceptions'; +import { LogContext } from '@common/enums'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; +import { Account } from '@domain/space/account/account.entity'; +import { ISpace } from '@domain/space/space/space.interface'; + +export class LicenseEntitlementUsageService { + constructor( + @InjectEntityManager('default') + private entityManager: EntityManager, + @Inject(WINSTON_MODULE_NEST_PROVIDER) + private readonly logger: LoggerService + ) {} + + async getEntitlementUsageForAccount( + licenseID: string, + entitlementType: LicenseEntitlementType + ): Promise { + const account = await this.entityManager.findOne(Account, { + loadEagerRelations: false, + where: { + license: { + id: licenseID, + }, + }, + relations: { + spaces: { + license: { + entitlements: true, + }, + }, + virtualContributors: true, + innovationHubs: true, + innovationPacks: true, + }, + }); + if (!account) { + throw new EntityNotFoundException( + `Unable to find Account with license with ID: ${licenseID}`, + LogContext.LICENSE + ); + } + switch (entitlementType) { + case LicenseEntitlementType.ACCOUNT_SPACE_FREE: + return this.getAccountSpacesTypeCount( + account.spaces, + LicenseEntitlementType.SPACE_FREE + ); + case LicenseEntitlementType.ACCOUNT_SPACE_PLUS: + return this.getAccountSpacesTypeCount( + account.spaces, + LicenseEntitlementType.SPACE_PLUS + ); + case LicenseEntitlementType.ACCOUNT_SPACE_PREMIUM: + return this.getAccountSpacesTypeCount( + account.spaces, + LicenseEntitlementType.SPACE_PREMIUM + ); + case LicenseEntitlementType.ACCOUNT_VIRTUAL_CONTRIBUTOR: + return account.virtualContributors.length; + case LicenseEntitlementType.ACCOUNT_INNOVATION_HUB: + return account.innovationHubs.length; + case LicenseEntitlementType.ACCOUNT_INNOVATION_PACK: + return account.innovationPacks.length; + default: + throw new RelationshipNotFoundException( + `Unexpected entitlement type encountered: ${entitlementType}`, + LogContext.LICENSE + ); + } + } + + private getAccountSpacesTypeCount( + spaces: ISpace[], + entitlementType: LicenseEntitlementType + ): number { + const matchingSpaces = spaces.filter(space => + this.hasMatchingLicenseEntitlement(space, entitlementType) + ); + return matchingSpaces.length; + } + + private hasMatchingLicenseEntitlement( + space: ISpace, + entitlementType: LicenseEntitlementType + ): boolean { + const entitlements = space.license?.entitlements; + if (!entitlements) { + throw new RelationshipNotFoundException( + `Unable to load entitlemets for space: ${space.id}`, + LogContext.LICENSE + ); + } + for (const entitlement of entitlements) { + if (entitlement.type === entitlementType) { + return entitlement.enabled; + } + } + return false; + } +} diff --git a/src/services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service.ts b/src/services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service.ts index a927c8c5df..6292e6aeee 100644 --- a/src/services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service.ts +++ b/src/services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service.ts @@ -1,4 +1,9 @@ -import { Inject, Injectable, LoggerService } from '@nestjs/common'; +import { + Inject, + Injectable, + LoggerService, + NotImplementedException, +} from '@nestjs/common'; import { EntityManager, FindOneOptions, Repository } from 'typeorm'; import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; import { LogContext } from '@common/enums'; @@ -155,7 +160,7 @@ export class StorageAggregatorResolverService { if (!isUUID(templatesManagerId)) { throw new InvalidUUID( 'Invalid UUID provided to find the StorageAggregator of a templatesManager', - LogContext.STORAGE_AGGREGATOR, + LogContext.COMMUNITY, { provided: templatesManagerId } ); } @@ -188,7 +193,7 @@ export class StorageAggregatorResolverService { if (platform && platform.storageAggregator) { return this.getStorageAggregatorOrFail(platform.storageAggregator.id); } - throw new EntityNotFoundException( + throw new NotImplementedException( `Unable to retrieve storage aggregator to use for TemplatesManager ${templatesManagerId}`, LogContext.STORAGE_AGGREGATOR ); diff --git a/test/mocks/account.service.mock.ts b/test/mocks/account.service.mock.ts index ea58876036..0577b8276f 100644 --- a/test/mocks/account.service.mock.ts +++ b/test/mocks/account.service.mock.ts @@ -6,6 +6,6 @@ export const MockAccountService: ValueProvider> = { provide: AccountService, useValue: { getAccount: jest.fn(), - deleteAccount: jest.fn(), + deleteAccountOrFail: jest.fn(), }, }; From 4e4464fd39c1a19cb09436fab4485344ad08d32f Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Fri, 15 Nov 2024 11:16:48 +0200 Subject: [PATCH 02/19] Major version bump --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6f4d195062..2cff6fc9ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "alkemio-server", - "version": "0.95.4", + "version": "0.96.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "alkemio-server", - "version": "0.95.4", + "version": "0.96.0", "license": "EUPL-1.2", "dependencies": { "@alkemio/matrix-adapter-lib": "^0.4.1", diff --git a/package.json b/package.json index 7d9ccd4704..e5e7352dbf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alkemio-server", - "version": "0.95.4", + "version": "0.96.0", "description": "Alkemio server, responsible for managing the shared Alkemio platform", "author": "Alkemio Foundation", "private": false, From af58f6dc617304730d3cdc984ed4a2c55a804226 Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Fri, 15 Nov 2024 17:02:15 +0200 Subject: [PATCH 03/19] Add license lookup --- src/services/api/lookup/lookup.module.ts | 2 ++ .../api/lookup/lookup.resolver.fields.ts | 25 ++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/services/api/lookup/lookup.module.ts b/src/services/api/lookup/lookup.module.ts index c4dbf5063c..383291f3bb 100644 --- a/src/services/api/lookup/lookup.module.ts +++ b/src/services/api/lookup/lookup.module.ts @@ -32,6 +32,7 @@ import { TemplateModule } from '@domain/template/template/template.module'; import { TemplatesSetModule } from '@domain/template/templates-set/templates.set.module'; import { RoleSetModule } from '@domain/access/role-set/role.set.module'; import { TemplatesManagerModule } from '@domain/template/templates-manager/templates.manager.module'; +import { LicenseModule } from '@domain/common/license/license.module'; @Module({ imports: [ @@ -65,6 +66,7 @@ import { TemplatesManagerModule } from '@domain/template/templates-manager/templ CommunityGuidelinesModule, VirtualContributorModule, RoleSetModule, + LicenseModule, ], providers: [LookupService, LookupResolverQueries, LookupResolverFields], exports: [LookupService], diff --git a/src/services/api/lookup/lookup.resolver.fields.ts b/src/services/api/lookup/lookup.resolver.fields.ts index 44d39bb53e..8f782c99af 100644 --- a/src/services/api/lookup/lookup.resolver.fields.ts +++ b/src/services/api/lookup/lookup.resolver.fields.ts @@ -65,6 +65,8 @@ import { IRoleSet } from '@domain/access/role-set/role.set.interface'; import { IUser } from '@domain/community/user/user.interface'; import { TemplatesManagerService } from '@domain/template/templates-manager/templates.manager.service'; import { ITemplatesManager } from '@domain/template/templates-manager/templates.manager.interface'; +import { ILicense } from '@domain/common/license/license.interface'; +import { LicenseService } from '@domain/common/license/license.service'; @Resolver(() => LookupQueryResults) export class LookupResolverFields { @@ -98,7 +100,8 @@ export class LookupResolverFields { private guidelinesService: CommunityGuidelinesService, private virtualContributorService: VirtualContributorService, private innovationHubService: InnovationHubService, - private roleSetService: RoleSetService + private roleSetService: RoleSetService, + private licenseService: LicenseService ) {} @UseGuards(GraphqlGuard) @@ -707,4 +710,24 @@ export class LookupResolverFields { return guidelines; } + + @UseGuards(GraphqlGuard) + @ResolveField(() => ILicense, { + nullable: true, + description: 'Lookup the specified License', + }) + async license( + @CurrentUser() agentInfo: AgentInfo, + @Args('ID', { type: () => UUID }) id: string + ): Promise { + const license = await this.licenseService.getLicenseOrFail(id); + this.authorizationService.grantAccessOrFail( + agentInfo, + license.authorization, + AuthorizationPrivilege.READ, + `lookup license: ${license.id}` + ); + + return license; + } } From 4141b500c1fe24e61d3cf6b73d3dfc768a7e5e2e Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Fri, 15 Nov 2024 17:11:56 +0200 Subject: [PATCH 04/19] removed redundant checks --- src/services/api/lookup/lookup.resolver.fields.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/services/api/lookup/lookup.resolver.fields.ts b/src/services/api/lookup/lookup.resolver.fields.ts index 8f782c99af..944f4068fd 100644 --- a/src/services/api/lookup/lookup.resolver.fields.ts +++ b/src/services/api/lookup/lookup.resolver.fields.ts @@ -717,16 +717,9 @@ export class LookupResolverFields { description: 'Lookup the specified License', }) async license( - @CurrentUser() agentInfo: AgentInfo, @Args('ID', { type: () => UUID }) id: string ): Promise { const license = await this.licenseService.getLicenseOrFail(id); - this.authorizationService.grantAccessOrFail( - agentInfo, - license.authorization, - AuthorizationPrivilege.READ, - `lookup license: ${license.id}` - ); return license; } From e79586c3c7876eb738f9efd5ec434bd13cab2c7f Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Mon, 18 Nov 2024 12:04:21 +0200 Subject: [PATCH 05/19] Fixed proper L0 space agent selection in recursive license assignment (#4708) * Fixed proper L0 account agent selection in recursive license assignment * provide root space agent for sub-sub space --- src/domain/common/license/license.service.ts | 4 ++-- src/domain/space/space/space.service.license.ts | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/domain/common/license/license.service.ts b/src/domain/common/license/license.service.ts index 845200ca25..795af917b9 100644 --- a/src/domain/common/license/license.service.ts +++ b/src/domain/common/license/license.service.ts @@ -180,7 +180,7 @@ export class LicenseService { e => e.type === childEntitlement.type ); if (!parentEntitlement) { - throw new RelationshipNotFoundException( + throw new EntityNotFoundException( `Parent entitlement not found: ${childEntitlement.type}`, LogContext.LICENSE ); @@ -194,7 +194,7 @@ export class LicenseService { license: ILicense | undefined ): ILicenseEntitlement[] | never { if (!license) { - throw new RelationshipNotFoundException( + throw new EntityNotFoundException( 'Unable to load Entitlements for License', LogContext.LICENSE ); diff --git a/src/domain/space/space/space.service.license.ts b/src/domain/space/space/space.service.license.ts index 33092bff30..f92deda7da 100644 --- a/src/domain/space/space/space.service.license.ts +++ b/src/domain/space/space/space.service.license.ts @@ -25,7 +25,10 @@ export class SpaceLicenseService { @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService ) {} - async applyLicensePolicy(spaceID: string): Promise { + async applyLicensePolicy( + spaceID: string, + agent?: IAgent + ): Promise { const space = await this.spaceService.getSpaceOrFail(spaceID, { relations: { agent: { @@ -59,8 +62,12 @@ export class SpaceLicenseService { // Ensure always applying from a clean state space.license = this.licenseService.reset(space.license); + const rootLevelSpaceAgent = agent ?? space.agent; - space.license = await this.extendLicensePolicy(space.license, space.agent); + space.license = await this.extendLicensePolicy( + space.license, + rootLevelSpaceAgent + ); updatedLicenses.push(space.license); @@ -78,7 +85,10 @@ export class SpaceLicenseService { updatedLicenses.push(...collaborationLicenses); for (const subspace of space.subspaces) { - const subspaceLicenses = await this.applyLicensePolicy(subspace.id); + const subspaceLicenses = await this.applyLicensePolicy( + subspace.id, + rootLevelSpaceAgent + ); updatedLicenses.push(...subspaceLicenses); } From 194cdd281f8057332c815cbe42dc1b80d311cb12 Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Mon, 18 Nov 2024 12:04:48 +0200 Subject: [PATCH 06/19] Look up correct entitlement for VC creation (#4707) Co-authored-by: Evgeni Dimitrov --- src/domain/access/role-set/role.set.resolver.mutations.ts | 4 ++-- src/domain/common/license/license.service.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/domain/access/role-set/role.set.resolver.mutations.ts b/src/domain/access/role-set/role.set.resolver.mutations.ts index 49c0a6aa50..80803d6693 100644 --- a/src/domain/access/role-set/role.set.resolver.mutations.ts +++ b/src/domain/access/role-set/role.set.resolver.mutations.ts @@ -201,10 +201,10 @@ export class RoleSetResolverMutations { `assign virtual community role: ${roleSet.id}` ); - // Also require ACCOUNT_VIRTUAL_CONTRIBUTOR entitlement for the RoleSet + // Also require SPACE_FLAG_VIRTUAL_CONTRIBUTOR_ACCESS entitlement for the RoleSet this.licenseService.isEntitlementEnabledOrFail( roleSet.license, - LicenseEntitlementType.ACCOUNT_VIRTUAL_CONTRIBUTOR + LicenseEntitlementType.SPACE_FLAG_VIRTUAL_CONTRIBUTOR_ACCESS ); await this.roleSetService.assignVirtualToRole( diff --git a/src/domain/common/license/license.service.ts b/src/domain/common/license/license.service.ts index 795af917b9..e9531956ff 100644 --- a/src/domain/common/license/license.service.ts +++ b/src/domain/common/license/license.service.ts @@ -216,7 +216,7 @@ export class LicenseService { entitlement => entitlement.type === type ); if (!entitlement) { - throw new RelationshipNotFoundException( + throw new EntityNotFoundException( `Unable to find entitlement of type ${type} in Entitlements for License: ${JSON.stringify(entitlements)}`, LogContext.LICENSE ); From 9b5c8e6638c37ae5c4d76ca856b54e82ee6c3d14 Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Mon, 18 Nov 2024 12:07:39 +0200 Subject: [PATCH 07/19] filter out demo spaces for unauthenticated users search in elastic (#4712) * filter out demo spaces for unauthenticated users search in elastic * improvements --------- Co-authored-by: Svetoslav --- .../search/v2/extract/build.search.query.ts | 112 +++++++++++------- .../v2/extract/search.extract.service.ts | 9 +- .../search/v2/result/search.result.service.ts | 4 +- src/services/api/search/v2/search2.service.ts | 4 +- 4 files changed, 83 insertions(+), 46 deletions(-) diff --git a/src/services/api/search/v2/extract/build.search.query.ts b/src/services/api/search/v2/extract/build.search.query.ts index 252c1ef73e..3491db5949 100644 --- a/src/services/api/search/v2/extract/build.search.query.ts +++ b/src/services/api/search/v2/extract/build.search.query.ts @@ -1,55 +1,87 @@ +import { SpaceVisibility } from '@common/enums/space.visibility'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; export const buildSearchQuery = ( terms: string, - spaceIdFilter?: string -): QueryDslQueryContainer => ({ - bool: { - must: [ - { - // match the terms in any TEXT field - // accumulate the score from all fields - more matches on more fields will result in a higher score - multi_match: { - query: terms, - type: 'most_fields', - fields: ['*'], - }, - }, - ], - // filter the results by the spaceID - filter: buildFilter(spaceIdFilter), - }, -}); - -const buildFilter = ( - spaceIdFilter?: string -): QueryDslQueryContainer | undefined => { - if (!spaceIdFilter) { - return undefined; + options?: { + spaceIdFilter?: string; + excludeDemoSpaces?: boolean; } - +): QueryDslQueryContainer => { + const { spaceIdFilter, excludeDemoSpaces } = options ?? {}; return { bool: { - minimum_should_match: 1, - should: [ - // the spaceID field is not applicable for some entities, - // so we want them included in the results + must: [ { - bool: { - must_not: { - exists: { - field: 'spaceID', + // Match the terms in any TEXT field + // Accumulate the score from all fields - more matches on more fields will result in a higher score + multi_match: { + query: terms, + type: 'most_fields', + fields: ['*'], + }, + }, + ], + // Filter the results by the spaceID and visibility + filter: buildFilter({ + spaceIdFilter, + excludeDemoSpaces, + }), + }, + }; +}; + +const buildFilter = (opts?: { + spaceIdFilter?: string; + excludeDemoSpaces?: boolean; +}): QueryDslQueryContainer | undefined => { + const { spaceIdFilter, excludeDemoSpaces } = opts ?? {}; + + const filters: QueryDslQueryContainer[] = []; + + if (spaceIdFilter) { + filters.push({ + bool: { + // match either of the two conditions + minimum_should_match: 1, + should: [ + // the spaceID field is not applicable for some entities, + // so we want them included in the results + { + bool: { + must_not: { + exists: { + field: 'spaceID', + }, }, }, }, - }, - // if the spaceID field exists, we want to filter by it - { - term: { - spaceID: spaceIdFilter, + // if the spaceID field exists, we want to filter by it + { + term: { + spaceID: spaceIdFilter, + }, }, - }, - ], + ], + }, + }); + } + + if (excludeDemoSpaces) { + filters.push({ + term: { + visibility: SpaceVisibility.ACTIVE, + }, + }); + } + + if (filters.length === 0) { + return undefined; + } + + return { + bool: { + must: filters, }, }; }; diff --git a/src/services/api/search/v2/extract/search.extract.service.ts b/src/services/api/search/v2/extract/search.extract.service.ts index 2d2a3b095c..de76888acc 100644 --- a/src/services/api/search/v2/extract/search.extract.service.ts +++ b/src/services/api/search/v2/extract/search.extract.service.ts @@ -70,7 +70,7 @@ export class SearchExtractService { public async search( searchData: SearchInput, - onlyPublicResults: boolean + excludeDemoSpaces: boolean ): Promise { if (!this.client) { throw new Error('Elasticsearch client not initialized'); @@ -82,10 +82,13 @@ export class SearchExtractService { const terms = filteredTerms.join(' '); const indicesToSearchOn = this.getIndices( searchData.typesFilter, - onlyPublicResults + excludeDemoSpaces ); // the main search query built using query DSL - const query = buildSearchQuery(terms, searchData.searchInSpaceFilter); + const query = buildSearchQuery(terms, { + spaceIdFilter: searchData.searchInSpaceFilter, + excludeDemoSpaces, + }); // used with function_score to boost results based on visibility const functions = functionScoreFunctions; diff --git a/src/services/api/search/v2/result/search.result.service.ts b/src/services/api/search/v2/result/search.result.service.ts index 359c44ca14..f7471fdc37 100644 --- a/src/services/api/search/v2/result/search.result.service.ts +++ b/src/services/api/search/v2/result/search.result.service.ts @@ -182,7 +182,9 @@ export class SearchResultService { const subspaceIds = rawSearchResults.map(hit => hit.result.id); const subspaces = await this.entityManager.find(Space, { - where: { id: In(subspaceIds) }, + where: { + id: In(subspaceIds), + }, relations: { parentSpace: true }, }); diff --git a/src/services/api/search/v2/search2.service.ts b/src/services/api/search/v2/search2.service.ts index 9110c3239d..217e2a625d 100644 --- a/src/services/api/search/v2/search2.service.ts +++ b/src/services/api/search/v2/search2.service.ts @@ -23,7 +23,7 @@ export class Search2Service { searchData: SearchInput, agentInfo: AgentInfo ): Promise { - const onlyPublicResults = !agentInfo.email; + const excludeDemoSpaces = !agentInfo.email; if ( searchData.searchInSpaceFilter && !isUUID(searchData.searchInSpaceFilter) @@ -45,7 +45,7 @@ export class Search2Service { } const searchResults = await this.searchExtractService.search( searchData, - onlyPublicResults + excludeDemoSpaces ); return this.searchResultService.resolveSearchResults( searchResults, From e44fe40c2a7da1a04155637df5b27cd72d53ca67 Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Mon, 18 Nov 2024 17:46:26 +0200 Subject: [PATCH 08/19] Properly assign VC_CAMPAIGN and BETA_TESTER --- .../account/account.service.authorization.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/domain/space/account/account.service.authorization.ts b/src/domain/space/account/account.service.authorization.ts index 72f7844e54..d3551851d2 100644 --- a/src/domain/space/account/account.service.authorization.ts +++ b/src/domain/space/account/account.service.authorization.ts @@ -282,10 +282,24 @@ export class AccountAuthorizationService { accountHostManage.cascade = true; newRules.push(accountHostManage); + const vcCampaignCredential: ICredentialDefinition = { + type: AuthorizationCredential.VC_CAMPAIGN, + resourceID: '', + }; + + const betaTesterCredential: ICredentialDefinition = { + type: AuthorizationCredential.BETA_TESTER, + resourceID: '', + }; + + const createResourcesUnderAccountCredentials = [ + vcCampaignCredential, + betaTesterCredential, + ]; // If the user is a beta tester or part of VC campaign then can create the resources const createSpace = this.authorizationPolicyService.createCredentialRule( [AuthorizationPrivilege.CREATE_SPACE], - [...hostCredentials], + createResourcesUnderAccountCredentials, CREDENTIAL_RULE_PLATFORM_CREATE_SPACE ); createSpace.cascade = false; @@ -293,7 +307,7 @@ export class AccountAuthorizationService { const createVC = this.authorizationPolicyService.createCredentialRule( [AuthorizationPrivilege.CREATE_VIRTUAL_CONTRIBUTOR], - [...hostCredentials], + createResourcesUnderAccountCredentials, CREDENTIAL_RULE_PLATFORM_CREATE_VC ); createVC.cascade = false; @@ -302,7 +316,7 @@ export class AccountAuthorizationService { const createInnovationPack = this.authorizationPolicyService.createCredentialRule( [AuthorizationPrivilege.CREATE_INNOVATION_PACK], - [...hostCredentials], + createResourcesUnderAccountCredentials, CREDENTIAL_RULE_PLATFORM_CREATE_INNOVATION_PACK ); createInnovationPack.cascade = false; From 4040b8f82a07eec25c7d1ab337d102005a8d3ed5 Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Mon, 18 Nov 2024 17:46:51 +0200 Subject: [PATCH 09/19] validate license limits fix --- .../account/account.resolver.mutations.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/domain/space/account/account.resolver.mutations.ts b/src/domain/space/account/account.resolver.mutations.ts index 3cd23310ea..dc5bf541bb 100644 --- a/src/domain/space/account/account.resolver.mutations.ts +++ b/src/domain/space/account/account.resolver.mutations.ts @@ -85,7 +85,7 @@ export class AccountResolverMutations { } ); - this.validateSoftLicenseLimitOrFail( + await this.validateSoftLicenseLimitOrFail( account, agentInfo, AuthorizationPrivilege.CREATE_SPACE, @@ -149,7 +149,7 @@ export class AccountResolverMutations { } ); - this.validateSoftLicenseLimitOrFail( + await this.validateSoftLicenseLimitOrFail( account, agentInfo, AuthorizationPrivilege.CREATE_INNOVATION_HUB, @@ -192,7 +192,7 @@ export class AccountResolverMutations { } ); - this.validateSoftLicenseLimitOrFail( + await this.validateSoftLicenseLimitOrFail( account, agentInfo, AuthorizationPrivilege.CREATE_VIRTUAL_CONTRIBUTOR, @@ -252,7 +252,7 @@ export class AccountResolverMutations { } ); - this.validateSoftLicenseLimitOrFail( + await this.validateSoftLicenseLimitOrFail( account, agentInfo, AuthorizationPrivilege.CREATE_INNOVATION_PACK, @@ -554,7 +554,7 @@ export class AccountResolverMutations { ); } - private validateSoftLicenseLimitOrFail( + private async validateSoftLicenseLimitOrFail( account: IAccount, agentInfo: AgentInfo, authorizationPrivilege: AuthorizationPrivilege, @@ -581,16 +581,14 @@ export class AccountResolverMutations { authorizationPrivilege, `create ${licenseType} on account: ${account.id}` ); - const isEntitleMentEnabled = this.licenseService.isEntitlementAvailable( - license, - licenseType - ); + const isEntitlementEnabled = + await this.licenseService.isEntitlementAvailable(license, licenseType); const isPlatformAdmin = this.authorizationService.isAccessGranted( agentInfo, authorization, AuthorizationPrivilege.PLATFORM_ADMIN ); - if (!isPlatformAdmin && !isEntitleMentEnabled) { + if (!isPlatformAdmin && !isEntitlementEnabled) { const entitlementLimit = this.licenseService.getEntitlementLimit( license, licenseType From aaa6bb400f306d419885456ada146ecb95e21882 Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Mon, 18 Nov 2024 17:47:47 +0200 Subject: [PATCH 10/19] remove broken check --- .../license.entitlement.usage.service.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/services/infrastructure/license-entitlement-usage/license.entitlement.usage.service.ts b/src/services/infrastructure/license-entitlement-usage/license.entitlement.usage.service.ts index 196b830ad1..a914fa0751 100644 --- a/src/services/infrastructure/license-entitlement-usage/license.entitlement.usage.service.ts +++ b/src/services/infrastructure/license-entitlement-usage/license.entitlement.usage.service.ts @@ -79,11 +79,13 @@ export class LicenseEntitlementUsageService { private getAccountSpacesTypeCount( spaces: ISpace[], - entitlementType: LicenseEntitlementType + _entitlementType: LicenseEntitlementType ): number { - const matchingSpaces = spaces.filter(space => - this.hasMatchingLicenseEntitlement(space, entitlementType) - ); + const matchingSpaces = spaces; + //toDo - fix this, at the moment this path is not working + // .filter(space => + // this.hasMatchingLicenseEntitlement(space, entitlementType) + // ); return matchingSpaces.length; } From 3be9f07ae980479d8822d9d7337ba195b2ef615c Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Mon, 18 Nov 2024 17:51:28 +0200 Subject: [PATCH 11/19] remove redundant auth privileges (#4716) --- .../credential.rule.types.constants.ts | 2 -- .../authorization/policy.rule.constants.ts | 2 -- src/common/enums/authorization.privilege.ts | 2 -- .../collaboration.service.authorization.ts | 19 ------------------- 4 files changed, 25 deletions(-) diff --git a/src/common/constants/authorization/credential.rule.types.constants.ts b/src/common/constants/authorization/credential.rule.types.constants.ts index 54f9535db5..3fc6d70d3a 100644 --- a/src/common/constants/authorization/credential.rule.types.constants.ts +++ b/src/common/constants/authorization/credential.rule.types.constants.ts @@ -16,8 +16,6 @@ export const CREDENTIAL_RULE_TYPES_SPACE_COMMUNITY_APPLY_GLOBAL_REGISTERED = 'credentialRuleTypes-spaceCommunityApplyGlobalRegistered'; export const CREDENTIAL_RULE_TYPES_SPACE_COMMUNITY_JOIN_GLOBAL_REGISTERED = 'credentialRuleTypes-spaceCommunityJoinGlobalRegistered'; -export const CREDENTIAL_RULE_TYPES_CALLOUT_SAVE_AS_TEMPLATE = - 'credentialRuleTypes-calloutSaveAsTemplate'; export const CREDENTIAL_RULE_TYPES_CALLOUT_UPDATE_PUBLISHER_ADMINS = 'credentialRuleTypes-calloutUpdatePublisherGlobalAdmins'; export const CREDENTIAL_RULE_TYPES_COMMUNITY_ADD_MEMBERS = diff --git a/src/common/constants/authorization/policy.rule.constants.ts b/src/common/constants/authorization/policy.rule.constants.ts index fc07daeff6..70e1fb8d40 100644 --- a/src/common/constants/authorization/policy.rule.constants.ts +++ b/src/common/constants/authorization/policy.rule.constants.ts @@ -14,8 +14,6 @@ export const POLICY_RULE_COLLABORATION_CREATE = 'policyRule-collaborationCreate'; export const POLICY_RULE_COLLABORATION_WHITEBOARD_CREATE = 'policyRule-collaborationWhiteboardCreate'; -export const POLICY_RULE_COLLABORATION_WHITEBOARD_CONTRIBUTORS_CREATE = - 'policyRule-collaborationWhiteboardContributorsCreate'; export const POLICY_RULE_STORAGE_BUCKET_UPDATER_FILE_UPLOAD = 'policyRule-storageBucketUpdaterFileUpload'; export const POLICY_RULE_STORAGE_BUCKET_CONTRIBUTOR_FILE_UPLOAD = diff --git a/src/common/enums/authorization.privilege.ts b/src/common/enums/authorization.privilege.ts index 3396955e01..48eb4cb722 100644 --- a/src/common/enums/authorization.privilege.ts +++ b/src/common/enums/authorization.privilege.ts @@ -18,7 +18,6 @@ export enum AuthorizationPrivilege { CREATE_MESSAGE_REPLY = 'create-message-reply', CREATE_MESSAGE_REACTION = 'create-message-reaction', CREATE_WHITEBOARD = 'create-whiteboard', - CREATE_WHITEBOARD_RT = 'create-whiteboard-rt', CREATE_SPACE = 'create-space', CREATE_SUBSPACE = 'create-subspace', CREATE_ORGANIZATION = 'create-organization', @@ -42,7 +41,6 @@ export enum AuthorizationPrivilege { MOVE_CONTRIBUTION = 'move-contribution', ACCESS_INTERACTIVE_GUIDANCE = 'access-interactive-guidance', UPDATE_CONTENT = 'update-content', - SAVE_AS_TEMPLATE = 'save-as-template', TRANSFER_RESOURCE = 'transfer-resource', } diff --git a/src/domain/collaboration/collaboration/collaboration.service.authorization.ts b/src/domain/collaboration/collaboration/collaboration.service.authorization.ts index f3c1b3ec80..0b0c89df65 100644 --- a/src/domain/collaboration/collaboration/collaboration.service.authorization.ts +++ b/src/domain/collaboration/collaboration/collaboration.service.authorization.ts @@ -15,8 +15,6 @@ import { CREDENTIAL_RULE_COLLABORATION_CONTRIBUTORS, POLICY_RULE_COLLABORATION_CREATE, POLICY_RULE_CALLOUT_CONTRIBUTE, - CREDENTIAL_RULE_TYPES_CALLOUT_SAVE_AS_TEMPLATE, - POLICY_RULE_COLLABORATION_WHITEBOARD_CONTRIBUTORS_CREATE, } from '@common/constants'; import { CommunityRoleType } from '@common/enums/community.role'; import { TimelineAuthorizationService } from '@domain/timeline/timeline/timeline.service.authorization'; @@ -233,15 +231,6 @@ export class CollaborationAuthorizationService { type: AuthorizationCredential.GLOBAL_ADMIN, resourceID: '', }); - const saveAsTemplateRule = - this.authorizationPolicyService.createCredentialRule( - [AuthorizationPrivilege.SAVE_AS_TEMPLATE], - adminCriterias, - CREDENTIAL_RULE_TYPES_CALLOUT_SAVE_AS_TEMPLATE - ); - - saveAsTemplateRule.cascade = false; - newRules.push(saveAsTemplateRule); return this.authorizationPolicyService.appendCredentialAuthorizationRules( authorization, @@ -304,14 +293,6 @@ export class CollaborationAuthorizationService { POLICY_RULE_CALLOUT_CONTRIBUTE ); privilegeRules.push(createCalloutPrivilege); - - const createWhiteboardRtContributePrivilege = - new AuthorizationPolicyRulePrivilege( - [AuthorizationPrivilege.CREATE_WHITEBOARD_RT], - AuthorizationPrivilege.CONTRIBUTE, - POLICY_RULE_COLLABORATION_WHITEBOARD_CONTRIBUTORS_CREATE - ); - privilegeRules.push(createWhiteboardRtContributePrivilege); } return this.authorizationPolicyService.appendPrivilegeAuthorizationRules( From ffe45bdab246622bc6809a53bf908203820a0eeb Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Wed, 20 Nov 2024 12:26:44 +0200 Subject: [PATCH 12/19] fix creation of platform invitation for global role --- src/platform/platform.role/platform.role.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/platform.role/platform.role.service.ts b/src/platform/platform.role/platform.role.service.ts index 4638bb898d..9a97b36627 100644 --- a/src/platform/platform.role/platform.role.service.ts +++ b/src/platform/platform.role/platform.role.service.ts @@ -45,12 +45,12 @@ export class PlatformRoleService { LogContext.PLATFORM ); } + platformInvitationData.createdBy = agentInfo.userID; const platformInvitation = await this.platformInvitationService.createPlatformInvitation( platformInvitationData ); platformInvitation.platform = platform; - platformInvitation.createdBy = agentInfo.userID; return await this.platformInvitationService.save(platformInvitation); } From 91760fd826a3927a5c87cbe93db755920c0ad506 Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Wed, 20 Nov 2024 12:40:34 +0200 Subject: [PATCH 13/19] Add license.myLicensePrivileges similar to authorization.myPrivileges --- .../common/license/license.resolver.fields.ts | 11 +++++++++ src/domain/common/license/license.service.ts | 24 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/domain/common/license/license.resolver.fields.ts b/src/domain/common/license/license.resolver.fields.ts index ee5e1d99eb..0310b6224c 100644 --- a/src/domain/common/license/license.resolver.fields.ts +++ b/src/domain/common/license/license.resolver.fields.ts @@ -2,6 +2,7 @@ import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; import { ILicense } from './license.interface'; import { LicenseService } from './license.service'; import { ILicenseEntitlement } from '../license-entitlement/license.entitlement.interface'; +import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; @Resolver(() => ILicense) export class LicenseResolverFields { @@ -17,4 +18,14 @@ export class LicenseResolverFields { ): Promise { return await this.licenseService.getEntitlements(license); } + + @ResolveField('myLicensePrivileges', () => [LicenseEntitlementType], { + nullable: false, + description: 'The set of License Entitlements on that entity.', + }) + async myLicensePrivileges( + @Parent() license: ILicense + ): Promise { + return await this.licenseService.getMyLicensePrivilegesOrFail(license); + } } diff --git a/src/domain/common/license/license.service.ts b/src/domain/common/license/license.service.ts index e9531956ff..1ac7d8ca66 100644 --- a/src/domain/common/license/license.service.ts +++ b/src/domain/common/license/license.service.ts @@ -112,6 +112,30 @@ export class LicenseService { return this.getEntitlementsFromLicenseOrFail(license); } + public async getMyLicensePrivilegesOrFail( + licenseInput: ILicense + ): Promise { + let license = licenseInput; + if (!license.entitlements) { + license = await this.getLicenseOrFail(licenseInput.id, { + relations: { + entitlements: true, + }, + }); + } + const entitlements = this.getEntitlementsFromLicenseOrFail(license); + + return entitlements + .filter(entitlement => entitlement.enabled) + .filter( + async entitlement => + await this.licenseEntitlementService.isEntitlementAvailable( + entitlement.id + ) + ) + .map(entitlement => entitlement.type); + } + public reset(license: ILicense): ILicense { const entitlements = this.getEntitlementsFromLicenseOrFail(license); for (const entitlement of entitlements) { From c5765c3f76a0fdef7583788d37b99290d6ce7974 Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Wed, 20 Nov 2024 14:38:55 +0200 Subject: [PATCH 14/19] fix myLicensePrivileges type and reset license policy on account reset --- src/domain/common/license/license.resolver.fields.ts | 2 +- src/domain/space/account/account.resolver.mutations.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/domain/common/license/license.resolver.fields.ts b/src/domain/common/license/license.resolver.fields.ts index 0310b6224c..ecadd23aca 100644 --- a/src/domain/common/license/license.resolver.fields.ts +++ b/src/domain/common/license/license.resolver.fields.ts @@ -20,7 +20,7 @@ export class LicenseResolverFields { } @ResolveField('myLicensePrivileges', () => [LicenseEntitlementType], { - nullable: false, + nullable: true, description: 'The set of License Entitlements on that entity.', }) async myLicensePrivileges( diff --git a/src/domain/space/account/account.resolver.mutations.ts b/src/domain/space/account/account.resolver.mutations.ts index dc5bf541bb..76654f7f4b 100644 --- a/src/domain/space/account/account.resolver.mutations.ts +++ b/src/domain/space/account/account.resolver.mutations.ts @@ -301,6 +301,10 @@ export class AccountResolverMutations { const accountAuthorizations = await this.accountAuthorizationService.applyAuthorizationPolicy(account); await this.authorizationPolicyService.saveAll(accountAuthorizations); + const updatedLicenses = await this.accountLicenseService.applyLicensePolicy( + account.id + ); + await this.licenseService.saveAll(updatedLicenses); return await this.accountService.getAccountOrFail(account.id); } From f94993677908be0278998be26d7e21a055d02554 Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Wed, 20 Nov 2024 16:37:40 +0200 Subject: [PATCH 15/19] Fixed subspace entitlement assignment on creation --- .../space/space/space.resolver.mutations.ts | 19 ++++++++++++++++++- .../space/space/space.service.license.ts | 4 ++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/domain/space/space/space.resolver.mutations.ts b/src/domain/space/space/space.resolver.mutations.ts index 268094737d..ce249bf065 100644 --- a/src/domain/space/space/space.resolver.mutations.ts +++ b/src/domain/space/space/space.resolver.mutations.ts @@ -20,6 +20,8 @@ import { UpdateSpacePlatformSettingsInput } from './dto/space.dto.update.platfor import { SUBSCRIPTION_SUBSPACE_CREATED } from '@common/constants/providers'; import { UpdateSpaceSettingsInput } from './dto/space.dto.update.settings'; import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service'; +import { SpaceLicenseService } from './space.service.license'; +import { LicenseService } from '@domain/common/license/license.service'; @Resolver() export class SpaceResolverMutations { @@ -32,7 +34,9 @@ export class SpaceResolverMutations { private spaceAuthorizationService: SpaceAuthorizationService, @Inject(SUBSCRIPTION_SUBSPACE_CREATED) private subspaceCreatedSubscription: PubSubEngine, - private namingReporter: NameReporterService + private namingReporter: NameReporterService, + private spaceLicenseService: SpaceLicenseService, + private licenseService: LicenseService ) {} @UseGuards(GraphqlGuard) @@ -227,6 +231,19 @@ export class SpaceResolverMutations { subspaceCreatedEvent ); + const level0Space = await this.spaceService.getSpaceOrFail( + subspace.levelZeroSpaceID, + { + relations: { agent: { credentials: true } }, + } + ); + + const updatedLicenses = await this.spaceLicenseService.applyLicensePolicy( + subspace.id, + level0Space.agent + ); + await this.licenseService.saveAll(updatedLicenses); + return this.spaceService.getSpaceOrFail(subspace.id); } } diff --git a/src/domain/space/space/space.service.license.ts b/src/domain/space/space/space.service.license.ts index f92deda7da..a933e67d3b 100644 --- a/src/domain/space/space/space.service.license.ts +++ b/src/domain/space/space/space.service.license.ts @@ -27,7 +27,7 @@ export class SpaceLicenseService { async applyLicensePolicy( spaceID: string, - agent?: IAgent + level0SpaceAgent?: IAgent ): Promise { const space = await this.spaceService.getSpaceOrFail(spaceID, { relations: { @@ -62,7 +62,7 @@ export class SpaceLicenseService { // Ensure always applying from a clean state space.license = this.licenseService.reset(space.license); - const rootLevelSpaceAgent = agent ?? space.agent; + const rootLevelSpaceAgent = level0SpaceAgent ?? space.agent; space.license = await this.extendLicensePolicy( space.license, From d49affcf16f848b1d66d07788b0880a85a613dbc Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Wed, 20 Nov 2024 17:03:53 +0200 Subject: [PATCH 16/19] fix license check on myLicensePrivileges --- src/domain/common/license/license.service.ts | 23 ++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/domain/common/license/license.service.ts b/src/domain/common/license/license.service.ts index 1ac7d8ca66..0e74d83899 100644 --- a/src/domain/common/license/license.service.ts +++ b/src/domain/common/license/license.service.ts @@ -124,16 +124,21 @@ export class LicenseService { }); } const entitlements = this.getEntitlementsFromLicenseOrFail(license); - - return entitlements - .filter(entitlement => entitlement.enabled) - .filter( - async entitlement => - await this.licenseEntitlementService.isEntitlementAvailable( - entitlement.id - ) + const availableEntitlements = ( + await Promise.all( + entitlements.map(async entitlement => ({ + entitlement, + isAvailable: + await this.licenseEntitlementService.isEntitlementAvailable( + entitlement.id + ), + })) ) - .map(entitlement => entitlement.type); + ) + .filter(({ isAvailable }) => isAvailable) + .map(({ entitlement }) => entitlement.type); + + return availableEntitlements; } public reset(license: ILicense): ILicense { From 935796497df3f30dfe8eb8c5531f3d6aecbc76a3 Mon Sep 17 00:00:00 2001 From: Neil Smyth Date: Fri, 22 Nov 2024 16:26:13 +0200 Subject: [PATCH 17/19] revert change to account auth --- .../account/account.service.authorization.ts | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/domain/space/account/account.service.authorization.ts b/src/domain/space/account/account.service.authorization.ts index d3551851d2..72f7844e54 100644 --- a/src/domain/space/account/account.service.authorization.ts +++ b/src/domain/space/account/account.service.authorization.ts @@ -282,24 +282,10 @@ export class AccountAuthorizationService { accountHostManage.cascade = true; newRules.push(accountHostManage); - const vcCampaignCredential: ICredentialDefinition = { - type: AuthorizationCredential.VC_CAMPAIGN, - resourceID: '', - }; - - const betaTesterCredential: ICredentialDefinition = { - type: AuthorizationCredential.BETA_TESTER, - resourceID: '', - }; - - const createResourcesUnderAccountCredentials = [ - vcCampaignCredential, - betaTesterCredential, - ]; // If the user is a beta tester or part of VC campaign then can create the resources const createSpace = this.authorizationPolicyService.createCredentialRule( [AuthorizationPrivilege.CREATE_SPACE], - createResourcesUnderAccountCredentials, + [...hostCredentials], CREDENTIAL_RULE_PLATFORM_CREATE_SPACE ); createSpace.cascade = false; @@ -307,7 +293,7 @@ export class AccountAuthorizationService { const createVC = this.authorizationPolicyService.createCredentialRule( [AuthorizationPrivilege.CREATE_VIRTUAL_CONTRIBUTOR], - createResourcesUnderAccountCredentials, + [...hostCredentials], CREDENTIAL_RULE_PLATFORM_CREATE_VC ); createVC.cascade = false; @@ -316,7 +302,7 @@ export class AccountAuthorizationService { const createInnovationPack = this.authorizationPolicyService.createCredentialRule( [AuthorizationPrivilege.CREATE_INNOVATION_PACK], - createResourcesUnderAccountCredentials, + [...hostCredentials], CREDENTIAL_RULE_PLATFORM_CREATE_INNOVATION_PACK ); createInnovationPack.cascade = false; From 9a587c70f28fc9fbd5a834152c89f9264b125590 Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Mon, 25 Nov 2024 13:11:35 +0200 Subject: [PATCH 18/19] update licence policy on L0 space creation --- src/domain/space/account/account.resolver.mutations.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/domain/space/account/account.resolver.mutations.ts b/src/domain/space/account/account.resolver.mutations.ts index 76654f7f4b..d108258e70 100644 --- a/src/domain/space/account/account.resolver.mutations.ts +++ b/src/domain/space/account/account.resolver.mutations.ts @@ -43,6 +43,7 @@ import { LicenseService } from '@domain/common/license/license.service'; import { LicenseEntitlementType } from '@common/enums/license.entitlement.type'; import { AccountLicenseResetInput } from './dto/account.dto.reset.license'; import { AccountLicenseService } from './account.service.license'; +import { SpaceLicenseService } from '../space/space.service.license'; @Resolver() export class AccountResolverMutations { @@ -61,6 +62,7 @@ export class AccountResolverMutations { private namingReporter: NameReporterService, private spaceService: SpaceService, private spaceAuthorizationService: SpaceAuthorizationService, + private spaceLicenseService: SpaceLicenseService, private notificationAdapter: NotificationAdapter, private temporaryStorageService: TemporaryStorageService, private licenseService: LicenseService @@ -102,6 +104,11 @@ export class AccountResolverMutations { await this.spaceAuthorizationService.applyAuthorizationPolicy(space); await this.authorizationPolicyService.saveAll(spaceAuthorizations); + const updatedLicenses = await this.spaceLicenseService.applyLicensePolicy( + space.id + ); + await this.licenseService.saveAll(updatedLicenses); + space = await this.spaceService.getSpaceOrFail(space.id, { relations: { profile: true, From 2bbe67266c401fdd30c8e067625325916ac133ae Mon Sep 17 00:00:00 2001 From: Valentin Yanakiev Date: Mon, 25 Nov 2024 18:05:25 +0200 Subject: [PATCH 19/19] Rename myLicensePrivileges to availableEntitlements --- src/domain/common/license/license.resolver.fields.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/domain/common/license/license.resolver.fields.ts b/src/domain/common/license/license.resolver.fields.ts index ecadd23aca..40bd623160 100644 --- a/src/domain/common/license/license.resolver.fields.ts +++ b/src/domain/common/license/license.resolver.fields.ts @@ -19,11 +19,11 @@ export class LicenseResolverFields { return await this.licenseService.getEntitlements(license); } - @ResolveField('myLicensePrivileges', () => [LicenseEntitlementType], { + @ResolveField('availableEntitlements', () => [LicenseEntitlementType], { nullable: true, - description: 'The set of License Entitlements on that entity.', + description: 'The set of License Entitlement Types on that entity.', }) - async myLicensePrivileges( + async availableEntitlements( @Parent() license: ILicense ): Promise { return await this.licenseService.getMyLicensePrivilegesOrFail(license);