Skip to content

Commit

Permalink
feat: add unit test (#2)
Browse files Browse the repository at this point in the history
Signed-off-by: SuZhou-Joe <suzhou@amazon.com>
  • Loading branch information
SuZhou-Joe authored and wanglam committed Oct 16, 2023
1 parent 075b441 commit d9b5890
Show file tree
Hide file tree
Showing 9 changed files with 383 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/core/server/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export {
RouteValidationResultFactory,
DestructiveRouteMethod,
SafeRouteMethod,
ensureRawRequest,
} from './router';
export { BasePathProxyServer } from './base_path_proxy_server';
export { OnPreRoutingHandler, OnPreRoutingToolkit } from './lifecycle/on_pre_routing';
Expand Down
5 changes: 5 additions & 0 deletions src/core/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export {
SessionStorageFactory,
DestructiveRouteMethod,
SafeRouteMethod,
ensureRawRequest,
} from './http';

export {
Expand Down Expand Up @@ -320,6 +321,10 @@ export {
importSavedObjectsFromStream,
resolveSavedObjectsImportErrors,
SavedObjectsDeleteByWorkspaceOptions,
ACL,
Principals,
TransformedPermission,
PrincipalType,
} from './saved_objects';

export {
Expand Down
8 changes: 8 additions & 0 deletions src/core/server/saved_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,11 @@ 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';
12 changes: 12 additions & 0 deletions src/plugins/workspace/server/permission_control/client.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/
import { SavedObjectsPermissionControlContract } from './client';

export const savedObjectsPermissionControlMock: SavedObjectsPermissionControlContract = {
validate: jest.fn(),
batchValidate: jest.fn(),
getPrincipalsOfObjects: jest.fn(),
setup: jest.fn(),
};
144 changes: 144 additions & 0 deletions src/plugins/workspace/server/permission_control/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { loggerMock } from '@osd/logging/target/mocks';
import { SavedObjectsPermissionControl } from './client';
import { httpServerMock, savedObjectsClientMock } from '../../../../core/server/mocks';

describe('workspace utils', () => {
it('should return principals when calling getPrincipalsOfObjects', async () => {
const permissionControlClient = new SavedObjectsPermissionControl(loggerMock.create());
const getScopedClient = jest.fn();
getScopedClient.mockImplementation((request) => {
const clientMock = savedObjectsClientMock.create();
clientMock.bulkGet.mockResolvedValue({
saved_objects: [
{
id: 'foo',
permissions: {
read: {
users: ['foo_user'],
},
},
},
],
});
return clientMock;
});
permissionControlClient.setup(getScopedClient);
const result = await permissionControlClient.getPrincipalsOfObjects(
httpServerMock.createOpenSearchDashboardsRequest(),
[]
);
expect(result).toEqual({
foo: [
{
type: 'users',
name: 'foo_user',
permissions: ['read'],
},
],
});
});

it('validate should return error when no saved objects can be found', async () => {
const permissionControlClient = new SavedObjectsPermissionControl(loggerMock.create());
const getScopedClient = jest.fn();
const clientMock = savedObjectsClientMock.create();
getScopedClient.mockImplementation((request) => {
return clientMock;
});
permissionControlClient.setup(getScopedClient);
clientMock.bulkGet.mockResolvedValue({
saved_objects: [],
});
const result = await permissionControlClient.validate(
httpServerMock.createOpenSearchDashboardsRequest(),
{ id: 'foo', type: 'dashboard' },
['read']
);
expect(result.success).toEqual(false);
});

it('validate should return error when bulkGet return error', async () => {
const permissionControlClient = new SavedObjectsPermissionControl(loggerMock.create());
const getScopedClient = jest.fn();
const clientMock = savedObjectsClientMock.create();
getScopedClient.mockImplementation((request) => {
return clientMock;
});
permissionControlClient.setup(getScopedClient);

clientMock.bulkGet.mockResolvedValue({
saved_objects: [
{
id: 'foo',
type: 'dashboard',
references: [],
attributes: {},
error: {
error: 'error_bar',
message: 'error_bar',
statusCode: 500,
},
},
],
});
const errorResult = await permissionControlClient.validate(
httpServerMock.createOpenSearchDashboardsRequest(),
{ id: 'foo', type: 'dashboard' },
['read']
);
expect(errorResult.success).toEqual(false);
expect(errorResult.error).toEqual('error_bar');
});

it('validate should return success normally', async () => {
const permissionControlClient = new SavedObjectsPermissionControl(loggerMock.create());
const getScopedClient = jest.fn();
const clientMock = savedObjectsClientMock.create();
getScopedClient.mockImplementation((request) => {
return clientMock;
});
permissionControlClient.setup(getScopedClient);

clientMock.bulkGet.mockResolvedValue({
saved_objects: [
{
id: 'foo',
type: 'dashboard',
references: [],
attributes: {},
},
{
id: 'bar',
type: 'dashboard',
references: [],
attributes: {},
permissions: {
read: {
users: ['bar'],
},
},
},
],
});
const batchValidateResult = await permissionControlClient.batchValidate(
httpServerMock.createOpenSearchDashboardsRequest({
auth: {
credentials: {
authInfo: {
user_name: 'bar',
},
},
} as any,
}),
[],
['read']
);
expect(batchValidateResult.success).toEqual(true);
expect(batchValidateResult.result).toEqual(true);
});
});
136 changes: 136 additions & 0 deletions src/plugins/workspace/server/permission_control/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { i18n } from '@osd/i18n';
import { OpenSearchDashboardsRequest } from '../../../../core/server';
import {
ACL,
TransformedPermission,
SavedObjectsBulkGetObject,
SavedObjectsServiceStart,
Logger,
} from '../../../../core/server';
import { WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID } from '../../common/constants';
import { getPrincipalsFromRequest } from '../utils';

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

export type SavedObjectsPermissionModes = string[];

export class SavedObjectsPermissionControl {
private readonly logger: Logger;
private _getScopedClient?: SavedObjectsServiceStart['getScopedClient'];
private getScopedClient(request: OpenSearchDashboardsRequest) {
return this._getScopedClient?.(request, {
excludedWrappers: [WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID],
});
}

constructor(logger: Logger) {
this.logger = logger;
}

private async bulkGetSavedObjects(
request: OpenSearchDashboardsRequest,
savedObjects: SavedObjectsBulkGetObject[]
) {
return (await this.getScopedClient?.(request)?.bulkGet(savedObjects))?.saved_objects || [];
}
public async setup(getScopedClient: SavedObjectsServiceStart['getScopedClient']) {
this._getScopedClient = getScopedClient;
}
public async validate(
request: OpenSearchDashboardsRequest,
savedObject: SavedObjectsBulkGetObject,
permissionModes: SavedObjectsPermissionModes
) {
return await this.batchValidate(request, [savedObject], permissionModes);
}

/**
* In batch validate case, the logic is a.withPermission && b.withPermission
* @param request
* @param savedObjects
* @param permissionModes
* @returns
*/
public async batchValidate(
request: OpenSearchDashboardsRequest,
savedObjects: SavedObjectsBulkGetObject[],
permissionModes: SavedObjectsPermissionModes
) {
const savedObjectsGet = await this.bulkGetSavedObjects(request, savedObjects);
if (!savedObjectsGet.length) {
return {
success: false,
error: i18n.translate('savedObjects.permission.notFound', {
defaultMessage: 'Can not find target saved objects.',
}),
};
}

if (savedObjectsGet.some((item) => item.error)) {
return {
success: false,
error: savedObjectsGet
.filter((item) => item.error)
.map((item) => item.error?.error)
.join('\n'),
};
}

const principals = getPrincipalsFromRequest(request);
let savedObjectsBasicInfo: any[] = [];
const hasAllPermission = savedObjectsGet.every((item) => {
// for object that doesn't contain ACL like config, return true
if (!item.permissions) {
return true;
}
const aclInstance = new ACL(item.permissions);
const hasPermission = aclInstance.hasPermission(permissionModes, principals);
if (!hasPermission) {
savedObjectsBasicInfo = [
...savedObjectsBasicInfo,
{
id: item.id,
type: item.type,
workspaces: item.workspaces,
permissions: item.permissions,
},
];
}
return hasPermission;
});
if (!hasAllPermission) {
this.logger.debug(
`Authorization failed, principals: ${JSON.stringify(
principals
)} has no [${permissionModes}] permissions on the requested saved object: ${JSON.stringify(
savedObjectsBasicInfo
)}`
);
}
return {
success: true,
result: hasAllPermission,
};
}

public async getPrincipalsOfObjects(
request: OpenSearchDashboardsRequest,
savedObjects: SavedObjectsBulkGetObject[]
): Promise<Record<string, TransformedPermission>> {
const detailedSavedObjects = await this.bulkGetSavedObjects(request, savedObjects);
return detailedSavedObjects.reduce((total, current) => {
return {
...total,
[current.id]: new ACL(current.permissions).toFlatList(),
};
}, {});
}
}
5 changes: 5 additions & 0 deletions src/plugins/workspace/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,8 @@ export type IResponse<T> =
success: false;
error?: string;
};

export interface AuthInfo {
backend_roles?: string[];
user_name?: string;
}
38 changes: 37 additions & 1 deletion src/plugins/workspace/server/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { generateRandomId } from './utils';
import { httpServerMock } from '../../../core/server/mocks';
import { generateRandomId, getPrincipalsFromRequest } from './utils';

describe('workspace utils', () => {
it('should generate id with the specified size', () => {
Expand All @@ -18,4 +19,39 @@ describe('workspace utils', () => {
}
expect(ids.size).toBe(NUM_OF_ID);
});

it('should return empty map when request do not have authentication', () => {
const mockRequest = httpServerMock.createOpenSearchDashboardsRequest();
const result = getPrincipalsFromRequest(mockRequest);
expect(result).toEqual({});
});

it('should return normally when request has authentication', () => {
const mockRequest = httpServerMock.createOpenSearchDashboardsRequest({
auth: {
credentials: {
authInfo: {
backend_roles: ['foo'],
user_name: 'bar',
},
},
} as any,
});
const result = getPrincipalsFromRequest(mockRequest);
expect(result.users).toEqual(['bar']);
expect(result.groups).toEqual(['foo']);
});

it('should return a fake user when there is auth field but no backend_roles or user name', () => {
const mockRequest = httpServerMock.createOpenSearchDashboardsRequest({
auth: {
credentials: {
authInfo: {},
},
} as any,
});
const result = getPrincipalsFromRequest(mockRequest);
expect(result.users?.[0].startsWith('_user_fake_')).toEqual(true);
expect(result.groups).toEqual(undefined);
});
});
Loading

0 comments on commit d9b5890

Please sign in to comment.