diff --git a/x-pack/platform/plugins/shared/actions/server/actions_client/actions_client.test.ts b/x-pack/platform/plugins/shared/actions/server/actions_client/actions_client.test.ts index d54687abcf10d..49490b5770072 100644 --- a/x-pack/platform/plugins/shared/actions/server/actions_client/actions_client.test.ts +++ b/x-pack/platform/plugins/shared/actions/server/actions_client/actions_client.test.ts @@ -1902,6 +1902,58 @@ describe('delete()', () => { expect(connectorTokenClient.deleteConnectorTokens).toHaveBeenCalledTimes(1); }); + describe('when connector has authMode per-user', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockReset(); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'action', + attributes: { + actionTypeId: 'my-action-delete', + isMissingSecrets: false, + config: {}, + secrets: {}, + authMode: 'per-user', + }, + references: [], + }); + }); + + test(`passes authMode per-user to deleteConnectorTokens`, async () => { + await actionsClient.delete({ id: '1' }); + expect(connectorTokenClient.deleteConnectorTokens).toHaveBeenCalledWith({ + connectorId: '1', + authMode: 'per-user', + }); + }); + }); + + describe('when connector has authMode shared', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockReset(); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'action', + attributes: { + actionTypeId: 'my-action-delete', + isMissingSecrets: false, + config: {}, + secrets: {}, + authMode: 'shared', + }, + references: [], + }); + }); + + test(`passes authMode shared to deleteConnectorTokens`, async () => { + await actionsClient.delete({ id: '1' }); + expect(connectorTokenClient.deleteConnectorTokens).toHaveBeenCalledWith({ + connectorId: '1', + authMode: 'shared', + }); + }); + }); + test(`failing to delete tokens logs error instead of throw`, async () => { connectorTokenClient.deleteConnectorTokens.mockRejectedValueOnce(new Error('Fail')); await expect(actionsClient.delete({ id: '1' })).resolves.toBeUndefined(); @@ -2133,6 +2185,84 @@ describe('update()', () => { expect(connectorTokenClient.deleteConnectorTokens).toHaveBeenCalledTimes(1); }); + describe('when connector has authMode per-user', () => { + beforeEach(() => { + actionTypeRegistry.register(getConnectorType()); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'action', + attributes: { + actionTypeId: 'my-connector-type', + isMissingSecrets: false, + authMode: 'per-user', + }, + references: [], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: 'my-action', + type: 'action', + attributes: { + actionTypeId: 'my-connector-type', + isMissingSecrets: false, + name: 'my name', + config: {}, + secrets: {}, + }, + references: [], + }); + }); + + test(`passes authMode per-user to deleteConnectorTokens`, async () => { + await actionsClient.update({ + id: 'my-action', + action: { name: 'my name', config: {}, secrets: {} }, + }); + expect(connectorTokenClient.deleteConnectorTokens).toHaveBeenCalledWith({ + connectorId: 'my-action', + authMode: 'per-user', + }); + }); + }); + + describe('when connector has authMode shared', () => { + beforeEach(() => { + actionTypeRegistry.register(getConnectorType()); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'action', + attributes: { + actionTypeId: 'my-connector-type', + isMissingSecrets: false, + authMode: 'shared', + }, + references: [], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: 'my-action', + type: 'action', + attributes: { + actionTypeId: 'my-connector-type', + isMissingSecrets: false, + name: 'my name', + config: {}, + secrets: {}, + }, + references: [], + }); + }); + + test(`passes authMode shared to deleteConnectorTokens`, async () => { + await actionsClient.update({ + id: 'my-action', + action: { name: 'my name', config: {}, secrets: {} }, + }); + expect(connectorTokenClient.deleteConnectorTokens).toHaveBeenCalledWith({ + connectorId: 'my-action', + authMode: 'shared', + }); + }); + }); + test(`failing to delete tokens logs error instead of throw`, async () => { connectorTokenClient.deleteConnectorTokens.mockRejectedValueOnce(new Error('Fail')); await expect(updateOperation()).resolves.toBeTruthy(); diff --git a/x-pack/platform/plugins/shared/actions/server/actions_client/actions_client.ts b/x-pack/platform/plugins/shared/actions/server/actions_client/actions_client.ts index 77729b8e7c250..d0787230a778a 100644 --- a/x-pack/platform/plugins/shared/actions/server/actions_client/actions_client.ts +++ b/x-pack/platform/plugins/shared/actions/server/actions_client/actions_client.ts @@ -550,17 +550,9 @@ export class ActionsClient { }) ); - try { - await this.context.connectorTokenClient.deleteConnectorTokens({ connectorId: id }); - } catch (e) { - this.context.logger.error( - `Failed to delete auth tokens for connector "${id}" after delete: ${e.message}` - ); - } - const rawAction = await this.context.unsecuredSavedObjectsClient.get('action', id); const { - attributes: { actionTypeId, config }, + attributes: { actionTypeId, config, authMode }, } = rawAction; let actionType: ActionType | undefined; @@ -595,6 +587,18 @@ export class ActionsClient { ); } } + + try { + await this.context.connectorTokenClient.deleteConnectorTokens({ + connectorId: id, + authMode, + }); + } catch (e) { + this.context.logger.error( + `Failed to delete auth tokens for connector "${id}" after delete: ${e.message}` + ); + } + return result; } diff --git a/x-pack/platform/plugins/shared/actions/server/application/connector/methods/update/update.ts b/x-pack/platform/plugins/shared/actions/server/application/connector/methods/update/update.ts index 1e026c64dd0b6..84912d73e4e5f 100644 --- a/x-pack/platform/plugins/shared/actions/server/application/connector/methods/update/update.ts +++ b/x-pack/platform/plugins/shared/actions/server/application/connector/methods/update/update.ts @@ -60,7 +60,7 @@ export async function update({ context, id, action }: ConnectorUpdateParams): Pr } const { attributes, references, version } = await context.unsecuredSavedObjectsClient.get('action', id); - const { actionTypeId } = attributes; + const { actionTypeId, authMode } = attributes; const { name, config, secrets } = action; const actionType = context.actionTypeRegistry.get(actionTypeId); const configurationUtilities = context.actionTypeRegistry.getUtils(); @@ -163,7 +163,7 @@ export async function update({ context, id, action }: ConnectorUpdateParams): Pr } try { - await context.connectorTokenClient.deleteConnectorTokens({ connectorId: id }); + await context.connectorTokenClient.deleteConnectorTokens({ connectorId: id, authMode }); } catch (e) { context.logger.error( `Failed to delete auth tokens for connector "${id}" after update: ${e.message}` diff --git a/x-pack/platform/plugins/shared/actions/server/lib/connector_token_client.test.ts b/x-pack/platform/plugins/shared/actions/server/lib/connector_token_client.test.ts index bf456378811ae..751ea10f7024a 100644 --- a/x-pack/platform/plugins/shared/actions/server/lib/connector_token_client.test.ts +++ b/x-pack/platform/plugins/shared/actions/server/lib/connector_token_client.test.ts @@ -516,6 +516,112 @@ describe('delete()', () => { ] `); }); + + describe('scope routing via authMode and profileUid', () => { + const sharedFindResult = { + total: 1, + per_page: 10, + page: 1, + saved_objects: [ + { + id: 'shared-token-1', + type: 'connector_token', + attributes: { connectorId: '123', tokenType: 'access_token' }, + score: 1, + references: [], + }, + ], + }; + + const userFindResult = { + total: 1, + per_page: 10, + page: 1, + saved_objects: [ + { + id: 'user-token-1', + type: 'user_connector_token', + attributes: { connectorId: '123', profileUid: 'user-123', credentialType: 'oauth' }, + score: 1, + references: [], + }, + ], + }; + + test('routes to shared client when authMode is shared', async () => { + unsecuredSavedObjectsClient.delete.mockResolvedValue({}); + unsecuredSavedObjectsClient.find.mockResolvedValueOnce(sharedFindResult); + + await connectorTokenClient.deleteConnectorTokens({ connectorId: '123', authMode: 'shared' }); + + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith( + 'connector_token', + 'shared-token-1' + ); + }); + + test('routes to user client when authMode is per-user', async () => { + unsecuredSavedObjectsClient.delete.mockResolvedValue({}); + unsecuredSavedObjectsClient.find.mockResolvedValueOnce(userFindResult); + + await connectorTokenClient.deleteConnectorTokens({ + connectorId: '123', + authMode: 'per-user', + }); + + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith( + 'user_connector_token', + 'user-token-1' + ); + }); + + test('routes to user client when profileUid is provided', async () => { + unsecuredSavedObjectsClient.delete.mockResolvedValue({}); + unsecuredSavedObjectsClient.find.mockResolvedValueOnce(userFindResult); + + await connectorTokenClient.deleteConnectorTokens({ + connectorId: '123', + profileUid: 'user-123', + }); + + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith( + 'user_connector_token', + 'user-token-1' + ); + }); + + test('profileUid takes priority over authMode shared', async () => { + unsecuredSavedObjectsClient.delete.mockResolvedValue({}); + unsecuredSavedObjectsClient.find.mockResolvedValueOnce(userFindResult); + + await connectorTokenClient.deleteConnectorTokens({ + connectorId: '123', + profileUid: 'user-123', + authMode: 'shared', + }); + + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith( + 'user_connector_token', + 'user-token-1' + ); + }); + + test('profileUid takes priority over authMode per-user', async () => { + unsecuredSavedObjectsClient.delete.mockResolvedValue({}); + unsecuredSavedObjectsClient.find.mockResolvedValueOnce(userFindResult); + + await connectorTokenClient.deleteConnectorTokens({ + connectorId: '123', + profileUid: 'user-123', + authMode: 'per-user', + }); + + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith( + 'user_connector_token', + 'user-token-1' + ); + }); + }); }); describe('updateOrReplace()', () => { diff --git a/x-pack/platform/plugins/shared/actions/server/lib/connector_token_client.ts b/x-pack/platform/plugins/shared/actions/server/lib/connector_token_client.ts index 2832e51c7bd21..bea8e45e3aad1 100644 --- a/x-pack/platform/plugins/shared/actions/server/lib/connector_token_client.ts +++ b/x-pack/platform/plugins/shared/actions/server/lib/connector_token_client.ts @@ -65,8 +65,13 @@ export class ConnectorTokenClient { this.userClient = new UserConnectorTokenClient(options); } - private getScope(profileUid?: string): typeof PER_USER_TOKEN_SCOPE | typeof SHARED_TOKEN_SCOPE { - return profileUid ? PER_USER_TOKEN_SCOPE : SHARED_TOKEN_SCOPE; + private getScope( + profileUid?: string, + authMode?: typeof PER_USER_TOKEN_SCOPE | typeof SHARED_TOKEN_SCOPE + ): typeof PER_USER_TOKEN_SCOPE | typeof SHARED_TOKEN_SCOPE { + return profileUid || (authMode && authMode === PER_USER_TOKEN_SCOPE) + ? PER_USER_TOKEN_SCOPE + : SHARED_TOKEN_SCOPE; } private parseTokenId(id: string): { @@ -193,8 +198,9 @@ export class ConnectorTokenClient { connectorId: string; tokenType?: string; credentialType?: string; + authMode?: typeof PER_USER_TOKEN_SCOPE | typeof SHARED_TOKEN_SCOPE; }): Promise { - const scope = this.getScope(options.profileUid); + const scope = this.getScope(options.profileUid, options.authMode); this.log({ method: 'deleteConnectorTokens', scope, diff --git a/x-pack/platform/plugins/shared/actions/server/lib/user_connector_token_client.test.ts b/x-pack/platform/plugins/shared/actions/server/lib/user_connector_token_client.test.ts index 9cbea9edc54af..365f028240e7c 100644 --- a/x-pack/platform/plugins/shared/actions/server/lib/user_connector_token_client.test.ts +++ b/x-pack/platform/plugins/shared/actions/server/lib/user_connector_token_client.test.ts @@ -563,6 +563,70 @@ describe('UserConnectorTokenClient', () => { }) ); }); + + test('deletes all user tokens for connectorId when profileUid is not provided', async () => { + unsecuredSavedObjectsClient.delete.mockResolvedValue({}); + + const findResult = { + total: 2, + per_page: 10, + page: 1, + saved_objects: [ + { + id: 'token-user-a', + type: 'user_connector_token', + attributes: { + profileUid: 'user-a', + connectorId: '123', + credentialType: 'oauth', + credentials: {}, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + score: 1, + references: [], + }, + { + id: 'token-user-b', + type: 'user_connector_token', + attributes: { + profileUid: 'user-b', + connectorId: '123', + credentialType: 'oauth', + credentials: {}, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + score: 1, + references: [], + }, + ], + }; + + unsecuredSavedObjectsClient.find.mockResolvedValueOnce(findResult); + + // Simulate connector deletion: no profileUid, delete all tokens for the connector + await userClient.deleteConnectorTokens({ + profileUid: undefined as unknown as string, + connectorId: '123', + }); + + expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'user_connector_token', + filter: expect.not.stringContaining('profileUid'), + }) + ); + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(2); + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith( + 'user_connector_token', + 'token-user-a' + ); + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith( + 'user_connector_token', + 'token-user-b' + ); + }); }); describe('updateOrReplace()', () => { diff --git a/x-pack/platform/plugins/shared/actions/server/lib/user_connector_token_client.ts b/x-pack/platform/plugins/shared/actions/server/lib/user_connector_token_client.ts index 806c596bf8b34..cb280b78b3bc3 100644 --- a/x-pack/platform/plugins/shared/actions/server/lib/user_connector_token_client.ts +++ b/x-pack/platform/plugins/shared/actions/server/lib/user_connector_token_client.ts @@ -422,7 +422,9 @@ export class UserConnectorTokenClient { ? ` AND ${USER_CONNECTOR_TOKEN_SAVED_OBJECT_TYPE}.attributes.credentialType: "${credentialType}"` : ''; - const profileUidFilter = `${USER_CONNECTOR_TOKEN_SAVED_OBJECT_TYPE}.attributes.profileUid: "${profileUid}" AND `; + const profileUidFilter = profileUid + ? `${USER_CONNECTOR_TOKEN_SAVED_OBJECT_TYPE}.attributes.profileUid: "${profileUid}" AND ` + : ''; try { const result = await this.unsecuredSavedObjectsClient.find({