Skip to content

Commit

Permalink
[8.x] [ResponseOps] Granular Connector RBAC - adding API key to event…
Browse files Browse the repository at this point in the history
… log (#204114) (#204996)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[ResponseOps] Granular Connector RBAC - adding API key to event log
(#204114)](#204114)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Alexi
Doak","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-12-19T18:30:15Z","message":"[ResponseOps]
Granular Connector RBAC - adding API key to event log (#204114)\n\nPart
of https://github.com/elastic/kibana/issues/180908\r\n\r\n##
Summary\r\n\r\nThis change is part of adding granular RBAC for
SecuritySolution\r\nconnectors. In this PR, I updated the action
executor to log API key\r\ndetails when a connector is executed by a
user authenticated via API\r\nkey. The public name and id of the API key
are now included in the event\r\nlog.\r\n\r\n### Checklist\r\n\r\nCheck
the PR satisfies following conditions. \r\n\r\n- [ ] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n\r\n### To
verify\r\n\r\n1. Create an API key\r\n2. Create a connector that will
successfully run, it doesn't have to be\r\nSentinelOne.\r\n3. Run the
following with the ID and correct params for your
connector\r\ntype.\r\n```\r\ncurl -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'\r\n{\r\n \"params\": {\r\n
\"message\": \"hi\"\r\n }\r\n}'\r\n```\r\n4. Go to dev tools and run the
following query to verify that the API\r\nkey information is stored in
the event log\r\n```\r\nGET /.kibana-event-log*/_search\r\n{\r\n
\"sort\": [\r\n {\r\n \"@timestamp\": {\r\n \"order\": \"desc\"\r\n
}\r\n }\r\n ],\r\n \"query\": {\r\n \"bool\": {\r\n \"filter\": [\r\n
{\r\n \"term\": {\r\n \"event.provider\": {\r\n \"value\":
\"actions\"\r\n }\r\n }\r\n }\r\n ]\r\n }\r\n
}\r\n```","sha":"1ba2716c7b00086b35788d7714781b252be1d6a0","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:ResponseOps","v9.0.0","backport:prev-minor","v8.18.0"],"title":"[ResponseOps]
Granular Connector RBAC - adding API key to event
log","number":204114,"url":"https://github.com/elastic/kibana/pull/204114","mergeCommit":{"message":"[ResponseOps]
Granular Connector RBAC - adding API key to event log (#204114)\n\nPart
of https://github.com/elastic/kibana/issues/180908\r\n\r\n##
Summary\r\n\r\nThis change is part of adding granular RBAC for
SecuritySolution\r\nconnectors. In this PR, I updated the action
executor to log API key\r\ndetails when a connector is executed by a
user authenticated via API\r\nkey. The public name and id of the API key
are now included in the event\r\nlog.\r\n\r\n### Checklist\r\n\r\nCheck
the PR satisfies following conditions. \r\n\r\n- [ ] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n\r\n### To
verify\r\n\r\n1. Create an API key\r\n2. Create a connector that will
successfully run, it doesn't have to be\r\nSentinelOne.\r\n3. Run the
following with the ID and correct params for your
connector\r\ntype.\r\n```\r\ncurl -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'\r\n{\r\n \"params\": {\r\n
\"message\": \"hi\"\r\n }\r\n}'\r\n```\r\n4. Go to dev tools and run the
following query to verify that the API\r\nkey information is stored in
the event log\r\n```\r\nGET /.kibana-event-log*/_search\r\n{\r\n
\"sort\": [\r\n {\r\n \"@timestamp\": {\r\n \"order\": \"desc\"\r\n
}\r\n }\r\n ],\r\n \"query\": {\r\n \"bool\": {\r\n \"filter\": [\r\n
{\r\n \"term\": {\r\n \"event.provider\": {\r\n \"value\":
\"actions\"\r\n }\r\n }\r\n }\r\n ]\r\n }\r\n
}\r\n```","sha":"1ba2716c7b00086b35788d7714781b252be1d6a0"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/204114","number":204114,"mergeCommit":{"message":"[ResponseOps]
Granular Connector RBAC - adding API key to event log (#204114)\n\nPart
of https://github.com/elastic/kibana/issues/180908\r\n\r\n##
Summary\r\n\r\nThis change is part of adding granular RBAC for
SecuritySolution\r\nconnectors. In this PR, I updated the action
executor to log API key\r\ndetails when a connector is executed by a
user authenticated via API\r\nkey. The public name and id of the API key
are now included in the event\r\nlog.\r\n\r\n### Checklist\r\n\r\nCheck
the PR satisfies following conditions. \r\n\r\n- [ ] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n\r\n### To
verify\r\n\r\n1. Create an API key\r\n2. Create a connector that will
successfully run, it doesn't have to be\r\nSentinelOne.\r\n3. Run the
following with the ID and correct params for your
connector\r\ntype.\r\n```\r\ncurl -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'\r\n{\r\n \"params\": {\r\n
\"message\": \"hi\"\r\n }\r\n}'\r\n```\r\n4. Go to dev tools and run the
following query to verify that the API\r\nkey information is stored in
the event log\r\n```\r\nGET /.kibana-event-log*/_search\r\n{\r\n
\"sort\": [\r\n {\r\n \"@timestamp\": {\r\n \"order\": \"desc\"\r\n
}\r\n }\r\n ],\r\n \"query\": {\r\n \"bool\": {\r\n \"filter\": [\r\n
{\r\n \"term\": {\r\n \"event.provider\": {\r\n \"value\":
\"actions\"\r\n }\r\n }\r\n }\r\n ]\r\n }\r\n
}\r\n```","sha":"1ba2716c7b00086b35788d7714781b252be1d6a0"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Alexi Doak <[email protected]>
  • Loading branch information
kibanamachine and doakalexi authored Dec 19, 2024
1 parent 6c05f46 commit b79e3ac
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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;
}
91 changes: 79 additions & 12 deletions x-pack/plugins/actions/server/lib/action_executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,25 +229,26 @@ 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();
jest.clearAllMocks();
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);
});
Expand Down Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/actions/server/lib/action_executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,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',
Expand Down
10 changes: 10 additions & 0 deletions x-pack/plugins/event_log/generated/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,16 @@
}
}
}
},
"user_api_key": {
"properties": {
"id": {
"type": "keyword"
},
"name": {
"type": "keyword"
}
}
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions x-pack/plugins/event_log/generated/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ export const EventSchema = schema.maybe(
),
})
),
user_api_key: schema.maybe(
schema.object({
id: ecsString(),
name: ecsString(),
})
),
})
),
})
Expand Down
10 changes: 10 additions & 0 deletions x-pack/plugins/event_log/scripts/mappings.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,16 @@ exports.EcsCustomPropertyMappings = {
},
},
},
user_api_key: {
properties: {
id: {
type: 'keyword',
},
name: {
type: 'keyword',
},
},
},
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`);
}
});
});
}
});
Expand Down

0 comments on commit b79e3ac

Please sign in to comment.