diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c7114d87386b0..069d1e4f85ec7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2416,6 +2416,7 @@ src/platform/packages/shared/kbn-connector-specs/src/specs/** src/platform/packages/shared/kbn-connector-specs/src/specs/abuseipdb/** @elastic/workflows-eng src/platform/packages/shared/kbn-connector-specs/src/specs/alienvault_otx/** @elastic/workflows-eng src/platform/packages/shared/kbn-connector-specs/src/specs/brave_search/** @elastic/workchat-eng +src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/** @elastic/workchat-eng src/platform/packages/shared/kbn-connector-specs/src/specs/greynoise/** @elastic/workflows-eng src/platform/packages/shared/kbn-connector-specs/src/specs/jina/** @elastic/jinastic src/platform/packages/shared/kbn-connector-specs/src/specs/notion/** @elastic/workchat-eng diff --git a/docs/reference/connectors-kibana/_snippets/data-context-sources-connectors-list.md b/docs/reference/connectors-kibana/_snippets/data-context-sources-connectors-list.md index 5a09a5d2dad04..5690f7a21507b 100644 --- a/docs/reference/connectors-kibana/_snippets/data-context-sources-connectors-list.md +++ b/docs/reference/connectors-kibana/_snippets/data-context-sources-connectors-list.md @@ -1,6 +1,7 @@ **Third-party search** * [Brave Search](/reference/connectors-kibana/brave-search-action-type.md): Search the web using the Brave Search API. * [Jina Reader](/reference/connectors-kibana/jina-action-type.md): Convert web pages into markdown from their URL and search the web for better LLM grounding. +* [Google Drive](/reference/connectors-kibana/google-drive-action-type.md): Search and access files and folders in Google Drive. * [Notion](/reference/connectors-kibana/notion-action-type.md): Explore content and databases in Notion. * [Sharepoint online](/reference/connectors-kibana/sharepoint-online-action-type.md): Search across SharePoint sites, pages, and content using the Microsoft Graph API. diff --git a/docs/reference/connectors-kibana/google-drive-action-type.md b/docs/reference/connectors-kibana/google-drive-action-type.md new file mode 100644 index 0000000000000..160737d9d5d6f --- /dev/null +++ b/docs/reference/connectors-kibana/google-drive-action-type.md @@ -0,0 +1,67 @@ +--- +navigation_title: "Google Drive" +mapped_pages: + - https://www.elastic.co/guide/en/kibana/current/google-drive-action-type.html +applies_to: + stack: preview 9.4 + serverless: preview +--- + +# Google Drive connector [google-drive-action-type] + +The Google Drive connector enables searching and accessing files and folders in Google Drive. + +## Create connectors in {{kib}} [define-google-drive-ui] + +You can create connectors in **{{stack-manage-app}} > {{connectors-ui}}**. + +### Connector configuration [google-drive-connector-configuration] + +Google Drive connectors have the following configuration properties: + +Bearer Token +: A Google OAuth 2.0 access token with Google Drive API scopes. Check the [Get API credentials](#google-drive-api-credentials) for instructions. + +## Test connectors [google-drive-action-configuration] + +You can test connectors as you're creating or editing the connector in {{kib}}. The test verifies connectivity by fetching the authenticated user's information from the Google Drive API. + +The Google Drive connector has the following actions: + +Search files +: Search for files in Google Drive using Google's query syntax. + - **query** (required): Google Drive query string. Use `fullText contains 'term'` for content search, `name contains 'term'` for filename search, `mimeType='application/pdf'` for type filtering, `modifiedTime > '2024-01-01'` for date filtering. Combine with `and`/`or`. + - **pageSize** (optional): Maximum number of files to return (1–1000). Defaults to 250. + - **pageToken** (optional): Token for pagination from a previous response. + - **orderBy** (optional): Sort order. Valid values: `createdTime`, `createdTime desc`, `modifiedTime`, `modifiedTime desc`, `name`, `name desc`. + +List files +: List files and subfolders in a Google Drive folder. + - **folderId** (optional): Parent folder ID. Use `root` for the root folder, or a folder ID from search/list results. Defaults to `root`. + - **pageSize** (optional): Maximum number of files to return (1–1000). Defaults to 250. + - **pageToken** (optional): Token for pagination from a previous response. + - **orderBy** (optional): Sort order: `name`, `modifiedTime`, or `createdTime`. + - **includeTrashed** (optional): Include trashed files in results. Defaults to `false`. + +Download file +: Download a file's content. For native files (PDF, DOCX, etc.), downloads the file directly. For Google Workspace documents (Docs, Sheets, Slides), exports to a standard format (PDF for documents, XLSX for spreadsheets). + - **fileId** (required): The ID of the file to download. + +Get file metadata +: Get detailed metadata for specific files, including ownership, sharing status, permissions, and descriptions. Use after search or list to inspect specific files. + - **fileIds** (required): Array of file IDs to fetch metadata for. Returns: `id`, `name`, `mimeType`, `size`, `createdTime`, `modifiedTime`, `owners`, `lastModifyingUser`, `sharingUser`, `shared`, `starred`, `trashed`, `permissions`, `description`, `parents`, `labelInfo`, `webViewLink`. + +## Connector networking configuration [google-drive-connector-networking-configuration] + +Use the [Action configuration settings](/reference/configuration-reference/alerting-settings.md#action-settings) to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations. + +## Get API credentials [google-drive-api-credentials] + +To use the Google Drive connector, you need a Google OAuth 2.0 access token with Drive API scopes. You can obtain one using the [Google OAuth 2.0 Playground](https://developers.google.com/oauthplayground/): + +1. Open the [OAuth 2.0 Playground](https://developers.google.com/oauthplayground/). +2. In the list of APIs, select **Drive API v3** and choose the `https://www.googleapis.com/auth/drive.readonly` scope (or `https://www.googleapis.com/auth/drive` for full access). +3. Click **Authorize APIs** and sign in with your Google account. +4. Click **Exchange authorization code for tokens**. +5. Copy the **Access token** and use it as the **Bearer Token** in the connector configuration. + diff --git a/docs/reference/toc.yml b/docs/reference/toc.yml index fb5e1428c5f0c..df16677313b95 100644 --- a/docs/reference/toc.yml +++ b/docs/reference/toc.yml @@ -75,6 +75,7 @@ toc: - file: connectors-kibana/abuseipdb-action-type.md - file: connectors-kibana/alienvault-otx-action-type.md - file: connectors-kibana/brave-search-action-type.md + - file: connectors-kibana/google-drive-action-type.md - file: connectors-kibana/greynoise-action-type.md - file: connectors-kibana/jina-action-type.md - file: connectors-kibana/notion-action-type.md diff --git a/src/platform/packages/shared/kbn-connector-specs/src/all_specs.ts b/src/platform/packages/shared/kbn-connector-specs/src/all_specs.ts index ef2593dc8c3aa..373ffaaea291f 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/all_specs.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/all_specs.ts @@ -11,6 +11,7 @@ export * from './specs/abuseipdb/abuseipdb'; export * from './specs/alienvault_otx/alienvault_otx'; export * from './specs/brave_search/brave_search'; export * from './specs/github/github'; +export * from './specs/google_drive/google_drive'; export * from './specs/greynoise/greynoise'; export * from './specs/notion/notion'; export * from './specs/shodan/shodan'; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/connector_icons_map.ts b/src/platform/packages/shared/kbn-connector-specs/src/connector_icons_map.ts index 350405cb9fcb5..33e86aec6084e 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/connector_icons_map.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/connector_icons_map.ts @@ -73,4 +73,10 @@ export const ConnectorIconsMap: Map< '.urlvoid', lazy(() => import(/* webpackChunkName: "connectorIconUrlvoid" */ './specs/urlvoid/icon')), ], + [ + '.google_drive', + lazy( + () => import(/* webpackChunkName: "connectorIconGoogleDrive" */ './specs/google_drive/icon') + ), + ], ]); diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/google_drive.test.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/google_drive.test.ts new file mode 100644 index 0000000000000..a09abe1f25cbd --- /dev/null +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/google_drive.test.ts @@ -0,0 +1,652 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { ActionContext } from '../../connector_spec'; +import { GoogleDriveConnector } from './google_drive'; + +describe('GoogleDriveConnector', () => { + const mockClient = { + get: jest.fn(), + post: jest.fn(), + }; + + const mockContext = { + client: mockClient, + log: { debug: jest.fn() }, + } as unknown as ActionContext; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('searchFiles action', () => { + it('should search files with a query', async () => { + const mockResponse = { + data: { + files: [ + { + id: 'file-1', + name: 'Report.pdf', + mimeType: 'application/pdf', + size: '1024', + modifiedTime: '2025-01-01T00:00:00.000Z', + webViewLink: 'https://drive.google.com/file/d/file-1/view', + }, + ], + nextPageToken: undefined, + }, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await GoogleDriveConnector.actions.searchFiles.handler(mockContext, { + query: "fullText contains 'report'", + pageSize: 250, + }); + + expect(mockClient.get).toHaveBeenCalledWith('https://www.googleapis.com/drive/v3/files', { + params: { + q: "fullText contains 'report'", + pageSize: 250, + fields: + 'nextPageToken, files(id, name, mimeType, size, createdTime, modifiedTime, webViewLink)', + }, + }); + expect(result).toEqual({ + files: mockResponse.data.files, + nextPageToken: undefined, + }); + }); + + it('should include pageToken when provided', async () => { + const mockResponse = { + data: { + files: [], + nextPageToken: undefined, + }, + }; + mockClient.get.mockResolvedValue(mockResponse); + + await GoogleDriveConnector.actions.searchFiles.handler(mockContext, { + query: "name contains 'test'", + pageSize: 10, + pageToken: 'next-page-token', + }); + + expect(mockClient.get).toHaveBeenCalledWith('https://www.googleapis.com/drive/v3/files', { + params: { + q: "name contains 'test'", + pageSize: 10, + pageToken: 'next-page-token', + fields: + 'nextPageToken, files(id, name, mimeType, size, createdTime, modifiedTime, webViewLink)', + }, + }); + }); + + it('should cap pageSize at 1000', async () => { + const mockResponse = { data: { files: [] } }; + mockClient.get.mockResolvedValue(mockResponse); + + await GoogleDriveConnector.actions.searchFiles.handler(mockContext, { + query: 'test', + pageSize: 5000, + }); + + expect(mockClient.get).toHaveBeenCalledWith( + 'https://www.googleapis.com/drive/v3/files', + expect.objectContaining({ + params: expect.objectContaining({ pageSize: 1000 }), + }) + ); + }); + + it('should include orderBy when provided', async () => { + const mockResponse = { data: { files: [], nextPageToken: undefined } }; + mockClient.get.mockResolvedValue(mockResponse); + + await GoogleDriveConnector.actions.searchFiles.handler(mockContext, { + query: "'me' in owners and trashed=false", + pageSize: 1, + orderBy: 'createdTime desc', + }); + + expect(mockClient.get).toHaveBeenCalledWith( + 'https://www.googleapis.com/drive/v3/files', + expect.objectContaining({ + params: expect.objectContaining({ + q: "'me' in owners and trashed=false", + pageSize: 1, + orderBy: 'createdTime desc', + }), + }) + ); + }); + + it('should throw Google Drive API error when present', async () => { + const error = { + response: { + data: { + error: { message: 'Invalid query', code: 400 }, + }, + }, + }; + mockClient.get.mockRejectedValue(error); + + await expect( + GoogleDriveConnector.actions.searchFiles.handler(mockContext, { + query: 'bad query', + pageSize: 250, + }) + ).rejects.toThrow('Google Drive API error (400)'); + }); + + it('should rethrow non-Google errors', async () => { + mockClient.get.mockRejectedValue(new Error('Network error')); + + await expect( + GoogleDriveConnector.actions.searchFiles.handler(mockContext, { + query: 'test', + pageSize: 250, + }) + ).rejects.toThrow('Network error'); + }); + }); + + describe('listFiles action', () => { + it('should list files in root folder by default', async () => { + const mockResponse = { + data: { + files: [ + { + id: 'file-1', + name: 'Document.docx', + mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + size: '2048', + modifiedTime: '2025-01-01T00:00:00.000Z', + webViewLink: 'https://drive.google.com/file/d/file-1/view', + }, + ], + nextPageToken: undefined, + }, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await GoogleDriveConnector.actions.listFiles.handler(mockContext, { + folderId: 'root', + pageSize: 250, + includeTrashed: false, + }); + + expect(mockClient.get).toHaveBeenCalledWith('https://www.googleapis.com/drive/v3/files', { + params: { + q: "'root' in parents and trashed=false", + pageSize: 250, + fields: + 'nextPageToken, files(id, name, mimeType, size, createdTime, modifiedTime, webViewLink)', + }, + }); + expect(result).toEqual({ + files: mockResponse.data.files, + nextPageToken: undefined, + }); + }); + + it('should list files in a specific folder', async () => { + const mockResponse = { data: { files: [], nextPageToken: undefined } }; + mockClient.get.mockResolvedValue(mockResponse); + + await GoogleDriveConnector.actions.listFiles.handler(mockContext, { + folderId: 'folder-abc123', + pageSize: 250, + includeTrashed: false, + }); + + expect(mockClient.get).toHaveBeenCalledWith( + 'https://www.googleapis.com/drive/v3/files', + expect.objectContaining({ + params: expect.objectContaining({ + q: "'folder-abc123' in parents and trashed=false", + }), + }) + ); + }); + + it('should include trashed files when requested', async () => { + const mockResponse = { data: { files: [], nextPageToken: undefined } }; + mockClient.get.mockResolvedValue(mockResponse); + + await GoogleDriveConnector.actions.listFiles.handler(mockContext, { + folderId: 'root', + pageSize: 250, + includeTrashed: true, + }); + + expect(mockClient.get).toHaveBeenCalledWith( + 'https://www.googleapis.com/drive/v3/files', + expect.objectContaining({ + params: expect.objectContaining({ + q: "'root' in parents", + }), + }) + ); + }); + + it('should include orderBy when provided', async () => { + const mockResponse = { data: { files: [], nextPageToken: undefined } }; + mockClient.get.mockResolvedValue(mockResponse); + + await GoogleDriveConnector.actions.listFiles.handler(mockContext, { + folderId: 'root', + pageSize: 250, + orderBy: 'modifiedTime', + includeTrashed: false, + }); + + expect(mockClient.get).toHaveBeenCalledWith( + 'https://www.googleapis.com/drive/v3/files', + expect.objectContaining({ + params: expect.objectContaining({ + orderBy: 'modifiedTime', + }), + }) + ); + }); + + it('should include pageToken when provided', async () => { + const mockResponse = { data: { files: [], nextPageToken: undefined } }; + mockClient.get.mockResolvedValue(mockResponse); + + await GoogleDriveConnector.actions.listFiles.handler(mockContext, { + folderId: 'root', + pageSize: 250, + pageToken: 'page-token-xyz', + includeTrashed: false, + }); + + expect(mockClient.get).toHaveBeenCalledWith( + 'https://www.googleapis.com/drive/v3/files', + expect.objectContaining({ + params: expect.objectContaining({ + pageToken: 'page-token-xyz', + }), + }) + ); + }); + + it('should escape special characters in folder IDs', async () => { + const mockResponse = { data: { files: [], nextPageToken: undefined } }; + mockClient.get.mockResolvedValue(mockResponse); + + await GoogleDriveConnector.actions.listFiles.handler(mockContext, { + folderId: "folder's\\id", + pageSize: 250, + includeTrashed: false, + }); + + expect(mockClient.get).toHaveBeenCalledWith( + 'https://www.googleapis.com/drive/v3/files', + expect.objectContaining({ + params: expect.objectContaining({ + q: "'folder\\'s\\\\id' in parents and trashed=false", + }), + }) + ); + }); + + it('should throw Google Drive API error when present', async () => { + const error = { + response: { + data: { + error: { message: 'Folder not found', code: 404 }, + }, + }, + }; + mockClient.get.mockRejectedValue(error); + + await expect( + GoogleDriveConnector.actions.listFiles.handler(mockContext, { + folderId: 'nonexistent', + pageSize: 250, + includeTrashed: false, + }) + ).rejects.toThrow('Google Drive API error (404)'); + }); + }); + + describe('downloadFile action', () => { + it('should download a native file', async () => { + const metadataResponse = { + data: { + id: 'file-1', + name: 'report.pdf', + mimeType: 'application/pdf', + size: '1024', + }, + }; + const contentResponse = { + data: Buffer.from('pdf content'), + }; + + mockClient.get.mockResolvedValueOnce(metadataResponse).mockResolvedValueOnce(contentResponse); + + const result = await GoogleDriveConnector.actions.downloadFile.handler(mockContext, { + fileId: 'file-1', + }); + + // First call: metadata + expect(mockClient.get).toHaveBeenCalledWith( + 'https://www.googleapis.com/drive/v3/files/file-1', + { + params: { fields: 'id, name, mimeType, size' }, + } + ); + + // Second call: content download + expect(mockClient.get).toHaveBeenCalledWith( + 'https://www.googleapis.com/drive/v3/files/file-1', + { + params: { alt: 'media' }, + responseType: 'arraybuffer', + } + ); + + expect(result).toEqual({ + id: 'file-1', + name: 'report.pdf', + mimeType: 'application/pdf', + size: '1024', + content: Buffer.from('pdf content').toString('base64'), + encoding: 'base64', + }); + }); + + it('should export a Google Doc as PDF', async () => { + const metadataResponse = { + data: { + id: 'doc-1', + name: 'My Document', + mimeType: 'application/vnd.google-apps.document', + size: undefined, + }, + }; + const contentResponse = { + data: Buffer.from('exported pdf'), + }; + + mockClient.get.mockResolvedValueOnce(metadataResponse).mockResolvedValueOnce(contentResponse); + + const result = await GoogleDriveConnector.actions.downloadFile.handler(mockContext, { + fileId: 'doc-1', + }); + + // Second call: export + expect(mockClient.get).toHaveBeenCalledWith( + 'https://www.googleapis.com/drive/v3/files/doc-1/export', + { + params: { mimeType: 'application/pdf' }, + responseType: 'arraybuffer', + } + ); + + expect(result).toEqual({ + id: 'doc-1', + name: 'My Document', + mimeType: 'application/pdf', + size: undefined, + content: Buffer.from('exported pdf').toString('base64'), + encoding: 'base64', + }); + }); + + it('should export a Google Spreadsheet as XLSX', async () => { + const metadataResponse = { + data: { + id: 'sheet-1', + name: 'My Spreadsheet', + mimeType: 'application/vnd.google-apps.spreadsheet', + size: undefined, + }, + }; + const contentResponse = { + data: Buffer.from('exported xlsx'), + }; + + mockClient.get.mockResolvedValueOnce(metadataResponse).mockResolvedValueOnce(contentResponse); + + const result = await GoogleDriveConnector.actions.downloadFile.handler(mockContext, { + fileId: 'sheet-1', + }); + + // The export API call should use XLSX mime type for spreadsheets + expect(mockClient.get).toHaveBeenCalledWith( + 'https://www.googleapis.com/drive/v3/files/sheet-1/export', + { + params: { + mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }, + responseType: 'arraybuffer', + } + ); + + expect(result).toEqual( + expect.objectContaining({ + mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }) + ); + }); + + it('should throw Google Drive API error when present', async () => { + const error = { + response: { + data: { + error: { message: 'File not found', code: 404 }, + }, + }, + }; + mockClient.get.mockRejectedValue(error); + + await expect( + GoogleDriveConnector.actions.downloadFile.handler(mockContext, { + fileId: 'nonexistent', + }) + ).rejects.toThrow('Google Drive API error (404)'); + }); + }); + + describe('getFileMetadata action', () => { + const metadataFields = + 'id,name,mimeType,size,createdTime,modifiedTime,owners,lastModifyingUser,sharingUser,shared,starred,trashed,permissions,description,parents,labelInfo,webViewLink'; + + it('should fetch metadata for a single file', async () => { + const mockResponse = { + data: { + id: 'file-1', + name: 'Report.pdf', + mimeType: 'application/pdf', + size: '1024', + createdTime: '2025-01-01T00:00:00.000Z', + modifiedTime: '2025-06-01T00:00:00.000Z', + owners: [{ displayName: 'Alice', emailAddress: 'alice@example.com' }], + lastModifyingUser: { displayName: 'Bob', emailAddress: 'bob@example.com' }, + shared: true, + permissions: [ + { id: '1', role: 'owner', type: 'user', emailAddress: 'alice@example.com' }, + ], + webViewLink: 'https://drive.google.com/file/d/file-1/view', + }, + }; + mockClient.get.mockResolvedValue(mockResponse); + + const result = await GoogleDriveConnector.actions.getFileMetadata.handler(mockContext, { + fileIds: ['file-1'], + }); + + expect(mockClient.get).toHaveBeenCalledWith( + 'https://www.googleapis.com/drive/v3/files/file-1', + { params: { fields: metadataFields } } + ); + expect(result).toEqual({ files: [mockResponse.data] }); + }); + + it('should fetch metadata for multiple files in parallel', async () => { + const mockResponse1 = { + data: { id: 'file-1', name: 'Doc 1', mimeType: 'application/pdf' }, + }; + const mockResponse2 = { + data: { id: 'file-2', name: 'Doc 2', mimeType: 'application/pdf' }, + }; + const mockResponse3 = { + data: { id: 'file-3', name: 'Doc 3', mimeType: 'application/pdf' }, + }; + + mockClient.get + .mockResolvedValueOnce(mockResponse1) + .mockResolvedValueOnce(mockResponse2) + .mockResolvedValueOnce(mockResponse3); + + const result = await GoogleDriveConnector.actions.getFileMetadata.handler(mockContext, { + fileIds: ['file-1', 'file-2', 'file-3'], + }); + + expect(mockClient.get).toHaveBeenCalledTimes(3); + expect(mockClient.get).toHaveBeenCalledWith( + 'https://www.googleapis.com/drive/v3/files/file-1', + { params: { fields: metadataFields } } + ); + expect(mockClient.get).toHaveBeenCalledWith( + 'https://www.googleapis.com/drive/v3/files/file-2', + { params: { fields: metadataFields } } + ); + expect(mockClient.get).toHaveBeenCalledWith( + 'https://www.googleapis.com/drive/v3/files/file-3', + { params: { fields: metadataFields } } + ); + expect(result).toEqual({ + files: [mockResponse1.data, mockResponse2.data, mockResponse3.data], + }); + }); + + it('should throw Google Drive API error when present', async () => { + const error = { + response: { + data: { + error: { message: 'File not found', code: 404 }, + }, + }, + }; + mockClient.get.mockRejectedValue(error); + + await expect( + GoogleDriveConnector.actions.getFileMetadata.handler(mockContext, { + fileIds: ['nonexistent'], + }) + ).rejects.toThrow('Google Drive API error (404)'); + }); + + it('should fail if any file in the batch fails', async () => { + const mockResponse = { + data: { id: 'file-1', name: 'Doc 1' }, + }; + const error = { + response: { + data: { + error: { message: 'Permission denied', code: 403 }, + }, + }, + }; + + mockClient.get.mockResolvedValueOnce(mockResponse).mockRejectedValueOnce(error); + + await expect( + GoogleDriveConnector.actions.getFileMetadata.handler(mockContext, { + fileIds: ['file-1', 'file-2'], + }) + ).rejects.toThrow('Google Drive API error (403)'); + }); + }); + + describe('test handler', () => { + it('should return success when API is accessible', async () => { + const mockResponse = { + status: 200, + data: { + user: { + emailAddress: 'user@example.com', + }, + }, + }; + mockClient.get.mockResolvedValue(mockResponse); + + if (!GoogleDriveConnector.test) { + throw new Error('Test handler not defined'); + } + const result = await GoogleDriveConnector.test.handler(mockContext); + + expect(mockClient.get).toHaveBeenCalledWith('https://www.googleapis.com/drive/v3/about', { + params: { fields: 'user' }, + }); + expect(result).toEqual({ + ok: true, + message: 'Successfully connected to Google Drive API as user@example.com', + }); + }); + + it('should fall back to generic user label when email is missing', async () => { + const mockResponse = { + status: 200, + data: { + user: {}, + }, + }; + mockClient.get.mockResolvedValue(mockResponse); + + if (!GoogleDriveConnector.test) { + throw new Error('Test handler not defined'); + } + const result = await GoogleDriveConnector.test.handler(mockContext); + + expect(result).toEqual({ + ok: true, + message: 'Successfully connected to Google Drive API as user', + }); + }); + + it('should return failure when API returns non-200 status', async () => { + const mockResponse = { + status: 401, + data: {}, + }; + mockClient.get.mockResolvedValue(mockResponse); + + if (!GoogleDriveConnector.test) { + throw new Error('Test handler not defined'); + } + const result = await GoogleDriveConnector.test.handler(mockContext); + + expect(result).toEqual({ + ok: false, + message: 'Failed to connect to Google Drive API', + }); + }); + + it('should return failure when API throws an error', async () => { + mockClient.get.mockRejectedValue(new Error('Invalid credentials')); + + if (!GoogleDriveConnector.test) { + throw new Error('Test handler not defined'); + } + const result = await GoogleDriveConnector.test.handler(mockContext); + + expect(result).toEqual({ + ok: false, + message: 'Failed to connect to Google Drive API: Invalid credentials', + }); + }); + }); +}); diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/google_drive.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/google_drive.ts new file mode 100644 index 0000000000000..00e3e6d2e661f --- /dev/null +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/google_drive.ts @@ -0,0 +1,376 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import { i18n } from '@kbn/i18n'; +import { z } from '@kbn/zod/v4'; +import type { ConnectorSpec } from '../../connector_spec'; + +// Google Drive API constants +const GOOGLE_DRIVE_API_BASE = 'https://www.googleapis.com/drive/v3'; +const DEFAULT_PAGE_SIZE = 250; +const MAX_PAGE_SIZE = 1000; +const DEFAULT_FOLDER_ID = 'root'; +const GOOGLE_WORKSPACE_MIME_PREFIX = 'application/vnd.google-apps.'; +const DEFAULT_EXPORT_MIME_TYPE = 'application/pdf'; +// XLSX preserves tabular structure better than PDF for spreadsheets +const SHEETS_EXPORT_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + +/** + * Escapes special characters in a string for use in Google Drive query syntax. + * Google Drive queries use single quotes for string values, so backslashes + * and single quotes must be escaped to avoid syntax errors and injection. + */ +function escapeQueryValue(value: string): string { + // Escape backslashes first, then single quotes + return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); +} + +/** + * Extracts and throws a meaningful error from Google Drive API responses. + * Returns void if the error doesn't have Google API error details. + */ +function throwGoogleDriveError(error: unknown): void { + const axiosError = error as { + response?: { data?: { error?: { message?: string; code?: number } } }; + }; + const googleError = axiosError.response?.data?.error; + if (googleError) { + throw new Error(`Google Drive API error (${googleError.code})`); + } +} + +export const GoogleDriveConnector: ConnectorSpec = { + metadata: { + id: '.google_drive', + displayName: 'Google Drive', + description: i18n.translate('core.kibanaConnectorSpecs.googleDrive.metadata.description', { + defaultMessage: 'Search and access files and folders in Google Drive', + }), + minimumLicense: 'enterprise', + supportedFeatureIds: ['workflows'], + }, + auth: { + types: ['bearer'], + headers: { + Accept: 'application/json', + }, + }, + + actions: { + searchFiles: { + isTool: true, + input: z.object({ + query: z + .string() + .min(1) + .describe( + 'Google Drive search query. ' + + "Examples: name contains 'budget' and trashed=false | " + + "fullText contains 'quarterly report' and mimeType='application/pdf' | " + + "'me' in owners and modifiedTime > '2024-01-01' | " + + "mimeType='application/vnd.google-apps.folder' and trashed=false. " + + "Operators: contains, =, !=, <, >, <=, >=. Combine with 'and'/'or'. " + + "String values use single quotes. Add 'and trashed=false' to exclude trashed files." + ), + pageSize: z + .number() + .optional() + .default(DEFAULT_PAGE_SIZE) + .describe('Maximum number of files to return (1-1000)'), + pageToken: z.string().optional().describe('Token for pagination'), + orderBy: z + .preprocess( + (val) => (val === '' ? undefined : val), + z + .enum([ + 'createdTime', + 'createdTime desc', + 'modifiedTime', + 'modifiedTime desc', + 'name', + 'name desc', + ]) + .optional() + ) + .describe('Field and direction to order results by'), + }), + handler: async (ctx, input) => { + const typedInput = input as { + query: string; + pageSize: number; + pageToken?: string; + orderBy?: string; + }; + + const params: Record = { + q: typedInput.query, + pageSize: Math.min(typedInput.pageSize || DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE), + fields: + 'nextPageToken, files(id, name, mimeType, size, createdTime, modifiedTime, webViewLink)', + }; + + if (typedInput.pageToken) { + params.pageToken = typedInput.pageToken; + } + + if (typedInput.orderBy) { + params.orderBy = typedInput.orderBy; + } + + try { + const response = await ctx.client.get(`${GOOGLE_DRIVE_API_BASE}/files`, { + params, + }); + + return { + files: response.data.files || [], + nextPageToken: response.data.nextPageToken, + }; + } catch (error: unknown) { + throwGoogleDriveError(error); + throw error; + } + }, + }, + + listFiles: { + isTool: true, + input: z.object({ + folderId: z + .preprocess((val) => (val === '' ? undefined : val), z.string().optional()) + .default(DEFAULT_FOLDER_ID) + .describe("Parent folder ID ('root' for root folder)"), + pageSize: z + .number() + .optional() + .default(DEFAULT_PAGE_SIZE) + .describe('Maximum number of files to return (1-1000)'), + pageToken: z.string().optional().describe('Token for pagination'), + orderBy: z + .preprocess( + (val) => (val === '' ? undefined : val), + z.enum(['name', 'modifiedTime', 'createdTime']).optional() + ) + .describe('Field to order results by'), + includeTrashed: z + .boolean() + .optional() + .default(false) + .describe('Include trashed files in results (default: false)'), + }), + handler: async (ctx, input) => { + const typedInput = input as { + folderId: string; + pageSize: number; + pageToken?: string; + orderBy?: string; + includeTrashed: boolean; + }; + + const folderId = typedInput.folderId || DEFAULT_FOLDER_ID; + const trashedFilter = typedInput.includeTrashed ? '' : ' and trashed=false'; + const params: Record = { + q: `'${escapeQueryValue(folderId)}' in parents${trashedFilter}`, + pageSize: Math.min(typedInput.pageSize || DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE), + fields: + 'nextPageToken, files(id, name, mimeType, size, createdTime, modifiedTime, webViewLink)', + }; + + if (typedInput.pageToken) { + params.pageToken = typedInput.pageToken; + } + + if (typedInput.orderBy) { + params.orderBy = typedInput.orderBy; + } + + try { + const response = await ctx.client.get(`${GOOGLE_DRIVE_API_BASE}/files`, { + params, + }); + + return { + files: response.data.files || [], + nextPageToken: response.data.nextPageToken, + }; + } catch (error: unknown) { + throwGoogleDriveError(error); + throw error; + } + }, + }, + + downloadFile: { + isTool: true, + input: z.object({ + fileId: z.string().min(1).describe('The ID of the file to download'), + }), + handler: async (ctx, input) => { + const typedInput = input as { + fileId: string; + }; + + try { + // First, get file metadata to determine if it's a Google Workspace document + const metadataResponse = await ctx.client.get( + `${GOOGLE_DRIVE_API_BASE}/files/${typedInput.fileId}`, + { + params: { + fields: 'id, name, mimeType, size', + }, + } + ); + + const fileMetadata = metadataResponse.data; + const isGoogleDoc = fileMetadata.mimeType?.startsWith(GOOGLE_WORKSPACE_MIME_PREFIX); + + let contentResponse; + let resolvedMimeType: string = fileMetadata.mimeType; + + if (isGoogleDoc) { + // Export Google Workspace documents + // Use XLSX for Sheets (preserves tabular structure), PDF for everything else + const defaultExport = + fileMetadata.mimeType === 'application/vnd.google-apps.spreadsheet' + ? SHEETS_EXPORT_MIME_TYPE + : DEFAULT_EXPORT_MIME_TYPE; + resolvedMimeType = defaultExport; + contentResponse = await ctx.client.get( + `${GOOGLE_DRIVE_API_BASE}/files/${typedInput.fileId}/export`, + { + params: { + mimeType: resolvedMimeType, + }, + responseType: 'arraybuffer', + } + ); + } else { + // Download native files + contentResponse = await ctx.client.get( + `${GOOGLE_DRIVE_API_BASE}/files/${typedInput.fileId}`, + { + params: { + alt: 'media', + }, + responseType: 'arraybuffer', + } + ); + } + + const buffer = Buffer.from(contentResponse.data); + const base64Content = buffer.toString('base64'); + + return { + id: fileMetadata.id, + name: fileMetadata.name, + mimeType: resolvedMimeType, + size: fileMetadata.size, + content: base64Content, + encoding: 'base64', + }; + } catch (error: unknown) { + throwGoogleDriveError(error); + throw error; + } + }, + }, + + getFileMetadata: { + isTool: true, + input: z.object({ + fileIds: z + .array(z.string().min(1)) + .min(1) + .describe( + 'Array of file IDs to fetch metadata for. Use after search/list to get ownership, ' + + 'sharing, permissions, and other details for specific files.' + ), + }), + handler: async (ctx, input) => { + const typedInput = input as { + fileIds: string[]; + }; + + const metadataFields = [ + 'id', + 'name', + 'mimeType', + 'size', + 'createdTime', + 'modifiedTime', + 'owners', + 'lastModifyingUser', + 'sharingUser', + 'shared', + 'starred', + 'trashed', + 'permissions', + 'description', + 'parents', + 'labelInfo', + 'webViewLink', + ].join(','); + + try { + const results = await Promise.all( + typedInput.fileIds.map(async (fileId) => { + try { + const response = await ctx.client.get(`${GOOGLE_DRIVE_API_BASE}/files/${fileId}`, { + params: { fields: metadataFields }, + }); + return response.data; + } catch (error: unknown) { + throwGoogleDriveError(error); + throw error; + } + }) + ); + + return { files: results }; + } catch (error: unknown) { + throwGoogleDriveError(error); + throw error; + } + }, + }, + }, + + test: { + description: i18n.translate('core.kibanaConnectorSpecs.googleDrive.test.description', { + defaultMessage: 'Verifies Google Drive connection by fetching user information', + }), + handler: async (ctx) => { + ctx.log.debug('Google Drive test handler'); + try { + const response = await ctx.client.get(`${GOOGLE_DRIVE_API_BASE}/about`, { + params: { + fields: 'user', + }, + }); + + if (response.status !== 200) { + return { ok: false, message: 'Failed to connect to Google Drive API' }; + } + + return { + ok: true, + message: `Successfully connected to Google Drive API as ${ + response.data.user?.emailAddress || 'user' + }`, + }; + } catch (error) { + return { + ok: false, + message: `Failed to connect to Google Drive API: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + }; + } + }, + }, +}; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/icon/google_drive.png b/src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/icon/google_drive.png new file mode 100644 index 0000000000000..5385d299d8c81 Binary files /dev/null and b/src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/icon/google_drive.png differ diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/icon/index.tsx b/src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/icon/index.tsx new file mode 100644 index 0000000000000..3451c833d1579 --- /dev/null +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/icon/index.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; + +import { EuiIcon } from '@elastic/eui'; +import type { ConnectorIconProps } from '../../../types'; + +import icon from './google_drive.png'; + +export default (props: ConnectorIconProps) => { + return ; +}; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/notion/notion.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/notion/notion.ts index bccee2df15f1b..3a25a9d12e031 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/notion/notion.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/notion/notion.ts @@ -124,7 +124,7 @@ export const NotionConnector: ConnectorSpec = { }, test: { - description: i18n.translate('ore.kibanaConnectorSpecs.notion.test.description', { + description: i18n.translate('core.kibanaConnectorSpecs.notion.test.description', { defaultMessage: 'Verifies Notion connection by fetching metadata about given data source', }), // TODO: might need to accept some input here in order to pass to the API endpoint to test diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/sharepoint_online/sharepoint_online.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/sharepoint_online/sharepoint_online.ts index ae6ceb66c6a1a..3179b2c27b7b7 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/sharepoint_online/sharepoint_online.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/sharepoint_online/sharepoint_online.ts @@ -405,7 +405,6 @@ export const SharepointOnline: ConnectorSpec = { ], }; - ctx.log.debug(`SharePoint search: ${JSON.stringify(typedInput.query)}`); const response = await ctx.client.post( 'https://graph.microsoft.com/v1.0/search/query', searchRequest diff --git a/x-pack/platform/plugins/shared/data_sources/server/sources/github/data_type.ts b/x-pack/platform/plugins/shared/data_sources/server/sources/github/data_type.ts index 1cd95da50d093..a28fab1e1e4df 100644 --- a/x-pack/platform/plugins/shared/data_sources/server/sources/github/data_type.ts +++ b/x-pack/platform/plugins/shared/data_sources/server/sources/github/data_type.ts @@ -8,7 +8,6 @@ import { i18n } from '@kbn/i18n'; import { MCPAuthType } from '@kbn/connector-schemas/mcp'; import type { DataSource } from '@kbn/data-catalog-plugin'; -import { EARSSupportedOAuthProvider } from '@kbn/data-catalog-plugin'; export const githubDataSource: DataSource = { id: 'github', @@ -19,13 +18,6 @@ export const githubDataSource: DataSource = { iconType: '.github', - oauthConfiguration: { - provider: EARSSupportedOAuthProvider.GITHUB, - initiatePath: '/oauth/start/github', - fetchSecretsPath: '/oauth/fetch_request_secrets', - oauthBaseUrl: 'https://localhost:8052', // update once EARS deploys to QA - }, - stackConnector: { type: '.mcp', config: { diff --git a/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/data_type.ts b/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/data_type.ts new file mode 100644 index 0000000000000..9a65c8476d200 --- /dev/null +++ b/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/data_type.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { DataSource } from '@kbn/data-catalog-plugin'; + +export const googleDriveDataSource: DataSource = { + id: 'google_drive', + name: 'Google Drive', + description: i18n.translate('xpack.dataSources.googleDrive.description', { + defaultMessage: 'Connect to Google Drive to access files and folders.', + }), + iconType: '.google_drive', + + stackConnector: { + type: '.google_drive', + config: {}, + }, + + workflows: { + directory: __dirname + '/workflows', + }, +}; diff --git a/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/index.ts b/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/index.ts new file mode 100644 index 0000000000000..5e44ffd88ccab --- /dev/null +++ b/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { googleDriveDataSource } from './data_type'; diff --git a/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/workflows/download.yaml b/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/workflows/download.yaml new file mode 100644 index 0000000000000..bd7171762672a --- /dev/null +++ b/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/workflows/download.yaml @@ -0,0 +1,93 @@ +version: '1' +name: 'sources.google_drive.download' +description: Download files and extract their text content (best for PDFs, Word docs, etc.). You can optionally set rerank to true and specify topK to use semantic reranking - this is useful when downloading many documents and you want to avoid using too much of your context window by only keeping the top K most relevant documents based on the rerankQuery. +tags: ['agent-builder-tool'] +enabled: true +triggers: + - type: manual +inputs: + - name: fileIds + type: array + description: Array of file IDs from search or list results. Works with PDFs, Office docs, Google Docs, and other text-based formats + - name: rerank + type: boolean + required: false + default: false + description: Set to true to rerank results by relevance to rerankQuery. Useful when downloading many documents to reduce context window usage by keeping only the most relevant ones. + - name: topK + type: number + required: false + default: 5 + description: When rerank is true, return only the top K most relevant documents after reranking. + - name: rerankQuery + type: string + required: false + description: The query to rerank documents against. Required when rerank is true. Documents will be scored by relevance to this query. +steps: + - name: init_results + type: data.set + with: + results: [] + - name: process_files + type: foreach + foreach: "${{inputs.fileIds}}" + steps: + - name: download_file + type: google_drive.downloadFile + connector-id: <%= stackConnectorId %> + with: + fileId: "${{foreach.item}}" + - name: extract_content + type: elasticsearch.request + with: + method: POST + path: /_ingest/pipeline/_simulate + body: + pipeline: + processors: + - attachment: + field: data + indexed_chars: -1 + remove_binary: true + docs: + - _id: "${{foreach.item}}" + _source: + filename: "${{steps.download_file.output.name}}" + data: "${{steps.download_file.output.content}}" + - name: normalize_result + type: data.set + with: + normalized: + fileId: "${{foreach.item}}" + filename: "${{steps.download_file.output.name}}" + content: "${{steps.extract_content.output.docs[0].doc._source.attachment.content}}" + content_type: "${{steps.extract_content.output.docs[0].doc._source.attachment.content_type}}" + - name: accumulate_result + type: data.set + with: + results: '${{variables.results | push: variables.normalized}}' + - name: conditional_rerank + type: if + condition: "${{inputs.rerank}}" + steps: + - name: do_rerank + type: search.rerank + with: + rerank_text: "${{inputs.rerankQuery}}" + data: ${{variables.results}} + fields: + - ["content"] + rank_window_size: ${{inputs.topK}} + - name: store_reranked + type: data.set + with: + final_results: "${{steps.do_rerank.output}}" + else: + - name: store_all + type: data.set + with: + final_results: "${{variables.results}}" + - name: output_results + type: data.set + with: + results: "${{variables.final_results}}" diff --git a/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/workflows/list.yaml b/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/workflows/list.yaml new file mode 100644 index 0000000000000..26a449ae06e96 --- /dev/null +++ b/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/workflows/list.yaml @@ -0,0 +1,33 @@ +version: '1' +name: 'sources.google_drive.list' +description: List files and subfolders in a Google Drive folder +tags: ['agent-builder-tool'] +enabled: true +triggers: + - type: manual +inputs: + - name: folderId + type: string + default: root + description: "Folder ID to list contents of. Use 'root' for the root folder, or a folder ID from search/list results" + - name: pageSize + type: number + required: false + description: Number of results to return (default 250, max 1000) + - name: pageToken + type: string + required: false + description: "Pagination token. Pass the 'nextPageToken' value from a previous response to get the next page. When nextPageToken is absent in the response, there are no more results." + - name: orderBy + type: string + required: false + description: "Sort order: 'name', 'modifiedTime', or 'createdTime'" +steps: + - name: list_files + type: google_drive.listFiles + connector-id: <%= stackConnectorId %> + with: + folderId: "${{inputs.folderId}}" + pageSize: ${{inputs.pageSize}} + pageToken: "${{inputs.pageToken}}" + orderBy: "${{inputs.orderBy}}" diff --git a/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/workflows/metadata.yaml b/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/workflows/metadata.yaml new file mode 100644 index 0000000000000..521ad1be36fb9 --- /dev/null +++ b/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/workflows/metadata.yaml @@ -0,0 +1,17 @@ +version: '1' +name: 'sources.google_drive.metadata' +description: Get detailed metadata for specific files including ownership, sharing, permissions, and descriptions. Use after search or list to inspect specific files. +tags: ['agent-builder-tool'] +enabled: true +triggers: + - type: manual +inputs: + - name: fileIds + type: array + description: Array of file IDs to fetch metadata for. Use IDs from search or list results. +steps: + - name: get_metadata + type: google_drive.getFileMetadata + connector-id: <%= stackConnectorId %> + with: + fileIds: ${{inputs.fileIds}} diff --git a/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/workflows/search.yaml b/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/workflows/search.yaml new file mode 100644 index 0000000000000..4beb323bb39d3 --- /dev/null +++ b/x-pack/platform/plugins/shared/data_sources/server/sources/google_drive/workflows/search.yaml @@ -0,0 +1,32 @@ +version: '1' +name: 'sources.google_drive.search' +description: Search for files in Google Drive using Google's query syntax +tags: ['agent-builder-tool'] +enabled: true +triggers: + - type: manual +inputs: + - name: query + type: string + description: "Google Drive search query. Examples: name contains 'budget' and trashed=false | fullText contains 'quarterly report' and mimeType='application/pdf' | 'me' in owners and modifiedTime > '2024-01-01' | mimeType='application/vnd.google-apps.folder' and trashed=false. Operators: contains, =, !=, <, >, <=, >=. Combine with 'and'/'or'. String values use single quotes. Add 'and trashed=false' to exclude trashed files." + - name: pageSize + type: number + required: false + description: Number of results to return (default 250, max 1000) + - name: pageToken + type: string + required: false + description: "Pagination token. Pass the 'nextPageToken' value from a previous response to get the next page. When nextPageToken is absent in the response, there are no more results." + - name: orderBy + type: string + required: false + description: "Sort order: 'createdTime', 'createdTime desc', 'modifiedTime', 'modifiedTime desc', 'name', or 'name desc'" +steps: + - name: search_files + type: google_drive.searchFiles + connector-id: <%= stackConnectorId %> + with: + query: "${{inputs.query}}" + pageSize: ${{inputs.pageSize}} + pageToken: "${{inputs.pageToken}}" + orderBy: "${{inputs.orderBy}}" diff --git a/x-pack/platform/plugins/shared/data_sources/server/sources/index.ts b/x-pack/platform/plugins/shared/data_sources/server/sources/index.ts index de06ed348941a..deb08245a50bf 100644 --- a/x-pack/platform/plugins/shared/data_sources/server/sources/index.ts +++ b/x-pack/platform/plugins/shared/data_sources/server/sources/index.ts @@ -7,10 +7,12 @@ import type { DataCatalogPluginSetup } from '@kbn/data-catalog-plugin/server'; import { notionDataSource } from './notion'; import { githubDataSource } from './github'; +import { googleDriveDataSource } from './google_drive'; import { sharepointOnlineDataSource } from './sharepoint_online'; export function registerDataSources(dataCatalog: DataCatalogPluginSetup) { dataCatalog.register(notionDataSource); dataCatalog.register(githubDataSource); + dataCatalog.register(googleDriveDataSource); dataCatalog.register(sharepointOnlineDataSource); } diff --git a/x-pack/platform/plugins/shared/data_sources/server/sources/notion/data_type.ts b/x-pack/platform/plugins/shared/data_sources/server/sources/notion/data_type.ts index 76d0b585c5bd2..2f47ba63ad01e 100644 --- a/x-pack/platform/plugins/shared/data_sources/server/sources/notion/data_type.ts +++ b/x-pack/platform/plugins/shared/data_sources/server/sources/notion/data_type.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import type { DataSource } from '@kbn/data-catalog-plugin'; -import { EARSSupportedOAuthProvider } from '@kbn/data-catalog-plugin'; export const notionDataSource: DataSource = { id: 'notion', @@ -18,13 +17,6 @@ export const notionDataSource: DataSource = { iconType: '.notion', - oauthConfiguration: { - provider: EARSSupportedOAuthProvider.NOTION, - initiatePath: '/oauth/start/notion', - fetchSecretsPath: '/oauth/fetch_request_secrets', - oauthBaseUrl: 'https://localhost:8052', - }, - stackConnector: { type: '.notion', config: {},