Skip to content

Commit

Permalink
Integration ACL check with saved objects. (opensearch-project#74)
Browse files Browse the repository at this point in the history
* feat: enable find with acl permission check

Signed-off-by: SuZhou-Joe <[email protected]>

* fix: bootstrap error

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: add public

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: enable name change

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: make test run

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: some optimization

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: some optimization on authentication part

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: optimize authentication

Signed-off-by: SuZhou-Joe <[email protected]>

* feat: update

Signed-off-by: SuZhou-Joe <[email protected]>

---------

Signed-off-by: SuZhou-Joe <[email protected]>
  • Loading branch information
SuZhou-Joe committed Aug 31, 2023
1 parent 1621644 commit d3e602e
Show file tree
Hide file tree
Showing 14 changed files with 192 additions and 94 deletions.
1 change: 1 addition & 0 deletions src/core/public/saved_objects/saved_objects_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ export class SavedObjectsClient {
namespaces: 'namespaces',
preference: 'preference',
workspaces: 'workspaces',
queryDSL: 'queryDSL',
};

const workspaces = [
Expand Down
4 changes: 2 additions & 2 deletions src/core/server/saved_objects/permission_control/acl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,13 @@ describe('SavedObjectTypeRegistry', () => {
expect(result?.length).toEqual(3);
});

it('test genereate query DSL', () => {
it('test generate query DSL', () => {
const principals = {
users: ['user1'],
groups: ['group1'],
};
const result = ACL.genereateGetPermittedSavedObjectsQueryDSL(
PermissionMode.Read,
[PermissionMode.Read],
principals,
'workspace'
);
Expand Down
48 changes: 21 additions & 27 deletions src/core/server/saved_objects/permission_control/acl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface Principals {
groups?: string[];
}

export type Permissions = Partial<Record<string, Principals>>;
export type Permissions = Record<string, Principals>;

export interface TransformedPermission {
type: string;
Expand Down Expand Up @@ -121,11 +121,14 @@ export class ACL {
}

for (const permissionType of permissionTypes) {
this.permissions[permissionType] = deleteFromPrincipals(
const result = deleteFromPrincipals(
this.permissions![permissionType],
principals.users,
principals.groups
);
if (result) {
this.permissions[permissionType] = result;
}
}

return this;
Expand Down Expand Up @@ -204,11 +207,11 @@ export class ACL {
generate query DSL by the specific conditions, used for fetching saved objects from the saved objects index
*/
public static genereateGetPermittedSavedObjectsQueryDSL(
permissionType: string,
permissionTypes: string[],
principals: Principals,
savedObjectType?: string | string[]
) {
if (!principals || !permissionType) {
if (!principals || !permissionTypes) {
return {
query: {
match_none: {},
Expand All @@ -222,30 +225,21 @@ export class ACL {
const subBool: any = {
should: [],
};
if (!!principals.users) {
subBool.should.push({
terms: {
['permissions.' + permissionType + '.users']: principals.users,
},
});
subBool.should.push({
term: {
['permissions.' + permissionType + '.users']: '*',
},
});
}
if (!!principals.groups) {
subBool.should.push({
terms: {
['permissions.' + permissionType + '.groups']: principals.groups,
},
});
subBool.should.push({
term: {
['permissions.' + permissionType + '.groups']: '*',
},

permissionTypes.forEach((permissionType) => {
Object.entries(principals).forEach(([principalType, principalsInCurrentType]) => {
subBool.should.push({
terms: {
['permissions.' + permissionType + `.${principalType}`]: principalsInCurrentType,
},
});
subBool.should.push({
term: {
['permissions.' + permissionType + `.${principalType}`]: '*',
},
});
});
}
});

bool.filter.push({
bool: subBool,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

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(),
getPrincipalsOfObjects: jest.fn(),
getPermittedWorkspaceIds: jest.fn(),
getPrincipalsFromRequest: jest.fn(),
};
129 changes: 91 additions & 38 deletions src/core/server/saved_objects/permission_control/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

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 { ACL, Principals, TransformedPermission } from './acl';
import { PrincipalType } from '../../../utils/constants';
import { WORKSPACE_TYPE } from '../../workspaces';

export type SavedObjectsPermissionControlContract = Pick<
SavedObjectsPermissionControl,
Expand All @@ -14,78 +18,127 @@ export type SavedObjectsPermissionControlContract = Pick<

export type SavedObjectsPermissionModes = string[];

export interface AuthInfo {
backend_roles?: string[];
user_name?: string;
}

export class SavedObjectsPermissionControl {
private getScopedClient?: SavedObjectsServiceStart['getScopedClient'];
private getScopedSavedObjectsClient(request: OpenSearchDashboardsRequest) {
return this.getScopedClient?.(request);
private createInternalRepository?: SavedObjectsServiceStart['createInternalRepository'];
private getInternalRepository() {
return this.createInternalRepository?.();
}
public getPrincipalsFromRequest(request: OpenSearchDashboardsRequest): Principals {
const rawRequest = ensureRawRequest(request);
const authInfo = rawRequest?.auth?.credentials?.authInfo as AuthInfo | null;
const payload: Principals = {};
if (!authInfo) {
/**
* Login user have access to all the workspaces when no authentication is presented.
* The logic will be used when users create workspaces with authentication enabled but turn off authentication for any reason.
*/
return payload;
}
if (!authInfo?.backend_roles?.length && !authInfo.user_name) {
/**
* It means OSD can not recognize who the user is even if authentication is enabled,
* use a fake user that won't be granted permission explicitly.
*/
payload[PrincipalType.Users] = [`_user_fake_${Date.now()}_`];
return payload;
}
if (authInfo?.backend_roles) {
payload[PrincipalType.Groups] = authInfo.backend_roles;
}
if (authInfo?.user_name) {
payload[PrincipalType.Users] = [authInfo.user_name];
}
return payload;
}
private async bulkGetSavedObjects(
request: OpenSearchDashboardsRequest,
savedObjects: SavedObjectsBulkGetObject[]
) {
return (
(await this.getScopedSavedObjectsClient(request)?.bulkGet(savedObjects))?.saved_objects || []
);
return (await this.getInternalRepository()?.bulkGet(savedObjects))?.saved_objects || [];
}
public async setup(getScopedClient: SavedObjectsServiceStart['getScopedClient']) {
this.getScopedClient = getScopedClient;
public async setup(
createInternalRepository: SavedObjectsServiceStart['createInternalRepository']
) {
this.createInternalRepository = createInternalRepository;
}
public async validate(
request: OpenSearchDashboardsRequest,
savedObject: SavedObjectsBulkGetObject,
permissionModeOrModes: SavedObjectsPermissionModes
permissionModes: SavedObjectsPermissionModes
) {
return await this.batchValidate(request, [savedObject], permissionModeOrModes);
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[],
permissionModeOrModes: SavedObjectsPermissionModes
permissionModes: SavedObjectsPermissionModes
) {
const savedObjectsGet = await this.bulkGetSavedObjects(request, savedObjects);
if (savedObjectsGet) {
const principals = this.getPrincipalsFromRequest(request);
const hasAllPermission = savedObjectsGet.every((item) => {
// item.permissions
const aclInstance = new ACL(item.permissions);
return aclInstance.hasPermission(permissionModes, principals);
});
return {
success: true,
result: true,
result: hasAllPermission,
};
}

return {
success: true,
result: false,
success: false,
error: i18n.translate('savedObjects.permission.notFound', {
defaultMessage: 'Can not find target saved objects.',
}),
};
}

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(
public async getPrincipalsOfObjects(
request: OpenSearchDashboardsRequest,
savedObjects: SavedObjectsBulkGetObject[]
): Promise<Record<string, unknown>> {
return {};
): Promise<Record<string, TransformedPermission>> {
const detailedSavedObjects = await this.bulkGetSavedObjects(request, savedObjects);
return detailedSavedObjects.reduce((total, current) => {
return {
...total,
[current.id]: new ACL(current.permissions).transformPermissions(),
};
}, {});
}

public async getPermittedWorkspaceIds(
request: OpenSearchDashboardsRequest,
permissionModeOrModes: SavedObjectsPermissionModes
permissionModes: SavedObjectsPermissionModes
) {
return [];
const principals = this.getPrincipalsFromRequest(request);
const queryDSL = ACL.genereateGetPermittedSavedObjectsQueryDSL(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 [];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const registerListRoute = (
) => {
router.post(
{
path: '/principles',
path: '/principals',
validate: {
body: schema.object({
objects: schema.arrayOf(
Expand All @@ -26,7 +26,7 @@ export const registerListRoute = (
},
},
router.handleLegacyErrors(async (context, req, res) => {
const result = await permissionControl.getPrinciplesOfObjects(req, req.body.objects);
const result = await permissionControl.getPrincipalsOfObjects(req, req.body.objects);
return res.ok({ body: result });
})
);
Expand Down
2 changes: 1 addition & 1 deletion src/core/server/saved_objects/saved_objects_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ export class SavedObjectsService
this.started = true;

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

return {
getScopedClient,
Expand Down
2 changes: 2 additions & 0 deletions src/core/server/saved_objects/serialization/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* under the License.
*/

import { Permissions } from '../permission_control/acl';
import { SavedObjectsMigrationVersion, SavedObjectReference } from '../types';

/**
Expand All @@ -53,6 +54,7 @@ export interface SavedObjectsRawDocSource {
references?: SavedObjectReference[];
originId?: string;
workspaces?: string[];
permissions?: Permissions;

[typeMapping: string]: any;
}
Expand Down
5 changes: 4 additions & 1 deletion src/core/server/saved_objects/service/lib/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,7 @@ export class SavedObjectsRepository {
filter,
preference,
workspaces,
queryDSL,
} = options;

if (!type && !typeToNamespacesMap) {
Expand Down Expand Up @@ -843,6 +844,7 @@ export class SavedObjectsRepository {
hasReference,
kueryNode,
workspaces,
queryDSL,
}),
},
};
Expand Down Expand Up @@ -1897,7 +1899,7 @@ function getSavedObjectFromSource<T>(
id: string,
doc: { _seq_no?: number; _primary_term?: number; _source: SavedObjectsRawDocSource }
): SavedObject<T> {
const { originId, updated_at: updatedAt, workspaces } = doc._source;
const { originId, updated_at: updatedAt, workspaces, permissions } = doc._source;

let namespaces: string[] = [];
if (!registry.isNamespaceAgnostic(type)) {
Expand All @@ -1917,6 +1919,7 @@ function getSavedObjectFromSource<T>(
attributes: doc._source[type],
references: doc._source.references || [],
migrationVersion: doc._source.migrationVersion,
permissions,
};
}

Expand Down
Loading

0 comments on commit d3e602e

Please sign in to comment.