From 54db7e8e47abc87fda8839e1b4b072a9c231e3af Mon Sep 17 00:00:00 2001 From: Cathia Archidoit Date: Mon, 18 May 2026 10:48:35 +0200 Subject: [PATCH 1/6] [backend] Sharing saved filters APIs (#11624) --- .../modules/savedFilter/savedFilter-domain.ts | 34 +- .../savedFilter/savedFilter-resolver.ts | 17 +- .../modules/savedFilter/savedFilter.graphql | 4 + .../src/modules/savedFilter/savedFilter.ts | 3 +- .../02-resolvers/savedFilter-test.ts | 299 ++++++++++++++++++ .../02-resolvers/savedFilterResolver-test.ts | 169 ---------- 6 files changed, 352 insertions(+), 174 deletions(-) create mode 100644 opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/savedFilter-test.ts delete mode 100644 opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/savedFilterResolver-test.ts diff --git a/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter-domain.ts b/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter-domain.ts index 64b61578ab7d..88ba53f052cf 100644 --- a/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter-domain.ts +++ b/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter-domain.ts @@ -1,12 +1,14 @@ import { publishUserAction } from '../../listener/UserActionListener'; -import { FunctionalError } from '../../config/errors'; +import { ForbiddenAccess, FunctionalError } from '../../config/errors'; import { updateAttribute } from '../../database/middleware'; import { type BasicStoreEntitySavedFilter, ENTITY_TYPE_SAVED_FILTER, type StoreEntitySavedFilter } from './savedFilter-types'; import type { AuthContext, AuthUser } from '../../types/user'; import { pageEntitiesConnection, storeLoadById } from '../../database/middleware-loader'; -import type { MutationSavedFilterFieldPatchArgs, QuerySavedFiltersArgs, SavedFilterAddInput } from '../../generated/graphql'; +import type { MemberAccessInput, MutationSavedFilterFieldPatchArgs, QuerySavedFiltersArgs, SavedFilterAddInput } from '../../generated/graphql'; import { createInternalObject, deleteInternalObject } from '../../domain/internalObject'; -import { MEMBER_ACCESS_RIGHT_ADMIN } from '../../utils/access'; +import { getUserAccessRight, KNOWLEDGE_KNSHAREFILTERS, MEMBER_ACCESS_RIGHT_ADMIN } from '../../utils/access'; +import { editAuthorizedMembers } from '../../utils/authorizedMembers'; +import { isFeatureEnabled } from '../../config/conf'; const findById = (context: AuthContext, user: AuthUser, id: string) => { return storeLoadById(context, user, id, ENTITY_TYPE_SAVED_FILTER); @@ -46,3 +48,29 @@ export const fieldPatchSavedFilter = async (context: AuthContext, user: AuthUser return element; }; + +export const savedFilterEditAuthorizedMembers = async ( + context: AuthContext, + user: AuthUser, + savedFilterId: string, + input: MemberAccessInput[], +) => { + if (!isFeatureEnabled('SHARE_FILTERS')) { + throw ForbiddenAccess('Sharing saved filters is disabled'); + } + const args = { + entityId: savedFilterId, + input, + requiredCapabilities: [KNOWLEDGE_KNSHAREFILTERS], + entityType: ENTITY_TYPE_SAVED_FILTER, + }; + return editAuthorizedMembers(context, user, args); +}; + +export const getCurrentUserAccessRight = ( + _context: AuthContext, + user: AuthUser, + savedFilter: BasicStoreEntitySavedFilter, +) => { + return getUserAccessRight(user, savedFilter); +}; diff --git a/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter-resolver.ts b/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter-resolver.ts index ee1a6e6a2402..cf99ef0d0855 100644 --- a/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter-resolver.ts +++ b/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter-resolver.ts @@ -1,10 +1,22 @@ import type { Resolvers } from '../../generated/graphql'; -import { addSavedFilter, deleteSavedFilter, fieldPatchSavedFilter, findSaveFilterPaginated } from './savedFilter-domain'; +import { + addSavedFilter, + deleteSavedFilter, + fieldPatchSavedFilter, + findSaveFilterPaginated, + getCurrentUserAccessRight, + savedFilterEditAuthorizedMembers, +} from './savedFilter-domain'; +import { getAuthorizedMembers } from '../../utils/authorizedMembers'; const savedFilterResolver: Resolvers = { Query: { savedFilters: (_, args, context) => findSaveFilterPaginated(context, context.user, args), }, + SavedFilter: { + authorizedMembers: (savedFilter, _, context) => getAuthorizedMembers(context, context.user, savedFilter), + currentUserAccessRight: (savedFilter, _, context) => getCurrentUserAccessRight(context, context.user, savedFilter), + }, Mutation: { savedFilterAdd: (_, { input }, context) => { return addSavedFilter(context, context.user, input); @@ -15,6 +27,9 @@ const savedFilterResolver: Resolvers = { savedFilterFieldPatch: (_, args, context) => { return fieldPatchSavedFilter(context, context.user, args); }, + savedFilterEditAuthorizedMembers: (_, { id, input }, context) => { + return savedFilterEditAuthorizedMembers(context, context.user, id, input); + }, }, }; diff --git a/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter.graphql b/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter.graphql index 1e2d8f986d64..55d99dabec2b 100644 --- a/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter.graphql +++ b/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter.graphql @@ -8,6 +8,9 @@ type SavedFilter implements InternalObject & BasicObject { name: String! filters: String! scope: String! + # Sharing + authorizedMembers: [MemberAccess!]! @ff(flags: ["SHARE_FILTERS"], softFail: true) + currentUserAccessRight: String @ff(flags: ["SHARE_FILTERS"], softFail: true) } type SavedFilterEdge { @@ -47,4 +50,5 @@ type Mutation { savedFilterAdd(input: SavedFilterAddInput!): SavedFilter @auth savedFilterDelete(id: ID!): ID @auth savedFilterFieldPatch(id: ID!, input: [EditInput!]): SavedFilter @auth + savedFilterEditAuthorizedMembers(id: ID!, input: [MemberAccessInput!]!): SavedFilter @auth(for: [KNOWLEDGE_KNSHAREFILTERS]) @ff(flags: ["SHARE_FILTERS"]) } \ No newline at end of file diff --git a/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter.ts b/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter.ts index 29419060020c..f44ed9ee1a9c 100644 --- a/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter.ts +++ b/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter.ts @@ -3,7 +3,7 @@ import convertSavedFiltersToStix from './savedFilter-converter'; import { ENTITY_TYPE_SAVED_FILTER, type StoreEntitySavedFilter, type StixSavedFilter } from './savedFilter-types'; import { ABSTRACT_INTERNAL_OBJECT } from '../../schema/general'; import { type ModuleDefinition, registerDefinition } from '../../schema/module'; -import { creators, createdAt } from '../../schema/attribute-definition'; +import { authorizedMembers, creators, createdAt } from '../../schema/attribute-definition'; const SAVED_FILTER_DEFINITION: ModuleDefinition = { type: { @@ -20,6 +20,7 @@ const SAVED_FILTER_DEFINITION: ModuleDefinition { + let createdFilterId: string = ''; + const newFilter = { + mode: 'and', + filters: [], + filterGroups: [], + }; + + describe('savedFilterAdd', () => { + describe('If I use the addSavedFilter mutation', () => { + it('should create a filter', async () => { + const input = { + name: 'my new filter', + filters: JSON.stringify(newFilter), + scope: 'Incident', + }; + + const result = await queryAsAdminWithSuccess({ + query: CREATE_SAVED_FILTER_MUTATION, + variables: { + input: { ...input }, + }, + }); + + expect(result?.data?.savedFilterAdd).toBeDefined(); + expect(result?.data?.savedFilterAdd.name).toEqual('my new filter'); + createdFilterId = result?.data?.savedFilterAdd.id as string; + }); + + it('should have the creator as admin in authorized members', async () => { + const result = await queryAsAdminWithSuccess({ + query: GET_SAVED_FILTERS_QUERY, + variables: { first: 10 }, + }); + const savedFilters = result.data?.savedFilters.edges; + const filter = savedFilters.find((e: any) => e.node.id === createdFilterId); + expect(filter).toBeDefined(); + expect(filter.node.authorizedMembers).toBeDefined(); + expect(filter.node.authorizedMembers.length).toEqual(1); + expect(filter.node.authorizedMembers[0].id).toEqual(ADMIN_USER.id); + expect(filter.node.authorizedMembers[0].access_right).toEqual('admin'); + }); + }); + }); + + describe('savedFilters', () => { + describe('If I use the savedFilters query', () => { + it('gives the list of saved filters', async () => { + const result = await queryAsAdminWithSuccess({ + query: GET_SAVED_FILTERS_QUERY, + variables: {}, + }); + + const savedFilters = result.data?.savedFilters.edges; + expect(savedFilters).toBeDefined(); + expect(savedFilters.length).toEqual(1); + }); + it('gives the list of saved filters with restricted members', async () => { + const result = await queryAsUserWithSuccess(USER_EDITOR, { + query: GET_SAVED_FILTERS_QUERY, + variables: {}, + }); + + const savedFilters = result.data?.savedFilters.edges; + expect(savedFilters).toBeDefined(); + expect(savedFilters.length).toEqual(0); + }); + }); + }); + + describe('savedFilterEdit', () => { + describe('If I edit the filter of a saved Filter', async () => { + const editedFilters = { + ...newFilter, + filters: [{ key: 'entity_type', operator: 'eq', mode: 'or', values: ['Task'] }], + }; + const input = { + key: 'filters', + value: [JSON.stringify(editedFilters)], + }; + + it('should have a filter different than the initial value', async () => { + const result = await queryAsAdminWithSuccess({ + query: EDIT_SAVED_FILTER_MUTATION, + variables: { + id: createdFilterId, + input, + }, + }); + + expect(result?.data?.savedFilterFieldPatch?.filters).not.equal(JSON.stringify(newFilter)); + }); + }); + }); + + describe('savedFilterEditAuthorizedMembers (sharing)', () => { + it('should not be visible to another user before sharing', async () => { + const result = await queryAsUserWithSuccess(USER_EDITOR, { + query: GET_SAVED_FILTERS_QUERY, + variables: {}, + }); + const savedFilters = result.data?.savedFilters.edges; + expect(savedFilters.length).toEqual(0); + }); + + it('should share the saved filter with ALL members (view access)', async () => { + const input = [ + { id: ADMIN_USER.id, access_right: 'admin' }, + { id: MEMBER_ACCESS_ALL, access_right: 'view' }, + ]; + const result = await queryAsAdminWithSuccess({ + query: EDIT_AUTHORIZED_MEMBERS_MUTATION, + variables: { id: createdFilterId, input }, + }); + const authorizedMembers = result.data?.savedFilterEditAuthorizedMembers?.authorizedMembers; + expect(authorizedMembers).toBeDefined(); + expect(authorizedMembers.length).toEqual(2); + expect(authorizedMembers.some((m: any) => m.access_right === 'admin')).toBeTruthy(); + expect(authorizedMembers.some((m: any) => m.access_right === 'view')).toBeTruthy(); + }); + + it('should be visible to another user after sharing with ALL', async () => { + const result = await queryAsUserWithSuccess(USER_EDITOR, { + query: GET_SAVED_FILTERS_QUERY, + variables: {}, + }); + const savedFilters = result.data?.savedFilters.edges; + expect(savedFilters.length).toEqual(1); + expect(savedFilters[0].node.id).toEqual(createdFilterId); + }); + + it('should expose currentUserAccessRight for the shared filter', async () => { + const result = await queryAsAdminWithSuccess({ + query: GET_SAVED_FILTERS_QUERY, + variables: { first: 10 }, + }); + const savedFilters = result.data?.savedFilters.edges; + const filter = savedFilters.find((e: any) => e.node.id === createdFilterId); + expect(filter).toBeDefined(); + expect(filter.node.currentUserAccessRight).toEqual('admin'); + }); + + it('should not allow editing authorized members without valid admin', async () => { + const input = [ + { id: 'non_existing_id', access_right: 'admin' }, + ]; + const result = await queryAsUser(USER_EDITOR, { + query: EDIT_AUTHORIZED_MEMBERS_MUTATION, + variables: { id: createdFilterId, input }, + }); + expect(result.errors).toBeDefined(); + expect(result.errors!.length).toBeGreaterThan(0); + }); + + it('should not allow removing all admins from authorized members', async () => { + const input = [ + { id: ADMIN_USER.id, access_right: 'view' }, + { id: MEMBER_ACCESS_ALL, access_right: 'view' }, + ]; + const result = await queryAsUser(USER_EDITOR, { + query: EDIT_AUTHORIZED_MEMBERS_MUTATION, + variables: { id: createdFilterId, input }, + }); + expect(result.errors).toBeDefined(); + expect(result.errors!.length).toBeGreaterThan(0); + expect(result.errors![0].message).toEqual('It should have at least one valid member with admin access'); + }); + + it('should revoke sharing (restrict back to creator only)', async () => { + const input = [ + { id: ADMIN_USER.id, access_right: 'admin' }, + ]; + const result = await queryAsAdminWithSuccess({ + query: EDIT_AUTHORIZED_MEMBERS_MUTATION, + variables: { id: createdFilterId, input }, + }); + const authorizedMembers = result.data?.savedFilterEditAuthorizedMembers?.authorizedMembers; + expect(authorizedMembers.length).toEqual(1); + }); + + it('should no longer be visible to another user after revoking sharing', async () => { + const result = await queryAsUserWithSuccess(USER_EDITOR, { + query: GET_SAVED_FILTERS_QUERY, + variables: {}, + }); + const savedFilters = result.data?.savedFilters.edges; + expect(savedFilters.length).toEqual(0); + }); + }); + + describe('savedFilterDelete', () => { + describe('If I take the last created filter', () => { + it('should have found the filter', async () => { + const savedFilter = await elLoadById(testContext, ADMIN_USER, createdFilterId); + expect(savedFilter).toBeDefined(); + }); + + describe('If I use the deleteSavedFilter function', () => { + beforeAll(async () => { + await queryAsAdminWithSuccess({ + query: DELETE_SAVED_FILTER_MUTATION, + variables: { + id: createdFilterId, + }, + }); + }); + + it('should have deleted the last created filter', async () => { + const savedFilter = await elLoadById(testContext, ADMIN_USER, createdFilterId); + expect(savedFilter).toBeUndefined(); + }); + }); + }); + }); +}); diff --git a/opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/savedFilterResolver-test.ts b/opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/savedFilterResolver-test.ts deleted file mode 100644 index 509f5166e1d9..000000000000 --- a/opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/savedFilterResolver-test.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { describe, it, expect, beforeAll } from 'vitest'; -import gql from 'graphql-tag'; -import { ADMIN_USER, testContext, USER_EDITOR } from '../../utils/testQuery'; -import { queryAsAdminWithSuccess, queryAsUserWithSuccess } from '../../utils/testQueryHelper'; -import { elLoadById } from '../../../src/database/engine'; - -const GET_SAVED_FILTERS_QUERY = gql` - query savedFilters( - $first: Int - $after: ID - $orderBy: SavedFilterOrdering - $orderMode: OrderingMode - $filters: FilterGroup - $search: String - ) { - savedFilters( - first: $first - after: $after - orderBy: $orderBy - orderMode: $orderMode - filters: $filters - search: $search - ) { - edges { - node { - id - name - filters - scope - } - } - } - } -`; - -const CREATE_SAVED_FILTER_MUTATION = gql` - mutation savedFilterAdd($input: SavedFilterAddInput!) { - savedFilterAdd(input: $input) { - id - name - } - } -`; - -const DELETE_SAVED_FILTER_MUTATION = gql` - mutation savedFilterDelete($id: ID!) { - savedFilterDelete(id: $id) - } -`; - -const EDIT_SAVED_FILTER_MUTATION = gql` - mutation savedFilterEdit($id: ID!, $input: [EditInput!]!) { - savedFilterFieldPatch(id: $id, input: $input) { - id - name - filters - scope - } - } -`; - -describe('Saved Filter Resolver', () => { - let createdFilterId: string = ''; - const newFilter = { - mode: 'and', - filters: [], - filterGroups: [], - }; - - describe('savedFilterAdd', () => { - describe('If I use the addSavedFilter mutation', () => { - it('should create a filter', async () => { - const input = { - name: 'my new filter', - filters: JSON.stringify(newFilter), - scope: 'Incident' - }; - - const result = await queryAsAdminWithSuccess({ - query: CREATE_SAVED_FILTER_MUTATION, - variables: { - input: { ...input } - }, - }); - - expect(result?.data?.savedFilterAdd).toBeDefined(); - expect(result?.data?.savedFilterAdd.name).toEqual('my new filter'); - createdFilterId = result?.data?.savedFilterAdd.id as string; - }); - }); - }); - - describe('savedFilters', () => { - describe('If I use the savedFilters query', () => { - it('gives the list of saved filters', async () => { - const result = await queryAsAdminWithSuccess({ - query: GET_SAVED_FILTERS_QUERY, - variables: {}, - }); - - const savedFilters = result.data?.savedFilters.edges; - expect(savedFilters).toBeDefined(); - expect(savedFilters.length).toEqual(1); - }); - it('gives the list of saved filters with restricted members', async () => { - const result = await queryAsUserWithSuccess(USER_EDITOR, { - query: GET_SAVED_FILTERS_QUERY, - variables: {}, - }); - - const savedFilters = result.data?.savedFilters.edges; - expect(savedFilters).toBeDefined(); - expect(savedFilters.length).toEqual(0); - }); - }); - }); - - describe('savedFilterEdit', () => { - describe('If I edit the filter of a saved Filter', async () => { - const editedFilters = { - ...newFilter, - filters: [{ key: 'entity_type', operator: 'eq', mode: 'or', values: ['Task'] }] - }; - const input = { - key: 'filters', - value: [JSON.stringify(editedFilters)], - }; - - it('should have a filter different than the initial value', async () => { - const result = await queryAsAdminWithSuccess({ - query: EDIT_SAVED_FILTER_MUTATION, - variables: { - id: createdFilterId, - input, - }, - }); - - expect(result?.data?.savedFilterFieldPatch?.filters).not.equal(JSON.stringify(newFilter)); - }); - }); - }); - - describe('savedFilterDelete', () => { - describe('If I take the last created filter', () => { - it('should have found the filter', async () => { - const savedFilter = await elLoadById(testContext, ADMIN_USER, createdFilterId); - - expect(savedFilter).toBeDefined(); - }); - - describe('If I use the deleteSavedFilter function', () => { - beforeAll(async () => { - await queryAsAdminWithSuccess({ - query: DELETE_SAVED_FILTER_MUTATION, - variables: { - id: createdFilterId - }, - }); - }); - - it('should have deleted the last created filter', async () => { - const savedFilter = await elLoadById(testContext, ADMIN_USER, createdFilterId); - - expect(savedFilter).toBeUndefined(); - }); - }); - }); - }); -}); From a97c277d529ad9c41305fabb64562ffbff843fbf Mon Sep 17 00:00:00 2001 From: Cathia Archidoit Date: Mon, 18 May 2026 11:05:21 +0200 Subject: [PATCH 2/6] [backend] Sharing saved filters APIs (#11624) --- .../opencti-front/src/schema/relay.schema.graphql | 3 +++ .../opencti-graphql/src/generated/graphql.ts | 12 ++++++++++++ .../modules/draftWorkspace/draftWorkspace-domain.ts | 1 - .../draftWorkspace/draftWorkspace-resolvers.ts | 2 +- .../src/modules/savedFilter/savedFilter-domain.ts | 1 - .../src/modules/savedFilter/savedFilter-resolver.ts | 2 +- .../src/modules/workspace/workspace-domain.ts | 1 - .../src/modules/workspace/workspace-resolver.ts | 2 +- 8 files changed, 18 insertions(+), 6 deletions(-) diff --git a/opencti-platform/opencti-front/src/schema/relay.schema.graphql b/opencti-platform/opencti-front/src/schema/relay.schema.graphql index 22794ac30fc8..65b662027a19 100644 --- a/opencti-platform/opencti-front/src/schema/relay.schema.graphql +++ b/opencti-platform/opencti-front/src/schema/relay.schema.graphql @@ -10661,6 +10661,7 @@ type Mutation { savedFilterAdd(input: SavedFilterAddInput!): SavedFilter savedFilterDelete(id: ID!): ID savedFilterFieldPatch(id: ID!, input: [EditInput!]): SavedFilter + savedFilterEditAuthorizedMembers(id: ID!, input: [MemberAccessInput!]!): SavedFilter requestAccessAdd(input: RequestAccessAddInput!): ID requestAccessConfigure(input: RequestAccessConfigureInput!): RequestAccessConfiguration pirAdd(input: PirAddInput!): Pir @@ -15150,6 +15151,8 @@ type SavedFilter implements InternalObject & BasicObject { name: String! filters: String! scope: String! + authorizedMembers: [MemberAccess!]! + currentUserAccessRight: String } type SavedFilterEdge { diff --git a/opencti-platform/opencti-graphql/src/generated/graphql.ts b/opencti-platform/opencti-graphql/src/generated/graphql.ts index b4a6482bcba7..06a48919ee4d 100644 --- a/opencti-platform/opencti-graphql/src/generated/graphql.ts +++ b/opencti-platform/opencti-graphql/src/generated/graphql.ts @@ -17221,6 +17221,7 @@ export type Mutation = { samlProviderEdit?: Maybe; savedFilterAdd?: Maybe; savedFilterDelete?: Maybe; + savedFilterEditAuthorizedMembers?: Maybe; savedFilterFieldPatch?: Maybe; sectorAdd?: Maybe; sectorEdit?: Maybe; @@ -19213,6 +19214,12 @@ export type MutationSavedFilterDeleteArgs = { }; +export type MutationSavedFilterEditAuthorizedMembersArgs = { + id: Scalars['ID']['input']; + input: Array; +}; + + export type MutationSavedFilterFieldPatchArgs = { id: Scalars['ID']['input']; input?: InputMaybe>; @@ -28976,6 +28983,8 @@ export type SamlInput = { export type SavedFilter = BasicObject & InternalObject & { __typename?: 'SavedFilter'; + authorizedMembers: Array; + currentUserAccessRight?: Maybe; entity_type: Scalars['String']['output']; filters: Scalars['String']['output']; id: Scalars['ID']['output']; @@ -47054,6 +47063,7 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; savedFilterAdd?: Resolver, ParentType, ContextType, RequireFields>; savedFilterDelete?: Resolver, ParentType, ContextType, RequireFields>; + savedFilterEditAuthorizedMembers?: Resolver, ParentType, ContextType, RequireFields>; savedFilterFieldPatch?: Resolver, ParentType, ContextType, RequireFields>; sectorAdd?: Resolver, ParentType, ContextType, RequireFields>; sectorEdit?: Resolver, ParentType, ContextType, RequireFields>; @@ -49472,6 +49482,8 @@ export type SamlConfigurationResolvers; export type SavedFilterResolvers = ResolversObject<{ + authorizedMembers?: Resolver, ParentType, ContextType>; + currentUserAccessRight?: Resolver, ParentType, ContextType>; entity_type?: Resolver; filters?: Resolver; id?: Resolver; diff --git a/opencti-platform/opencti-graphql/src/modules/draftWorkspace/draftWorkspace-domain.ts b/opencti-platform/opencti-graphql/src/modules/draftWorkspace/draftWorkspace-domain.ts index 0748713bc20e..87b489b320a5 100644 --- a/opencti-platform/opencti-graphql/src/modules/draftWorkspace/draftWorkspace-domain.ts +++ b/opencti-platform/opencti-graphql/src/modules/draftWorkspace/draftWorkspace-domain.ts @@ -174,7 +174,6 @@ export const getProcessingCount = async (context: AuthContext, user: AuthUser, d }; export const getCurrentUserAccessRight = async ( - context: AuthContext, user: AuthUser, draft: BasicStoreEntityDraftWorkspace, ) => { diff --git a/opencti-platform/opencti-graphql/src/modules/draftWorkspace/draftWorkspace-resolvers.ts b/opencti-platform/opencti-graphql/src/modules/draftWorkspace/draftWorkspace-resolvers.ts index ddcbc352a28e..324d993a4f4b 100644 --- a/opencti-platform/opencti-graphql/src/modules/draftWorkspace/draftWorkspace-resolvers.ts +++ b/opencti-platform/opencti-graphql/src/modules/draftWorkspace/draftWorkspace-resolvers.ts @@ -48,7 +48,7 @@ const draftWorkspaceResolvers: Resolvers = { }, validationWork: (draft, _, context) => (draft.validation_work_id ? findWorkById(context, context.user, draft.validation_work_id) as any : null), authorizedMembers: (workspace, _, context) => getAuthorizedMembers(context, context.user, workspace), - currentUserAccessRight: (workspace, _, context) => getCurrentUserAccessRight(context, context.user, workspace), + currentUserAccessRight: (workspace, _, context) => getCurrentUserAccessRight(context.user, workspace), objectParticipant: async (workspace, _, context) => loadParticipants(context, context.user, workspace), objectAssignee: async (workspace, _, context) => loadAssignees(context, context.user, workspace), createdBy: (rel, _, context) => loadThroughDenormalized(context, context.user, rel, INPUT_CREATED_BY), diff --git a/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter-domain.ts b/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter-domain.ts index 88ba53f052cf..1c24fceadccd 100644 --- a/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter-domain.ts +++ b/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter-domain.ts @@ -68,7 +68,6 @@ export const savedFilterEditAuthorizedMembers = async ( }; export const getCurrentUserAccessRight = ( - _context: AuthContext, user: AuthUser, savedFilter: BasicStoreEntitySavedFilter, ) => { diff --git a/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter-resolver.ts b/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter-resolver.ts index cf99ef0d0855..c580573b2247 100644 --- a/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter-resolver.ts +++ b/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter-resolver.ts @@ -15,7 +15,7 @@ const savedFilterResolver: Resolvers = { }, SavedFilter: { authorizedMembers: (savedFilter, _, context) => getAuthorizedMembers(context, context.user, savedFilter), - currentUserAccessRight: (savedFilter, _, context) => getCurrentUserAccessRight(context, context.user, savedFilter), + currentUserAccessRight: (savedFilter, _, context) => getCurrentUserAccessRight(context.user, savedFilter), }, Mutation: { savedFilterAdd: (_, { input }, context) => { diff --git a/opencti-platform/opencti-graphql/src/modules/workspace/workspace-domain.ts b/opencti-platform/opencti-graphql/src/modules/workspace/workspace-domain.ts index 0115cb9fd904..a3e7026940b0 100644 --- a/opencti-platform/opencti-graphql/src/modules/workspace/workspace-domain.ts +++ b/opencti-platform/opencti-graphql/src/modules/workspace/workspace-domain.ts @@ -83,7 +83,6 @@ export const workspaceEditAuthorizedMembers = async ( }; export const getCurrentUserAccessRight = async ( - _context: AuthContext, user: AuthUser, workspace: BasicStoreEntityWorkspace, ) => { diff --git a/opencti-platform/opencti-graphql/src/modules/workspace/workspace-resolver.ts b/opencti-platform/opencti-graphql/src/modules/workspace/workspace-resolver.ts index bdffedeec4dd..61d144bafc61 100644 --- a/opencti-platform/opencti-graphql/src/modules/workspace/workspace-resolver.ts +++ b/opencti-platform/opencti-graphql/src/modules/workspace/workspace-resolver.ts @@ -33,7 +33,7 @@ const workspaceResolvers: Resolvers = { }, Workspace: { authorizedMembers: (workspace, _, context) => getAuthorizedMembers(context, context.user, workspace), - currentUserAccessRight: (workspace, _, context) => getCurrentUserAccessRight(context, context.user, workspace), + currentUserAccessRight: (workspace, _, context) => getCurrentUserAccessRight(context.user, workspace), owner: (workspace, _, context) => loadCreator(context, context.user, getOwnerId(workspace)), objects: (workspace, args, context) => { return objects(context, context.user, workspace, args) as any; From 5b722379bd37c07964314e2dfe727d02dddf0a6c Mon Sep 17 00:00:00 2001 From: Cathia Archidoit Date: Mon, 18 May 2026 12:18:11 +0200 Subject: [PATCH 3/6] [backend] improve tests (#11624) --- .../02-resolvers/savedFilter-test.ts | 58 ++++++++++--------- .../02-resolvers/workspace-test.js | 12 +--- .../opencti-graphql/tests/utils/testQuery.ts | 4 +- 3 files changed, 36 insertions(+), 38 deletions(-) diff --git a/opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/savedFilter-test.ts b/opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/savedFilter-test.ts index 6c06415d31c0..86cd569d30c9 100644 --- a/opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/savedFilter-test.ts +++ b/opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/savedFilter-test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import gql from 'graphql-tag'; -import { ADMIN_USER, testContext, USER_EDITOR } from '../../utils/testQuery'; -import { queryAsAdminWithSuccess, queryAsUser, queryAsUserWithSuccess } from '../../utils/testQueryHelper'; +import { ADMIN_USER, testContext, USER_PARTICIPATE } from '../../utils/testQuery'; +import { queryAsAdminWithError, queryAsAdminWithSuccess, queryAsUserIsExpectedForbidden, queryAsUserWithSuccess } from '../../utils/testQueryHelper'; import { elLoadById } from '../../../src/database/engine'; import { MEMBER_ACCESS_ALL } from '../../../src/utils/access'; @@ -46,6 +46,12 @@ const CREATE_SAVED_FILTER_MUTATION = gql` savedFilterAdd(input: $input) { id name + authorizedMembers { + id + name + entity_type + access_right + } } } `; @@ -92,7 +98,7 @@ describe('Saved Filter Resolver', () => { describe('savedFilterAdd', () => { describe('If I use the addSavedFilter mutation', () => { - it('should create a filter', async () => { + it('should create a filter with the creator as admin in authorized members', async () => { const input = { name: 'my new filter', filters: JSON.stringify(newFilter), @@ -109,20 +115,11 @@ describe('Saved Filter Resolver', () => { expect(result?.data?.savedFilterAdd).toBeDefined(); expect(result?.data?.savedFilterAdd.name).toEqual('my new filter'); createdFilterId = result?.data?.savedFilterAdd.id as string; - }); - it('should have the creator as admin in authorized members', async () => { - const result = await queryAsAdminWithSuccess({ - query: GET_SAVED_FILTERS_QUERY, - variables: { first: 10 }, - }); - const savedFilters = result.data?.savedFilters.edges; - const filter = savedFilters.find((e: any) => e.node.id === createdFilterId); - expect(filter).toBeDefined(); - expect(filter.node.authorizedMembers).toBeDefined(); - expect(filter.node.authorizedMembers.length).toEqual(1); - expect(filter.node.authorizedMembers[0].id).toEqual(ADMIN_USER.id); - expect(filter.node.authorizedMembers[0].access_right).toEqual('admin'); + const { authorizedMembers } = result.data.savedFilterAdd; + expect(authorizedMembers).toBeDefined(); + expect(authorizedMembers.length).toEqual(1); + expect(authorizedMembers[0].access_right).toEqual('admin'); }); }); }); @@ -140,7 +137,7 @@ describe('Saved Filter Resolver', () => { expect(savedFilters.length).toEqual(1); }); it('gives the list of saved filters with restricted members', async () => { - const result = await queryAsUserWithSuccess(USER_EDITOR, { + const result = await queryAsUserWithSuccess(USER_PARTICIPATE, { query: GET_SAVED_FILTERS_QUERY, variables: {}, }); @@ -179,7 +176,7 @@ describe('Saved Filter Resolver', () => { describe('savedFilterEditAuthorizedMembers (sharing)', () => { it('should not be visible to another user before sharing', async () => { - const result = await queryAsUserWithSuccess(USER_EDITOR, { + const result = await queryAsUserWithSuccess(USER_PARTICIPATE, { query: GET_SAVED_FILTERS_QUERY, variables: {}, }); @@ -204,7 +201,7 @@ describe('Saved Filter Resolver', () => { }); it('should be visible to another user after sharing with ALL', async () => { - const result = await queryAsUserWithSuccess(USER_EDITOR, { + const result = await queryAsUserWithSuccess(USER_PARTICIPATE, { query: GET_SAVED_FILTERS_QUERY, variables: {}, }); @@ -228,12 +225,10 @@ describe('Saved Filter Resolver', () => { const input = [ { id: 'non_existing_id', access_right: 'admin' }, ]; - const result = await queryAsUser(USER_EDITOR, { + await queryAsAdminWithError({ query: EDIT_AUTHORIZED_MEMBERS_MUTATION, variables: { id: createdFilterId, input }, - }); - expect(result.errors).toBeDefined(); - expect(result.errors!.length).toBeGreaterThan(0); + }, 'It should have at least one valid member with admin access', 'FUNCTIONAL_ERROR'); }); it('should not allow removing all admins from authorized members', async () => { @@ -241,13 +236,20 @@ describe('Saved Filter Resolver', () => { { id: ADMIN_USER.id, access_right: 'view' }, { id: MEMBER_ACCESS_ALL, access_right: 'view' }, ]; - const result = await queryAsUser(USER_EDITOR, { + await queryAsAdminWithError({ + query: EDIT_AUTHORIZED_MEMBERS_MUTATION, + variables: { id: createdFilterId, input }, + }, 'It should have at least one valid member with admin access', 'FUNCTIONAL_ERROR'); + }); + + it('should not allow editing authorized members without "share filters" capability', async () => { + const input = [ + { id: ADMIN_USER.id, access_right: 'admin' }, + ]; + await queryAsUserIsExpectedForbidden(USER_PARTICIPATE, { query: EDIT_AUTHORIZED_MEMBERS_MUTATION, variables: { id: createdFilterId, input }, }); - expect(result.errors).toBeDefined(); - expect(result.errors!.length).toBeGreaterThan(0); - expect(result.errors![0].message).toEqual('It should have at least one valid member with admin access'); }); it('should revoke sharing (restrict back to creator only)', async () => { @@ -263,7 +265,7 @@ describe('Saved Filter Resolver', () => { }); it('should no longer be visible to another user after revoking sharing', async () => { - const result = await queryAsUserWithSuccess(USER_EDITOR, { + const result = await queryAsUserWithSuccess(USER_PARTICIPATE, { query: GET_SAVED_FILTERS_QUERY, variables: {}, }); diff --git a/opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/workspace-test.js b/opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/workspace-test.js index 5bf78cc815de..b4e49a5508d1 100644 --- a/opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/workspace-test.js +++ b/opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/workspace-test.js @@ -276,16 +276,10 @@ describe('Workspace resolver standard behavior', () => { }); expect(queryResult.data.workspaceDuplicate.id).toBeDefined(); - expect(queryResult.data.workspaceDuplicate.name).toBe( - 'Dashboard to duplicate', - ); + expect(queryResult.data.workspaceDuplicate.name).toBe('Dashboard to duplicate'); expect(queryResult.data.workspaceDuplicate.entity_type).toBe('Workspace'); - expect(queryResult.data.workspaceDuplicate.authorizedMembers.length).toBe( - 1, - ); - expect( - queryResult.data.workspaceDuplicate.authorizedMembers[0].access_right, - ).toBe('admin'); + expect(queryResult.data.workspaceDuplicate.authorizedMembers.length).toBe(1); + expect(queryResult.data.workspaceDuplicate.authorizedMembers[0].access_right).toBe('admin'); await queryAsAdmin({ query: DELETE_QUERY, variables: { id: queryResult.data.workspaceDuplicate.id }, diff --git a/opencti-platform/opencti-graphql/tests/utils/testQuery.ts b/opencti-platform/opencti-graphql/tests/utils/testQuery.ts index f2e1995de464..15056c1f2922 100644 --- a/opencti-platform/opencti-graphql/tests/utils/testQuery.ts +++ b/opencti-platform/opencti-graphql/tests/utils/testQuery.ts @@ -193,6 +193,7 @@ export const ROLE_EDITOR: Role = { capabilities: [ 'KNOWLEDGE_KNUPDATE_KNDELETE', 'KNOWLEDGE_KNUPDATE_KNMERGE', + 'KNOWLEDGE_KNSHAREFILTERS', 'EXPLORE_EXUPDATE_EXDELETE', 'EXPLORE_EXUPDATE_PUBLISH', 'TAXIIAPI_SETCOLLECTIONS', @@ -213,7 +214,8 @@ export const ROLE_SECURITY: Role = { 'EXPLORE_EXUPDATE_EXDELETE', 'INVESTIGATION_INUPDATE_INDELETE', 'SETTINGS_SETACCESSES', - 'SETTINGS_SETAUTH', 'SETTINGS_SECURITYACTIVITY', + 'SETTINGS_SETAUTH', + 'SETTINGS_SECURITYACTIVITY', 'AUTOMATION_AUTMANAGE', 'APIACCESS_USEBASICAUTH', ], From 742faf6c66c745021ef868d02ee39c02403403e9 Mon Sep 17 00:00:00 2001 From: Cathia Archidoit Date: Tue, 19 May 2026 09:27:43 +0200 Subject: [PATCH 4/6] [backend] fix tests --- .../tests/02-dataInjection/01-dataCount/entityCountHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opencti-platform/opencti-graphql/tests/02-dataInjection/01-dataCount/entityCountHelper.ts b/opencti-platform/opencti-graphql/tests/02-dataInjection/01-dataCount/entityCountHelper.ts index 77977a2bb9d6..99df483c2d8c 100644 --- a/opencti-platform/opencti-graphql/tests/02-dataInjection/01-dataCount/entityCountHelper.ts +++ b/opencti-platform/opencti-graphql/tests/02-dataInjection/01-dataCount/entityCountHelper.ts @@ -55,7 +55,7 @@ export const relationsCounter = { 'attributed-to': 2, 'created-by': 22, 'external-reference': 7, - 'has-capability': 73, + 'has-capability': 74, 'has-role': 9, indicates: 4, 'kill-chain-phase': 3, From 887b6869591db198824caaba5328e519fd08150a Mon Sep 17 00:00:00 2001 From: Cathia Archidoit Date: Tue, 19 May 2026 09:57:51 +0200 Subject: [PATCH 5/6] [backend] fix counters --- .../tests/03-integration/02-resolvers/user-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/user-test.ts b/opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/user-test.ts index d9f504fc29ed..219a9f9ca522 100644 --- a/opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/user-test.ts +++ b/opencti-platform/opencti-graphql/tests/03-integration/02-resolvers/user-test.ts @@ -909,7 +909,7 @@ describe('User has no settings capability and is organization admin query behavi const editorUserQueryResult = await queryAsAdmin({ query: READ_QUERY, variables: { id: userEditorId } }); expect(editorUserQueryResult).not.toBeNull(); expect(editorUserQueryResult.data?.user).not.toBeNull(); - expect(editorUserQueryResult.data?.user.capabilities.length).toEqual(10); + expect(editorUserQueryResult.data?.user.capabilities.length).toEqual(11); const capabilities = editorUserQueryResult.data?.user.capabilities ?? []; expect(capabilities.some((capa: Capability) => capa.name === VIRTUAL_ORGANIZATION_ADMIN)).toEqual(true); }); From a380891bdc2feed1d3afb8bf0ea62f8e60852104 Mon Sep 17 00:00:00 2001 From: Cathia Archidoit Date: Thu, 21 May 2026 10:35:34 +0200 Subject: [PATCH 6/6] [backend] fix --- .../src/modules/savedFilter/savedFilter.graphql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter.graphql b/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter.graphql index 55d99dabec2b..dc2e5cf3c7ec 100644 --- a/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter.graphql +++ b/opencti-platform/opencti-graphql/src/modules/savedFilter/savedFilter.graphql @@ -9,8 +9,8 @@ type SavedFilter implements InternalObject & BasicObject { filters: String! scope: String! # Sharing - authorizedMembers: [MemberAccess!]! @ff(flags: ["SHARE_FILTERS"], softFail: true) - currentUserAccessRight: String @ff(flags: ["SHARE_FILTERS"], softFail: true) + authorizedMembers: [MemberAccess!]! @ff(flags: ["SHARE_FILTERS"]) + currentUserAccessRight: String @ff(flags: ["SHARE_FILTERS"]) } type SavedFilterEdge {