From 15873b02508d6c5b56abac72d1596ab4eeba9abc Mon Sep 17 00:00:00 2001 From: Souad Hadjiat Date: Fri, 22 Nov 2024 16:42:51 +0100 Subject: [PATCH] [backend] Prevent retention rules to delete user individuals (#7787) --- .../src/database/data-consistency.ts | 87 +++++++++++++++++++ .../src/database/middleware.js | 35 +++----- .../opencti-graphql/src/domain/individual.js | 20 +---- .../src/manager/retentionManager.ts | 29 ++++--- .../src/manager/taskManager.js | 4 - .../organization/organization-domain.ts | 28 +----- .../src/resolvers/individual.js | 2 +- .../01-database/middleware-test.js | 14 ++- .../02-resolvers/individual-test.js | 45 ++++++++++ .../04-manager/retentionManager-test.ts | 34 +++++++- 10 files changed, 210 insertions(+), 88 deletions(-) create mode 100644 opencti-platform/opencti-graphql/src/database/data-consistency.ts diff --git a/opencti-platform/opencti-graphql/src/database/data-consistency.ts b/opencti-platform/opencti-graphql/src/database/data-consistency.ts new file mode 100644 index 000000000000..0a7e16bb107e --- /dev/null +++ b/opencti-platform/opencti-graphql/src/database/data-consistency.ts @@ -0,0 +1,87 @@ +import { getEntityFromCache } from './cache'; +import { FunctionalError } from '../config/errors'; +import { FilterMode } from '../generated/graphql'; +import { countAllThings, listEntitiesThroughRelationsPaginated } from './middleware-loader'; +import { type BasicStoreEntityOrganization, ENTITY_TYPE_IDENTITY_ORGANIZATION } from '../modules/organization/organization-types'; +import { ENTITY_TYPE_SETTINGS, ENTITY_TYPE_USER } from '../schema/internalObject'; +import { RELATION_PARTICIPATE_TO } from '../schema/internalRelationship'; +import { ENTITY_TYPE_IDENTITY_INDIVIDUAL } from '../schema/stixDomainObject'; +import type { AuthContext, AuthUser } from '../types/user'; +import type { BasicStoreEntity, StoreObject } from '../types/store'; +import type { BasicStoreSettings } from '../types/settings'; +import { isEmptyField, READ_INDEX_INTERNAL_OBJECTS } from './utils'; +import { isUserHasCapability, SETTINGS_SET_ACCESSES, SYSTEM_USER } from '../utils/access'; + +export const isIndividualAssociatedToUser = async (context: AuthContext, individual: BasicStoreEntity) => { + const individualContactInformation = individual.contact_information; + if (isEmptyField(individualContactInformation)) { + return false; + } + const args = { + filters: { + mode: FilterMode.And, + filters: [{ key: ['user_email'], values: [individualContactInformation] }], + filterGroups: [], + }, + noFiltersChecking: true, + indices: [READ_INDEX_INTERNAL_OBJECTS], + }; + const usersCount = await countAllThings(context, SYSTEM_USER, args); + return usersCount > 0; +}; + +export const verifyCanDeleteIndividual = async (context: AuthContext, user: AuthUser, individual: BasicStoreEntity, throwErrors = true) => { + const isAssociatedToUser = await isIndividualAssociatedToUser(context, individual); + if (isAssociatedToUser) { + if (throwErrors) throw FunctionalError('Cannot delete an individual corresponding to a user'); + return false; + } + return true; +}; + +export const verifyCanDeleteOrganization = async (context: AuthContext, user: AuthUser, organization: BasicStoreEntityOrganization, throwErrors = true) => { + const settings = await getEntityFromCache(context, SYSTEM_USER, ENTITY_TYPE_SETTINGS); + if (organization.id === settings.platform_organization) { + if (throwErrors) throw FunctionalError('Cannot delete the platform organization.'); + return false; + } + if (organization.authorized_authorities && organization.authorized_authorities.length > 0) { + if (isUserHasCapability(user, SETTINGS_SET_ACCESSES)) { + if (throwErrors) throw FunctionalError('Cannot delete an organization that has an administrator.'); + return false; + } + // no information leakage about the organization administrators or members + if (throwErrors) throw FunctionalError('Cannot delete the organization.'); + return false; + } + // organizationMembersPaginated + const members = await listEntitiesThroughRelationsPaginated( + context, + user, + organization.id, + RELATION_PARTICIPATE_TO, + ENTITY_TYPE_USER, + true, + { first: 1, indices: [READ_INDEX_INTERNAL_OBJECTS] } + ); + if (members.pageInfo.globalCount > 0) { + if (isUserHasCapability(user, SETTINGS_SET_ACCESSES)) { + if (throwErrors) throw FunctionalError('Cannot delete an organization that has members.'); + return false; + } + // no information leakage about the organization administrators or members + if (throwErrors) throw FunctionalError('Cannot delete the organization.'); + return false; + } + return true; +}; + +export const canDeleteElement = async (context: AuthContext, user: AuthUser, element: StoreObject) => { + if (element.entity_type === ENTITY_TYPE_IDENTITY_INDIVIDUAL) { + return verifyCanDeleteIndividual(context, user, element as BasicStoreEntity, false); + } + if (element.entity_type === ENTITY_TYPE_IDENTITY_ORGANIZATION) { + return verifyCanDeleteOrganization(context, user, element as unknown as BasicStoreEntityOrganization, false); + } + return true; +}; diff --git a/opencti-platform/opencti-graphql/src/database/middleware.js b/opencti-platform/opencti-graphql/src/database/middleware.js index 50ac4e7fffc1..301e140264c7 100644 --- a/opencti-platform/opencti-graphql/src/database/middleware.js +++ b/opencti-platform/opencti-graphql/src/database/middleware.js @@ -195,6 +195,7 @@ import { ENTITY_TYPE_INDICATOR } from '../modules/indicator/indicator-types'; import { ENTITY_TYPE_CONTAINER_FEEDBACK } from '../modules/case/feedback/feedback-types'; import { FilterMode, FilterOperator } from '../generated/graphql'; import { getMandatoryAttributesForSetting } from '../modules/entitySetting/entitySetting-attributeUtils'; +import { ENTITY_TYPE_IDENTITY_ORGANIZATION } from '../modules/organization/organization-types'; import { adaptUpdateInputsConfidence, controlCreateInputWithUserConfidence, @@ -202,6 +203,7 @@ import { controlUserConfidenceAgainstElement } from '../utils/confidence-level'; import { buildEntityData, buildInnerRelation, buildRelationData } from './data-builder'; +import { isIndividualAssociatedToUser, verifyCanDeleteIndividual, verifyCanDeleteOrganization } from './data-consistency'; import { deleteAllObjectFiles, moveAllFilesFromEntityToAnother, uploadToStorage } from './file-storage-helper'; import { storeFileConverter } from './file-storage'; import { getDraftContext } from '../utils/draftContext'; @@ -1883,17 +1885,8 @@ export const updateAttributeMetaResolved = async (context, user, initial, inputs // Individual check const { bypassIndividualUpdate } = opts; if (initial.entity_type === ENTITY_TYPE_IDENTITY_INDIVIDUAL && !isEmptyField(initial.contact_information) && !bypassIndividualUpdate) { - const args = { - filters: { - mode: 'and', - filters: [{ key: 'user_email', values: [initial.contact_information] }], - filterGroups: [], - }, - noFiltersChecking: true, - connectionFormat: false - }; - const users = await listEntities(context, SYSTEM_USER, [ENTITY_TYPE_USER], args); - if (users.length > 0) { + const isIndividualUser = await isIndividualAssociatedToUser(context, initial); + if (isIndividualUser) { throw FunctionalError('Cannot update an individual corresponding to a user'); } } @@ -3188,20 +3181,12 @@ export const internalDeleteElementById = async (context, user, id, opts = {}) => } // endregion // Prevent individual deletion if linked to a user - if (element.entity_type === ENTITY_TYPE_IDENTITY_INDIVIDUAL && !isEmptyField(element.contact_information)) { - const args = { - filters: { - mode: 'and', - filters: [{ key: 'user_email', values: [element.contact_information] }], - filterGroups: [], - }, - noFiltersChecking: true, - connectionFormat: false - }; - const users = await listEntities(context, SYSTEM_USER, [ENTITY_TYPE_USER], args); - if (users.length > 0) { - throw FunctionalError('Cannot delete an individual corresponding to a user'); - } + if (element.entity_type === ENTITY_TYPE_IDENTITY_INDIVIDUAL) { + await verifyCanDeleteIndividual(context, user, element); + } + // Prevent organization deletion if platform orga or has members + if (element.entity_type === ENTITY_TYPE_IDENTITY_ORGANIZATION) { + await verifyCanDeleteOrganization(context, user, element); } if (!validateUserAccessOperation(user, element, 'delete')) { throw ForbiddenAccess(); diff --git a/opencti-platform/opencti-graphql/src/domain/individual.js b/opencti-platform/opencti-graphql/src/domain/individual.js index 3cf11707daf4..577db0a3897f 100644 --- a/opencti-platform/opencti-graphql/src/domain/individual.js +++ b/opencti-platform/opencti-graphql/src/domain/individual.js @@ -2,13 +2,11 @@ import * as R from 'ramda'; import { createEntity } from '../database/middleware'; import { listEntities, listEntitiesThroughRelationsPaginated, storeLoadById } from '../database/middleware-loader'; import { BUS_TOPICS } from '../config/conf'; +import { isIndividualAssociatedToUser } from '../database/data-consistency'; import { notify } from '../database/redis'; import { ENTITY_TYPE_IDENTITY_INDIVIDUAL } from '../schema/stixDomainObject'; import { ABSTRACT_STIX_DOMAIN_OBJECT } from '../schema/general'; import { RELATION_PART_OF } from '../schema/stixCoreRelationship'; -import { SYSTEM_USER } from '../utils/access'; -import { ENTITY_TYPE_USER } from '../schema/internalObject'; -import { isEmptyField } from '../database/utils'; import { ENTITY_TYPE_IDENTITY_ORGANIZATION } from '../modules/organization/organization-types'; export const findById = (context, user, individualId) => { @@ -29,18 +27,6 @@ export const partOfOrganizationsPaginated = async (context, user, individualId, return listEntitiesThroughRelationsPaginated(context, user, individualId, RELATION_PART_OF, ENTITY_TYPE_IDENTITY_ORGANIZATION, false, args); }; -export const isUser = async (context, individualContactInformation) => { - if (isEmptyField(individualContactInformation)) { - return false; - } - const args = { - filters: { - mode: 'and', - filters: [{ key: 'user_email', values: [individualContactInformation] }], - filterGroups: [], - }, - connectionFormat: false - }; - const users = await listEntities(context, SYSTEM_USER, [ENTITY_TYPE_USER], args); - return users.length > 0; +export const isUser = async (context, individual) => { + return isIndividualAssociatedToUser(context, individual); }; diff --git a/opencti-platform/opencti-graphql/src/manager/retentionManager.ts b/opencti-platform/opencti-graphql/src/manager/retentionManager.ts index 6d344dee0b77..700407747f01 100644 --- a/opencti-platform/opencti-graphql/src/manager/retentionManager.ts +++ b/opencti-platform/opencti-graphql/src/manager/retentionManager.ts @@ -14,10 +14,9 @@ import { registerManager } from './managerModule'; import type { AuthContext } from '../types/user'; import type { FileEdge, RetentionRule } from '../generated/graphql'; import { RetentionRuleScope, RetentionUnit } from '../generated/graphql'; +import { canDeleteElement } from '../database/data-consistency'; import { deleteFile } from '../database/file-storage'; import { DELETABLE_FILE_STATUSES, paginatedForPathWithEnrichment } from '../modules/internal/document/document-domain'; -import { ENTITY_TYPE_IDENTITY_ORGANIZATION } from '../modules/organization/organization-types'; -import { organizationDelete } from '../modules/organization/organization-domain'; import type { BasicStoreCommonEdge, StoreObject } from '../types/store'; import { ALREADY_DELETED_ERROR } from '../config/errors'; @@ -35,12 +34,7 @@ export const RETENTION_UNIT_VALUES = Object.values(RetentionUnit); export const deleteElement = async (context: AuthContext, scope: string, nodeId: string, nodeEntityType?: string) => { if (scope === 'knowledge') { - if (nodeEntityType === ENTITY_TYPE_IDENTITY_ORGANIZATION) { - // call organizationDelete which will ensure protections (for platform organization & members) - await organizationDelete(context, RETENTION_MANAGER_USER, nodeId); - } else { - await deleteElementById(context, RETENTION_MANAGER_USER, nodeId, nodeEntityType, { forceDelete: true }); - } + await deleteElementById(context, RETENTION_MANAGER_USER, nodeId, nodeEntityType, { forceDelete: true }); } else if (scope === 'file' || scope === 'workbench') { await deleteFile(context, RETENTION_MANAGER_USER, nodeId); } else { @@ -73,8 +67,9 @@ const executeProcessing = async (context: AuthContext, retentionRule: RetentionR logApp.debug(`[OPENCTI] Executing retention manager rule ${name}`); const before = utcDate().subtract(maxNumber, unit ?? 'days'); const result = await getElementsToDelete(context, scope, before, filters); - const remainingDeletions = result.pageInfo.globalCount; + let remainingDeletions = result.pageInfo.globalCount; const elements = result.edges; + let deletedCount = elements.length; if (elements.length > 0) { logApp.debug(`[OPENCTI] Retention manager clearing ${elements.length} elements`); const start = new Date().getTime(); @@ -82,9 +77,17 @@ const executeProcessing = async (context: AuthContext, retentionRule: RetentionR const { node } = element; const { updated_at: up } = node; try { - const humanDuration = moment.duration(utcDate(up).diff(utcDate())).humanize(); - await deleteElement(context, scope, scope === 'knowledge' ? node.internal_id : node.id, node.entity_type); - logApp.debug(`[OPENCTI] Retention manager deleting ${node.id} after ${humanDuration}`); + const canElementBeDeleted = await canDeleteElement(context, RETENTION_MANAGER_USER, node); + if (canElementBeDeleted) { // filter elements that can't be deleted (ex: user individuals) + const humanDuration = moment.duration(utcDate(up).diff(utcDate())).humanize(); + await deleteElement(context, scope, scope === 'knowledge' ? node.internal_id : node.id, node.entity_type); + logApp.debug(`[OPENCTI] Retention manager deleting ${node.id} after ${humanDuration}`); + } else { + // remove element from counters, since we can't delete it + remainingDeletions -= 1; + deletedCount -= 1; + logApp.debug(`[OPENCTI] Retention manager cannot delete ${node.id}.`); + } } catch (err: any) { // Only log the error if not an already deleted message (that can happen though concurrency deletion) if (err?.extensions?.code !== ALREADY_DELETED_ERROR) { @@ -99,7 +102,7 @@ const executeProcessing = async (context: AuthContext, retentionRule: RetentionR const patch = { last_execution_date: now(), remaining_count: remainingDeletions, - last_deleted_count: elements.length, + last_deleted_count: deletedCount, }; await patchAttribute(context, RETENTION_MANAGER_USER, id, ENTITY_TYPE_RETENTION_RULE, patch); }; diff --git a/opencti-platform/opencti-graphql/src/manager/taskManager.js b/opencti-platform/opencti-graphql/src/manager/taskManager.js index d7d8b9fd3c61..24f893a41357 100644 --- a/opencti-platform/opencti-graphql/src/manager/taskManager.js +++ b/opencti-platform/opencti-graphql/src/manager/taskManager.js @@ -79,7 +79,6 @@ import { BackgroundTaskScope } from '../generated/graphql'; import { ENTITY_TYPE_INTERNAL_FILE } from '../schema/internalObject'; import { deleteFile } from '../database/file-storage'; import { checkUserIsAdminOnDashboard } from '../modules/publicDashboard/publicDashboard-utils'; -import { organizationDelete } from '../modules/organization/organization-domain'; import { ENTITY_TYPE_IDENTITY_ORGANIZATION } from '../modules/organization/organization-types'; // Task manager responsible to execute long manual tasks @@ -238,9 +237,6 @@ const executeDelete = async (context, user, element, scope) => { } if (scope === BackgroundTaskScope.Import) { await deleteFile(context, user, element.id); - } else if (element.entity_type === ENTITY_TYPE_IDENTITY_ORGANIZATION) { - // call organizationDelete which will ensure protections (for platform organization & members) - await organizationDelete(context, user, element.internal_id); } else { await deleteElementById(context, user, element.internal_id, element.entity_type); } diff --git a/opencti-platform/opencti-graphql/src/modules/organization/organization-domain.ts b/opencti-platform/opencti-graphql/src/modules/organization/organization-domain.ts index 1bbad63b9ad3..59eff74debf1 100644 --- a/opencti-platform/opencti-graphql/src/modules/organization/organization-domain.ts +++ b/opencti-platform/opencti-graphql/src/modules/organization/organization-domain.ts @@ -10,20 +10,18 @@ import { } from '../../database/middleware-loader'; import { BUS_TOPICS } from '../../config/conf'; import { notify } from '../../database/redis'; -import { getEntityFromCache } from '../../database/cache'; -import { READ_INDEX_INTERNAL_OBJECTS } from '../../database/utils'; +import { verifyCanDeleteOrganization } from '../../database/data-consistency'; import { ENTITY_TYPE_IDENTITY_SECTOR } from '../../schema/stixDomainObject'; import { RELATION_PART_OF } from '../../schema/stixCoreRelationship'; import { ABSTRACT_STIX_DOMAIN_OBJECT } from '../../schema/general'; import { RELATION_PARTICIPATE_TO } from '../../schema/internalRelationship'; -import { ENTITY_TYPE_SETTINGS, ENTITY_TYPE_USER } from '../../schema/internalObject'; +import { ENTITY_TYPE_USER } from '../../schema/internalObject'; import { type BasicStoreEntityOrganization, ENTITY_TYPE_IDENTITY_ORGANIZATION } from './organization-types'; import type { AuthContext, AuthUser } from '../../types/user'; import type { BasicObject, OrganizationAddInput, ResolversTypes } from '../../generated/graphql'; import { AlreadyDeletedError, FunctionalError } from '../../config/errors'; -import { isUserHasCapability, SETTINGS_SET_ACCESSES, SYSTEM_USER } from '../../utils/access'; +import { isUserHasCapability, SETTINGS_SET_ACCESSES } from '../../utils/access'; import { publishUserAction } from '../../listener/UserActionListener'; -import type { BasicStoreSettings } from '../../types/settings'; import type { BasicStoreCommon, BasicStoreEntity } from '../../types/store'; import { userSessionRefresh } from '../../domain/user'; @@ -142,25 +140,7 @@ export const organizationDelete = async (context: AuthContext, user: AuthUser, o if (!organization) { throw AlreadyDeletedError({ organizationId }); } - const settings = await getEntityFromCache(context, SYSTEM_USER, ENTITY_TYPE_SETTINGS); - if (organization.id === settings.platform_organization) { - throw FunctionalError('Cannot delete the platform organization.'); - } - if (organization.authorized_authorities && organization.authorized_authorities.length > 0) { - if (isUserHasCapability(user, SETTINGS_SET_ACCESSES)) { - throw FunctionalError('Cannot delete an organization that has an administrator.'); - } - // no information leakage about the organization administrators or members - throw FunctionalError('Cannot delete the organization.'); - } - const members = await organizationMembersPaginated(context, user, organization.id, { first: 1, indices: [READ_INDEX_INTERNAL_OBJECTS] }); - if (members.pageInfo.globalCount > 0) { - if (isUserHasCapability(user, SETTINGS_SET_ACCESSES)) { - throw FunctionalError('Cannot delete an organization that has members.'); - } - // no information leakage about the organization administrators or members - throw FunctionalError('Cannot delete the organization.'); - } + await verifyCanDeleteOrganization(context, user, organization); await deleteElementById(context, user, organizationId, ENTITY_TYPE_IDENTITY_ORGANIZATION); await notify(BUS_TOPICS[ABSTRACT_STIX_DOMAIN_OBJECT].DELETE_TOPIC, organizationId, user); return organizationId; diff --git a/opencti-platform/opencti-graphql/src/resolvers/individual.js b/opencti-platform/opencti-graphql/src/resolvers/individual.js index 77a7478ad022..e0cecdcab675 100644 --- a/opencti-platform/opencti-graphql/src/resolvers/individual.js +++ b/opencti-platform/opencti-graphql/src/resolvers/individual.js @@ -15,7 +15,7 @@ const individualResolvers = { }, Individual: { organizations: (individual, args, context) => partOfOrganizationsPaginated(context, context.user, individual.id, args), - isUser: (individual, _, context) => isUser(context, individual.contact_information), + isUser: (individual, _, context) => isUser(context, individual), }, Mutation: { individualEdit: (_, { id }, context) => ({ diff --git a/opencti-platform/opencti-graphql/tests/02-integration/01-database/middleware-test.js b/opencti-platform/opencti-graphql/tests/02-integration/01-database/middleware-test.js index 4622f6851e24..ff008ff813da 100644 --- a/opencti-platform/opencti-graphql/tests/02-integration/01-database/middleware-test.js +++ b/opencti-platform/opencti-graphql/tests/02-integration/01-database/middleware-test.js @@ -16,7 +16,7 @@ import { updateAttribute, } from '../../../src/database/middleware'; import { elFindByIds, elLoadById, elRawSearch } from '../../../src/database/engine'; -import { ADMIN_USER, buildStandardUser, testContext } from '../../utils/testQuery'; +import { ADMIN_USER, buildStandardUser, TEST_ORGANIZATION, testContext } from '../../utils/testQuery'; import { ENTITY_TYPE_ATTACK_PATTERN, ENTITY_TYPE_CAMPAIGN, @@ -1423,3 +1423,15 @@ describe('Elements deduplication behaviors', () => { await deleteElementById(testContext, ADMIN_USER, group2.id, ENTITY_TYPE_THREAT_ACTOR_GROUP); }); }); + +describe('Delete functional errors behaviors', () => { + it('should not be able to delete organization that has members', async () => { + await expect(() => deleteElementById(testContext, ADMIN_USER, TEST_ORGANIZATION.id, ENTITY_TYPE_IDENTITY_ORGANIZATION)) + .rejects.toThrowError('Cannot delete an organization that has members.'); + }); + it.skip('should not be able to delete individual associated to user', async () => { + const individualUserId = 'identity--cfb1de38-c40a-5f51-81f3-35036a4e3b91'; // admin individual + await expect(() => deleteElementById(testContext, ADMIN_USER, individualUserId, ENTITY_TYPE_IDENTITY_INDIVIDUAL)) + .rejects.toThrowError('Cannot delete an individual corresponding to a user'); + }); +}); diff --git a/opencti-platform/opencti-graphql/tests/02-integration/02-resolvers/individual-test.js b/opencti-platform/opencti-graphql/tests/02-integration/02-resolvers/individual-test.js index 7ce2ee17fac4..b6cdabfe3669 100644 --- a/opencti-platform/opencti-graphql/tests/02-integration/02-resolvers/individual-test.js +++ b/opencti-platform/opencti-graphql/tests/02-integration/02-resolvers/individual-test.js @@ -1,6 +1,7 @@ import { expect, it, describe } from 'vitest'; import gql from 'graphql-tag'; import { ADMIN_USER, testContext, queryAsAdmin } from '../../utils/testQuery'; +import { adminQueryWithError } from '../../utils/testQueryHelper'; import { elLoadById } from '../../../src/database/engine'; const LIST_QUERY = gql` @@ -38,6 +39,7 @@ const READ_QUERY = gql` standard_id name description + isUser organizations { edges { node { @@ -93,6 +95,7 @@ describe('Individual resolver standard behavior', () => { expect(queryResult).not.toBeNull(); expect(queryResult.data.individual).not.toBeNull(); expect(queryResult.data.individual.id).toEqual(individualInternalId); + expect(queryResult.data.individual.isUser).toBeFalsy(); }); it('should individual organizations to be accurate', async () => { const individual = await elLoadById(testContext, ADMIN_USER, 'identity--d37acc64-4a6f-4dc2-879a-a4c138d0a27f'); @@ -231,3 +234,45 @@ describe('Individual resolver standard behavior', () => { expect(queryResult.data.individual).toBeNull(); }); }); + +describe('Individual associated to user tests', () => { + const individualUserId = 'identity--cfb1de38-c40a-5f51-81f3-35036a4e3b91'; // admin individual + it('should individual loaded by internal id', async () => { + const queryResult = await queryAsAdmin({ query: READ_QUERY, variables: { id: individualUserId } }); + expect(queryResult).not.toBeNull(); + expect(queryResult.data.individual).not.toBeNull(); + expect(queryResult.data.individual.isUser).toBeTruthy(); + }); + it('should not delete individual associated to user', async () => { + const DELETE_QUERY = gql` + mutation individualDelete($id: ID!) { + individualEdit(id: $id) { + delete + } + } + `; + // Delete the individual + await adminQueryWithError({ + query: DELETE_QUERY, + variables: { id: individualUserId }, + }, 'Cannot delete an individual corresponding to a user', 'FUNCTIONAL_ERROR'); + }); + it('should not update individual if user', async () => { + const UPDATE_QUERY = gql` + mutation IndividualEdit($id: ID!, $input: [EditInput]!) { + individualEdit(id: $id) { + fieldPatch(input: $input) { + id + name + contact_information + isUser + } + } + } + `; + await adminQueryWithError({ + query: UPDATE_QUERY, + variables: { id: individualUserId, input: [{ key: 'name', value: ['Individual - test'] }] }, + }, 'Cannot update an individual corresponding to a user', 'FUNCTIONAL_ERROR'); + }); +}); diff --git a/opencti-platform/opencti-graphql/tests/02-integration/04-manager/retentionManager-test.ts b/opencti-platform/opencti-graphql/tests/02-integration/04-manager/retentionManager-test.ts index a1f627a16ac9..ccf1c76d8178 100644 --- a/opencti-platform/opencti-graphql/tests/02-integration/04-manager/retentionManager-test.ts +++ b/opencti-platform/opencti-graphql/tests/02-integration/04-manager/retentionManager-test.ts @@ -12,7 +12,8 @@ import { READ_INDEX_INTERNAL_OBJECTS, READ_INDEX_STIX_DOMAIN_OBJECTS } from '../ import { DatabaseError } from '../../../src/config/errors'; import { deleteFile, loadFile } from '../../../src/database/file-storage'; import { deleteElementById } from '../../../src/database/middleware'; -import { ENTITY_TYPE_CONTAINER_REPORT } from '../../../src/schema/stixDomainObject'; +import { canDeleteElement } from '../../../src/database/data-consistency'; +import { ENTITY_TYPE_CONTAINER_REPORT, ENTITY_TYPE_IDENTITY_INDIVIDUAL } from '../../../src/schema/stixDomainObject'; describe('Retention Manager tests ', () => { const context = testContext; @@ -303,11 +304,33 @@ describe('Retention Manager tests ', () => { expect(workbenchesToDelete.edges[0].node.id).toEqual(workbench1Id); }); it('should fetch the correct report to be deleted by a retention rule on knowledge', async () => { - // retention rule on workbenches not modified since 2023-07-01 + // retention rule on knowledge not modified since 2023-07-01 const before = utcDate('2023-07-01T00:00:00.000Z'); const reportsToDelete = await getElementsToDelete(context, 'knowledge', before); - expect(reportsToDelete.edges.length).toEqual(1); // workbench1 is the only workbench that has not been modified since 'before' + expect(reportsToDelete.edges.length).toEqual(1); expect(reportsToDelete.edges[0].node.id).toEqual(report1Id); + const canDeleteReport = await canDeleteElement(context, ADMIN_USER, reportsToDelete.edges[0].node); + expect(canDeleteReport).toBeTruthy(); + }); + it('should fetch individuals to delete', async () => { + // retention rule on workbenches not modified since 2023-07-01 + const before = utcDate(); + const filters = { + mode: 'and', + filters: [{ + key: ['entity_type'], + values: [ENTITY_TYPE_IDENTITY_INDIVIDUAL], + operator: 'eq', + mode: 'or', + }], + filterGroups: [], + }; + const elementsToDelete = await getElementsToDelete(context, 'knowledge', before, JSON.stringify(filters)); + expect(elementsToDelete.edges.length).toEqual(3); + const adminIndividual = elementsToDelete.edges.find((e: any) => e.node.name === 'admin'); + expect(await canDeleteElement(context, ADMIN_USER, adminIndividual.node)).toBeFalsy(); + const otherIndividual = elementsToDelete.edges.find((e: any) => !e.node.contact_information); + expect(await canDeleteElement(context, ADMIN_USER, otherIndividual.node)).toBeTruthy(); }); it('should delete the fetched files and workbenches', async () => { // delete file @@ -327,4 +350,9 @@ describe('Retention Manager tests ', () => { await expect(() => deleteElement(context, 'knowledge', TEST_ORGANIZATION.id, ENTITY_TYPE_IDENTITY_ORGANIZATION)) .rejects.toThrowError('Cannot delete an organization that has members.'); }); + it('should not delete individual associated to user', async () => { + const individualUserId = 'identity--cfb1de38-c40a-5f51-81f3-35036a4e3b91'; // admin individual + await expect(() => deleteElement(context, 'knowledge', individualUserId, ENTITY_TYPE_IDENTITY_INDIVIDUAL)) + .rejects.toThrowError('Cannot delete an individual corresponding to a user'); + }); });