Skip to content

Commit

Permalink
[Workspace]Add permission control logic for workspace (opensearch-pro…
Browse files Browse the repository at this point in the history
…ject#6052)

* Add permission control for workspace

Signed-off-by: Lin Wang <wonglam@amazon.com>

* Add changelog for permission control in workspace

Signed-off-by: Lin Wang <wonglam@amazon.com>

* Fix integration tests and remove no need type

Signed-off-by: Lin Wang <wonglam@amazon.com>

* Update permission enabled for workspace CRUD integration tests

Signed-off-by: Lin Wang <wonglam@amazon.com>

* Change back to config schema

Signed-off-by: Lin Wang <wonglam@amazon.com>

* feat: do not append workspaces field when no workspaces present (#6)

* feat: do not append workspaces field when no workspaces present

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

* feat: do not append workspaces field when no workspaces present

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

---------

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

* fix: authInfo destructure (#7)

* fix: authInfo destructure

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

* fix: unit test error

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

---------

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

* Fix permissions assign in attributes

Signed-off-by: Lin Wang <wonglam@amazon.com>

* Remove deleteByWorkspace since not exists

Signed-off-by: Lin Wang <wonglam@amazon.com>

* refactor: remove formatWorkspacePermissionModeToStringArray

Signed-off-by: Lin Wang <wonglam@amazon.com>

* Remove current not used code

Signed-off-by: Lin Wang <wonglam@amazon.com>

* Add missing unit tests for permission control

Signed-off-by: Lin Wang <wonglam@amazon.com>

* Update workspaces API test describe

Signed-off-by: Lin Wang <wonglam@amazon.com>

* Fix workspace CRUD API integration tests failed

Signed-off-by: Lin Wang <wonglam@amazon.com>

* Address PR comments

Signed-off-by: Lin Wang <wonglam@amazon.com>

* Store permissions when savedObjects.permissions.enabled

Signed-off-by: Lin Wang <wonglam@amazon.com>

* Add permission control for deleteByWorkspace

Signed-off-by: Lin Wang <wonglam@amazon.com>

* Update src/plugins/workspace/server/permission_control/client.ts

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

* Update src/plugins/workspace/server/permission_control/client.ts

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

* Refactor permissions field in workspace create and update API

Signed-off-by: Lin Wang <wonglam@amazon.com>

* Fix workspace CRUD API integration tests

Signed-off-by: Lin Wang <wonglam@amazon.com>

---------

Signed-off-by: Lin Wang <wonglam@amazon.com>
Signed-off-by: SuZhou-Joe <suzhou@amazon.com>
Co-authored-by: SuZhou-Joe <suzhou@amazon.com>
Signed-off-by: Lin Wang <wonglam@amazon.com>
  • Loading branch information
wanglam and SuZhou-Joe committed Apr 3, 2024
1 parent 55d9b82 commit 227a0d8
Show file tree
Hide file tree
Showing 18 changed files with 461 additions and 227 deletions.
3 changes: 1 addition & 2 deletions src/core/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,12 +321,11 @@ export {
exportSavedObjectsToStream,
importSavedObjectsFromStream,
resolveSavedObjectsImportErrors,
SavedObjectsDeleteByWorkspaceOptions,
ACL,
Principals,
TransformedPermission,
PrincipalType,
Permissions,
SavedObjectsDeleteByWorkspaceOptions,
} from './saved_objects';

export {
Expand Down
3 changes: 3 additions & 0 deletions src/core/server/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ export function pluginInitializerContextConfigMock<T>(config: T) {
path: { data: '/tmp' },
savedObjects: {
maxImportPayloadBytes: new ByteSizeValue(26214400),
permission: {
enabled: true,
},
},
};

Expand Down
7 changes: 6 additions & 1 deletion src/core/server/plugins/plugin_context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,12 @@ describe('createPluginInitializerContext', () => {
pingTimeout: duration(30, 's'),
},
path: { data: fromRoot('data') },
savedObjects: { maxImportPayloadBytes: new ByteSizeValue(26214400) },
savedObjects: {
maxImportPayloadBytes: new ByteSizeValue(26214400),
permission: {
enabled: false,
},
},
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/core/server/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ export const SharedGlobalConfigKeys = {
] as const,
opensearch: ['shardTimeout', 'requestTimeout', 'pingTimeout'] as const,
path: ['data'] as const,
savedObjects: ['maxImportPayloadBytes'] as const,
savedObjects: ['maxImportPayloadBytes', 'permission'] as const,
};

/**
Expand Down
8 changes: 1 addition & 7 deletions src/core/server/saved_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,4 @@ export {
export { savedObjectsConfig, savedObjectsMigrationConfig } from './saved_objects_config';
export { SavedObjectTypeRegistry, ISavedObjectTypeRegistry } from './saved_objects_type_registry';

export {
Permissions,
ACL,
Principals,
TransformedPermission,
PrincipalType,
} from './permission_control/acl';
export { Permissions, ACL, Principals, PrincipalType } from './permission_control/acl';
15 changes: 12 additions & 3 deletions src/core/server/saved_objects/service/lib/repository.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ describe('SavedObjectsRepository', () => {
});

const getMockGetResponse = (
{ type, id, references, namespace: objectNamespace, originId, workspaces, permissions },
{ type, id, references, namespace: objectNamespace, originId, permissions, workspaces },
namespace
) => {
const namespaceId = objectNamespace === 'default' ? undefined : objectNamespace ?? namespace;
Expand All @@ -181,9 +181,9 @@ describe('SavedObjectsRepository', () => {
_source: {
...(registry.isSingleNamespace(type) && { namespace: namespaceId }),
...(registry.isMultiNamespace(type) && { namespaces: [namespaceId ?? 'default'] }),
workspaces,
...(originId && { originId }),
...(permissions && { permissions }),
...(workspaces && { workspaces }),
type,
[type]: { title: 'Testing' },
references,
Expand Down Expand Up @@ -3169,7 +3169,7 @@ describe('SavedObjectsRepository', () => {
const namespace = 'foo-namespace';
const originId = 'some-origin-id';

const getSuccess = async (type, id, options, includeOriginId, permissions) => {
const getSuccess = async (type, id, options, includeOriginId, permissions, workspaces) => {
const response = getMockGetResponse(
{
type,
Expand All @@ -3178,6 +3178,7 @@ describe('SavedObjectsRepository', () => {
// operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response.
...(includeOriginId && { originId }),
...(permissions && { permissions }),
...(workspaces && { workspaces }),
},
options?.namespace
);
Expand Down Expand Up @@ -3343,6 +3344,14 @@ describe('SavedObjectsRepository', () => {
permissions: permissions,
});
});

it(`includes workspaces property if present`, async () => {
const workspaces = ['workspace-1'];
const result = await getSuccess(type, id, { namespace }, undefined, undefined, workspaces);
expect(result).toMatchObject({
workspaces: workspaces,
});
});
});
});

Expand Down
4 changes: 2 additions & 2 deletions src/core/server/saved_objects/service/lib/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1044,7 +1044,7 @@ export class SavedObjectsRepository {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}

const { originId, updated_at: updatedAt, workspaces, permissions } = body._source;
const { originId, updated_at: updatedAt, permissions, workspaces } = body._source;

let namespaces: string[] = [];
if (!this._registry.isNamespaceAgnostic(type)) {
Expand All @@ -1059,8 +1059,8 @@ export class SavedObjectsRepository {
namespaces,
...(originId && { originId }),
...(updatedAt && { updated_at: updatedAt }),
...(workspaces && { workspaces }),
...(permissions && { permissions }),
...(workspaces && { workspaces }),
version: encodeHitVersion(body),
attributes: body._source[type],
references: body._source.references || [],
Expand Down
177 changes: 107 additions & 70 deletions src/plugins/workspace/server/integration_tests/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@

import { WorkspaceAttribute } from 'src/core/types';
import * as osdTestServer from '../../../../core/test_helpers/osd_server';
import { WORKSPACE_TYPE } from '../../../../core/server';
import { WorkspacePermissionItem } from '../types';
import { WORKSPACE_TYPE, Permissions } from '../../../../core/server';

const omitId = <T extends { id?: string }>(object: T): Omit<T, 'id'> => {
const { id, ...others } = object;
Expand All @@ -19,7 +18,7 @@ const testWorkspace: WorkspaceAttribute = {
description: 'test_workspace_description',
};

describe('workspace service', () => {
describe('workspace service api integration test', () => {
let root: ReturnType<typeof osdTestServer.createRoot>;
let opensearchServer: osdTestServer.TestOpenSearchUtils;
let osd: osdTestServer.TestOpenSearchDashboardsUtils;
Expand All @@ -36,7 +35,7 @@ describe('workspace service', () => {
},
savedObjects: {
permission: {
enabled: true,
enabled: false,
},
},
migrations: { skip: false },
Expand Down Expand Up @@ -89,39 +88,6 @@ describe('workspace service', () => {
expect(result.body.success).toEqual(true);
expect(typeof result.body.result.id).toBe('string');
});
it('create with permissions', async () => {
await osdTestServer.request
.post(root, `/api/workspaces`)
.send({
attributes: omitId(testWorkspace),
permissions: [{ type: 'invalid-type', userId: 'foo', modes: ['read'] }],
})
.expect(400);

const result: any = await osdTestServer.request
.post(root, `/api/workspaces`)
.send({
attributes: omitId(testWorkspace),
permissions: [{ type: 'user', userId: 'foo', modes: ['read'] }],
})
.expect(200);

expect(result.body.success).toEqual(true);
expect(typeof result.body.result.id).toBe('string');
expect(
(
await osd.coreStart.savedObjects
.createInternalRepository([WORKSPACE_TYPE])
.get<{ permissions: WorkspacePermissionItem[] }>(WORKSPACE_TYPE, result.body.result.id)
).attributes.permissions
).toEqual([
{
modes: ['read'],
type: 'user',
userId: 'foo',
},
]);
});
it('get', async () => {
const result = await osdTestServer.request
.post(root, `/api/workspaces`)
Expand Down Expand Up @@ -162,39 +128,6 @@ describe('workspace service', () => {
expect(getResult.body.success).toEqual(true);
expect(getResult.body.result.name).toEqual('updated');
});
it('update with permissions', async () => {
const result: any = await osdTestServer.request
.post(root, `/api/workspaces`)
.send({
attributes: omitId(testWorkspace),
permissions: [{ type: 'user', userId: 'foo', modes: ['read'] }],
})
.expect(200);

await osdTestServer.request
.put(root, `/api/workspaces/${result.body.result.id}`)
.send({
attributes: {
...omitId(testWorkspace),
},
permissions: [{ type: 'user', userId: 'foo', modes: ['write'] }],
})
.expect(200);

expect(
(
await osd.coreStart.savedObjects
.createInternalRepository([WORKSPACE_TYPE])
.get<{ permissions: WorkspacePermissionItem[] }>(WORKSPACE_TYPE, result.body.result.id)
).attributes.permissions
).toEqual([
{
modes: ['write'],
type: 'user',
userId: 'foo',
},
]);
});
it('delete', async () => {
const result: any = await osdTestServer.request
.post(root, `/api/workspaces`)
Expand Down Expand Up @@ -339,3 +272,107 @@ describe('workspace service', () => {
});
});
});

describe('workspace service api integration test when savedObjects.permission.enabled equal true', () => {
let root: ReturnType<typeof osdTestServer.createRoot>;
let opensearchServer: osdTestServer.TestOpenSearchUtils;
let osd: osdTestServer.TestOpenSearchDashboardsUtils;
beforeAll(async () => {
const { startOpenSearch, startOpenSearchDashboards } = osdTestServer.createTestServers({
adjustTimeout: (t: number) => jest.setTimeout(t),
settings: {
osd: {
workspace: {
enabled: true,
},
savedObjects: {
permission: {
enabled: true,
},
},
migrations: { skip: false },
},
},
});
opensearchServer = await startOpenSearch();
osd = await startOpenSearchDashboards();
root = osd.root;
});
afterAll(async () => {
await root.shutdown();
await opensearchServer.stop();
});
describe('Workspace CRUD APIs', () => {
afterEach(async () => {
const listResult = await osdTestServer.request
.post(root, `/api/workspaces/_list`)
.send({
page: 1,
})
.expect(200);
const savedObjectsRepository = osd.coreStart.savedObjects.createInternalRepository([
WORKSPACE_TYPE,
]);
await Promise.all(
listResult.body.result.workspaces.map((item: WorkspaceAttribute) =>
// this will delete reserved workspace
savedObjectsRepository.delete(WORKSPACE_TYPE, item.id)
)
);
});
it('create', async () => {
await osdTestServer.request
.post(root, `/api/workspaces`)
.send({
attributes: omitId(testWorkspace),
permissions: { invalid_type: { users: ['foo'] } },
})
.expect(400);

const result: any = await osdTestServer.request
.post(root, `/api/workspaces`)
.send({
attributes: omitId(testWorkspace),
permissions: { read: { users: ['foo'] } },
})
.expect(200);

expect(result.body.success).toEqual(true);
expect(typeof result.body.result.id).toBe('string');
expect(
(
await osd.coreStart.savedObjects
.createInternalRepository([WORKSPACE_TYPE])
.get<{ permissions: Permissions }>(WORKSPACE_TYPE, result.body.result.id)
).permissions
).toEqual({ read: { users: ['foo'] } });
});
it('update', async () => {
const result: any = await osdTestServer.request
.post(root, `/api/workspaces`)
.send({
attributes: omitId(testWorkspace),
})
.expect(200);

const updateResult = await osdTestServer.request
.put(root, `/api/workspaces/${result.body.result.id}`)
.send({
attributes: {
...omitId(testWorkspace),
},
permissions: { write: { users: ['foo'] } },
})
.expect(200);
expect(updateResult.body.result).toBe(true);

expect(
(
await osd.coreStart.savedObjects
.createInternalRepository([WORKSPACE_TYPE])
.get<{ permissions: Permissions }>(WORKSPACE_TYPE, result.body.result.id)
).permissions
).toEqual({ write: { users: ['foo'] } });
});
});
});
Loading

0 comments on commit 227a0d8

Please sign in to comment.