Skip to content

Commit c56715f

Browse files
authored
Updatable API keys - logging audit trail event (#88276)
This PR adds a new audit trail event for when API keys are updated.
1 parent a2ee4c5 commit c56715f

File tree

4 files changed

+96
-6
lines changed

4 files changed

+96
-6
lines changed

docs/changelog/88276.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 88276
2+
summary: Updatable API keys - logging audit trail event
3+
area: Audit
4+
type: enhancement
5+
issues: []

x-pack/docs/en/security/auditing/event-types.asciidoc

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,29 @@ event action.
231231
["index-b*"],"privileges":["all"]}],"applications":[],"run_as":[]}]}}}
232232
====
233233

234+
[[event-change-apikey]]
235+
`change_apikey`::
236+
Logged when the <<security-api-update-api-key,update API key>> API is
237+
invoked to update the attributes of an existing API key.
238+
+
239+
You must include the `security_config_change` event type to audit the related
240+
event action.
241+
+
242+
.Example
243+
[%collapsible%open]
244+
====
245+
[source,js]
246+
{"type":"audit", "timestamp":"2020-12-31T00:33:52,521+0200", "node.id":
247+
"9clhpgjJRR-iKzOw20xBNQ", "event.type":"security_config_change", "event.action":
248+
"change_apikey", "request.id":"9FteCmovTzWHVI-9Gpa_vQ", "change":{"apikey":
249+
{"id":"zcwN3YEBBmnjw-K-hW5_","role_descriptors":[{"cluster":
250+
["monitor","manage_ilm"],"indices":[{"names":["index-a*"],"privileges":
251+
["read","maintenance"]},{"names":["in*","alias*"],"privileges":["read"],
252+
"field_security":{"grant":["field1*","@timestamp"],"except":["field11"]}}],
253+
"applications":[],"run_as":[]},{"cluster":["all"],"indices":[{"names":
254+
["index-b*"],"privileges":["all"]}],"applications":[],"run_as":[]}]}}}
255+
====
256+
234257
[[event-delete-privileges]]
235258
`delete_privileges`::
236259
Logged when the
@@ -535,8 +558,8 @@ In addition, if `event.type` equals <<security-config-change,`security_config_ch
535558
the `event.action` attribute takes one of the following values:
536559
`put_user`, `change_password`, `put_role`, `put_role_mapping`,
537560
`change_enable_user`, `change_disable_user`, `put_privileges`, `create_apikey`,
538-
`delete_user`, `delete_role`, `delete_role_mapping`, `invalidate_apikeys` or
539-
`delete_privileges`.
561+
`delete_user`, `delete_role`, `delete_role_mapping`, `invalidate_apikeys`,
562+
`delete_privileges`, or `change_apikey`.
540563

541564
`request.id` :: A synthetic identifier that can be used to correlate the events
542565
associated with a particular REST request.
@@ -626,7 +649,7 @@ ones):
626649
The events with the `event.type` attribute equal to `security_config_change` have one of the following
627650
`event.action` attribute values: `put_user`, `change_password`, `put_role`, `put_role_mapping`,
628651
`change_enable_user`, `change_disable_user`, `put_privileges`, `create_apikey`, `delete_user`,
629-
`delete_role`, `delete_role_mapping`, `invalidate_apikeys`, or `delete_privileges`.
652+
`delete_role`, `delete_role_mapping`, `invalidate_apikeys`, `delete_privileges`, or `change_apikey`.
630653

631654
These events also have *one* of the following extra attributes (in addition to the common
632655
ones), which is specific to the `event.type` attribute. The attribute's value is a nested JSON object:
@@ -640,7 +663,8 @@ ones), which is specific to the `event.type` attribute. The attribute's value is
640663
`role_mapping` or for application `privileges`.
641664
`change` :: The object representation of the security config that
642665
is being changed. It can be the `password`, `enable` or `disable`,
643-
config object for native or built-in users.
666+
config object for native or built-in users. If an API key is updated,
667+
the config object will be an `apikey`.
644668
`create` :: The object representation of the new security config that is being
645669
created. This is currently only used for API keys auditing.
646670
If the API key is created using the
@@ -740,6 +764,9 @@ the <<mapping-roles, API request for mapping roles>>.
740764
The `role_descriptors` objects have the same schema as the `role_descriptor`
741765
object that is part of the above `role` config object.
742766

767+
The object for an API key update will differ in that it will not include
768+
a `name` or `expiration`.
769+
743770
`grant` :: An object like:
744771
+
745772
[source,js]

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyRequest;
5050
import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyAction;
5151
import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyRequest;
52+
import org.elasticsearch.xpack.core.security.action.apikey.UpdateApiKeyAction;
53+
import org.elasticsearch.xpack.core.security.action.apikey.UpdateApiKeyRequest;
5254
import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction;
5355
import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesRequest;
5456
import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesAction;
@@ -287,7 +289,8 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener {
287289
DeleteServiceAccountTokenAction.NAME,
288290
ActivateProfileAction.NAME,
289291
UpdateProfileDataAction.NAME,
290-
SetProfileEnabledAction.NAME
292+
SetProfileEnabledAction.NAME,
293+
UpdateApiKeyAction.NAME
291294
);
292295
private static final String FILTER_POLICY_PREFIX = setting("audit.logfile.events.ignore_filters.");
293296
// because of the default wildcard value (*) for the field filter, a policy with
@@ -747,6 +750,9 @@ public void accessGranted(
747750
} else if (msg instanceof final SetProfileEnabledRequest setProfileEnabledRequest) {
748751
assert SetProfileEnabledAction.NAME.equals(action);
749752
securityChangeLogEntryBuilder(requestId).withRequestBody(setProfileEnabledRequest).build();
753+
} else if (msg instanceof final UpdateApiKeyRequest updateApiKeyRequest) {
754+
assert UpdateApiKeyAction.NAME.equals(action);
755+
securityChangeLogEntryBuilder(requestId).withRequestBody(updateApiKeyRequest).build();
750756
} else {
751757
throw new IllegalStateException(
752758
"Unknown message class type ["
@@ -1215,6 +1221,16 @@ LogEntryBuilder withRequestBody(GrantApiKeyRequest grantApiKeyRequest) throws IO
12151221
return this;
12161222
}
12171223

1224+
LogEntryBuilder withRequestBody(final UpdateApiKeyRequest updateApiKeyRequest) throws IOException {
1225+
logEntry.with(EVENT_ACTION_FIELD_NAME, "change_apikey");
1226+
XContentBuilder builder = JsonXContent.contentBuilder().humanReadable(true);
1227+
builder.startObject();
1228+
withRequestBody(builder, updateApiKeyRequest);
1229+
builder.endObject();
1230+
logEntry.with(CHANGE_CONFIG_FIELD_NAME, Strings.toString(builder));
1231+
return this;
1232+
}
1233+
12181234
private void withRequestBody(XContentBuilder builder, CreateApiKeyRequest createApiKeyRequest) throws IOException {
12191235
TimeValue expiration = createApiKeyRequest.getExpiration();
12201236
builder.startObject("apikey")
@@ -1228,6 +1244,18 @@ private void withRequestBody(XContentBuilder builder, CreateApiKeyRequest create
12281244
.endObject(); // apikey
12291245
}
12301246

1247+
private void withRequestBody(final XContentBuilder builder, final UpdateApiKeyRequest updateApiKeyRequest) throws IOException {
1248+
builder.startObject("apikey").field("id", updateApiKeyRequest.getId());
1249+
if (updateApiKeyRequest.getRoleDescriptors() != null) {
1250+
builder.startArray("role_descriptors");
1251+
for (RoleDescriptor roleDescriptor : updateApiKeyRequest.getRoleDescriptors()) {
1252+
withRoleDescriptor(builder, roleDescriptor);
1253+
}
1254+
builder.endArray();
1255+
}
1256+
builder.endObject();
1257+
}
1258+
12311259
private void withRoleDescriptor(XContentBuilder builder, RoleDescriptor roleDescriptor) throws IOException {
12321260
builder.startObject().array(RoleDescriptor.Fields.CLUSTER.getPreferredName(), roleDescriptor.getClusterPrivileges());
12331261
if (roleDescriptor.getConditionalClusterPrivileges() != null && roleDescriptor.getConditionalClusterPrivileges().length > 0) {

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,15 @@
4646
import org.elasticsearch.xcontent.XContentBuilder;
4747
import org.elasticsearch.xcontent.XContentType;
4848
import org.elasticsearch.xpack.core.XPackSettings;
49+
import org.elasticsearch.xpack.core.security.action.apikey.ApiKeyTests;
4950
import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyAction;
5051
import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest;
5152
import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyAction;
5253
import org.elasticsearch.xpack.core.security.action.apikey.GrantApiKeyRequest;
5354
import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyAction;
5455
import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyRequest;
56+
import org.elasticsearch.xpack.core.security.action.apikey.UpdateApiKeyAction;
57+
import org.elasticsearch.xpack.core.security.action.apikey.UpdateApiKeyRequest;
5558
import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction;
5659
import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesRequest;
5760
import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesAction;
@@ -605,6 +608,32 @@ public void testSecurityConfigChangeEventFormattingForRoles() throws IOException
605608
// clear log
606609
CapturingLogger.output(logger.getName(), Level.INFO).clear();
607610

611+
final String keyId = randomAlphaOfLength(10);
612+
final var updateApiKeyRequest = new UpdateApiKeyRequest(
613+
keyId,
614+
randomBoolean() ? null : keyRoleDescriptors,
615+
ApiKeyTests.randomMetadata()
616+
);
617+
auditTrail.accessGranted(requestId, authentication, UpdateApiKeyAction.NAME, updateApiKeyRequest, authorizationInfo);
618+
final var expectedUpdateKeyAuditEventString = """
619+
"change":{"apikey":{"id":"%s"%s}}\
620+
""".formatted(keyId, updateApiKeyRequest.getRoleDescriptors() == null ? "" : "," + roleDescriptorsStringBuilder);
621+
output = CapturingLogger.output(logger.getName(), Level.INFO);
622+
assertThat(output.size(), is(2));
623+
String generatedUpdateKeyAuditEventString = output.get(1);
624+
assertThat(generatedUpdateKeyAuditEventString, containsString(expectedUpdateKeyAuditEventString));
625+
generatedUpdateKeyAuditEventString = generatedUpdateKeyAuditEventString.replace(", " + expectedUpdateKeyAuditEventString, "");
626+
checkedFields = new MapBuilder<>(commonFields);
627+
checkedFields.remove(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME);
628+
checkedFields.remove(LoggingAuditTrail.ORIGIN_TYPE_FIELD_NAME);
629+
checkedFields.put("type", "audit")
630+
.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, "security_config_change")
631+
.put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "change_apikey")
632+
.put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId);
633+
assertMsg(generatedUpdateKeyAuditEventString, checkedFields.map());
634+
// clear log
635+
CapturingLogger.output(logger.getName(), Level.INFO).clear();
636+
608637
GrantApiKeyRequest grantApiKeyRequest = new GrantApiKeyRequest();
609638
grantApiKeyRequest.setRefreshPolicy(randomFrom(WriteRequest.RefreshPolicy.values()));
610639
grantApiKeyRequest.getGrant().setType(randomFrom(randomAlphaOfLength(8), null));
@@ -1800,7 +1829,8 @@ public void testSecurityConfigChangedEventSelection() {
18001829
new Tuple<>(
18011830
SetProfileEnabledAction.NAME,
18021831
new SetProfileEnabledRequest(randomAlphaOfLength(20), randomBoolean(), WriteRequest.RefreshPolicy.WAIT_UNTIL)
1803-
)
1832+
),
1833+
new Tuple<>(UpdateApiKeyAction.NAME, UpdateApiKeyRequest.usingApiKeyId(randomAlphaOfLength(10)))
18041834
);
18051835
auditTrail.accessGranted(requestId, authentication, actionAndRequest.v1(), actionAndRequest.v2(), authorizationInfo);
18061836
List<String> output = CapturingLogger.output(logger.getName(), Level.INFO);

0 commit comments

Comments
 (0)