Skip to content

Commit

Permalink
Permission control service for saved objects (#63)
Browse files Browse the repository at this point in the history
* feat: move permission control to saved objects directory

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

* feat: use bulkGetObjects and fix unit test

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

* feat: add http routes for validate & list

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

* feat: move permissionModes to common place

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

* feat: rename routes

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

* feat: some side effects

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

* feat: some side effects

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

---------

Signed-off-by: SuZhou-Joe <suzhou@amazon.com>
  • Loading branch information
SuZhou-Joe committed Aug 31, 2023
1 parent c33f737 commit 7c9fb61
Show file tree
Hide file tree
Showing 16 changed files with 320 additions and 63 deletions.
2 changes: 2 additions & 0 deletions src/core/server/legacy/legacy_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@ export class LegacyService implements CoreService {
addClientWrapper: setupDeps.core.savedObjects.addClientWrapper,
registerType: setupDeps.core.savedObjects.registerType,
getImportExportObjectLimit: setupDeps.core.savedObjects.getImportExportObjectLimit,
setRepositoryFactoryProvider: setupDeps.core.savedObjects.setRepositoryFactoryProvider,
permissionControl: setupDeps.core.savedObjects.permissionControl,
},
status: {
isStatusPageAnonymous: setupDeps.core.status.isStatusPageAnonymous,
Expand Down
2 changes: 2 additions & 0 deletions src/core/server/plugins/plugin_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
addClientWrapper: deps.savedObjects.addClientWrapper,
registerType: deps.savedObjects.registerType,
getImportExportObjectLimit: deps.savedObjects.getImportExportObjectLimit,
setRepositoryFactoryProvider: deps.savedObjects.setRepositoryFactoryProvider,
permissionControl: deps.savedObjects.permissionControl,
},
status: {
core$: deps.status.core$,
Expand Down
15 changes: 15 additions & 0 deletions src/core/server/saved_objects/permission_control/client.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { SavedObjectsPermissionControlContract } from './client';

export const savedObjectsPermissionControlMock: SavedObjectsPermissionControlContract = {
setup: jest.fn(),
validate: jest.fn(),
addPrinciplesToObjects: jest.fn(),
removePrinciplesFromObjects: jest.fn(),
getPrinciplesOfObjects: jest.fn(),
getPermittedWorkspaceIds: jest.fn(),
};
83 changes: 83 additions & 0 deletions src/core/server/saved_objects/permission_control/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { OpenSearchDashboardsRequest } from '../../http';
import { SavedObjectsServiceStart } from '../saved_objects_service';
import { SavedObjectsBulkGetObject } from '../service';

export type SavedObjectsPermissionControlContract = Pick<
SavedObjectsPermissionControl,
keyof SavedObjectsPermissionControl
>;

export type SavedObjectsPermissionModes = string[];

export class SavedObjectsPermissionControl {
private getScopedClient?: SavedObjectsServiceStart['getScopedClient'];
private getScopedSavedObjectsClient(request: OpenSearchDashboardsRequest) {
return this.getScopedClient?.(request);
}
private async bulkGetSavedObjects(
request: OpenSearchDashboardsRequest,
savedObjects: SavedObjectsBulkGetObject[]
) {
return (
(await this.getScopedSavedObjectsClient(request)?.bulkGet(savedObjects))?.saved_objects || []
);
}
public async setup(getScopedClient: SavedObjectsServiceStart['getScopedClient']) {
this.getScopedClient = getScopedClient;
}
public async validate(
request: OpenSearchDashboardsRequest,
savedObject: SavedObjectsBulkGetObject,
permissionModeOrModes: SavedObjectsPermissionModes
) {
const savedObjectsGet = await this.bulkGetSavedObjects(request, [savedObject]);
if (savedObjectsGet) {
return {
success: true,
result: true,
};
}

return {
success: true,
result: false,
};
}

public async addPrinciplesToObjects(
request: OpenSearchDashboardsRequest,
savedObjects: SavedObjectsBulkGetObject[],
personas: string[],
permissionModeOrModes: SavedObjectsPermissionModes
): Promise<boolean> {
return true;
}

public async removePrinciplesFromObjects(
request: OpenSearchDashboardsRequest,
savedObjects: SavedObjectsBulkGetObject[],
personas: string[],
permissionModeOrModes: SavedObjectsPermissionModes
): Promise<boolean> {
return true;
}

public async getPrinciplesOfObjects(
request: OpenSearchDashboardsRequest,
savedObjects: SavedObjectsBulkGetObject[]
): Promise<Record<string, unknown>> {
return {};
}

public async getPermittedWorkspaceIds(
request: OpenSearchDashboardsRequest,
permissionModeOrModes: SavedObjectsPermissionModes
) {
return [];
}
}
20 changes: 20 additions & 0 deletions src/core/server/saved_objects/permission_control/routes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { InternalHttpServiceSetup } from '../../../http';
import { SavedObjectsPermissionControlContract } from '../client';
import { registerValidateRoute } from './validate';

export function registerPermissionCheckRoutes({
http,
permissionControl,
}: {
http: InternalHttpServiceSetup;
permissionControl: SavedObjectsPermissionControlContract;
}) {
const router = http.createRouter('/api/saved_objects_permission_control/');

registerValidateRoute(router, permissionControl);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { schema } from '@osd/config-schema';
import { IRouter } from '../../../http';
import { SavedObjectsPermissionControlContract } from '../client';

export const registerListRoute = (
router: IRouter,
permissionControl: SavedObjectsPermissionControlContract
) => {
router.post(
{
path: '/principles',
validate: {
body: schema.object({
objects: schema.arrayOf(
schema.object({
type: schema.string(),
id: schema.string(),
})
),
}),
},
},
router.handleLegacyErrors(async (context, req, res) => {
const result = await permissionControl.getPrinciplesOfObjects(req, req.body.objects);
return res.ok({ body: result });
})
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { schema } from '@osd/config-schema';
import { IRouter } from '../../../http';
import { SavedObjectsPermissionControlContract } from '../client';

export const registerValidateRoute = (
router: IRouter,
permissionControl: SavedObjectsPermissionControlContract
) => {
router.post(
{
path: '/validate/{type}/{id}',
validate: {
params: schema.object({
type: schema.string(),
id: schema.string(),
}),
body: schema.object({
permissionModes: schema.arrayOf(schema.string()),
}),
},
},
router.handleLegacyErrors(async (context, req, res) => {
const { type, id } = req.params;
const result = await permissionControl.validate(
req,
{
type,
id,
},
req.body.permissionModes
);
return res.ok({ body: result });
})
);
};
3 changes: 3 additions & 0 deletions src/core/server/saved_objects/saved_objects_service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { typeRegistryMock } from './saved_objects_type_registry.mock';
import { migrationMocks } from './migrations/mocks';
import { ServiceStatusLevels } from '../status';
import { ISavedObjectTypeRegistry } from './saved_objects_type_registry';
import { savedObjectsPermissionControlMock } from './permission_control/client.mock';

type SavedObjectsServiceContract = PublicMethodsOf<SavedObjectsService>;

Expand Down Expand Up @@ -79,6 +80,8 @@ const createSetupContractMock = () => {
addClientWrapper: jest.fn(),
registerType: jest.fn(),
getImportExportObjectLimit: jest.fn(),
setRepositoryFactoryProvider: jest.fn(),
permissionControl: savedObjectsPermissionControlMock,
};

setupContract.getImportExportObjectLimit.mockReturnValue(100);
Expand Down
41 changes: 40 additions & 1 deletion src/core/server/saved_objects/saved_objects_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { ISavedObjectsRepository, SavedObjectsRepository } from './service/lib/r
import {
SavedObjectsClientFactoryProvider,
SavedObjectsClientWrapperFactory,
SavedObjectRepositoryFactoryProvider,
} from './service/lib/scoped_client_provider';
import { Logger } from '../logging';
import { SavedObjectTypeRegistry, ISavedObjectTypeRegistry } from './saved_objects_type_registry';
Expand All @@ -64,6 +65,11 @@ import { registerRoutes } from './routes';
import { ServiceStatus } from '../status';
import { calculateStatus$ } from './status';
import { createMigrationOpenSearchClient } from './migrations/core/';
import {
SavedObjectsPermissionControl,
SavedObjectsPermissionControlContract,
} from './permission_control/client';
import { registerPermissionCheckRoutes } from './permission_control/routes';
/**
* Saved Objects is OpenSearchDashboards's data persistence mechanism allowing plugins to
* use OpenSearch for storing and querying state. The SavedObjectsServiceSetup API exposes methods
Expand Down Expand Up @@ -166,6 +172,16 @@ export interface SavedObjectsServiceSetup {
* Returns the maximum number of objects allowed for import or export operations.
*/
getImportExportObjectLimit: () => number;

/**
* Set the default {@link SavedObjectRepositoryFactoryProvider | factory provider} for creating Saved Objects repository.
* Only one repository can be set, subsequent calls to this method will fail.
*/
setRepositoryFactoryProvider: (
respositoryFactoryProvider: SavedObjectRepositoryFactoryProvider
) => void;

permissionControl: SavedObjectsPermissionControlContract;
}

/**
Expand Down Expand Up @@ -291,6 +307,9 @@ export class SavedObjectsService
private typeRegistry = new SavedObjectTypeRegistry();
private started = false;

private respositoryFactoryProvider?: SavedObjectRepositoryFactoryProvider;
private permissionControl?: SavedObjectsPermissionControlContract;

constructor(private readonly coreContext: CoreContext) {
this.logger = coreContext.logger.get('savedobjects-service');
}
Expand All @@ -317,6 +336,13 @@ export class SavedObjectsService
migratorPromise: this.migrator$.pipe(first()).toPromise(),
});

this.permissionControl = new SavedObjectsPermissionControl();

registerPermissionCheckRoutes({
http: setupDeps.http,
permissionControl: this.permissionControl,
});

return {
status$: calculateStatus$(
this.migrator$.pipe(switchMap((migrator) => migrator.getStatus$())),
Expand Down Expand Up @@ -348,6 +374,16 @@ export class SavedObjectsService
this.typeRegistry.registerType(type);
},
getImportExportObjectLimit: () => this.config!.maxImportExportSize,
setRepositoryFactoryProvider: (repositoryProvider) => {
if (this.started) {
throw new Error('cannot call `setRepositoryFactoryProvider` after service startup.');
}
if (this.respositoryFactoryProvider) {
throw new Error('custom repository factory is already set, and can only be set once');
}
this.respositoryFactoryProvider = repositoryProvider;
},
permissionControl: this.permissionControl,
};
}

Expand Down Expand Up @@ -455,8 +491,11 @@ export class SavedObjectsService

this.started = true;

const getScopedClient = clientProvider.getClient.bind(clientProvider);
this.permissionControl?.setup(getScopedClient);

return {
getScopedClient: clientProvider.getClient.bind(clientProvider),
getScopedClient,
createScopedRepository: repositoryFactory.createScopedRepository,
createInternalRepository: repositoryFactory.createInternalRepository,
createSerializer: () => new SavedObjectsSerializer(this.typeRegistry),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,31 @@
import { PriorityCollection } from './priority_collection';
import { SavedObjectsClientContract } from '../../types';
import { SavedObjectsRepositoryFactory } from '../../saved_objects_service';
import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import {
ISavedObjectTypeRegistry,
SavedObjectTypeRegistry,
} from '../../saved_objects_type_registry';
import { OpenSearchDashboardsRequest } from '../../../http';
import { ISavedObjectsRepository } from './repository';
import { IOpenSearchDashboardsMigrator } from '../../migrations';

/**
* Options passed to each SavedObjectRepositoryFactoryProvider to aid in creating the repository instance.
* @public
*/
export interface SavedObjectsRepositoryOptions {
migrator: IOpenSearchDashboardsMigrator;
typeRegistry: SavedObjectTypeRegistry;
includedHiddenTypes: string[];
}

/**
* Provider to invoke to a factory function for creating ISavedObjectRepository {@link ISavedObjectRepository} instances.
* @public
*/
export type SavedObjectRepositoryFactoryProvider = (
options: SavedObjectsRepositoryOptions
) => ISavedObjectsRepository;

/**
* Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance.
Expand Down
1 change: 0 additions & 1 deletion src/core/server/workspaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,5 @@ export {

export { WorkspaceAttribute, WorkspaceFindOptions } from './types';

export { WorkspacePermissionControl } from './workspace_permission_control';
export { workspacesValidator, formatWorkspaces } from './utils';
export { WORKSPACE_TYPE } from './constants';
Loading

0 comments on commit 7c9fb61

Please sign in to comment.