diff --git a/src/core/bootstrap/bootstrap.service.ts b/src/core/bootstrap/bootstrap.service.ts index 4ea3e2c2ae..e9d9f548c0 100644 --- a/src/core/bootstrap/bootstrap.service.ts +++ b/src/core/bootstrap/bootstrap.service.ts @@ -520,7 +520,7 @@ export class BootstrapService { const space = await this.accountService.createSpaceOnAccount(spaceInput); const spaceAuthorizations = - await this.spaceAuthorizationService.applyAuthorizationPolicy(space); + await this.spaceAuthorizationService.applyAuthorizationPolicy(space.id); await this.authorizationPolicyService.saveAll(spaceAuthorizations); const accountEntitlements = diff --git a/src/domain/common/authorization-policy/authorization.policy.entity.ts b/src/domain/common/authorization-policy/authorization.policy.entity.ts index f88f364756..7f94a7b6e1 100644 --- a/src/domain/common/authorization-policy/authorization.policy.entity.ts +++ b/src/domain/common/authorization-policy/authorization.policy.entity.ts @@ -1,5 +1,5 @@ import { BaseAlkemioEntity } from '@domain/common/entity/base-entity'; -import { Column, Entity } from 'typeorm'; +import { Column, Entity, ManyToOne } from 'typeorm'; import { IAuthorizationPolicy } from './authorization.policy.interface'; import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type'; import { ENUM_LENGTH } from '@common/constants'; @@ -24,6 +24,16 @@ export class AuthorizationPolicy @Column('varchar', { length: ENUM_LENGTH, nullable: false }) type!: AuthorizationPolicyType; + // An authorization can optionally choose to store a reference to the parent authorization from which it inherits + // This is useful for when the entity wants to adjust its settings + may no longer have access without hacky code + // to the authorization of the containing entity + @ManyToOne(() => AuthorizationPolicy, { + eager: false, + cascade: false, // MUST not cascade + onDelete: 'SET NULL', + }) + parentAuthorizationPolicy?: AuthorizationPolicy; + constructor(type: AuthorizationPolicyType) { super(); this.anonymousReadAccess = false; diff --git a/src/domain/common/authorization-policy/authorization.policy.interface.ts b/src/domain/common/authorization-policy/authorization.policy.interface.ts index dc772c41ce..f79f0d62a9 100644 --- a/src/domain/common/authorization-policy/authorization.policy.interface.ts +++ b/src/domain/common/authorization-policy/authorization.policy.interface.ts @@ -12,6 +12,8 @@ export abstract class IAuthorizationPolicy extends IBaseAlkemio { verifiedCredentialRules!: string; privilegeRules!: string; + parentAuthorizationPolicy?: IAuthorizationPolicy; + @Field(() => AuthorizationPolicyType, { nullable: true, description: diff --git a/src/domain/common/authorization-policy/authorization.policy.service.ts b/src/domain/common/authorization-policy/authorization.policy.service.ts index 53416f4c86..0a27ed4b66 100644 --- a/src/domain/common/authorization-policy/authorization.policy.service.ts +++ b/src/domain/common/authorization-policy/authorization.policy.service.ts @@ -151,7 +151,7 @@ export class AuthorizationPolicyService { return authorization; } - reset( + public reset( authorizationPolicy: IAuthorizationPolicy | undefined ): IAuthorizationPolicy { if (!authorizationPolicy) { diff --git a/src/domain/space/account/account.resolver.mutations.ts b/src/domain/space/account/account.resolver.mutations.ts index d108258e70..3f7fbf8a29 100644 --- a/src/domain/space/account/account.resolver.mutations.ts +++ b/src/domain/space/account/account.resolver.mutations.ts @@ -101,7 +101,7 @@ export class AccountResolverMutations { space = await this.spaceService.save(space); const spaceAuthorizations = - await this.spaceAuthorizationService.applyAuthorizationPolicy(space); + await this.spaceAuthorizationService.applyAuthorizationPolicy(space.id); await this.authorizationPolicyService.saveAll(spaceAuthorizations); const updatedLicenses = await this.spaceLicenseService.applyLicensePolicy( @@ -424,7 +424,7 @@ export class AccountResolverMutations { space = await this.spaceService.save(space); const spaceAuthorizations = - await this.spaceAuthorizationService.applyAuthorizationPolicy(space); + await this.spaceAuthorizationService.applyAuthorizationPolicy(space.id); await this.authorizationPolicyService.saveAll(spaceAuthorizations); // TODO: check if still needed later return await this.spaceService.getSpaceOrFail(space.id); diff --git a/src/domain/space/account/account.service.authorization.ts b/src/domain/space/account/account.service.authorization.ts index 53de8d949e..8264ad3fef 100644 --- a/src/domain/space/account/account.service.authorization.ts +++ b/src/domain/space/account/account.service.authorization.ts @@ -144,12 +144,9 @@ export class AccountAuthorizationService { } const updatedAuthorizations: IAuthorizationPolicy[] = []; - const clonedAccountAuth = - await this.getClonedAccountAuthExtendedForChildEntities(account); - for (const space of account.spaces) { const spaceAuthorizations = - await this.spaceAuthorizationService.applyAuthorizationPolicy(space); + await this.spaceAuthorizationService.applyAuthorizationPolicy(space.id); this.logger.verbose?.( `space nameID ${space.nameID}: authorizations to reset count = ${spaceAuthorizations.length}`, LogContext.AUTH @@ -178,6 +175,10 @@ export class AccountAuthorizationService { ); updatedAuthorizations.push(...storageAggregatorAuthorizations); + // For the VCs, InnovationPacks + InnovationHubs use a cloned + extended authorization + const clonedAccountAuth = + await this.getClonedAccountAuthExtendedForChildEntities(account); + for (const vc of account.virtualContributors) { const updatedVcAuthorizations = await this.virtualContributorAuthorizationService.applyAuthorizationPolicy( @@ -281,7 +282,6 @@ export class AccountAuthorizationService { accountHostManage.cascade = true; newRules.push(accountHostManage); - // 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], diff --git a/src/domain/space/space/space.resolver.mutations.ts b/src/domain/space/space/space.resolver.mutations.ts index ce249bf065..606116cada 100644 --- a/src/domain/space/space/space.resolver.mutations.ts +++ b/src/domain/space/space/space.resolver.mutations.ts @@ -138,7 +138,7 @@ export class SpaceResolverMutations { // but not all settings will require this, so only update if necessary if (shouldUpdateAuthorization) { const updatedAuthorizations = - await this.spaceAuthorizationService.applyAuthorizationPolicy(space); + await this.spaceAuthorizationService.applyAuthorizationPolicy(space.id); await this.authorizationPolicyService.saveAll(updatedAuthorizations); } @@ -168,7 +168,7 @@ export class SpaceResolverMutations { ); space = await this.spaceService.save(space); const updatedAuthorizations = - await this.spaceAuthorizationService.applyAuthorizationPolicy(space); + await this.spaceAuthorizationService.applyAuthorizationPolicy(space.id); await this.authorizationPolicyService.saveAll(updatedAuthorizations); return await this.spaceService.getSpaceOrFail(space.id); @@ -200,7 +200,10 @@ export class SpaceResolverMutations { // Save here so can reuse it later without another load const displayName = subspace.profile.displayName; const updatedAuthorizations = - await this.spaceAuthorizationService.applyAuthorizationPolicy(subspace); + await this.spaceAuthorizationService.applyAuthorizationPolicy( + subspace.id, + space.authorization // Important, and will be stored + ); await this.authorizationPolicyService.saveAll(updatedAuthorizations); diff --git a/src/domain/space/space/space.service.authorization.ts b/src/domain/space/space/space.service.authorization.ts index d1dff412d8..2c52b038a1 100644 --- a/src/domain/space/space/space.service.authorization.ts +++ b/src/domain/space/space/space.service.authorization.ts @@ -25,6 +25,7 @@ import { CREDENTIAL_RULE_SUBSPACE_ADMINS, CREDENTIAL_RULE_SPACE_ADMIN_DELETE_SUBSPACE, CREDENTIAL_RULE_TYPES_SPACE_PLATFORM_SETTINGS, + CREDENTIAL_RULE_TYPES_GLOBAL_SPACE_READ, } from '@common/constants'; import { EntityNotInitializedException } from '@common/exceptions'; import { IAuthorizationPolicyRuleCredential } from '@core/authorization/authorization.policy.rule.credential.interface'; @@ -38,12 +39,13 @@ 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'; +import { PlatformAuthorizationPolicyService } from '@platform/authorization/platform.authorization.policy.service'; @Injectable() export class SpaceAuthorizationService { constructor( + private platformAuthorizationService: PlatformAuthorizationPolicyService, private authorizationPolicyService: AuthorizationPolicyService, private agentAuthorizationService: AgentAuthorizationService, private roleSetService: RoleSetService, @@ -60,18 +62,20 @@ export class SpaceAuthorizationService { ) {} async applyAuthorizationPolicy( - spaceInput: ISpace + spaceID: string, + providedParentAuthorization?: IAuthorizationPolicy | undefined ): Promise { - const space = await this.spaceService.getSpaceOrFail(spaceInput.id, { + const space = await this.spaceService.getSpaceOrFail(spaceID, { relations: { + authorization: { + parentAuthorizationPolicy: true, + }, parentSpace: { - authorization: true, community: { roleSet: true, }, }, agent: true, - authorization: true, community: { roleSet: true, }, @@ -81,9 +85,7 @@ export class SpaceAuthorizationService { storageAggregator: true, subspaces: true, templatesManager: true, - license: { - entitlements: true, - }, + license: true, }, }); if ( @@ -91,8 +93,7 @@ export class SpaceAuthorizationService { !space.community || !space.community.roleSet || !space.subspaces || - !space.license || - !space.license.entitlements + !space.license ) { throw new RelationshipNotFoundException( `Unable to load Space with entities at start of auth reset: ${space.id} `, @@ -100,70 +101,77 @@ export class SpaceAuthorizationService { ); } - // Get the root space agent for licensing related logic - const spaceLicense = space.license; - const updatedAuthorizations: IAuthorizationPolicy[] = []; const spaceVisibility = space.visibility; + const spaceSettings = this.spaceSettingsService.getSettings( + space.settingsStr + ); + const isPrivate = spaceSettings.privacy.mode === SpacePrivacyMode.PRIVATE; + + // Store the provided parent authorization so that later resets can happen + // without having access to it. + // Note: reset does not remove this setting. + if (providedParentAuthorization) { + space.authorization.parentAuthorizationPolicy = + providedParentAuthorization; + } + // Note: later will need additional logic here for Templates // Allow the parent admins to also delete subspaces let parentSpaceAdminCredentialCriterias: ICredentialDefinition[] = []; - if (space.parentSpace) { - const parentSpaceCommunity = space.parentSpace.community; - if (!parentSpaceCommunity || !parentSpaceCommunity.roleSet) { - throw new RelationshipNotFoundException( - `Unable to load Space with parent RoleSet in auth reset: ${space.id} `, - LogContext.SPACES + switch (space.level) { + case SpaceLevel.SPACE: { + space.authorization = this.resetToLevelZeroSpaceAuthorization( + space.authorization ); + if (!isPrivate) { + space.authorization.anonymousReadAccess = true; + } + break; } - - const spaceSettings = this.spaceSettingsService.getSettings( - spaceInput.settingsStr - ); - parentSpaceAdminCredentialCriterias = - await this.roleSetService.getCredentialsForRole( - parentSpaceCommunity.roleSet, - CommunityRoleType.ADMIN, - spaceSettings - ); - } - - space.authorization = this.authorizationPolicyService.reset( - space.authorization - ); - - const spaceSettings = this.spaceSettingsService.getSettings( - space.settingsStr - ); - const privateSpace = - spaceSettings.privacy.mode === SpacePrivacyMode.PRIVATE; - - // Choose what authorization to inherit from - let parentAuthorization: IAuthorizationPolicy | undefined; - if (space.level === SpaceLevel.SPACE || privateSpace) { - const accountAuthorization = await this.getAccountAuthorization(space); - parentAuthorization = accountAuthorization; - } else { - if (!space.parentSpace || !space.parentSpace.authorization) { - throw new EntityNotInitializedException( - `Parent authorization not found on subspace auth reset: ${space.id} `, - LogContext.SPACES - ); + case SpaceLevel.CHALLENGE: + case SpaceLevel.OPPORTUNITY: { + if (isPrivate) { + // Key: private get the base space authorization setup, that is then extended + space.authorization = this.resetToLevelZeroSpaceAuthorization( + space.authorization + ); + space.authorization = await this.extendPrivateSubspaceAdmins( + space.authorization, + space.community.roleSet, + spaceSettings + ); + } else { + // Pick up the parent authorization + const parentAuthorization = + this.getParentAuthorizationPolicyOrFail(space); + space.authorization = + this.authorizationPolicyService.inheritParentAuthorization( + space.authorization, + parentAuthorization + ); + space.authorization.anonymousReadAccess = + parentAuthorization.anonymousReadAccess; + } + // For subspace, the parent space admins credentials should be allowed to delete + const parentSpaceCommunity = space.parentSpace?.community; + if (!parentSpaceCommunity || !parentSpaceCommunity.roleSet) { + throw new RelationshipNotFoundException( + `Unable to load Space with parent RoleSet in auth reset: ${space.id} `, + LogContext.SPACES + ); + } + parentSpaceAdminCredentialCriterias = + await this.roleSetService.getCredentialsForRole( + parentSpaceCommunity.roleSet, + CommunityRoleType.ADMIN, + spaceSettings + ); + break; } - parentAuthorization = space.parentSpace.authorization; } - space.authorization = - this.authorizationPolicyService.inheritParentAuthorization( - space.authorization, - parentAuthorization - ); - - space.authorization = await this.extendPlatformSettingsAdmin( - space.authorization - ); - let spaceMembershipAllowed = true; // Extend rules depending on the Visibility switch (spaceVisibility) { @@ -176,23 +184,6 @@ export class SpaceAuthorizationService { parentSpaceAdminCredentialCriterias ); - // - if (privateSpace) { - space.authorization.anonymousReadAccess = false; - if (space.level !== SpaceLevel.SPACE) { - space.authorization = await this.extendPrivateSubspaceAdmins( - space.authorization, - space.community.roleSet, - spaceSettings - ); - } - } else { - // Public space. Inherit from parent, or if top level directly - if (space.level === SpaceLevel.SPACE) { - space.authorization.anonymousReadAccess = true; - } - } - break; case SpaceVisibility.ARCHIVED: // ensure it has visibility privilege set to private @@ -211,7 +202,6 @@ export class SpaceAuthorizationService { // propagate authorization rules for child entities const childAuthorzations = await this.propagateAuthorizationToChildEntities( space, - spaceLicense, spaceSettings, spaceMembershipAllowed ); @@ -219,8 +209,10 @@ export class SpaceAuthorizationService { // Finally propagate to child spaces for (const subspace of space.subspaces) { - const updatedSubspaceAuthorizations = - await this.applyAuthorizationPolicy(subspace); + const updatedSubspaceAuthorizations = await this.applyAuthorizationPolicy( + subspace.id, + space.authorization + ); this.logger.verbose?.( `Subspace (${subspace.id}) auth reset: saving ${updatedSubspaceAuthorizations.length} authorizations`, LogContext.AUTH @@ -233,24 +225,22 @@ export class SpaceAuthorizationService { return updatedAuthorizations; } - private async getAccountAuthorization( + private getParentAuthorizationPolicyOrFail( space: ISpace - ): Promise { - const account = - await this.spaceService.getAccountForLevelZeroSpaceOrFail(space); - const accountAuthorization = account?.authorization; - if (!accountAuthorization) { + ): IAuthorizationPolicy | never { + // This will either pick up the one that was passed in or the stored reference + const parentAuthorization = space.authorization?.parentAuthorizationPolicy; + if (!parentAuthorization) { throw new RelationshipNotFoundException( - `Coulnd't find authorization for space: ${space.id} `, + `Space auth reset: Non L0 or private Space found without a parent Authorization set: ${space.id} `, LogContext.SPACES ); } - return accountAuthorization; + return parentAuthorization; } public async propagateAuthorizationToChildEntities( space: ISpace, - spaceLicense: ILicense, spaceSettings: ISpaceSettings, spaceMembershipAllowed: boolean ): Promise { @@ -515,19 +505,21 @@ export class SpaceAuthorizationService { return authorization; } - private async extendPlatformSettingsAdmin( - authorization: IAuthorizationPolicy | undefined - ): Promise { - if (!authorization) { - throw new EntityNotInitializedException( - 'Authorization definition not found for account', - LogContext.ACCOUNT + private resetToLevelZeroSpaceAuthorization( + authorizationPolicy: IAuthorizationPolicy | undefined + ): IAuthorizationPolicy { + let updatedAuthorization = + this.authorizationPolicyService.reset(authorizationPolicy); + updatedAuthorization.anonymousReadAccess = false; + updatedAuthorization = + this.platformAuthorizationService.inheritRootAuthorizationPolicy( + updatedAuthorization ); - } const newRules: IAuthorizationPolicyRuleCredential[] = []; // Allow global admins to manage platform settings + // Later: to allow account admins to some settings? const platformSettings = this.authorizationPolicyService.createCredentialRuleUsingTypesOnly( [AuthorizationPrivilege.PLATFORM_ADMIN], @@ -540,8 +532,17 @@ export class SpaceAuthorizationService { platformSettings.cascade = false; newRules.push(platformSettings); + // Allow Global Spaces Read to view Spaces + const globalSpacesReader = + this.authorizationPolicyService.createCredentialRuleUsingTypesOnly( + [AuthorizationPrivilege.READ], + [AuthorizationCredential.GLOBAL_SPACES_READER], + CREDENTIAL_RULE_TYPES_GLOBAL_SPACE_READ + ); + newRules.push(globalSpacesReader); + return this.authorizationPolicyService.appendCredentialAuthorizationRules( - authorization, + updatedAuthorization, newRules ); } diff --git a/src/migrations/1731781160588-authParent.ts b/src/migrations/1731781160588-authParent.ts new file mode 100644 index 0000000000..9db0e9eb7d --- /dev/null +++ b/src/migrations/1731781160588-authParent.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AuthParent1731781160588 implements MigrationInterface { + name = 'AuthParent1731781160588'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`authorization_policy\` ADD \`parentAuthorizationPolicyId\` char(36) NULL` + ); + await queryRunner.query( + `ALTER TABLE \`authorization_policy\` ADD CONSTRAINT \`FK_24b8950effd9ba78caa48ba76df\` FOREIGN KEY (\`parentAuthorizationPolicyId\`) REFERENCES \`authorization_policy\`(\`id\`) ON DELETE SET NULL ON UPDATE NO ACTION` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`authorization_policy\` DROP FOREIGN KEY \`FK_24b8950effd9ba78caa48ba76df\`` + ); + await queryRunner.query( + `ALTER TABLE \`authorization_policy\` DROP COLUMN \`parentAuthorizationPolicyId\`` + ); + } +} diff --git a/src/services/api/conversion/conversion.resolver.mutations.ts b/src/services/api/conversion/conversion.resolver.mutations.ts index fa5d259d7f..f005bb12c2 100644 --- a/src/services/api/conversion/conversion.resolver.mutations.ts +++ b/src/services/api/conversion/conversion.resolver.mutations.ts @@ -16,6 +16,8 @@ import { SpaceAuthorizationService } from '@domain/space/space/space.service.aut import { ConvertSubsubspaceToSubspaceInput } from './dto/convert.dto.subsubspace.to.subspace.input'; import { SpaceService } from '@domain/space/space/space.service'; import { GLOBAL_POLICY_CONVERSION_GLOBAL_ADMINS } from '@common/constants/authorization/global.policy.constants'; +import { RelationshipNotFoundException } from '@common/exceptions'; +import { LogContext } from '@common/enums'; @Resolver() export class ConversionResolverMutations { @@ -60,7 +62,7 @@ export class ConversionResolverMutations { ); space = await this.spaceService.save(space); const updatedAuthorizations = - await this.spaceAuthorizationService.applyAuthorizationPolicy(space); + await this.spaceAuthorizationService.applyAuthorizationPolicy(space.id); await this.authorizationPolicyService.saveAll(updatedAuthorizations); return this.spaceService.getSpaceOrFail(space.id); @@ -89,9 +91,35 @@ export class ConversionResolverMutations { agentInfo ); subspace = await this.spaceService.save(subspace); + + const parentAuthorization = await this.getParentSpaceAuthorization( + subspace.id + ); const subspaceAuthorizations = - await this.spaceAuthorizationService.applyAuthorizationPolicy(subspace); + await this.spaceAuthorizationService.applyAuthorizationPolicy( + subspace.id, + parentAuthorization + ); await this.authorizationPolicyService.saveAll(subspaceAuthorizations); return await this.spaceService.getSpaceOrFail(subspace.id); } + + private async getParentSpaceAuthorization( + subspaceID: string + ): Promise { + const subspace = await this.spaceService.getSpaceOrFail(subspaceID, { + relations: { + parentSpace: { + authorization: true, + }, + }, + }); + if (!subspace.parentSpace || !subspace.parentSpace.authorization) { + throw new RelationshipNotFoundException( + `Unable to load parent space authorization for subspace: ${subspaceID}`, + LogContext.CONVERSION + ); + } + return subspace.parentSpace.authorization; + } }