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

share saved objects to workspace api #67

Merged
Merged
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
3 changes: 3 additions & 0 deletions src/core/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,9 @@ export {
exportSavedObjectsToStream,
importSavedObjectsFromStream,
resolveSavedObjectsImportErrors,
SavedObjectsShareObjects,
SavedObjectsAddToWorkspacesOptions,
SavedObjectsAddToWorkspacesResponse,
} from './saved_objects';

export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SavedObjectsPermissionControlContract } from './client';
export const savedObjectsPermissionControlMock: SavedObjectsPermissionControlContract = {
setup: jest.fn(),
validate: jest.fn(),
batchValidate: jest.fn(),
addPrinciplesToObjects: jest.fn(),
removePrinciplesFromObjects: jest.fn(),
getPrinciplesOfObjects: jest.fn(),
Expand Down
10 changes: 9 additions & 1 deletion src/core/server/saved_objects/permission_control/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,15 @@ export class SavedObjectsPermissionControl {
savedObject: SavedObjectsBulkGetObject,
permissionModeOrModes: SavedObjectsPermissionModes
) {
const savedObjectsGet = await this.bulkGetSavedObjects(request, [savedObject]);
return await this.batchValidate(request, [savedObject], permissionModeOrModes);
}

public async batchValidate(
request: OpenSearchDashboardsRequest,
savedObjects: SavedObjectsBulkGetObject[],
permissionModeOrModes: SavedObjectsPermissionModes
) {
const savedObjectsGet = await this.bulkGetSavedObjects(request, savedObjects);
if (savedObjectsGet) {
return {
success: true,
Expand Down
3 changes: 3 additions & 0 deletions src/core/server/saved_objects/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { registerImportRoute } from './import';
import { registerResolveImportErrorsRoute } from './resolve_import_errors';
import { registerMigrateRoute } from './migrate';
import { registerCopyRoute } from './copy';
import { registerShareRoute } from './share';

export function registerRoutes({
http,
Expand Down Expand Up @@ -73,6 +74,8 @@ export function registerRoutes({
registerImportRoute(router, config);
registerCopyRoute(router, config);
registerResolveImportErrorsRoute(router, config);
// TODO disable when workspace is not enabled
registerShareRoute(router);

const internalRouter = http.createRouter('/internal/saved_objects/');

Expand Down
102 changes: 102 additions & 0 deletions src/core/server/saved_objects/routes/share.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { schema } from '@osd/config-schema';
import { IRouter } from '../../http';
import { exportSavedObjectsToStream } from '../export';
import { validateObjects } from './utils';
import { collectSavedObjects } from '../import/collect_saved_objects';
import { WORKSPACE_TYPE } from '../../workspaces';
import { GLOBAL_WORKSPACE_ID } from '../../workspaces/constants';

const SHARE_LIMIT = 10000;

export const registerShareRoute = (router: IRouter) => {
router.post(
{
path: '/_share',
validate: {
body: schema.object({
sourceWorkspaceId: schema.maybe(schema.string()),
objects: schema.arrayOf(
schema.object({
id: schema.string(),
type: schema.string(),
})
),
targetWorkspaceIds: schema.arrayOf(schema.string()),
}),
},
},
router.handleLegacyErrors(async (context, req, res) => {
const savedObjectsClient = context.core.savedObjects.client;
const { sourceWorkspaceId, objects, targetWorkspaceIds } = req.body;

// need to access the registry for type validation, can't use the schema for this
const supportedTypes = context.core.savedObjects.typeRegistry
.getAllTypes()
.filter((type) => type.name !== WORKSPACE_TYPE)
.map((t) => t.name);

if (objects) {
const validationError = validateObjects(objects, supportedTypes);
if (validationError) {
return res.badRequest({
body: {
message: validationError,
},
});
}
}

const objectsListStream = await exportSavedObjectsToStream({
savedObjectsClient,
objects,
exportSizeLimit: SHARE_LIMIT,
includeReferencesDeep: true,
excludeExportDetails: true,
});

const collectSavedObjectsResult = await collectSavedObjects({
readStream: objectsListStream,
objectLimit: SHARE_LIMIT,
supportedTypes,
});

const savedObjects = collectSavedObjectsResult.collectedObjects;

const nonPublicSharedObjects = savedObjects
// non-public
.filter(
(obj) =>
obj.workspaces &&
obj.workspaces.length > 0 &&
!obj.workspaces.includes(GLOBAL_WORKSPACE_ID)
)
.map((obj) => ({ id: obj.id, type: obj.type, workspaces: obj.workspaces }));

if (nonPublicSharedObjects.length === 0) {
return res.ok({
body: savedObjects.map((savedObject) => ({
type: savedObject.type,
id: savedObject.id,
workspaces: savedObject.workspaces,
})),
});
}

const response = await savedObjectsClient.addToWorkspaces(
nonPublicSharedObjects,
targetWorkspaceIds,
{
workspaces: sourceWorkspaceId ? [sourceWorkspaceId] : undefined,
}
);
return res.ok({
body: response,
});
})
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const create = (): jest.Mocked<ISavedObjectsRepository> => ({
deleteFromNamespaces: jest.fn(),
deleteByNamespace: jest.fn(),
incrementCounter: jest.fn(),
addToWorkspaces: jest.fn(),
});

export const savedObjectsRepositoryMock = { create };
138 changes: 124 additions & 14 deletions src/core/server/saved_objects/service/lib/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,62 +28,66 @@
* under the License.
*/

import { omit } from 'lodash';
import { omit, intersection } from 'lodash';
import type { opensearchtypes } from '@opensearch-project/opensearch';
import uuid from 'uuid';
import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { OpenSearchClient, DeleteDocumentResponse } from '../../../opensearch/';
import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { DeleteDocumentResponse, OpenSearchClient } from '../../../opensearch/';
import { getRootPropertiesObjects, IndexMapping } from '../../mappings';
import {
createRepositoryOpenSearchClient,
RepositoryOpenSearchClient,
} from './repository_opensearch_client';
import { getSearchDsl } from './search_dsl';
import { includedFields } from './included_fields';
import { SavedObjectsErrorHelpers, DecoratedError } from './errors';
import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version';
import { DecoratedError, SavedObjectsErrorHelpers } from './errors';
import { decodeRequestVersion, encodeHitVersion, encodeVersion } from '../../version';
import { IOpenSearchDashboardsMigrator } from '../../migrations';
import {
SavedObjectsSerializer,
SavedObjectSanitizedDoc,
SavedObjectsRawDoc,
SavedObjectsRawDocSource,
SavedObjectsSerializer,
} from '../../serialization';
import {
SavedObjectsAddToNamespacesOptions,
SavedObjectsAddToNamespacesResponse,
SavedObjectsAddToWorkspacesOptions,
SavedObjectsAddToWorkspacesResponse,
SavedObjectsBulkCreateObject,
SavedObjectsBulkGetObject,
SavedObjectsBulkResponse,
SavedObjectsBulkUpdateObject,
SavedObjectsBulkUpdateOptions,
SavedObjectsBulkUpdateResponse,
SavedObjectsCheckConflictsObject,
SavedObjectsCheckConflictsResponse,
SavedObjectsCreateOptions,
SavedObjectsDeleteFromNamespacesOptions,
SavedObjectsDeleteFromNamespacesResponse,
SavedObjectsDeleteOptions,
SavedObjectsFindResponse,
SavedObjectsFindResult,
SavedObjectsShareObjects,
SavedObjectsUpdateOptions,
SavedObjectsUpdateResponse,
SavedObjectsBulkUpdateObject,
SavedObjectsBulkUpdateOptions,
SavedObjectsDeleteOptions,
SavedObjectsAddToNamespacesOptions,
SavedObjectsAddToNamespacesResponse,
SavedObjectsDeleteFromNamespacesOptions,
SavedObjectsDeleteFromNamespacesResponse,
} from '../saved_objects_client';
import {
MutatingOperationRefreshSetting,
SavedObject,
SavedObjectsBaseOptions,
SavedObjectsFindOptions,
SavedObjectsMigrationVersion,
MutatingOperationRefreshSetting,
} from '../../types';
import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { validateConvertFilterToKueryNode } from './filter_utils';
import {
ALL_NAMESPACES_STRING,
FIND_DEFAULT_PAGE,
FIND_DEFAULT_PER_PAGE,
SavedObjectsUtils,
} from './utils';
import { GLOBAL_WORKSPACE_ID } from '../../../workspaces/constants';

// BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository
// so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient.
Expand Down Expand Up @@ -1272,6 +1276,112 @@ export class SavedObjectsRepository {
}
}

async addToWorkspaces(
savedObjects: SavedObjectsShareObjects[],
workspaces: string[],
options: SavedObjectsAddToWorkspacesOptions = {}
): Promise<SavedObjectsAddToWorkspacesResponse[]> {
if (!savedObjects.length) {
throw SavedObjectsErrorHelpers.createBadRequestError(
'shared savedObjects must not be an empty array'
);
}

// saved objects must exist in specified workspace
if (options.workspaces) {
const invalidObjects = savedObjects.filter((obj) => {
if (
obj.workspaces &&
obj.workspaces.length > 0 &&
!obj.workspaces.includes(GLOBAL_WORKSPACE_ID)
) {
return intersection(obj.workspaces, options.workspaces).length === 0;
}
return false;
});
if (invalidObjects && invalidObjects.length > 0) {
const [savedObj] = invalidObjects;
throw SavedObjectsErrorHelpers.createConflictError(savedObj.type, savedObj.id);
}
}

savedObjects.forEach(({ type, id }) => {
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
});

if (!workspaces.length) {
throw SavedObjectsErrorHelpers.createBadRequestError(
'workspaces must be a non-empty array of strings'
);
}

const { refresh = DEFAULT_REFRESH_SETTING } = options;
const savedObjectsBulkResponse = await this.bulkGet(savedObjects);

const docs = savedObjectsBulkResponse.saved_objects.map((obj) => {
const { type, id } = obj;
const rawId = this._serializer.generateRawId(undefined, type, id);
const time = this._getCurrentTime();

return [
{
update: {
_id: rawId,
_index: this.getIndexForType(type),
},
},
{
script: {
source: `
if (params.workspaces != null && ctx._source.workspaces != null && !ctx._source.workspaces?.contains(params.globalWorkspaceId)) {
ctx._source.workspaces.addAll(params.workspaces);
HashSet workspacesSet = new HashSet(ctx._source.workspaces);
ctx._source.workspaces = new ArrayList(workspacesSet);
}
ctx._source.updated_at = params.time;
`,
lang: 'painless',
params: {
time,
workspaces,
globalWorkspaceId: GLOBAL_WORKSPACE_ID,
},
},
},
];
});

const bulkUpdateResponse = await this.client.bulk({
Hailong-am marked this conversation as resolved.
Show resolved Hide resolved
refresh,
body: docs.flat(),
_source_includes: ['workspaces'],
});

if (bulkUpdateResponse.body.errors) {
const failures = bulkUpdateResponse.body.items
.map((item) => item.update?.error?.reason)
.join(',');
throw SavedObjectsErrorHelpers.createBadRequestError(
'Add to workspace failed with: ' + failures
);
}

const savedObjectIdWorkspaceMap = bulkUpdateResponse.body.items.reduce((map, item) => {
return map.set(item.update?._id!, item.update?.get?._source.workspaces);
}, new Map<string, string[]>());

return savedObjects.map((obj) => {
const rawId = this._serializer.generateRawId(undefined, obj.type, obj.id);
return {
type: obj.type,
id: obj.id,
workspaces: savedObjectIdWorkspaceMap.get(rawId),
} as SavedObjectsAddToWorkspacesResponse;
});
}

/**
* Updates multiple objects in bulk
*
Expand Down
Loading
Loading