forked from opensearch-project/OpenSearch-Dashboards
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[workspace]feat: Add ACL auditor (opensearch-project#8557)
* feat: enable acl auditor Signed-off-by: SuZhou-Joe <[email protected]> * Changeset file for PR opensearch-project#8557 created/updated * feat: optmize code Signed-off-by: SuZhou-Joe <[email protected]> * feat: optimize code Signed-off-by: SuZhou-Joe <[email protected]> * fix: update workspace metadata is giving error log Signed-off-by: SuZhou-Joe <[email protected]> * feat: update Signed-off-by: SuZhou-Joe <[email protected]> * feat: refactor clientCallAuditor Signed-off-by: SuZhou-Joe <[email protected]> * feat: add unit test Signed-off-by: SuZhou-Joe <[email protected]> * feat: update Signed-off-by: SuZhou-Joe <[email protected]> * feat: optimize code and wording Signed-off-by: SuZhou-Joe <[email protected]> * feat: optimize code and wording Signed-off-by: SuZhou-Joe <[email protected]> * feat: add comments Signed-off-by: SuZhou-Joe <[email protected]> * feat: update Signed-off-by: SuZhou-Joe <[email protected]> * feat: optimize comment Signed-off-by: SuZhou-Joe <[email protected]> * fix: type error in workspace_saved_objects_client_wrapper.test.ts Signed-off-by: SuZhou-Joe <[email protected]> * feat: update Signed-off-by: SuZhou-Joe <[email protected]> * feat: add unit test Signed-off-by: SuZhou-Joe <[email protected]> --------- Signed-off-by: SuZhou-Joe <[email protected]> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
- Loading branch information
Showing
15 changed files
with
869 additions
and
123 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
feat: | ||
- Add ACL auditor ([#8557](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8557)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { loggerMock } from '../logging/logger.mock'; | ||
import { httpServerMock } from '../mocks'; | ||
import { | ||
initializeACLAuditor, | ||
getACLAuditor, | ||
cleanUpACLAuditor, | ||
ACLAuditorStateKey, | ||
} from './acl_auditor'; | ||
|
||
describe('#getACLAuditor', () => { | ||
it('should be able to get ACL auditor if request initialized', () => { | ||
const mockRequest = httpServerMock.createOpenSearchDashboardsRequest(); | ||
const uninilizedMockRequest = httpServerMock.createOpenSearchDashboardsRequest(); | ||
const mockedLogger = loggerMock.create(); | ||
initializeACLAuditor(mockRequest, mockedLogger); | ||
expect(getACLAuditor(mockRequest)).not.toBeFalsy(); | ||
expect(getACLAuditor(uninilizedMockRequest)).toBeFalsy(); | ||
}); | ||
}); | ||
|
||
describe('#cleanUpACLAuditor', () => { | ||
it('should be able to destroy the auditor', () => { | ||
const mockRequest = httpServerMock.createOpenSearchDashboardsRequest(); | ||
const mockedLogger = loggerMock.create(); | ||
initializeACLAuditor(mockRequest, mockedLogger); | ||
expect(getACLAuditor(mockRequest)).not.toBeFalsy(); | ||
cleanUpACLAuditor(mockRequest); | ||
expect(getACLAuditor(mockRequest)).toBeFalsy(); | ||
}); | ||
}); | ||
|
||
describe('#ACLAuditor', () => { | ||
it('should log error when auditor value is not correct', () => { | ||
const mockRequest = httpServerMock.createOpenSearchDashboardsRequest(); | ||
const mockedLogger = loggerMock.create(); | ||
initializeACLAuditor(mockRequest, mockedLogger); | ||
const ACLAuditorInstance = getACLAuditor(mockRequest); | ||
ACLAuditorInstance?.increment(ACLAuditorStateKey.DATABASE_OPERATION, 1); | ||
ACLAuditorInstance?.checkout(); | ||
expect( | ||
mockedLogger.error.mock.calls[0][0].toString().startsWith('[ACLCounterCheckoutFailed]') | ||
).toEqual(true); | ||
ACLAuditorInstance?.checkout(); | ||
expect(mockedLogger.error).toBeCalledTimes(1); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { OpenSearchDashboardsRequest, ensureRawRequest } from '../http/router'; | ||
import { Logger } from '../logging'; | ||
|
||
const ACLAuditorKey = Symbol('ACLAuditor'); | ||
|
||
export const ACLAuditorStateKey = { | ||
VALIDATE_SUCCESS: 'validateSuccess', | ||
VALIDATE_FAILURE: 'validateFailure', | ||
DATABASE_OPERATION: 'databaseOperation', | ||
} as const; | ||
|
||
const defaultState = { | ||
[ACLAuditorStateKey.VALIDATE_SUCCESS]: 0, | ||
[ACLAuditorStateKey.VALIDATE_FAILURE]: 0, | ||
[ACLAuditorStateKey.DATABASE_OPERATION]: 0, | ||
}; | ||
|
||
type ValueOf<T> = T[keyof T]; | ||
|
||
class ACLAuditor { | ||
private state = { ...defaultState }; | ||
|
||
constructor(private logger: Logger) {} | ||
|
||
reset = () => { | ||
this.state = { ...defaultState }; | ||
}; | ||
|
||
increment = (key: ValueOf<typeof ACLAuditorStateKey>, count: number) => { | ||
if (typeof count !== 'number' || !this.state.hasOwnProperty(key)) { | ||
return; | ||
} | ||
|
||
this.state[key] = this.state[key] + count; | ||
}; | ||
|
||
checkout = (requestInfo?: string) => { | ||
/** | ||
* VALIDATE_FAILURE represents the count for unauthorized call to a specific objects | ||
* VALIDATE_SUCCESS represents the count for authorized call to a specific objects | ||
* DATABASE_OPERATION represents the count for operations call to the database. | ||
* | ||
* Normally the operations call to the database should always <= the AuthZ check(VALIDATE_FAILURE + VALIDATE_SUCCESS) | ||
* If DATABASE_OPERATION > AuthZ check, it means we have somewhere bypasses the AuthZ check and we should audit this bypass behavior. | ||
*/ | ||
if ( | ||
this.state[ACLAuditorStateKey.VALIDATE_FAILURE] + | ||
this.state[ACLAuditorStateKey.VALIDATE_SUCCESS] < | ||
this.state[ACLAuditorStateKey.DATABASE_OPERATION] | ||
) { | ||
this.logger.error( | ||
`[ACLCounterCheckoutFailed] counter state: ${JSON.stringify(this.state)}, ${ | ||
requestInfo ? `requestInfo: ${requestInfo}` : '' | ||
}` | ||
); | ||
} | ||
|
||
this.reset(); | ||
}; | ||
|
||
getState = () => this.state; | ||
} | ||
|
||
interface AppState { | ||
[ACLAuditorKey]?: ACLAuditor; | ||
} | ||
|
||
/** | ||
* This function will be used to initialize a new app state to the request | ||
* | ||
* @param request OpenSearchDashboardsRequest | ||
* @returns void | ||
*/ | ||
export const initializeACLAuditor = (request: OpenSearchDashboardsRequest, logger: Logger) => { | ||
const rawRequest = ensureRawRequest(request); | ||
const appState: AppState = rawRequest.app; | ||
const ACLCounterInstance = appState[ACLAuditorKey]; | ||
|
||
if (ACLCounterInstance) { | ||
return; | ||
} | ||
|
||
appState[ACLAuditorKey] = new ACLAuditor(logger); | ||
}; | ||
|
||
export const getACLAuditor = (request: OpenSearchDashboardsRequest): ACLAuditor | undefined => { | ||
return (ensureRawRequest(request).app as AppState)[ACLAuditorKey]; | ||
}; | ||
|
||
export const cleanUpACLAuditor = (request: OpenSearchDashboardsRequest) => { | ||
(ensureRawRequest(request).app as AppState)[ACLAuditorKey] = undefined; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { httpServerMock } from '../../../core/server/mocks'; | ||
import { | ||
initializeClientCallAuditor, | ||
getClientCallAuditor, | ||
CLIENT_CALL_AUDITOR_KEY, | ||
cleanUpClientCallAuditor, | ||
} from './client_call_auditor'; | ||
|
||
describe('#getClientCallAuditor', () => { | ||
it('should be able to get clientCallAuditor if request initialized', () => { | ||
const mockRequest = httpServerMock.createOpenSearchDashboardsRequest(); | ||
const uninilizedMockRequest = httpServerMock.createOpenSearchDashboardsRequest(); | ||
initializeClientCallAuditor(mockRequest); | ||
expect(getClientCallAuditor(mockRequest)).not.toBeFalsy(); | ||
expect(getClientCallAuditor(uninilizedMockRequest)).toBeFalsy(); | ||
}); | ||
}); | ||
|
||
describe('#cleanUpClientCallAuditor', () => { | ||
it('should be able to destroy the auditor', () => { | ||
const mockRequest = httpServerMock.createOpenSearchDashboardsRequest(); | ||
initializeClientCallAuditor(mockRequest); | ||
expect(getClientCallAuditor(mockRequest)).not.toBeFalsy(); | ||
cleanUpClientCallAuditor(mockRequest); | ||
expect(getClientCallAuditor(mockRequest)).toBeFalsy(); | ||
}); | ||
}); | ||
|
||
describe('#ClientCallAuditor', () => { | ||
it('should return false when auditor incoming not equal outgoing', () => { | ||
const mockRequest = httpServerMock.createOpenSearchDashboardsRequest(); | ||
initializeClientCallAuditor(mockRequest); | ||
const ACLAuditorInstance = getClientCallAuditor(mockRequest); | ||
ACLAuditorInstance?.increment(CLIENT_CALL_AUDITOR_KEY.incoming); | ||
expect(ACLAuditorInstance?.isAsyncClientCallsBalanced()).toEqual(false); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { OpenSearchDashboardsRequest, ensureRawRequest } from '../http/router'; | ||
|
||
const clientCallAuditorKey = Symbol('clientCallAuditor'); | ||
|
||
interface AppState { | ||
[clientCallAuditorKey]?: ClientCallAuditor; | ||
} | ||
|
||
export const CLIENT_CALL_AUDITOR_KEY = { | ||
incoming: 'incoming', | ||
outgoing: 'outgoing', | ||
} as const; | ||
|
||
/** | ||
* This class will be used to audit all the async calls to saved objects client. | ||
* For example, `/api/sample_data` will call savedObjectsClient.get() 3 times parallely and for ACL auditor, | ||
* it should only `checkout` when the incoming calls equal outgoing call. | ||
*/ | ||
class ClientCallAuditor { | ||
private state: { | ||
incoming?: number; | ||
outgoing?: number; | ||
} = {}; | ||
increment(key: keyof typeof CLIENT_CALL_AUDITOR_KEY) { | ||
this.state[key] = (this.state[key] || 0) + 1; | ||
} | ||
isAsyncClientCallsBalanced() { | ||
return this.state.incoming === this.state.outgoing; | ||
} | ||
} | ||
|
||
/** | ||
* This function will be used to initialize a new app state to the request | ||
* | ||
* @param request OpenSearchDashboardsRequest | ||
* @returns void | ||
*/ | ||
export const initializeClientCallAuditor = (request: OpenSearchDashboardsRequest) => { | ||
const rawRequest = ensureRawRequest(request); | ||
const appState: AppState = rawRequest.app; | ||
const clientCallAuditorInstance = appState[clientCallAuditorKey]; | ||
|
||
if (clientCallAuditorInstance) { | ||
return; | ||
} | ||
|
||
appState[clientCallAuditorKey] = new ClientCallAuditor(); | ||
}; | ||
|
||
export const getClientCallAuditor = ( | ||
request: OpenSearchDashboardsRequest | ||
): ClientCallAuditor | undefined => { | ||
return (ensureRawRequest(request).app as AppState)[clientCallAuditorKey]; | ||
}; | ||
|
||
export const cleanUpClientCallAuditor = (request: OpenSearchDashboardsRequest) => { | ||
(ensureRawRequest(request).app as AppState)[clientCallAuditorKey] = undefined; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.