mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[ResponseOps] Granular Connector RBAC - adding API key to event log (#204114)
Part of https://github.com/elastic/kibana/issues/180908 ## Summary This change is part of adding granular RBAC for SecuritySolution connectors. In this PR, I updated the action executor to log API key details when a connector is executed by a user authenticated via API key. The public name and id of the API key are now included in the event log. ### Checklist Check the PR satisfies following conditions. - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### To verify 1. Create an API key 2. Create a connector that will successfully run, it doesn't have to be SentinelOne. 3. Run the following with the ID and correct params for your connector type. ``` curl -X POST "http://localhost:5601/api/actions/connector/$CONNECTOR_ID/_execute" -H 'Authorization: ApiKey $API_KEY' -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d' { "params": { "message": "hi" } }' ``` 4. Go to dev tools and run the following query to verify that the API key information is stored in the event log ``` GET /.kibana-event-log*/_search { "sort": [ { "@timestamp": { "order": "desc" } } ], "query": { "bool": { "filter": [ { "term": { "event.provider": { "value": "actions" } } } ] } } ```
This commit is contained in:
parent
3b61e7bea7
commit
1ba2716c7b
7 changed files with 204 additions and 12 deletions
|
@ -25,6 +25,21 @@ export interface UserRealm {
|
|||
type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the metadata of an API key.
|
||||
*/
|
||||
export interface ApiKeyDescriptor {
|
||||
/**
|
||||
* Name of the API key.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The ID of the API key.
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the currently authenticated user.
|
||||
*/
|
||||
|
@ -65,4 +80,9 @@ export interface AuthenticatedUser extends User {
|
|||
* Indicates whether user is an operator.
|
||||
*/
|
||||
operator?: boolean;
|
||||
|
||||
/**
|
||||
* Metadata of the API key that was used to authenticate the user.
|
||||
*/
|
||||
api_key?: ApiKeyDescriptor;
|
||||
}
|
||||
|
|
|
@ -229,6 +229,18 @@ const getBaseExecuteEventLogDoc = (
|
|||
};
|
||||
|
||||
const mockGetRequestBodyByte = jest.spyOn(ConnectorUsageCollector.prototype, 'getRequestBodyByte');
|
||||
const mockRealm = { name: 'default_native', type: 'native' };
|
||||
const mockUser = {
|
||||
authentication_realm: mockRealm,
|
||||
authentication_provider: mockRealm,
|
||||
authentication_type: 'realm',
|
||||
lookup_realm: mockRealm,
|
||||
elastic_cloud_user: true,
|
||||
enabled: true,
|
||||
profile_uid: '123',
|
||||
roles: ['superuser'],
|
||||
username: 'coolguy',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
@ -236,18 +248,7 @@ beforeEach(() => {
|
|||
mockGetRequestBodyByte.mockReturnValue(0);
|
||||
spacesMock.getSpaceId.mockReturnValue('some-namespace');
|
||||
loggerMock.get.mockImplementation(() => loggerMock);
|
||||
const mockRealm = { name: 'default_native', type: 'native' };
|
||||
securityMockStart.authc.getCurrentUser.mockImplementation(() => ({
|
||||
authentication_realm: mockRealm,
|
||||
authentication_provider: mockRealm,
|
||||
authentication_type: 'realm',
|
||||
lookup_realm: mockRealm,
|
||||
elastic_cloud_user: true,
|
||||
enabled: true,
|
||||
profile_uid: '123',
|
||||
roles: ['superuser'],
|
||||
username: 'coolguy',
|
||||
}));
|
||||
securityMockStart.authc.getCurrentUser.mockImplementation(() => mockUser);
|
||||
|
||||
getActionsAuthorizationWithRequest.mockReturnValue(authorizationMock);
|
||||
});
|
||||
|
@ -1563,6 +1564,72 @@ describe('Event log', () => {
|
|||
message: 'action started: test:1: action-1',
|
||||
});
|
||||
});
|
||||
|
||||
test('writes to the api key to the event log', async () => {
|
||||
securityMockStart.authc.getCurrentUser.mockImplementationOnce(() => ({
|
||||
...mockUser,
|
||||
authentication_type: 'api_key',
|
||||
api_key: {
|
||||
id: '456',
|
||||
name: 'test api key',
|
||||
},
|
||||
}));
|
||||
|
||||
const executorMock = setupActionExecutorMock();
|
||||
executorMock.mockResolvedValue({
|
||||
actionId: '1',
|
||||
status: 'ok',
|
||||
});
|
||||
await actionExecutor.execute(executeParams);
|
||||
expect(eventLogger.logEvent).toHaveBeenCalledTimes(2);
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, {
|
||||
event: {
|
||||
action: 'execute',
|
||||
kind: 'action',
|
||||
outcome: 'success',
|
||||
},
|
||||
kibana: {
|
||||
action: {
|
||||
execution: {
|
||||
usage: {
|
||||
request_body_bytes: 0,
|
||||
},
|
||||
uuid: '2',
|
||||
},
|
||||
id: '1',
|
||||
name: 'action-1',
|
||||
type_id: 'test',
|
||||
},
|
||||
alert: {
|
||||
rule: {
|
||||
execution: {
|
||||
uuid: '123abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
user_api_key: {
|
||||
id: '456',
|
||||
name: 'test api key',
|
||||
},
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
namespace: 'some-namespace',
|
||||
rel: 'primary',
|
||||
type: 'action',
|
||||
type_id: 'test',
|
||||
},
|
||||
],
|
||||
space_ids: ['some-namespace'],
|
||||
},
|
||||
message: 'action executed: test:1: action-1',
|
||||
user: {
|
||||
id: '123',
|
||||
name: 'coolguy',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const mockGenAi = {
|
||||
id: 'chatcmpl-7LztF5xsJl2z5jcNpJKvaPm4uWt8x',
|
||||
object: 'chat.completion',
|
||||
|
|
|
@ -552,6 +552,7 @@ export class ActionExecutor {
|
|||
event.user = event.user || {};
|
||||
event.user.name = currentUser?.username;
|
||||
event.user.id = currentUser?.profile_uid;
|
||||
event.kibana!.user_api_key = currentUser?.api_key;
|
||||
set(
|
||||
event,
|
||||
'kibana.action.execution.usage.request_body_bytes',
|
||||
|
|
|
@ -523,6 +523,16 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_api_key": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"name": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -237,6 +237,12 @@ export const EventSchema = schema.maybe(
|
|||
),
|
||||
})
|
||||
),
|
||||
user_api_key: schema.maybe(
|
||||
schema.object({
|
||||
id: ecsString(),
|
||||
name: ecsString(),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
|
|
@ -299,6 +299,16 @@ exports.EcsCustomPropertyMappings = {
|
|||
},
|
||||
},
|
||||
},
|
||||
user_api_key: {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -573,6 +573,84 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should log api key information from execute request', async () => {
|
||||
const { body: createdApiKey } = await supertest
|
||||
.post(`/internal/security/api_key`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({ name: 'test user managed key' })
|
||||
.expect(200);
|
||||
const apiKey = createdApiKey.encoded;
|
||||
|
||||
const connectorTypeId = 'test.index-record';
|
||||
const { body: createdConnector } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'My Connector',
|
||||
connector_type_id: connectorTypeId,
|
||||
config: {
|
||||
unencrypted: `This value shouldn't get encrypted`,
|
||||
},
|
||||
secrets: {
|
||||
encrypted: 'This value should be encrypted',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
objectRemover.add(space.id, createdConnector.id, 'connector', 'actions');
|
||||
|
||||
const reference = `actions-execute-1:${user.username}`;
|
||||
const response = await supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(space.id)}/api/actions/connector/${createdConnector.id}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set('Authorization', `ApiKey ${apiKey}`)
|
||||
.send({
|
||||
params: {
|
||||
reference,
|
||||
index: ES_TEST_INDEX_NAME,
|
||||
message: 'Testing 123',
|
||||
},
|
||||
});
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all_alerts_none_actions at space1':
|
||||
case 'space_1_all at space2':
|
||||
case 'global_read at space1':
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
case 'system_actions at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body).to.be.an('object');
|
||||
const searchResult = await esTestIndexTool.search(
|
||||
'action:test.index-record',
|
||||
reference
|
||||
);
|
||||
// @ts-expect-error doesnt handle total: number
|
||||
expect(searchResult.body.hits.total.value > 0).to.be(true);
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: space.id,
|
||||
type: 'action',
|
||||
id: createdConnector.id,
|
||||
provider: 'actions',
|
||||
actions: new Map([
|
||||
['execute-start', { equal: 1 }],
|
||||
['execute', { equal: 1 }],
|
||||
]),
|
||||
});
|
||||
});
|
||||
const executeEvent = events[1];
|
||||
expect(executeEvent?.kibana?.user_api_key?.id).to.eql(createdApiKey.id);
|
||||
expect(executeEvent?.kibana?.user_api_key?.name).to.eql(createdApiKey.name);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue