diff --git a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts index f66686d53f1cb..3026e2eec4d83 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts @@ -85,7 +85,7 @@ describe('checking migration metadata changes on all registered SO types', () => "event_loop_delays_daily": "d2ed39cf669577d90921c176499908b4943fb7bd", "exception-list": "fe8cc004fd2742177cdb9300f4a67689463faf9c", "exception-list-agnostic": "49fae8fcd1967cc4be45ba2a2c66c4afbc1e341b", - "file": "70c2a768473057157f6ee5d29a436e5288d22ff4", + "file": "05c14a75e5e20b12ca514a1d7de231f420facf2c", "file-upload-usage-collection-telemetry": "8478924cf0057bd90df737155b364f98d05420a5", "fileShare": "3f88784b041bb8728a7f40763a08981828799a75", "fleet-fleet-server-host": "f00ca963f1bee868806319789cdc33f1f53a97e2", diff --git a/src/plugins/files/common/types.ts b/src/plugins/files/common/types.ts index 48b05076fe02d..247340c583ea6 100644 --- a/src/plugins/files/common/types.ts +++ b/src/plugins/files/common/types.ts @@ -66,12 +66,10 @@ export type BaseFileMetadata = { * ISO string representing the file creation date */ created?: string; - /** * Size of the file */ size?: number; - /** * Hash of the file's contents */ @@ -107,6 +105,25 @@ export type BaseFileMetadata = { [hashName: string]: string | undefined; }; + /** + * Data about the user that created the file + */ + user?: { + /** + * The human-friendly user name of the owner of the file + * + * @note this field cannot be used to uniquely ID a user. See {@link BaseFileMetadata['user']['id']}. + */ + name?: string; + /** + * The unique ID of the user who created the file, taken from the user profile + * ID. + * + * See https://www.elastic.co/guide/en/elasticsearch/reference/master/user-profile.html. + */ + id?: string; + }; + /** * The file extension, for example "jpg", "png", "svg" and so forth */ @@ -153,7 +170,7 @@ export type FileMetadata = Required< FileKind: string; /** - * User-defined metadata + * User-defined metadata. */ Meta?: Meta; }; @@ -222,6 +239,10 @@ export interface FileJSON { * See {@link FileStatus} for more details. */ status: FileMetadata['Status']; + /** + * User data associated with this file + */ + user?: FileMetadata['user']; } /** diff --git a/src/plugins/files/public/components/file_picker/file_picker.stories.tsx b/src/plugins/files/public/components/file_picker/file_picker.stories.tsx index 1d38a9893ed57..2492c366f5d0d 100644 --- a/src/plugins/files/public/components/file_picker/file_picker.stories.tsx +++ b/src/plugins/files/public/components/file_picker/file_picker.stories.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { ComponentMeta, ComponentStory } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import type { FileJSON } from '../../../common'; +import type { FileJSON, FileImageMetadata } from '../../../common'; import { FilesClient, FilesClientResponses } from '../../types'; import { register } from '../stories_shared'; import { base64dLogo } from '../image/image.constants.stories'; @@ -59,7 +59,7 @@ export const Empty = Template.bind({}); const d = new Date(); let id = 0; -function createFileJSON(file?: Partial): FileJSON { +function createFileJSON(file?: Partial>): FileJSON { return { alt: '', created: d.toISOString(), diff --git a/src/plugins/files/server/file/to_json.ts b/src/plugins/files/server/file/to_json.ts index d1cb00277549a..2d01655c3a4e0 100644 --- a/src/plugins/files/server/file/to_json.ts +++ b/src/plugins/files/server/file/to_json.ts @@ -9,18 +9,20 @@ import { pickBy } from 'lodash'; import type { FileMetadata, FileJSON } from '../../common/types'; -export function serializeJSON(attrs: Partial): Partial { - const { name, mimeType, size, created, updated, fileKind, status, alt, extension, meta } = attrs; +export function serializeJSON(attrs: Partial): Partial> { + const { name, mimeType, size, created, updated, fileKind, status, alt, extension, meta, user } = + attrs; return pickBy( { name, mime_type: mimeType, size, + user, created, extension, Alt: alt, Status: status, - Meta: meta as M, + Meta: meta, Updated: updated, FileKind: fileKind, }, @@ -28,11 +30,12 @@ export function serializeJSON(attrs: Partial): Partial(id: string, attrs: FileMetadata): FileJSON { +export function toJSON(id: string, attrs: FileMetadata): FileJSON { const { name, mime_type: mimeType, size, + user, created, Updated, FileKind, @@ -44,6 +47,7 @@ export function toJSON(id: string, attrs: FileMetadata): FileJSON>( { id, + user, name, mimeType, size, @@ -51,7 +55,7 @@ export function toJSON(id: string, attrs: FileMetadata): FileJSON { /** * Unique file ID */ @@ -35,7 +35,7 @@ export interface CreateArgs { /** * The file's metadata */ - metadata: Omit; + metadata: Omit, 'fileKind'>; } /** @@ -71,7 +71,7 @@ export interface FileClient { * * @param arg - Arg to create a file. * */ - create(arg: CreateArgs): Promise>; + create(arg: CreateArgs): Promise>; /** * See {@link FileMetadataClient.get} diff --git a/src/plugins/files/server/file_service/file_action_types.ts b/src/plugins/files/server/file_service/file_action_types.ts index dfe5ed2f0ba9b..58aaef579f14b 100644 --- a/src/plugins/files/server/file_service/file_action_types.ts +++ b/src/plugins/files/server/file_service/file_action_types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { Pagination, UpdatableFileMetadata } from '../../common/types'; +import type { FileMetadata, Pagination, UpdatableFileMetadata } from '../../common/types'; /** * Arguments to create a new file. @@ -32,6 +32,10 @@ export interface CreateFileArgs { * The MIME type of the file. */ mime?: string; + /** + * User data associated with this file + */ + user?: FileMetadata['user']; } /** diff --git a/src/plugins/files/server/plugin.ts b/src/plugins/files/server/plugin.ts index 95408f1fc18c5..7edd42985b616 100755 --- a/src/plugins/files/server/plugin.ts +++ b/src/plugins/files/server/plugin.ts @@ -23,7 +23,12 @@ import { import { BlobStorageService } from './blob_storage_service'; import { FileServiceFactory } from './file_service'; -import type { FilesPluginSetupDependencies, FilesSetup, FilesStart } from './types'; +import type { + FilesPluginSetupDependencies, + FilesPluginStartDependencies, + FilesSetup, + FilesStart, +} from './types'; import type { FilesRequestHandlerContext, FilesRouter } from './routes/types'; import { registerRoutes, registerFileKindRoutes } from './routes'; @@ -33,6 +38,7 @@ export class FilesPlugin implements Plugin { return { + security: this.securityStart, fileService: { asCurrentUser: () => this.fileServiceFactory!.asScoped(req), asInternalUser: () => this.fileServiceFactory!.asInternal(), @@ -81,8 +88,9 @@ export class FilesPlugin implements Plugin = CreateRouteDefinition }>; export const handler: CreateHandler = async ({ fileKind, files }, req, res) => { - const { fileService } = await files; + const { fileService, security } = await files; const { body: { name, alt, meta, mimeType }, } = req; - const file = await fileService - .asCurrentUser() - .create({ fileKind, name, alt, meta, mime: mimeType }); + const user = security?.authc.getCurrentUser(req); + const file = await fileService.asCurrentUser().create({ + fileKind, + name, + alt, + meta, + user: user ? { name: user.username, id: user.profile_uid } : undefined, + mime: mimeType, + }); const body: Endpoint['output'] = { file: file.toJSON(), }; diff --git a/src/plugins/files/server/routes/file_kind/integration_tests/file_kind_http.test.ts b/src/plugins/files/server/routes/file_kind/integration_tests/file_kind_http.test.ts index 58a6d195a0bdb..c53783e28602d 100644 --- a/src/plugins/files/server/routes/file_kind/integration_tests/file_kind_http.test.ts +++ b/src/plugins/files/server/routes/file_kind/integration_tests/file_kind_http.test.ts @@ -40,6 +40,9 @@ describe('File kind HTTP API', () => { mimeType: 'image/png', extension: 'png', meta: {}, + user: { + name: expect.any(String), + }, alt: 'a picture of my dog', }); }); @@ -81,7 +84,7 @@ describe('File kind HTTP API', () => { } = await request.get(root, `/api/files/files/${fileKind}/${id}`).expect(200); expect(file.name).toBe('acoolfilename'); - const updatedFileAttrs: UpdatableFileMetadata = { + const updatedFileAttrs: UpdatableFileMetadata<{ something: string }> = { name: 'anothercoolfilename', alt: 'a picture of my cat', meta: { @@ -89,20 +92,24 @@ describe('File kind HTTP API', () => { }, }; - const { - body: { file: updatedFile }, - } = await request - .patch(root, `/api/files/files/${fileKind}/${id}`) - .send(updatedFileAttrs) - .expect(200); + { + const { + body: { file: updatedFile }, + } = await request + .patch(root, `/api/files/files/${fileKind}/${id}`) + .send(updatedFileAttrs) + .expect(200); - expect(updatedFile).toEqual(expect.objectContaining(updatedFileAttrs)); + expect(updatedFile).toMatchObject(updatedFileAttrs); + } - const { - body: { file: file2 }, - } = await request.get(root, `/api/files/files/${fileKind}/${id}`).expect(200); + { + const { + body: { file: updatedFile }, + } = await request.get(root, `/api/files/files/${fileKind}/${id}`).expect(200); - expect(file2).toEqual(expect.objectContaining(updatedFileAttrs)); + expect(updatedFile).toMatchObject(updatedFileAttrs); + } }); test('list current files', async () => { diff --git a/src/plugins/files/server/routes/types.ts b/src/plugins/files/server/routes/types.ts index cfe47a7652359..1c803c71b78c0 100644 --- a/src/plugins/files/server/routes/types.ts +++ b/src/plugins/files/server/routes/types.ts @@ -15,12 +15,14 @@ import type { IKibanaResponse, Logger, } from '@kbn/core/server'; +import type { SecurityPluginStart } from '@kbn/security-plugin/server'; import type { FileServiceStart } from '../file_service'; import { Counters } from '../usage'; import { AnyEndpoint } from './api_routes'; export interface FilesRequestHandlerContext extends RequestHandlerContext { files: Promise<{ + security?: SecurityPluginStart; fileService: { asCurrentUser: () => FileServiceStart; asInternalUser: () => FileServiceStart; diff --git a/src/plugins/files/server/saved_objects/file.ts b/src/plugins/files/server/saved_objects/file.ts index 54d7fb57a3a07..c4259e6d679c3 100644 --- a/src/plugins/files/server/saved_objects/file.ts +++ b/src/plugins/files/server/saved_objects/file.ts @@ -25,6 +25,9 @@ const properties: Properties = { name: { type: 'text', }, + user: { + type: 'flattened', + }, Status: { type: 'keyword', }, diff --git a/src/plugins/files/server/types.ts b/src/plugins/files/server/types.ts index eb5cbff029678..25f6861d451d5 100644 --- a/src/plugins/files/server/types.ts +++ b/src/plugins/files/server/types.ts @@ -5,8 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; +import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { FileKind } from '../common'; import { FileServiceFactory } from './file_service/file_service_factory'; @@ -31,6 +30,10 @@ export interface FilesPluginSetupDependencies { usageCollection?: UsageCollectionSetup; } +export interface FilesPluginStartDependencies { + security?: SecurityPluginStart; +} + /** * Files plugin start contract */