Skip to content

Commit

Permalink
[Automatic Import] Add RBAC to APIs (#203882)
Browse files Browse the repository at this point in the history
## Release Note

Adds RBAC to the Automatic Import APIs

## Summary

This PR adds RBAC privileges to Automatic Import APIs

It adds `all` access to the users having `fleet:all` `fleetv2:all`
`actions:all` UI access

This PR also adds a validation to the `integrationName` and
`dataStreamName`.

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
bhapas and kibanamachine authored Dec 16, 2024
1 parent a8eb7ca commit eb1c70a
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ export enum LogFormat {
STRUCTURED = 'structured',
UNSTRUCTURED = 'unstructured',
}
export const FLEET_ALL_ROLE = 'fleet-all' as const;
export const INTEGRATIONS_ALL_ROLE = 'integrations-all' as const;
export const ACTIONS_AND_CONNECTORS_ALL_ROLE = 'actions:execute-advanced-connectors' as const;
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { buildPackage, renderPackageManifestYAML } from './build_integration';
import { buildPackage, isValidName, renderPackageManifestYAML } from './build_integration';
import { testIntegration } from '../../__jest__/fixtures/build_integration';
import { generateUniqueId, ensureDirSync, createSync } from '../util';
import { createDataStream } from './data_stream';
Expand Down Expand Up @@ -39,15 +39,16 @@ jest.mock('adm-zip', () => {
return jest.fn().mockImplementation(() => ({
addLocalFolder: jest.fn(),
toBuffer: jest.fn(),
addFile: jest.fn(),
}));
});

describe('buildPackage', () => {
const packagePath = `${mockedDataPath}/integration-assistant-${mockedId}`;
const integrationPath = `${packagePath}/integration-1.0.0`;

const firstDatastreamName = 'datastream_1';
const secondDatastreamName = 'datastream_2';
const firstDatastreamName = 'datastream_one';
const secondDatastreamName = 'datastream_two';

const firstDataStreamInputTypes: InputType[] = ['filestream', 'kafka'];
const secondDataStreamInputTypes: InputType[] = ['kafka'];
Expand All @@ -74,8 +75,8 @@ describe('buildPackage', () => {

const firstDataStream: DataStream = {
name: firstDatastreamName,
title: 'Datastream_1',
description: 'Datastream_1 description',
title: 'datastream_one',
description: 'datastream_one description',
inputTypes: firstDataStreamInputTypes,
docs: firstDataStreamDocs,
rawSamples: ['{"test1": "test1"}'],
Expand All @@ -85,8 +86,8 @@ describe('buildPackage', () => {

const secondDataStream: DataStream = {
name: secondDatastreamName,
title: 'Datastream_2',
description: 'Datastream_2 description',
title: 'datastream_two',
description: 'datastream_two description',
inputTypes: secondDataStreamInputTypes,
docs: secondDataStreamDocs,
rawSamples: ['{"test1": "test1"}'],
Expand Down Expand Up @@ -123,15 +124,6 @@ describe('buildPackage', () => {
expect(createSync).toHaveBeenCalledWith(`${integrationPath}/manifest.yml`, expect.any(String));
});

it('Should create logo files if info is present in the integration', async () => {
testIntegration.logo = 'logo';

await buildPackage(testIntegration);

expect(ensureDirSync).toHaveBeenCalledWith(`${integrationPath}/img`);
expect(createSync).toHaveBeenCalledWith(`${integrationPath}/img/logo.svg`, expect.any(Buffer));
});

it('Should not create logo files if info is not present in the integration', async () => {
jest.clearAllMocks();
testIntegration.logo = undefined;
Expand Down Expand Up @@ -186,19 +178,19 @@ describe('buildPackage', () => {
it('Should call createReadme once with sorted fields', async () => {
jest.clearAllMocks();

const firstDSFieldsMapping = [{ name: 'name a', description: 'description 1', type: 'type 1' }];
const firstDSFieldsMapping = [{ name: 'name_a', description: 'description 1', type: 'type 1' }];

const firstDataStreamFields = [
{ name: 'name b', description: 'description 1', type: 'type 1' },
{ name: 'name_b', description: 'description 1', type: 'type 1' },
];

const secondDSFieldsMapping = [
{ name: 'name c', description: 'description 2', type: 'type 2' },
{ name: 'name e', description: 'description 3', type: 'type 3' },
{ name: 'name_c', description: 'description 2', type: 'type 2' },
{ name: 'name_e', description: 'description 3', type: 'type 3' },
];

const secondDataStreamFields = [
{ name: 'name d', description: 'description 2', type: 'type 2' },
{ name: 'name_d', description: 'description 2', type: 'type 2' },
];

(createFieldMapping as jest.Mock).mockReturnValueOnce(firstDSFieldsMapping);
Expand All @@ -217,17 +209,17 @@ describe('buildPackage', () => {
{
datastream: firstDatastreamName,
fields: [
{ name: 'name a', description: 'description 1', type: 'type 1' },
{ name: 'name_a', description: 'description 1', type: 'type 1' },

{ name: 'name b', description: 'description 1', type: 'type 1' },
{ name: 'name_b', description: 'description 1', type: 'type 1' },
],
},
{
datastream: secondDatastreamName,
fields: [
{ name: 'name c', description: 'description 2', type: 'type 2' },
{ name: 'name d', description: 'description 2', type: 'type 2' },
{ name: 'name e', description: 'description 3', type: 'type 3' },
{ name: 'name_c', description: 'description 2', type: 'type 2' },
{ name: 'name_d', description: 'description 2', type: 'type 2' },
{ name: 'name_e', description: 'description 3', type: 'type 3' },
],
},
]
Expand All @@ -239,13 +231,13 @@ describe('renderPackageManifestYAML', () => {
test('generates the package manifest correctly', () => {
const integration: Integration = {
title: 'Sample Integration',
name: 'sample-integration',
name: 'sample_integration',
description:
' This is a sample integration\n\nWith multiple lines and weird spacing. \n\n And more lines ',
logo: 'some-logo.png',
dataStreams: [
{
name: 'data-stream-1',
name: 'data_stream_one',
title: 'Data Stream 1',
description: 'This is data stream 1',
inputTypes: ['filestream'],
Expand All @@ -257,7 +249,7 @@ describe('renderPackageManifestYAML', () => {
samplesFormat: { name: 'ndjson', multiline: false },
},
{
name: 'data-stream-2',
name: 'data_stream_two',
title: 'Data Stream 2',
description:
'This is data stream 2\nWith multiple lines of description\nBut otherwise, nothing special',
Expand Down Expand Up @@ -292,3 +284,59 @@ describe('renderPackageManifestYAML', () => {
});
});
});

describe('isValidName', () => {
it('should return true for valid names', () => {
expect(isValidName('validName')).toBe(true);
expect(isValidName('Valid_Name')).toBe(true);
expect(isValidName('anotherValidName')).toBe(true);
});

it('should return false for names with numbers', () => {
expect(isValidName('invalid123')).toBe(false);
expect(isValidName('123invalid')).toBe(false);
expect(isValidName('invalid_123')).toBe(false);
});

it('should return false for empty string', () => {
expect(isValidName('')).toBe(false);
});

it('should return false for names with spaces', () => {
expect(isValidName('invalid name')).toBe(false);
expect(isValidName(' invalid')).toBe(false);
expect(isValidName('invalid ')).toBe(false);
expect(isValidName('invalid name with spaces')).toBe(false);
});

it('should return false for names with special characters', () => {
expect(isValidName('invalid@name')).toBe(false);
expect(isValidName('invalid#name')).toBe(false);
expect(isValidName('invalid$name')).toBe(false);
expect(isValidName('invalid%name')).toBe(false);
expect(isValidName('invalid^name')).toBe(false);
expect(isValidName('invalid&name')).toBe(false);
expect(isValidName('invalid*name')).toBe(false);
expect(isValidName('invalid(name')).toBe(false);
expect(isValidName('invalid/name')).toBe(false);
});

it('should return false for names with dashes', () => {
expect(isValidName('invalid-name')).toBe(false);
expect(isValidName('invalid-name-with-dashes')).toBe(false);
});

it('should return false for names with periods', () => {
expect(isValidName('invalid.name')).toBe(false);
expect(isValidName('invalid.name.with.periods')).toBe(false);
});

it('should return false for names with mixed invalid characters', () => {
expect(isValidName('invalid@name#with$special%characters')).toBe(false);
expect(isValidName('invalid name with spaces and 123')).toBe(false);
});

it('should return false for names with empty string', () => {
expect(isValidName('')).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,24 @@ function configureNunjucks() {
export async function buildPackage(integration: Integration): Promise<Buffer> {
configureNunjucks();

if (!isValidName(integration.name)) {
throw new Error(
`Invalid integration name: ${integration.name}, Should only contain letters and underscores`
);
}

const workingDir = joinPath(getDataPath(), `integration-assistant-${generateUniqueId()}`);
const packageDirectoryName = `${integration.name}-${initialVersion}`;
const packageDir = createDirectories(workingDir, integration, packageDirectoryName);

const dataStreamsDir = joinPath(packageDir, 'data_stream');
const fieldsPerDatastream = integration.dataStreams.map((dataStream) => {
const dataStreamName = dataStream.name;
if (!isValidName(dataStreamName)) {
throw new Error(
`Invalid datastream name: ${dataStreamName}, Should only contain letters and underscores`
);
}
const specificDataStreamDir = joinPath(dataStreamsDir, dataStreamName);

const dataStreamFields = createDataStream(integration.name, specificDataStreamDir, dataStream);
Expand All @@ -60,12 +71,15 @@ export async function buildPackage(integration: Integration): Promise<Buffer> {
});

createReadme(packageDir, integration.name, integration.dataStreams, fieldsPerDatastream);
const zipBuffer = await createZipArchive(workingDir, packageDirectoryName);
const zipBuffer = await createZipArchive(integration, workingDir, packageDirectoryName);

removeDirSync(workingDir);
return zipBuffer;
}

export function isValidName(input: string): boolean {
const regex = /^[a-zA-Z_]+$/;
return input.length > 0 && regex.test(input);
}
function createDirectories(
workingDir: string,
integration: Integration,
Expand All @@ -84,17 +98,6 @@ function createPackage(packageDir: string, integration: Integration): void {
createPackageManifest(packageDir, integration);
// Skipping creation of system tests temporarily for custom package generation
// createPackageSystemTests(packageDir, integration);
if (integration?.logo !== undefined) {
createLogo(packageDir, integration.logo);
}
}

function createLogo(packageDir: string, logo: string): void {
const logoDir = joinPath(packageDir, 'img');
ensureDirSync(logoDir);

const buffer = Buffer.from(logo, 'base64');
createSync(joinPath(logoDir, 'logo.svg'), buffer);
}

function createBuildFile(packageDir: string): void {
Expand All @@ -113,10 +116,20 @@ function createChangelog(packageDir: string): void {
createSync(joinPath(packageDir, 'changelog.yml'), changelogTemplate);
}

async function createZipArchive(workingDir: string, packageDirectoryName: string): Promise<Buffer> {
async function createZipArchive(
integration: Integration,
workingDir: string,
packageDirectoryName: string
): Promise<Buffer> {
const tmpPackageDir = joinPath(workingDir, packageDirectoryName);
const zip = new AdmZip();
zip.addLocalFolder(tmpPackageDir, packageDirectoryName);

if (integration.logo) {
const logoDir = joinPath(packageDirectoryName, 'img/logo.svg');
const logoBuffer = Buffer.from(integration.logo, 'base64');
zip.addFile(logoDir, logoBuffer);
}
const buffer = zip.toBuffer();
return buffer;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import { getRequestAbortedSignal } from '@kbn/data-plugin/server';
import { APMTracer } from '@kbn/langchain/server/tracers/apm';
import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith';
import { ANALYZE_LOGS_PATH, AnalyzeLogsRequestBody, AnalyzeLogsResponse } from '../../common';
import { ROUTE_HANDLER_TIMEOUT } from '../constants';
import {
ACTIONS_AND_CONNECTORS_ALL_ROLE,
FLEET_ALL_ROLE,
INTEGRATIONS_ALL_ROLE,
ROUTE_HANDLER_TIMEOUT,
} from '../constants';
import { getLogFormatDetectionGraph } from '../graphs/log_type_detection/graph';
import type { IntegrationAssistantRouteHandlerContext } from '../plugin';
import { getLLMClass, getLLMType } from '../util/llm';
Expand Down Expand Up @@ -39,9 +44,11 @@ export function registerAnalyzeLogsRoutes(
version: '1',
security: {
authz: {
enabled: false,
reason:
'This route is opted out from authorization because the privileges are not defined yet.',
requiredPrivileges: [
FLEET_ALL_ROLE,
INTEGRATIONS_ALL_ROLE,
ACTIONS_AND_CONNECTORS_ALL_ROLE,
],
},
},
validate: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import { withAvailability } from './with_availability';
import { isErrorThatHandlesItsOwnResponse } from '../lib/errors';
import { handleCustomErrors } from './routes_util';
import { GenerationErrorCode } from '../../common/constants';
import {
ACTIONS_AND_CONNECTORS_ALL_ROLE,
FLEET_ALL_ROLE,
INTEGRATIONS_ALL_ROLE,
} from '../constants';
export function registerIntegrationBuilderRoutes(
router: IRouter<IntegrationAssistantRouteHandlerContext>
) {
Expand All @@ -27,9 +32,11 @@ export function registerIntegrationBuilderRoutes(
version: '1',
security: {
authz: {
enabled: false,
reason:
'This route is opted out from authorization because the privileges are not defined yet.',
requiredPrivileges: [
FLEET_ALL_ROLE,
INTEGRATIONS_ALL_ROLE,
ACTIONS_AND_CONNECTORS_ALL_ROLE,
],
},
},
validate: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import {
CategorizationRequestBody,
CategorizationResponse,
} from '../../common';
import { ROUTE_HANDLER_TIMEOUT } from '../constants';
import {
ACTIONS_AND_CONNECTORS_ALL_ROLE,
FLEET_ALL_ROLE,
INTEGRATIONS_ALL_ROLE,
ROUTE_HANDLER_TIMEOUT,
} from '../constants';
import { getCategorizationGraph } from '../graphs/categorization';
import type { IntegrationAssistantRouteHandlerContext } from '../plugin';
import { getLLMClass, getLLMType } from '../util/llm';
Expand Down Expand Up @@ -42,9 +47,11 @@ export function registerCategorizationRoutes(
version: '1',
security: {
authz: {
enabled: false,
reason:
'This route is opted out from authorization because the privileges are not defined yet.',
requiredPrivileges: [
FLEET_ALL_ROLE,
INTEGRATIONS_ALL_ROLE,
ACTIONS_AND_CONNECTORS_ALL_ROLE,
],
},
},
validate: {
Expand Down
Loading

0 comments on commit eb1c70a

Please sign in to comment.