Skip to content

Commit

Permalink
Setup mechanism for register customized query DSL logic (opensearch-p…
Browse files Browse the repository at this point in the history
…roject#128)

* feat: setup mechanism to register different implementation on query DSL

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>

* feat: some optimization on error message and response

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>

* feat: remove useless clause

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>

* feat: move some code to a util function

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>

---------

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>
  • Loading branch information
SuZhou-Joe authored and ruanyl committed Sep 15, 2023
1 parent 12855ad commit a23b39c
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 131 deletions.
23 changes: 7 additions & 16 deletions src/core/server/saved_objects/permission_control/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import { i18n } from '@osd/i18n';
import { OpenSearchDashboardsRequest } from '../../http';
import { ensureRawRequest } from '../../http/router';
import { SavedObjectsServiceStart } from '../saved_objects_service';
import { SavedObjectsBulkGetObject } from '../service';
import { SavedObjectsBulkGetObject, SavedObjectsRepository, SavedObjectsUtils } from '../service';
import { ACL, Principals, TransformedPermission, PrincipalType } from './acl';
import { WORKSPACE_TYPE } from '../../../utils';

export type SavedObjectsPermissionControlContract = Pick<
SavedObjectsPermissionControl,
Expand Down Expand Up @@ -128,19 +127,11 @@ export class SavedObjectsPermissionControl {
permissionModes: SavedObjectsPermissionModes
) {
const principals = this.getPrincipalsFromRequest(request);
const queryDSL = ACL.generateGetPermittedSavedObjectsQueryDSL(permissionModes, principals, [
WORKSPACE_TYPE,
]);
const repository = this.getInternalRepository();
try {
const result = await repository?.find({
type: [WORKSPACE_TYPE],
queryDSL,
perPage: 999,
});
return result?.saved_objects.map((item) => item.id);
} catch (e) {
return [];
}
const repository = this.getInternalRepository() as SavedObjectsRepository;
return await SavedObjectsUtils.getPermittedWorkspaceIds({
principals,
repository,
permissionModes,
});
}
}
2 changes: 2 additions & 0 deletions src/core/server/saved_objects/service/lib/repository.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ const create = (): jest.Mocked<ISavedObjectsRepository> => ({
deleteByNamespace: jest.fn(),
incrementCounter: jest.fn(),
addToWorkspaces: jest.fn(),
getPermissionQuery: jest.fn(),
processFindOptions: jest.fn(),
});

export const savedObjectsRepositoryMock = { create };
120 changes: 119 additions & 1 deletion src/core/server/saved_objects/service/lib/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import { omit, intersection } from 'lodash';
import type { opensearchtypes } from '@opensearch-project/opensearch';
import uuid from 'uuid';
import { i18n } from '@osd/i18n';
import type { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { DeleteDocumentResponse, OpenSearchClient } from '../../../opensearch/';
Expand Down Expand Up @@ -87,7 +88,9 @@ import {
FIND_DEFAULT_PER_PAGE,
SavedObjectsUtils,
} from './utils';
import { PUBLIC_WORKSPACE_ID } from '../../../../utils/constants';
import { PUBLIC_WORKSPACE_ID, WorkspacePermissionMode } from '../../../../utils/constants';
import { ACL, Principals } from '../../permission_control/acl';
import { WORKSPACE_TYPE } from '../../../../server';

// 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 @@ -1766,6 +1769,111 @@ export class SavedObjectsRepository {
};
}

async getPermissionQuery(props: {
permissionTypes: string[];
principals: Principals;
savedObjectType?: string[];
}) {
return ACL.generateGetPermittedSavedObjectsQueryDSL(
props.permissionTypes,
props.principals,
props.savedObjectType
);
}

async processFindOptions(props: {
options: SavedObjectsFindOptions & { permissionModes?: string[] };
principals: Principals;
}): Promise<SavedObjectsFindOptions> {
const { principals } = props;
const options = { ...props.options };
if (this.isRelatedToWorkspace(options.type)) {
options.queryDSL = await this.getPermissionQuery({
permissionTypes: options.permissionModes ?? [
WorkspacePermissionMode.LibraryRead,
WorkspacePermissionMode.LibraryWrite,
WorkspacePermissionMode.Management,
],
principals,
savedObjectType: [WORKSPACE_TYPE],
});
} else {
const permittedWorkspaceIds = await SavedObjectsUtils.getPermittedWorkspaceIds({
permissionModes: [
WorkspacePermissionMode.LibraryRead,
WorkspacePermissionMode.LibraryWrite,
WorkspacePermissionMode.Management,
],
principals,
repository: this,
});

if (options.workspaces) {
const permittedWorkspaces = options.workspaces.filter((item) =>
(permittedWorkspaceIds || []).includes(item)
);
if (!permittedWorkspaces.length) {
/**
* If user does not have any one workspace access
* deny the request
*/
throw SavedObjectsErrorHelpers.decorateNotAuthorizedError(
new Error(
i18n.translate('workspace.permission.invalidate', {
defaultMessage: 'Invalid workspace permission',
})
)
);
}

/**
* Overwrite the options.workspaces when user has access on partial workspaces.
*/
options.workspaces = permittedWorkspaces;
} else {
const queryDSL = await this.getPermissionQuery({
permissionTypes: [WorkspacePermissionMode.Read, WorkspacePermissionMode.Write],
principals,
savedObjectType: Array.isArray(options.type) ? options.type : [options.type],
});
options.workspaces = undefined;
/**
* Select all the docs that
* 1. ACL matches read or write permission OR
* 2. workspaces matches library_read or library_write or management OR
* 3. Advanced settings
*/
options.queryDSL = {
query: {
bool: {
filter: [
{
bool: {
should: [
{
term: {
type: 'config',
},
},
queryDSL.query,
{
terms: {
workspaces: permittedWorkspaceIds,
},
},
],
},
},
],
},
},
};
}
}

return options;
}

/**
* Returns index specified by the given type or the default index
*
Expand Down Expand Up @@ -1894,6 +2002,16 @@ export class SavedObjectsRepository {
}
return body;
}

/**
* check if the type include workspace
* Workspace permission check is totally different from object permission check.
* @param type
* @returns
*/
private isRelatedToWorkspace(type: string | string[]): boolean {
return type === WORKSPACE_TYPE || (Array.isArray(type) && type.includes(WORKSPACE_TYPE));
}
}

function getBulkOperationError(error: { type: string; reason?: string }, type: string, id: string) {
Expand Down
28 changes: 27 additions & 1 deletion src/core/server/saved_objects/service/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
*/

import { SavedObjectsFindOptions } from '../../types';
import { SavedObjectsFindResponse } from '..';
import { SavedObjectsFindResponse, SavedObjectsRepository } from '..';
import { Principals } from '../../permission_control/acl';
import { SavedObjectsPermissionModes } from '../../permission_control/client';
import { WORKSPACE_TYPE } from '../../../index';

export const DEFAULT_NAMESPACE_STRING = 'default';
export const ALL_NAMESPACES_STRING = '*';
Expand Down Expand Up @@ -87,4 +90,27 @@ export class SavedObjectsUtils {
): string[] {
return targetWorkspaces?.filter((item) => !baseWorkspaces?.includes(item)) || [];
}

public static async getPermittedWorkspaceIds(props: {
principals: Principals;
repository: SavedObjectsRepository;
permissionModes: SavedObjectsPermissionModes;
}) {
const { principals, repository, permissionModes } = props;
const queryDSL = await repository.getPermissionQuery({
permissionTypes: permissionModes,
principals,
savedObjectType: [WORKSPACE_TYPE],
});
try {
const result = await repository?.find({
type: [WORKSPACE_TYPE],
queryDSL,
perPage: 999,
});
return result?.saved_objects.map((item) => item.id);
} catch (e) {
return [];
}
}
}
24 changes: 22 additions & 2 deletions src/core/server/saved_objects/service/saved_objects_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
* under the License.
*/

import { Permissions } from '../permission_control/acl';
import { ISavedObjectsRepository } from './lib';
import { Permissions, Principals } from '../permission_control/acl';
import { ISavedObjectsRepository, SavedObjectsRepository } from './lib';
import {
SavedObject,
SavedObjectError,
Expand Down Expand Up @@ -465,6 +465,26 @@ export class SavedObjectsClient {
return await this._repository.addToWorkspaces(objects, workspaces, options);
};

/**
* Different DB may have different query DSL for given params
*/
getPermissionQuery = async (
props: Parameters<SavedObjectsRepository['getPermissionQuery']>[0]
) => {
return await this._repository.getPermissionQuery(props);
};

/**
* Different DB may have different query to find granted objects,
* provide a placeholder here for other query implementation
*/
processFindOptions = async (props: {
options: SavedObjectsFindOptions;
principals: Principals;
}): Promise<SavedObjectsFindOptions> => {
return await this._repository.processFindOptions(props);
};

/**
* Bulk Updates multiple SavedObject at once
*
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/workspace/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ export function plugin(initializerContext: PluginInitializerContext) {
export const config: PluginConfigDescriptor = {
schema: configSchema,
};

export { WorkspaceFindOptions } from './types';
Loading

0 comments on commit a23b39c

Please sign in to comment.