Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Supporting share-capable saved objects for rules and connectors #106630

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions x-pack/plugins/actions/server/actions_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
SavedObject,
KibanaRequest,
SavedObjectsUtils,
SavedObjectsResolveResponse,
} from '../../../../src/core/server';
import { AuditLogger } from '../../security/server';
import { ActionType } from '../common';
Expand Down Expand Up @@ -209,10 +210,10 @@ export class ActionsClient {
throw error;
}
const {
attributes,
references,
version,
} = await this.unsecuredSavedObjectsClient.get<RawAction>('action', id);
saved_object: { attributes, references, version },
/* resolvedResponse */
} = await this.unsecuredSavedObjectsClient.resolve<RawAction>('action', id);
// TODO: How to handle `resolveResponse` here
const { actionTypeId } = attributes;
const { name, config, secrets } = action;
const actionType = this.actionTypeRegistry.get(actionTypeId);
Expand Down Expand Up @@ -263,7 +264,13 @@ export class ActionsClient {
/**
* Get an action
*/
public async get({ id }: { id: string }): Promise<ActionResult> {
public async get({
id,
}: {
id: string;
}): Promise<
ActionResult & { resolveResponse?: Omit<SavedObjectsResolveResponse, 'saved_object'> }
> {
try {
await this.authorization.ensureAuthorized('get');
} catch (error) {
Expand Down Expand Up @@ -296,7 +303,12 @@ export class ActionsClient {
};
}

const result = await this.unsecuredSavedObjectsClient.get<RawAction>('action', id);
const {
saved_object: result,
...resolveResponse
} = await this.unsecuredSavedObjectsClient.resolve<RawAction>('action', id);

// TODO: How to handle `resolveResponse` here

this.auditLogger?.log(
connectorAuditEvent({
Expand All @@ -312,6 +324,7 @@ export class ActionsClient {
name: result.attributes.name,
config: result.attributes.config,
isPreconfigured: false,
resolveResponse,
};
}

Expand All @@ -331,12 +344,24 @@ export class ActionsClient {
throw error;
}

const savedObjectsActions = (
const discoveredSavedObjects = (
await this.unsecuredSavedObjectsClient.find<RawAction>({
perPage: MAX_ACTIONS_RETURNED,
type: 'action',
})
).saved_objects.map(actionFromSavedObject);
).saved_objects;
const savedObjectsActions = (
await Promise.all(
discoveredSavedObjects.map(({ id }) =>
this.unsecuredSavedObjectsClient.resolve<RawAction>('action', id)
)
)
).map(({ saved_object: action, ...resolveResponse }) => {
return {
...actionFromSavedObject(action),
resolveResponse,
};
});

savedObjectsActions.forEach(({ id }) =>
this.auditLogger?.log(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@ export async function getAuthorizationModeBySource(
unsecuredSavedObjectsClient: SavedObjectsClientContract,
executionSource?: ActionExecutionSource<unknown>
): Promise<AuthorizationMode> {
// TODO: How to handle `resolveResponse` here
return isSavedObjectExecutionSource(executionSource) &&
executionSource?.source?.type === ALERT_SAVED_OBJECT_TYPE &&
(
await unsecuredSavedObjectsClient.get<{
await unsecuredSavedObjectsClient.resolve<{
meta?: {
versionApiKeyLastmodified?: string;
};
}>(ALERT_SAVED_OBJECT_TYPE, executionSource.source.id)
).attributes.meta?.versionApiKeyLastmodified === LEGACY_VERSION
).saved_object.attributes.meta?.versionApiKeyLastmodified === LEGACY_VERSION
? AuthorizationMode.Legacy
: AuthorizationMode.RBAC;
}
28 changes: 21 additions & 7 deletions x-pack/plugins/actions/server/create_execute_function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
* 2.0.
*/

import { SavedObjectsClientContract } from '../../../../src/core/server';
import {
SavedObjectsClientContract,
SavedObjectsResolveResponse,
} from '../../../../src/core/server';
import { RunNowResult, TaskManagerStartContract } from '../../task_manager/server';
import {
RawAction,
Expand Down Expand Up @@ -53,9 +56,11 @@ export function createExecutionEnqueuerFunction({
);
}

const action = await getAction(unsecuredSavedObjectsClient, preconfiguredActions, id);
const { action } = await getAction(unsecuredSavedObjectsClient, preconfiguredActions, id);
validateCanActionBeUsed(action);

// TODO: How to handle `resolveResponse` here

const { actionTypeId } = action;
if (!actionTypeRegistry.isActionExecutable(id, actionTypeId, { notifyUsage: true })) {
actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
Expand Down Expand Up @@ -93,7 +98,7 @@ export function createEphemeralExecutionEnqueuerFunction({
unsecuredSavedObjectsClient: SavedObjectsClientContract,
{ id, params, spaceId, source, apiKey }: ExecuteOptions
): Promise<RunNowResult> {
const action = await getAction(unsecuredSavedObjectsClient, preconfiguredActions, id);
const { action } = await getAction(unsecuredSavedObjectsClient, preconfiguredActions, id);
validateCanActionBeUsed(action);

const { actionTypeId } = action;
Expand Down Expand Up @@ -148,12 +153,21 @@ async function getAction(
unsecuredSavedObjectsClient: SavedObjectsClientContract,
preconfiguredActions: PreConfiguredAction[],
actionId: string
): Promise<PreConfiguredAction | RawAction> {
): Promise<{
action: PreConfiguredAction | RawAction;
resolveResponse?: Omit<SavedObjectsResolveResponse, 'saved_object'>;
}> {
const pcAction = preconfiguredActions.find((action) => action.id === actionId);
if (pcAction) {
return pcAction;
return { action: pcAction };
}

const { attributes } = await unsecuredSavedObjectsClient.get<RawAction>('action', actionId);
return attributes;
const {
saved_object: { attributes },
...resolveResponse
} = await unsecuredSavedObjectsClient.resolve<RawAction>('action', actionId);
return {
action: attributes,
resolveResponse,
};
}
6 changes: 4 additions & 2 deletions x-pack/plugins/actions/server/lib/action_executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import type { PublicMethodsOf } from '@kbn/utility-types';
import { Logger, KibanaRequest } from 'src/core/server';
import { Logger, KibanaRequest, SavedObjectsResolveResponse } from 'src/core/server';
import { cloneDeep } from 'lodash';
import { withSpan } from '@kbn/apm-utils';
import { validateParams, validateConfig, validateSecrets } from './validate_with_schema';
Expand Down Expand Up @@ -288,7 +288,7 @@ async function getActionInfo(
preconfiguredActions: PreConfiguredAction[],
actionId: string,
namespace: string | undefined
): Promise<ActionInfo> {
): Promise<ActionInfo & { resolveResponse?: Omit<SavedObjectsResolveResponse, 'saved_object'> }> {
// check to see if it's a pre-configured action first
const pcAction = preconfiguredActions.find(
(preconfiguredAction) => preconfiguredAction.id === actionId
Expand All @@ -308,6 +308,7 @@ async function getActionInfo(

const {
attributes: { secrets },
resolveResponse,
} = await encryptedSavedObjectsClient.getDecryptedAsInternalUser<RawAction>('action', actionId, {
namespace: namespace === 'default' ? undefined : namespace,
});
Expand All @@ -317,5 +318,6 @@ async function getActionInfo(
name,
config,
secrets,
resolveResponse,
};
}
3 changes: 3 additions & 0 deletions x-pack/plugins/actions/server/lib/task_runner_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,15 @@ export class TaskRunnerFactory {
const {
attributes: { actionId, params, apiKey, relatedSavedObjects },
references,
/* resolveResponse, */
} = await getActionTaskParams(
actionTaskExecutorParams,
encryptedSavedObjectsClient,
spaceIdToNamespace
);

// TODO: How to handle `resolveResponse` here?

const requestHeaders: Record<string, string> = {};
if (apiKey) {
requestHeaders.authorization = `ApiKey ${apiKey}`;
Expand Down
23 changes: 14 additions & 9 deletions x-pack/plugins/actions/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ElasticsearchServiceStart,
SavedObjectsClientContract,
SavedObjectsBulkGetObject,
SavedObjectsBaseOptions,
} from '../../../../src/core/server';

import {
Expand Down Expand Up @@ -356,20 +357,24 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
// as authorizationContext which would then set a Legacy AuthorizationMode
const secureGetActionsClientWithRequest = (request: KibanaRequest) =>
getActionsClientWithRequest(request);
const getScopedSavedObjectsClientWithoutAccessToActions = (request: KibanaRequest) =>
core.savedObjects.getScopedClient(request);

this.eventLogService!.registerSavedObjectProvider('action', (request) => {
const client = secureGetActionsClientWithRequest(request);
return (objects?: SavedObjectsBulkGetObject[]) =>
objects
? Promise.all(
objects.map(async (objectItem) => await (await client).get({ id: objectItem.id }))
)
: Promise.resolve([]);
const soClient = getScopedSavedObjectsClientWithoutAccessToActions(request);
return {
bulkGetter: (objects?: SavedObjectsBulkGetObject[]) =>
objects
? Promise.all(
objects.map(async (objectItem) => await (await client).get({ id: objectItem.id }))
)
: Promise.resolve([]),
resolver: (type: string, id: string, options?: SavedObjectsBaseOptions) =>
soClient.resolve(type, id, options),
};
});

const getScopedSavedObjectsClientWithoutAccessToActions = (request: KibanaRequest) =>
core.savedObjects.getScopedClient(request);

actionExecutor!.initialize({
logger,
eventLogger: this.eventLogger!,
Expand Down
8 changes: 6 additions & 2 deletions x-pack/plugins/actions/server/saved_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export function setupSavedObjects(
savedObjects.registerType({
name: ACTION_SAVED_OBJECT_TYPE,
hidden: true,
namespaceType: 'single',
// namespaceType: 'single',
namespaceType: 'multiple-isolated',
convertToMultiNamespaceTypeVersion: '8.0.0',
mappings: mappings.action as SavedObjectsTypeMappingDefinition,
migrations: getMigrations(encryptedSavedObjects),
management: {
Expand Down Expand Up @@ -67,7 +69,9 @@ export function setupSavedObjects(
savedObjects.registerType({
name: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
hidden: true,
namespaceType: 'single',
// namespaceType: 'single',
namespaceType: 'multiple-isolated',
convertToMultiNamespaceTypeVersion: '8.0.0',
mappings: mappings.action_task_params as SavedObjectsTypeMappingDefinition,
});
encryptedSavedObjects.registerType({
Expand Down
9 changes: 9 additions & 0 deletions x-pack/plugins/actions/server/saved_objects/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,19 @@ export function getMigrations(
pipeMigrations(addisMissingSecretsField)
);

// This empty migration is necessary to ensure that the saved object is decrypted with its old descriptor/ and re-encrypted with its new
// descriptor, if necessary. This is included because the saved object is being converted to `namespaceType: 'multiple-isolated'` in 8.0
// (see the `convertToMultiNamespaceTypeVersion` field in the saved object type registration process).
const migrationActions800 = encryptedSavedObjects.createMigration<RawAction, RawAction>(
(doc): doc is SavedObjectUnsanitizedDoc<RawAction> => true,
(doc) => doc // no-op
);

return {
'7.10.0': executeMigrationWithErrorHandling(migrationActionsTen, '7.10.0'),
'7.11.0': executeMigrationWithErrorHandling(migrationActionsEleven, '7.11.0'),
'7.14.0': executeMigrationWithErrorHandling(migrationActionsFourteen, '7.14.0'),
'8.0.0': executeMigrationWithErrorHandling(migrationActions800, '8.0.0'),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ async function invalidateApiKeys(
'api_key_pending_invalidation',
apiKeyObj.id
);
// TODO: How to handle `resolveResponse` here?
return decryptedApiKey.attributes.apiKeyId;
})
);
Expand Down
18 changes: 13 additions & 5 deletions x-pack/plugins/alerting/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
ServiceStatus,
SavedObjectsBulkGetObject,
ServiceStatusLevels,
SavedObjectsBaseOptions,
} from '../../../../src/core/server';
import type { AlertingRequestHandlerContext } from './types';
import { defineRoutes } from './routes';
Expand Down Expand Up @@ -368,6 +369,7 @@ export class AlertingPlugin {
return alertingAuthorizationClientFactory!.create(request);
};

const internalSavedObjectsRepository = core.savedObjects.createInternalRepository(['alert']);
taskRunnerFactory.initialize({
logger,
getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch),
Expand All @@ -377,7 +379,7 @@ export class AlertingPlugin {
encryptedSavedObjectsClient,
basePathService: core.http.basePath,
eventLogger: this.eventLogger!,
internalSavedObjectsRepository: core.savedObjects.createInternalRepository(['alert']),
internalSavedObjectsRepository,
alertTypeRegistry: this.alertTypeRegistry!,
kibanaBaseUrl: this.kibanaBaseUrl,
supportsEphemeralTasks: plugins.taskManager.supportsEphemeralTasks(),
Expand All @@ -386,10 +388,16 @@ export class AlertingPlugin {

this.eventLogService!.registerSavedObjectProvider('alert', (request) => {
const client = getRulesClientWithRequest(request);
return (objects?: SavedObjectsBulkGetObject[]) =>
objects
? Promise.all(objects.map(async (objectItem) => await client.get({ id: objectItem.id })))
: Promise.resolve([]);
return {
bulkGetter: (objects?: SavedObjectsBulkGetObject[]) =>
objects
? Promise.all(
objects.map(async (objectItem) => await client.get({ id: objectItem.id }))
)
: Promise.resolve([]),
resolver: (type: string, id: string, options?: SavedObjectsBaseOptions) =>
internalSavedObjectsRepository.resolve(type, id, options),
};
});

scheduleAlertingTelemetry(this.telemetryLogger, plugins.taskManager);
Expand Down
Loading