Skip to content

Commit

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

Part of elastic#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"
            }
          }
        }
      ]
    }
  }
```
  • Loading branch information
doakalexi authored and benakansara committed Jan 2, 2025
1 parent a60d132 commit 91b764c
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 @@ -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',
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 91b764c

Please sign in to comment.